Skip to content

Commit f66360a

Browse files
committed
refactor: centralize lease scope projection
1 parent f7c1e8e commit f66360a

7 files changed

Lines changed: 422 additions & 143 deletions

File tree

src/client-normalizers.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import type { DaemonRequest, SessionRuntimeHints } from './daemon/types.ts';
44
import { AppError, type NormalizedError } from './utils/errors.ts';
55
import type { SnapshotNode } from './utils/snapshot.ts';
66
import { buildAppIdentifiers, buildDeviceIdentifiers } from './client-shared.ts';
7+
import {
8+
leaseScopeFromOptions,
9+
leaseScopeToCommandFlags,
10+
leaseScopeToRequestMeta,
11+
} from './core/lease-scope.ts';
712
import type {
813
AgentDeviceDevice,
914
AgentDeviceSession,
@@ -157,13 +162,12 @@ function buildClientDevicePlatformFields(
157162
}
158163

159164
export function normalizeRuntimeHints(value: unknown): SessionRuntimeHints | undefined {
160-
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
161-
const record = value as Record<string, unknown>;
162-
const platform = record.platform;
163-
const metroHost = readOptionalString(record, 'metroHost');
164-
const metroPort = typeof record.metroPort === 'number' ? record.metroPort : undefined;
165-
const bundleUrl = readOptionalString(record, 'bundleUrl');
166-
const launchUrl = readOptionalString(record, 'launchUrl');
165+
if (!isRecord(value)) return undefined;
166+
const platform = value.platform;
167+
const metroHost = readOptionalString(value, 'metroHost');
168+
const metroPort = typeof value.metroPort === 'number' ? value.metroPort : undefined;
169+
const bundleUrl = readOptionalString(value, 'bundleUrl');
170+
const launchUrl = readOptionalString(value, 'launchUrl');
167171
return {
168172
platform: platform === 'ios' || platform === 'android' ? platform : undefined,
169173
metroHost,
@@ -211,21 +215,20 @@ export function normalizeOpenDevice(
211215
}
212216

213217
export function normalizeStartupSample(value: unknown): StartupPerfSample | undefined {
214-
if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined;
215-
const record = value as Record<string, unknown>;
218+
if (!isRecord(value)) return undefined;
216219
if (
217-
typeof record.durationMs !== 'number' ||
218-
typeof record.measuredAt !== 'string' ||
219-
typeof record.method !== 'string'
220+
typeof value.durationMs !== 'number' ||
221+
typeof value.measuredAt !== 'string' ||
222+
typeof value.method !== 'string'
220223
) {
221224
return undefined;
222225
}
223226
return {
224-
durationMs: record.durationMs,
225-
measuredAt: record.measuredAt,
226-
method: record.method,
227-
appTarget: readOptionalString(record, 'appTarget'),
228-
appBundleId: readOptionalString(record, 'appBundleId'),
227+
durationMs: value.durationMs,
228+
measuredAt: value.measuredAt,
229+
method: value.method,
230+
appTarget: readOptionalString(value, 'appTarget'),
231+
appBundleId: readOptionalString(value, 'appBundleId'),
229232
};
230233
}
231234

@@ -268,17 +271,15 @@ export function readSnapshotNodes(value: unknown): SnapshotNode[] {
268271
}
269272

270273
export function buildFlags(options: InternalRequestOptions): CommandFlags {
274+
const leaseScope = leaseScopeFromOptions(options);
271275
return stripUndefined({
272276
stateDir: options.stateDir,
273277
daemonBaseUrl: options.daemonBaseUrl,
274278
daemonAuthToken: options.daemonAuthToken,
275279
daemonTransport: options.daemonTransport,
276280
daemonServerMode: options.daemonServerMode,
277-
tenant: options.tenant,
281+
...leaseScopeToCommandFlags(leaseScope),
278282
sessionIsolation: options.sessionIsolation,
279-
runId: options.runId,
280-
leaseId: options.leaseId,
281-
leaseBackend: options.leaseBackend,
282283
platform: options.platform,
283284
target: options.target,
284285
device: options.device,
@@ -352,21 +353,15 @@ export function buildFlags(options: InternalRequestOptions): CommandFlags {
352353
}
353354

354355
export function buildMeta(options: InternalRequestOptions): DaemonRequest['meta'] {
356+
const leaseScope = leaseScopeFromOptions(options);
355357
return stripUndefined({
356358
requestId: options.requestId,
357359
cwd: options.cwd,
358360
sessionExplicit: options.session !== undefined,
359361
debug: options.debug,
360362
lockPolicy: options.lockPolicy,
361363
lockPlatform: options.lockPlatform,
362-
tenantId: options.tenant,
363-
runId: options.runId,
364-
leaseId: options.leaseId,
365-
leaseBackend: options.leaseBackend,
366-
leaseTtlMs: options.leaseTtlMs,
367-
leaseProvider: options.leaseProvider,
368-
clientId: options.clientId,
369-
deviceKey: options.deviceKey,
364+
...leaseScopeToRequestMeta(leaseScope),
370365
sessionIsolation: options.sessionIsolation,
371366
installSource: options.installSource,
372367
retainMaterializedPaths: options.retainMaterializedPaths,

src/client.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,16 +215,10 @@ export function createAgentDeviceClient(
215215
await execute(INTERNAL_COMMANDS.leaseAllocate, [], {
216216
...options,
217217
leaseId: undefined,
218-
leaseTtlMs: options.ttlMs,
219218
}),
220219
),
221220
heartbeat: async (options) =>
222-
normalizeLease(
223-
await execute(INTERNAL_COMMANDS.leaseHeartbeat, [], {
224-
...options,
225-
leaseTtlMs: options.ttlMs,
226-
}),
227-
),
221+
normalizeLease(await execute(INTERNAL_COMMANDS.leaseHeartbeat, [], options)),
228222
release: async (options) => {
229223
const data = await execute(INTERNAL_COMMANDS.leaseRelease, [], options);
230224
return { released: data.released === true };
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import {
4+
findMissingProxyLeaseFields,
5+
leaseScopeFromOptions,
6+
leaseScopeFromRequest,
7+
leaseScopeToCommandFlags,
8+
leaseScopeToConnectionMetadata,
9+
leaseScopeToLeaseRpcParams,
10+
leaseScopeToRequestMeta,
11+
} from '../lease-scope.ts';
12+
13+
test('leaseScopeFromOptions normalizes public aliases and projects request meta', () => {
14+
const scope = leaseScopeFromOptions({
15+
tenant: 'tenant-a',
16+
runId: 'run-1',
17+
leaseId: 'lease-1',
18+
ttlMs: 120_000,
19+
leaseBackend: 'ios-instance',
20+
provider: 'proxy',
21+
deviceKey: 'ios:SIM-001',
22+
clientId: 'client-a',
23+
});
24+
25+
assert.deepEqual(scope, {
26+
tenantId: 'tenant-a',
27+
runId: 'run-1',
28+
leaseId: 'lease-1',
29+
leaseTtlMs: 120_000,
30+
leaseBackend: 'ios-instance',
31+
leaseProvider: 'proxy',
32+
deviceKey: 'ios:SIM-001',
33+
clientId: 'client-a',
34+
});
35+
assert.deepEqual(leaseScopeToRequestMeta(scope), {
36+
tenantId: 'tenant-a',
37+
runId: 'run-1',
38+
leaseId: 'lease-1',
39+
leaseTtlMs: 120_000,
40+
leaseBackend: 'ios-instance',
41+
leaseProvider: 'proxy',
42+
deviceKey: 'ios:SIM-001',
43+
clientId: 'client-a',
44+
});
45+
assert.deepEqual(leaseScopeToCommandFlags(scope), {
46+
tenant: 'tenant-a',
47+
runId: 'run-1',
48+
leaseId: 'lease-1',
49+
leaseBackend: 'ios-instance',
50+
});
51+
});
52+
53+
test('leaseScopeFromRequest prefers metadata and falls back to legacy flags', () => {
54+
assert.deepEqual(
55+
leaseScopeFromRequest({
56+
meta: {
57+
tenantId: 'tenant-meta',
58+
leaseProvider: 'limrun',
59+
},
60+
flags: {
61+
tenant: 'tenant-flag',
62+
runId: 'run-flag',
63+
leaseId: 'lease-flag',
64+
provider: 'proxy',
65+
deviceKey: 'ios:SIM-001',
66+
clientId: 'client-a',
67+
},
68+
}),
69+
{
70+
tenantId: 'tenant-meta',
71+
runId: 'run-flag',
72+
leaseId: 'lease-flag',
73+
leaseProvider: 'limrun',
74+
deviceKey: 'ios:SIM-001',
75+
clientId: 'client-a',
76+
},
77+
);
78+
});
79+
80+
test('leaseScopeToLeaseRpcParams preserves provider alias and command-specific fields', () => {
81+
const scope = leaseScopeFromOptions({
82+
tenant: 'tenant-a',
83+
runId: 'run-1',
84+
leaseId: 'lease-1',
85+
leaseTtlMs: 60_000,
86+
leaseBackend: 'android-instance',
87+
leaseProvider: 'proxy',
88+
deviceKey: 'android:emulator-5554',
89+
clientId: 'client-a',
90+
});
91+
92+
assert.deepEqual(
93+
leaseScopeToLeaseRpcParams(scope, 'lease_allocate', {
94+
includeTokenParam: true,
95+
token: 'token',
96+
session: 'default',
97+
}),
98+
{
99+
token: 'token',
100+
session: 'default',
101+
tenantId: 'tenant-a',
102+
runId: 'run-1',
103+
leaseProvider: 'proxy',
104+
provider: 'proxy',
105+
clientId: 'client-a',
106+
deviceKey: 'android:emulator-5554',
107+
ttlMs: 60_000,
108+
backend: 'android-instance',
109+
},
110+
);
111+
assert.deepEqual(
112+
leaseScopeToLeaseRpcParams(scope, 'lease_release', {
113+
includeTokenParam: false,
114+
token: 'token',
115+
session: 'default',
116+
}),
117+
{
118+
session: 'default',
119+
tenantId: 'tenant-a',
120+
runId: 'run-1',
121+
leaseProvider: 'proxy',
122+
provider: 'proxy',
123+
clientId: 'client-a',
124+
deviceKey: 'android:emulator-5554',
125+
leaseId: 'lease-1',
126+
},
127+
);
128+
});
129+
130+
test('leaseScopeToConnectionMetadata returns only connection lease fields', () => {
131+
assert.deepEqual(
132+
leaseScopeToConnectionMetadata(
133+
leaseScopeFromOptions({
134+
tenant: 'tenant-a',
135+
runId: 'run-1',
136+
leaseId: 'lease-1',
137+
leaseProvider: 'proxy',
138+
deviceKey: 'ios:SIM-001',
139+
clientId: 'client-a',
140+
}),
141+
),
142+
{
143+
leaseProvider: 'proxy',
144+
deviceKey: 'ios:SIM-001',
145+
clientId: 'client-a',
146+
},
147+
);
148+
assert.equal(leaseScopeToConnectionMetadata({}), undefined);
149+
});
150+
151+
test('findMissingProxyLeaseFields enforces complete proxy ownership scope', () => {
152+
assert.deepEqual(
153+
findMissingProxyLeaseFields({
154+
tenantId: 'tenant-a',
155+
runId: 'run-1',
156+
leaseProvider: 'proxy',
157+
clientId: 'client-a',
158+
}),
159+
['leaseId', 'deviceKey'],
160+
);
161+
assert.deepEqual(
162+
findMissingProxyLeaseFields({
163+
leaseProvider: 'limrun',
164+
}),
165+
[],
166+
);
167+
});

0 commit comments

Comments
 (0)