Skip to content

Commit d80fb35

Browse files
authored
fix(browserstack): carry the full provider-allocation config over the lease_allocate envelope (#2494) (#2495)
* fix(browserstack): forward provider session metadata over the lease envelope `--provider-project`, `--provider-build`, and `--provider-session-name` are stored in the connection profile and reach the daemon on the line transport (which forwards the whole request), but the compact JSON-RPC lease envelope carried only `providerApp`. Over HTTP/remote daemons the daemon's lease-lifecycle provider therefore saw no session-naming metadata and created BrowserStack sessions as "Untitled Project" / "Untitled Build" with an empty name. Read the four provider session-metadata flags through one shared projection (`readLeaseAllocateProviderMetadata`) used by both the client's `buildHttpRpcPayload` and the daemon's `toLeaseDaemonRequest`, so the transports agree and the producer and consumer cannot drop a sibling again. Closes #2494 * chore(gates): pin lease_allocate wire digests for the forwarded provider metadata (#2494) * test(daemon-http): move the lease provider-metadata check into its own file The provider-scenario daemon-http-server test is over the 1000-line tripwire and may not grow; the lease_allocate metadata assertion now lives in daemon-http-lease-allocate.test.ts, mirroring the lease projection in http-server.ts. * fix(browserstack): carry the full provider-allocation config over the lease envelope The lease envelope named only the session-label fields, so a fresh remote allocation still failed in prepareSession before the names could take effect: device selection (platform, device), providerOsVersion, and the configured device-feature/AWS knobs were dropped on the HTTP transport, while the line transport forwards the whole request for free. readLeaseAllocateProviderMetadata becomes readLeaseAllocateProviderFlags and projects the full set the lease-lifecycle provider reads, pinned exhaustive against CloudProviderProfileFields so a new field cannot silently miss it. The producer and consumer already share this reader, so both transports agree. Covered end to end in cloud-webdriver-lease-http.test.ts: the exact client envelope is driven through a real daemon HTTP server into the real BrowserStack prepareSession, asserting the capabilities that reach the hub. * chore(gates): re-pin lease_allocate wire digests for the broadened projection (#2494)
1 parent fd80c1e commit d80fb35

8 files changed

Lines changed: 432 additions & 13 deletions

File tree

packages/contracts/src/__tests__/lease-scope.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
leaseScopeToConnectionMetadata,
99
leaseScopeToLeaseRpcParams,
1010
leaseScopeToRequestMeta,
11+
readLeaseAllocateProviderFlags,
1112
} from '../lease-scope.ts';
1213

1314
test('leaseScopeFromOptions normalizes public aliases and projects request meta', () => {
@@ -166,3 +167,51 @@ test('findMissingProxyLeaseFields enforces complete proxy ownership scope', () =
166167
[],
167168
);
168169
});
170+
171+
test('readLeaseAllocateProviderFlags carries the provider-allocation flags and drops the rest', () => {
172+
assert.deepEqual(
173+
readLeaseAllocateProviderFlags({
174+
session: 'default',
175+
token: 'secret',
176+
runId: 'run-a',
177+
deviceKey: 'dk-1',
178+
provider: 'browserstack',
179+
ttlMs: 1000,
180+
platform: 'ios',
181+
device: 'iPhone 15',
182+
providerApp: 'bs://abc',
183+
providerOsVersion: '17',
184+
providerProject: 'MyProject',
185+
providerBuild: 'Build-1',
186+
providerSessionName: 'smoke',
187+
providerDeviceOrientation: 'landscape',
188+
providerGeoLocation: '52.5,13.4',
189+
providerLanguage: 'en',
190+
providerNoResignApp: true,
191+
awsRegion: 'us-west-2',
192+
awsProjectArn: 'arn:aws:devicefarm:0',
193+
awsInteractionMode: 'NO_VIDEO',
194+
}),
195+
{
196+
platform: 'ios',
197+
device: 'iPhone 15',
198+
providerApp: 'bs://abc',
199+
providerOsVersion: '17',
200+
providerProject: 'MyProject',
201+
providerBuild: 'Build-1',
202+
providerSessionName: 'smoke',
203+
providerDeviceOrientation: 'landscape',
204+
providerGeoLocation: '52.5,13.4',
205+
providerLanguage: 'en',
206+
providerNoResignApp: true,
207+
awsRegion: 'us-west-2',
208+
awsProjectArn: 'arn:aws:devicefarm:0',
209+
awsInteractionMode: 'NO_VIDEO',
210+
},
211+
);
212+
assert.deepEqual(
213+
readLeaseAllocateProviderFlags({ providerApp: 'bs://abc', providerBuild: undefined }),
214+
{ providerApp: 'bs://abc' },
215+
);
216+
assert.deepEqual(readLeaseAllocateProviderFlags(undefined), {});
217+
});

packages/contracts/src/lease-scope.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { LeaseBackend } from '@agent-device/kernel/contracts';
22
import { stripUndefined } from '@agent-device/kernel/record';
3+
import type { CloudProviderProfileFields } from './remote-config-fields.ts';
4+
import type { CommandFlags } from './command-flags.ts';
35

46
const PROXY_LEASE_PROVIDER = 'proxy';
57
export const DEFAULT_PROXY_LEASE_TTL_MS = 300_000;
@@ -197,6 +199,63 @@ export function leaseScopeToLeaseRpcParams(
197199
}
198200
}
199201

202+
/**
203+
* Request flags that must travel with `lease_allocate` so the daemon's lease-lifecycle provider can
204+
* prepare the session — device selection, the app/os, the session-naming fields, and the configured
205+
* device-feature and AWS knobs. The line transport forwards the whole request and these arrive for
206+
* free; the compact lease envelope is the only projection that has to name them, so the client's
207+
* producer and the daemon's consumer read the SAME list and cannot drop a sibling the provider needs.
208+
*/
209+
const LEASE_ALLOCATE_PROVIDER_FLAG_KEYS = [
210+
// Device selection the lease-lifecycle provider needs to create the session.
211+
'platform',
212+
'device',
213+
// The Cloud provider profile fields; pinned exhaustive against that vocabulary below.
214+
'providerApp',
215+
'providerOsVersion',
216+
'providerProject',
217+
'providerBuild',
218+
'providerSessionName',
219+
'providerDeviceOrientation',
220+
'providerGeoLocation',
221+
'providerTimezone',
222+
'providerLanguage',
223+
'providerLocale',
224+
'providerNetworkProfile',
225+
'providerCustomNetwork',
226+
'providerNoResignApp',
227+
'awsProjectArn',
228+
'awsDeviceArn',
229+
'awsAppArn',
230+
'awsRegion',
231+
'awsInteractionMode',
232+
] as const satisfies readonly (keyof CommandFlags)[];
233+
234+
type LeaseAllocateProviderFlagKey = (typeof LEASE_ALLOCATE_PROVIDER_FLAG_KEYS)[number];
235+
236+
// A Cloud provider profile field added without joining the projection would be dropped from the
237+
// envelope and fail `prepareSession` on the remote daemon — this makes that omission fail to build.
238+
type LeaseAllocateProfileKeysAreExhaustive =
239+
Exclude<keyof CloudProviderProfileFields, LeaseAllocateProviderFlagKey> extends never
240+
? true
241+
: Exclude<keyof CloudProviderProfileFields, LeaseAllocateProviderFlagKey>;
242+
const leaseAllocateProfileKeysExhaustive: LeaseAllocateProfileKeysAreExhaustive = true;
243+
void leaseAllocateProfileKeysExhaustive;
244+
245+
type LeaseAllocateProviderFlags = Partial<Pick<CommandFlags, LeaseAllocateProviderFlagKey>>;
246+
247+
/** Reads the provider-allocation flags out of a request flags bag or a lease-envelope param bag. */
248+
export function readLeaseAllocateProviderFlags(
249+
source: Record<string, unknown> | undefined,
250+
): LeaseAllocateProviderFlags {
251+
const flags: Record<string, string | boolean> = {};
252+
for (const key of LEASE_ALLOCATE_PROVIDER_FLAG_KEYS) {
253+
const value = source?.[key];
254+
if (typeof value === 'string' || typeof value === 'boolean') flags[key] = value;
255+
}
256+
return flags as LeaseAllocateProviderFlags;
257+
}
258+
200259
export function leaseScopeToConnectionMetadata(
201260
scope: LeaseScope,
202261
): Pick<LeaseScope, 'leaseProvider' | 'deviceKey' | 'clientId'> | undefined {

src/daemon-client/daemon-client-rpc.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,53 @@ test('lease allocation transports an optional initial provider app', () => {
3333
});
3434
});
3535

36+
test('lease allocation transports the provider configuration the session needs (#2494)', () => {
37+
const payload = buildHttpRpcPayload(
38+
{
39+
token: 'daemon-token',
40+
session: 'qa-ios',
41+
command: 'lease_allocate',
42+
positionals: [],
43+
flags: {
44+
platform: 'ios',
45+
device: 'iPhone 15',
46+
providerApp: 'bs://app-id',
47+
providerOsVersion: '17',
48+
providerProject: 'MyProject',
49+
providerBuild: 'Build-2026-09-11',
50+
providerSessionName: 'smoke — iOS',
51+
providerDeviceOrientation: 'portrait',
52+
providerNoResignApp: true,
53+
},
54+
meta: {
55+
requestId: 'lease-req',
56+
tenantId: 'acme',
57+
runId: 'run-123',
58+
leaseBackend: 'ios-instance',
59+
leaseProvider: 'browserstack',
60+
},
61+
},
62+
{ includeTokenParam: false },
63+
);
64+
65+
assert.deepEqual(payload.params, {
66+
session: 'qa-ios',
67+
tenantId: 'acme',
68+
runId: 'run-123',
69+
backend: 'ios-instance',
70+
leaseProvider: 'browserstack',
71+
platform: 'ios',
72+
device: 'iPhone 15',
73+
providerApp: 'bs://app-id',
74+
providerOsVersion: '17',
75+
providerProject: 'MyProject',
76+
providerBuild: 'Build-2026-09-11',
77+
providerSessionName: 'smoke — iOS',
78+
providerDeviceOrientation: 'portrait',
79+
providerNoResignApp: true,
80+
});
81+
});
82+
3683
test('HTTP RPC errors sanitize an untrusted cause before rehydration', () => {
3784
const secret = 'adc_agent_remote-secret';
3885
const oversizedCode = `apiKey=${secret} ${'c'.repeat(500)}`;

src/daemon-client/daemon-client-rpc.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { DaemonInfo } from './daemon-client-metadata.ts';
1313
import {
1414
leaseScopeFromRequest,
1515
leaseScopeToLeaseRpcParams,
16+
readLeaseAllocateProviderFlags,
1617
type LeaseRpcCommand,
1718
} from '@agent-device/contracts/lease-scope';
1819

@@ -183,9 +184,7 @@ export function buildHttpRpcPayload(
183184
method: leaseRpcMethodForCommand(req.command),
184185
params: {
185186
...buildLeaseRpcParams(req, req.command, options),
186-
...(req.command === 'lease_allocate' && typeof req.flags?.providerApp === 'string'
187-
? { providerApp: req.flags.providerApp }
188-
: {}),
187+
...(req.command === 'lease_allocate' ? readLeaseAllocateProviderFlags(req.flags) : {}),
189188
},
190189
};
191190
}

src/daemon/server/http-server.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
DAEMON_HTTP_TENANT_HEADER,
4141
} from '@agent-device/contracts/daemon-http';
4242
import { readVersion } from '@agent-device/host-kit/version';
43+
import { readLeaseAllocateProviderFlags } from '@agent-device/contracts/lease-scope';
4344
import { sendRestJsonError, statusCodeForNormalizedError } from '../http-errors.ts';
4445
import { tryHandleUploadHttpRoute } from '../upload-http.ts';
4546
import { tryHandleDownloadableArtifactHttpRoute } from '../downloadable-artifact-http.ts';
@@ -316,10 +317,7 @@ function toLeaseDaemonRequest(
316317
session: readStringParam(params, 'session') ?? 'default',
317318
command,
318319
positionals: [],
319-
flags:
320-
command === 'lease_allocate'
321-
? { providerApp: readStringParam(params, 'providerApp') }
322-
: undefined,
320+
flags: command === 'lease_allocate' ? readLeaseAllocateProviderFlags(params) : undefined,
323321
meta: {
324322
tenantId: readStringParam(params, 'tenantId') ?? readStringParam(params, 'tenant'),
325323
runId: readStringParam(params, 'runId'),

0 commit comments

Comments
 (0)