From 3f43bd83544b56c339d371880cfd4641686c1e1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 10 Oct 2026 19:10:38 +0200 Subject: [PATCH 1/3] refactor(android): drop the scope-level adb server port lane The deleted managed-device reachability was the only producer of a construction-time adb server port, so withAndroidAdbProvider's scoped host transport, the scope serverPort option on createLocalAndroidAdbProvider / createDeviceAdbExecutor and the serial spawner, and the scope checks that guarded them have no caller. A port now reaches adb only through the per-call AndroidAdbExecutorOptions.serverPort, which keeps its coverage. Also rewraps the device-claim rule comment. Co-Authored-By: Claude Opus 5.5 --- .../src/__tests__/adb-executor.test.ts | 31 +- .../src/adb-provider-scope.test.ts | 509 ++---------------- .../src/adb-provider-scope.ts | 200 ++----- .../src/adb-transport.test.ts | 13 +- .../platform-android/src/adb-transport.ts | 27 +- src/daemon/device/device-claim-rule.ts | 5 +- src/platform-runtime-android-adb-host.test.ts | 11 +- 7 files changed, 124 insertions(+), 672 deletions(-) diff --git a/packages/platform-android/src/__tests__/adb-executor.test.ts b/packages/platform-android/src/__tests__/adb-executor.test.ts index b0ee12ef55..3b2e8ed479 100644 --- a/packages/platform-android/src/__tests__/adb-executor.test.ts +++ b/packages/platform-android/src/__tests__/adb-executor.test.ts @@ -238,28 +238,25 @@ test('createLocalAndroidAdbProvider exposes local pull and install capabilities' ]); }); -test('createLocalAndroidAdbProvider carries a private server port through every adb capability', async () => { +test('createLocalAndroidAdbProvider carries a per-call server port through every adb capability', async () => { mockRunCmd.mockClear(); mockRunCmdBackground.mockClear(); - const provider = createLocalAndroidAdbProvider( - { - platform: 'android', - id: 'emulator-5554', - name: 'Pixel Emulator', - kind: 'emulator', - booted: true, - }, - { serverPort: 15_037 }, - ); + const provider = createLocalAndroidAdbProvider({ + platform: 'android', + id: 'emulator-5554', + name: 'Pixel Emulator', + kind: 'emulator', + booted: true, + }); + const serverPort = 15_037; - await provider.exec(deviceShellArgv('adb', 'shell', ['echo', 'ok'])); - provider.spawn?.(['logcat']); - await provider.reverse?.ensure({ local: 'tcp:8081', remote: 'tcp:8081' }); - await provider.pull?.('/sdcard/video.mp4', '/tmp/video.mp4'); - await provider.install?.('/tmp/app.apk'); + await provider.exec(deviceShellArgv('adb', 'shell', ['echo', 'ok']), { serverPort }); + provider.spawn?.(['logcat'], { serverPort }); + await provider.pull?.('/sdcard/video.mp4', '/tmp/video.mp4', { serverPort }); + await provider.install?.('/tmp/app.apk', { serverPort }); assert.equal(readServerPortArgv(mockRunCmdBackground.mock.calls[0]?.[1]), 15_037); - assert.equal(mockRunCmd.mock.calls.length, 4); + assert.equal(mockRunCmd.mock.calls.length, 3); for (const call of mockRunCmd.mock.calls) assert.equal(readServerPortArgv(call[1]), 15_037); }); diff --git a/packages/platform-android/src/adb-provider-scope.test.ts b/packages/platform-android/src/adb-provider-scope.test.ts index 09de97d713..85d74d85cb 100644 --- a/packages/platform-android/src/adb-provider-scope.test.ts +++ b/packages/platform-android/src/adb-provider-scope.test.ts @@ -10,19 +10,13 @@ import { resolveAndroidAdbExecutor, resolveAndroidAdbProvider, resolveAndroidTextInjector, - resolveAndroidTouchProvider, resolveScopedAndroidAdbBackgroundTransport, withAndroidAdbProvider, } from './adb-provider-scope.ts'; -import { runAndroidHostAdb } from './adb-host.ts'; import { type AndroidAdbExecutorOptions, type AndroidAdbExecutorResult, type AndroidAdbProvider, - androidAdbHostTarget, - androidAdbInvocation, - androidAdbSerialTarget, - parseAndroidAdbArgv, serializeAndroidAdbInvocation, type AndroidAdbInvocation, } from './adb-transport.ts'; @@ -55,8 +49,8 @@ function hostCall(invocation: AndroidAdbInvocation, options?: AndroidAdbExecutor }; } -/** A host call the lease answered for: this scope's serial, the lease's server, no option port. */ -function scoped(serial: string, ...command: string[]) { +/** A host call on the private server 15037: the server on the addressing, no option port. */ +function onPrivateServer(serial: string, ...command: string[]) { return { args: ['-s', serial, ...command], serverPort: 15_037, optionServerPort: undefined }; } @@ -355,6 +349,9 @@ test('the installed override routes only normalized device-scoped adb calls to t ).toBeDefined(); expect(captured?.('adb', ['-s', OTHER.id, 'shell', 'ls'], {})).toBeUndefined(); expect(captured?.('adb', ['devices'], {})).toBeUndefined(); + expect( + captured?.('/opt/android-sdk/platform-tools/adb', ['devices', '-l'], {}), + ).toBeUndefined(); expect(captured?.('emulator', ['-list-avds'], {})).toBeUndefined(); }); expect(providerCalls).toEqual([['shell', 'ls']]); @@ -389,95 +386,9 @@ test('the installed override refuses a device-shell argv the funnel did not mint expect(providerCalls).toEqual([]); }); -test('a managed port scope refuses global options the provider cannot restate', async () => { - const providerCalls: (readonly string[])[] = []; - const provider: AndroidAdbProvider = { - exec: async (args) => { - providerCalls.push(args); - return ok(); - }, - }; - const capture = async ( - scope: { serial: string; serverPort?: number }, - args: readonly string[], - ) => { - let captured: - | ((cmd: string, args: readonly string[], options: object) => Promise | undefined) - | undefined; - bindAndroidAdbHostStub({ - withAdbCommandExecutorOverride: async (override, fn) => { - captured = override; - return await fn(); - }, - }); - await withAndroidAdbProvider(provider, scope, async () => { - captured?.('adb', args, {}); - }); - }; - - // A private adb server cannot carry a caller's `-t`, so the call is refused and the provider - // never answers for an addressing set it did not choose. - await expect( - capture({ serial: DEVICE.id, serverPort: 15_037 }, [ - '-t', - '42', - '-s', - DEVICE.id, - 'shell', - 'ls', - ]), - ).rejects.toMatchObject({ details: { reason: 'managed-device-transport-mismatch' } }); - expect(providerCalls).toEqual([]); - - // A port typed into argv names a server the provider cannot address, so the server rule refuses - // it here — not the host-global rule, which is why the port has to parse. - providerCalls.length = 0; - await expect( - capture({ serial: DEVICE.id, serverPort: 15_037 }, [ - '-P', - '9999', - '-s', - DEVICE.id, - 'shell', - 'ls', - ]), - ).rejects.toThrowError(/cannot select another server/); - expect(providerCalls).toEqual([]); - - // The server this lease holds is the one port the provider may be handed a request for, argv - // included, and the request still travels as the caller wrote it. - await capture( - { serial: DEVICE.id, serverPort: 15_037 }, - deviceShellArgv('adb', 'shell', ['ls'], ['-P', '15037', '-s', DEVICE.id]), - ); - expect(providerCalls).toEqual([['-P', '15037', 'shell', 'ls']]); - - // Without a lease the caller's own adb invocation is what runs, globals and all: the provider - // receives the request with only this scope's `-s` pair removed, and no server rule applies. - providerCalls.length = 0; - await capture( - { serial: DEVICE.id }, - deviceShellArgv('adb', 'shell', ['ls'], ['-t', '42', '-s', DEVICE.id]), - ); - await capture( - { serial: DEVICE.id }, - deviceShellArgv('adb', 'shell', ['ls'], ['-P', '9999', '-s', DEVICE.id]), - ); - expect(providerCalls).toEqual([ - ['-t', '42', 'shell', 'ls'], - ['-P', '9999', 'shell', 'ls'], - ]); -}); - -test('the provider receives the caller request with only the scope serial removed', async () => { +test('the installed override hands the provider caller globals with only the scope serial removed', async () => { const providerCalls: (readonly string[])[] = []; const hostCalls: ReturnType[] = []; - const provider: AndroidAdbProvider = { - exec: async (args) => { - providerCalls.push(args); - return ok(); - }, - }; let captured: | ((cmd: string, args: readonly string[], options: object) => Promise | undefined) | undefined; @@ -491,216 +402,50 @@ test('the provider receives the caller request with only the scope serial remove return await fn(); }, }); + const provider: AndroidAdbProvider = { + exec: async (args) => { + providerCalls.push(args); + return ok(); + }, + }; - await withAndroidAdbProvider(provider, { serial: DEVICE.id, serverPort: 15_037 }, async () => { + await withAndroidAdbProvider(provider, { serial: DEVICE.id }, async () => { // A readiness token is part of the request the provider must honor, not addressing it owns, // so it travels ahead of the command instead of being parsed away. - captured?.( + await captured?.( 'adb', deviceShellArgv('adb', 'shell', ['getprop'], ['-s', DEVICE.id, 'wait-for-device']), {}, ); + await captured?.( + 'adb', + deviceShellArgv('adb', 'shell', ['getprop'], ['-d', '-s', DEVICE.id]), + {}, + ); + await captured?.( + 'adb', + deviceShellArgv('adb', 'shell', ['ls'], ['-t', '42', '-s', DEVICE.id]), + {}, + ); + await captured?.( + 'adb', + deviceShellArgv('adb', 'shell', ['ls'], ['-P', '9999', '-s', DEVICE.id]), + {}, + ); // A call that addresses no device is not the provider's to answer as a device command. - captured?.('adb', ['get-state'], {}); - }); - // A transport global under a lease is refused by the test above, not restated here. - await withAndroidAdbProvider(provider, { serial: DEVICE.id }, async () => { - captured?.('adb', deviceShellArgv('adb', 'shell', ['getprop'], ['-d', '-s', DEVICE.id]), {}); + expect(captured?.('adb', ['get-state'], {})).toBeUndefined(); }); expect(providerCalls).toEqual([ ['wait-for-device', 'shell', 'getprop'], ['-d', 'shell', 'getprop'], + ['-t', '42', 'shell', 'ls'], + ['-P', '9999', 'shell', 'ls'], ]); - // A server-level command addresses no device, so the lease answers it on its own transport. - expect(hostCalls).toEqual([scoped(DEVICE.id, 'get-state')]); -}); - -test('a managed port scope rejects foreign serials before host adb execution', async () => { - const hostCalls: ReturnType[] = []; - bindAndroidAdbHostStub({ - execAdb: async (invocation, options) => { - hostCalls.push(hostCall(invocation, options)); - return ok(); - }, - }); - - await withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - await runAndroidHostAdb(parseAndroidAdbArgv(['devices'])); - await runAndroidHostAdb( - androidAdbInvocation(androidAdbHostTarget(), deviceShellArgv('adb', 'shell', ['id'])), - { env: { ANDROID_SERIAL: OTHER.id } }, - ); - await runAndroidHostAdb( - androidAdbInvocation( - androidAdbSerialTarget(DEVICE.id), - deviceShellArgv('adb', 'shell', ['getprop']), - ), - ); - await expect( - runAndroidHostAdb( - androidAdbInvocation( - androidAdbSerialTarget(OTHER.id), - deviceShellArgv('adb', 'shell', ['getprop']), - ), - ), - ).rejects.toMatchObject({ - details: { reason: 'managed-device-transport-mismatch' }, - }); - }, - ); - await runAndroidHostAdb(parseAndroidAdbArgv(['devices'])); - - // The lease's server rides in the addressing, so no per-call option can move it afterwards. - expect(hostCalls).toEqual([ - scoped(DEVICE.id, 'devices'), - scoped(DEVICE.id, 'shell', 'id'), - scoped(DEVICE.id, 'shell', 'getprop'), - { args: ['devices'], serverPort: undefined, optionServerPort: undefined }, - ]); -}); - -test('a managed port scope classifies absolute adb commands and preserves the default boundary', async () => { - const providerCalls: (readonly string[])[] = []; - const hostCalls: (readonly string[])[] = []; - let captured: - | ((cmd: string, args: readonly string[], options: object) => Promise | undefined) - | undefined; - bindAndroidAdbHostStub({ - execAdb: async (invocation) => { - hostCalls.push(invokedArgv(invocation)); - return ok(); - }, - withAdbCommandExecutorOverride: async (override, fn) => { - captured = override; - return await fn(); - }, - }); - - await withAndroidAdbProvider( - { - exec: async (args) => { - providerCalls.push(args); - return ok(); - }, - }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - const global = captured?.('/opt/android-sdk/platform-tools/adb', ['devices', '-l'], {}); - const matching = captured?.( - '/opt/android-sdk/platform-tools/adb', - deviceShellArgv('adb', 'shell', ['ls'], ['-s', DEVICE.id]), - {}, - ); - expect(() => captured?.('adb', ['-s', OTHER.id, 'shell', 'ls'], {})).toThrowError( - expect.objectContaining({ details: { reason: 'managed-device-transport-mismatch' } }), - ); - expect(captured?.('emulator', ['-list-avds'], {})).toBeUndefined(); - expect(global).toBeDefined(); - expect(matching).toBeDefined(); - await global; - await matching; - }, - ); - - expect(hostCalls).toEqual([['-s', DEVICE.id, 'devices', '-l']]); - expect(providerCalls).toEqual([['shell', 'ls']]); -}); - -test('managed port scopes refuse foreign device resolvers before returning a local transport', async () => { - bindAndroidAdbHostStub(); - await withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - for (const resolve of [ - resolveAndroidAdbExecutor, - resolveAndroidAdbProvider, - resolveScopedAndroidAdbBackgroundTransport, - resolveAndroidTextInjector, - resolveAndroidTouchProvider, - ]) { - expect(() => resolve(OTHER)).toThrowError( - expect.objectContaining({ details: { reason: 'managed-device-transport-mismatch' } }), - ); - } - }, - ); -}); - -test('private-port execution contains local transports constructed before entering the scope', async () => { - const calls: Array<{ serial: string; serverPort?: number }> = []; - bindAndroidAdbHostStub({ - execAdb: async (invocation, options) => { - calls.push({ - serial: invokedSerial(invocation), - serverPort: options?.serverPort ?? invokedServerPort(invocation), - }); - return ok(); - }, - spawnAdb: (invocation, options) => { - calls.push({ - serial: invokedSerial(invocation), - serverPort: options?.serverPort ?? invokedServerPort(invocation), - }); - return undefined as never; - }, - }); - const matching = createLocalAndroidAdbProvider(DEVICE); - const foreign = createLocalAndroidAdbProvider(OTHER); - const wrongPort = createDeviceAdbExecutor(DEVICE, { serverPort: 15_038 }); - await withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - await matching.exec(deviceShellArgv('adb', 'shell', ['id'])); - matching.spawn?.(['logcat']); - await expect(foreign.exec(deviceShellArgv('adb', 'shell', ['id']))).rejects.toMatchObject({ - details: { reason: 'managed-device-transport-mismatch' }, - }); - expect(() => foreign.spawn?.(['logcat'])).toThrowError( - expect.objectContaining({ details: { reason: 'managed-device-transport-mismatch' } }), - ); - await expect(wrongPort(deviceShellArgv('adb', 'shell', ['id']))).rejects.toMatchObject({ - details: { reason: 'managed-device-transport-mismatch' }, - }); - }, - ); - expect(calls).toEqual([ - { serial: DEVICE.id, serverPort: 15_037 }, - { serial: DEVICE.id, serverPort: 15_037 }, - ]); -}); - -test('a leased host transport answers for its own server, and refuses one a call names', async () => { - const hostCalls: ReturnType[] = []; - bindAndroidAdbHostStub({ - execAdb: async (invocation, options) => { - hostCalls.push(hostCall(invocation, options)); - return ok(); - }, - }); - - await withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - await runAndroidHostAdb(parseAndroidAdbArgv(['devices']), { serverPort: 15_037 }); - await expect( - runAndroidHostAdb(parseAndroidAdbArgv(['devices']), { serverPort: 9_999 }), - ).rejects.toMatchObject({ - details: { reason: 'managed-device-transport-mismatch' }, - }); - }, - ); - - expect(hostCalls).toEqual([scoped(DEVICE.id, 'devices')]); + expect(hostCalls).toEqual([]); }); -test('a device route answers for the server it was built with, not one a call names', async () => { +test('a device route addresses the server a call names, and refuses an argv naming another', async () => { const calls: ReturnType[] = []; bindAndroidAdbHostStub({ execAdb: async (invocation, options) => { @@ -712,186 +457,30 @@ test('a device route answers for the server it was built with, not one a call na return undefined as never; }, }); + const route = createLocalAndroidAdbProvider(DEVICE); + const serverPort = 15_037; - // With no lease the port the route was built with is the one it addresses, so a per-call option - // is not a second channel that can move the same request onto another adb server. - const route = createLocalAndroidAdbProvider(DEVICE, { serverPort: 15_037 }); - await route.exec(deviceShellArgv('adb', 'shell', ['id']), { serverPort: 9_999 }); - route.spawn?.(['logcat'], { serverPort: 9_999 }); - expect(calls).toEqual([scoped(DEVICE.id, 'shell', 'id'), scoped(DEVICE.id, 'logcat')]); + // The per-call port moves onto the addressing, so the host never sees it as a second channel. + await route.exec(deviceShellArgv('adb', 'shell', ['id']), { serverPort }); + route.spawn?.(['logcat'], { serverPort }); + expect(calls).toEqual([ + onPrivateServer(DEVICE.id, 'shell', 'id'), + onPrivateServer(DEVICE.id, 'logcat'), + ]); - // Under a lease the same request is refused rather than answered on a server the lease lost. + // A port typed into argv that differs from the call's is refused before adb is asked anything. calls.length = 0; - await withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - await expect( - route.exec(deviceShellArgv('adb', 'shell', ['id']), { serverPort: 9_999 }), - ).rejects.toMatchObject({ - details: { reason: 'managed-device-transport-mismatch' }, - }); - expect(() => route.spawn?.(['logcat'], { serverPort: 9_999 })).toThrowError( - expect.objectContaining({ details: { reason: 'managed-device-transport-mismatch' } }), - ); - }, - ); - expect(calls).toEqual([]); - - // A port typed into argv names the same conflict as one passed as an option, and the route has to - // answer for it too: it is the arm that would otherwise overwrite the caller's `-P`. - await expect(route.exec(['-P', '9999', 'getprop'])).rejects.toMatchObject({ + await expect(route.exec(['-P', '9999', 'getprop'], { serverPort })).rejects.toMatchObject({ details: { reason: 'managed-device-transport-mismatch' }, }); - expect(() => route.spawn?.(['-P', '9999', 'logcat'])).toThrowError( + expect(() => route.spawn?.(['-P', '9999', 'logcat'], { serverPort })).toThrowError( expect.objectContaining({ details: { reason: 'managed-device-transport-mismatch' } }), ); expect(calls).toEqual([]); - // The port this route was built with is the one it may answer for, however the caller spells it. - await route.exec(['-P', '15037', 'getprop']); - expect(calls).toEqual([scoped(DEVICE.id, 'getprop')]); -}); - -test('a managed port scope keeps shell -s arguments on the private transport', async () => { - const hostCalls: ReturnType[] = []; - let captured: - | ((cmd: string, args: readonly string[], options: object) => Promise | undefined) - | undefined; - bindAndroidAdbHostStub({ - execAdb: async (invocation, options) => { - hostCalls.push(hostCall(invocation, options)); - return ok(); - }, - withAdbCommandExecutorOverride: async (override, fn) => { - captured = override; - return await fn(); - }, - }); - - await withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - await runAndroidHostAdb( - androidAdbInvocation( - androidAdbHostTarget(), - deviceShellArgv('adb', 'shell', ['echo', '-s', OTHER.id]), - ), - ); - const shellCommand = captured?.('adb', ['shell', 'echo', '-s', OTHER.id], {}); - expect(shellCommand).toBeDefined(); - await shellCommand; - }, - ); - - expect(hostCalls).toEqual([ - scoped(DEVICE.id, 'shell', 'echo', '-s', OTHER.id), - scoped(DEVICE.id, 'shell', 'echo', '-s', OTHER.id), - ]); -}); - -test('a managed port scope restores the default transport after task failure', async () => { - const ports: Array = []; - bindAndroidAdbHostStub({ - execAdb: async (invocation, options) => { - ports.push(options?.serverPort ?? invokedServerPort(invocation)); - return ok(); - }, - }); - - await expect( - withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - await runAndroidHostAdb(parseAndroidAdbArgv(['devices'])); - throw new Error('stop managed request'); - }, - ), - ).rejects.toThrow('stop managed request'); - await runAndroidHostAdb(parseAndroidAdbArgv(['devices'])); - - expect(ports).toEqual([15_037, undefined]); -}); - -test('a managed port scope carries its server through the local background transport', async () => { - const spawnCalls: Array<{ - serial: string; - args: readonly string[]; - serverPort?: number; - }> = []; - bindAndroidAdbHostStub({ - spawnAdb: (invocation, options) => { - spawnCalls.push({ - serial: invokedSerial(invocation), - args: invocation.command, - serverPort: options?.serverPort ?? invokedServerPort(invocation), - }); - return undefined as never; - }, - }); - const deviceProvider = createLocalAndroidAdbProvider(DEVICE, { serverPort: 15_037 }); - - await withAndroidAdbProvider( - deviceProvider, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => { - const transport = resolveScopedAndroidAdbBackgroundTransport(DEVICE); - expect(transport.mode).toBe('transport-composed'); - if (transport.mode === 'transport-composed') { - transport.spawn?.(['logcat', '-v', 'threadtime']); - } - }, - ); - - expect(spawnCalls).toEqual([ - { serial: DEVICE.id, args: ['logcat', '-v', 'threadtime'], serverPort: 15_037 }, - ]); -}); - -test('managed port scopes remain isolated across concurrent requests', async () => { - const hostCalls: Array<{ serial: string; serverPort?: number }> = []; - bindAndroidAdbHostStub({ - execAdb: async (invocation, options) => { - hostCalls.push({ - serial: invokedSerial(invocation), - serverPort: options?.serverPort ?? invokedServerPort(invocation), - }); - await Promise.resolve(); - return ok(); - }, - }); - - await Promise.all([ - withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: DEVICE.id, serverPort: 15_037 }, - async () => - await runAndroidHostAdb( - androidAdbInvocation( - androidAdbSerialTarget(DEVICE.id), - deviceShellArgv('adb', 'shell', ['id']), - ), - ), - ), - withAndroidAdbProvider( - { exec: async () => ok() }, - { serial: OTHER.id, serverPort: 15_038 }, - async () => - await runAndroidHostAdb( - androidAdbInvocation( - androidAdbSerialTarget(OTHER.id), - deviceShellArgv('adb', 'shell', ['id']), - ), - ), - ), - ]); - - expect(hostCalls).toEqual([ - { serial: DEVICE.id, serverPort: 15_037 }, - { serial: OTHER.id, serverPort: 15_038 }, - ]); + // The same port spelled in argv is the same request. + await route.exec(['-P', '15037', 'getprop'], { serverPort }); + expect(calls).toEqual([onPrivateServer(DEVICE.id, 'getprop')]); }); function invokedServerPort(invocation: AndroidAdbInvocation): number | undefined { diff --git a/packages/platform-android/src/adb-provider-scope.ts b/packages/platform-android/src/adb-provider-scope.ts index 892ae9bd46..65ec6c2205 100644 --- a/packages/platform-android/src/adb-provider-scope.ts +++ b/packages/platform-android/src/adb-provider-scope.ts @@ -3,7 +3,6 @@ import type { DeviceInfo } from '@agent-device/kernel/device'; import { assertDeviceShellArgv, deviceShellArgv, - deviceShellExecutableOf, type ShellWord, } from '@agent-device/kernel/device-shell'; import { AppError } from '@agent-device/kernel/errors'; @@ -14,28 +13,19 @@ import { normalizeAndroidAdbInstallOptions, parseAndroidAdbArgv, adoptAndroidAdbSerial, - requireManagedAndroidAdbAddressing, requireManagedAndroidAdbSerial, - requireSameAndroidAdbServer, - requireUnconflictedAndroidAdbSelector, type AndroidAdbExecutor, type AndroidAdbExecutorOptions, type AndroidAdbExecutorResult, type AndroidAdbInvocation, type AndroidAdbProvider, type AndroidAdbProviderScopeOptions, - type AndroidAdbSelector, type AndroidAdbSpawner, type AndroidTextInjector, type AndroidTouchProvider, type ScopedAndroidAdbBackgroundTransport, } from './adb-transport.ts'; -import { - requireAndroidAdbHost, - withAndroidHostAdbTransport, - type AndroidAdbCommandExecutorOverride, - type AndroidAdbHostTransport, -} from './adb-host.ts'; +import { requireAndroidAdbHost, type AndroidAdbCommandExecutorOverride } from './adb-host.ts'; import { attachAdbFailureHint, classifyAndroidAdbFailure, @@ -51,26 +41,22 @@ import { normalizeAndroidAdbProvider } from './adb-provider-normalization.ts'; type AndroidAdbProviderScope = { provider: AndroidAdbProvider; serial: string; - serverPort?: number; }; const androidAdbProviderScope = new AsyncLocalStorage(); -export function createDeviceAdbExecutor( - device: DeviceInfo, - options: Readonly<{ serverPort?: number }> = {}, -): AndroidAdbExecutor { - return guardDeviceShell(createSerialAdbExecutor(device.id, options.serverPort)); +export function createDeviceAdbExecutor(device: DeviceInfo): AndroidAdbExecutor { + return guardDeviceShell(createSerialAdbExecutor(device.id)); } -function createSerialAdbExecutor(serial: string, serverPort?: number): AndroidAdbExecutor { +function createSerialAdbExecutor(serial: string): AndroidAdbExecutor { return withAdbFailureHints(async (args, options) => { const host = requireAndroidAdbHost(); const exec = async ( argv: readonly string[], execOptions: AndroidAdbExecutorOptions | undefined, ): Promise => { - const request = deviceAdbRouteRequest(serial, serverPort, argv, execOptions); + const request = deviceAdbRouteRequest(serial, argv, execOptions); // A device-scoped executor is the terminal local route: an installed provider must not // capture it and route the call back into itself. return await host.withoutAdbCommandExecutorOverride( @@ -78,7 +64,7 @@ function createSerialAdbExecutor(serial: string, serverPort?: number): AndroidAd ); }; return await retryOnceAfterDeviceOffline( - `${serverPort ?? options?.serverPort ?? ''}/${serial}`, + `${options?.serverPort ?? ''}/${serial}`, options, async (attemptOptions) => await exec(args, attemptOptions), async (timeoutMs) => { @@ -223,9 +209,9 @@ function deviceAnswered(error: unknown): boolean { return typeof exitCode === 'number' && exitCode >= 0; } -function createSerialAdbSpawner(serial: string, serverPort?: number): AndroidAdbSpawner { +function createSerialAdbSpawner(serial: string): AndroidAdbSpawner { return guardDeviceShellSpawn((args, options) => { - const request = deviceAdbRouteRequest(serial, serverPort, args, options); + const request = deviceAdbRouteRequest(serial, args, options); return requireAndroidAdbHost().spawnAdb(request.invocation, request.options); }); } @@ -233,27 +219,18 @@ function createSerialAdbSpawner(serial: string, serverPort?: number): AndroidAdb /** One device-scoped request: addressing decided once, and the server's option channel removed. */ function deviceAdbRouteRequest( serial: string, - installed: number | undefined, args: readonly string[], options: Options | undefined, ): { invocation: AndroidAdbInvocation; options: Omit } { - const { serverPort: requested, ...rest } = options ?? ({} as Options); - return { - invocation: androidDeviceAdbInvocation( - serial, - args, - deviceServerPort(serial, installed, requested), - ), - options: rest, - }; + const { serverPort, ...rest } = options ?? ({} as Options); + return { invocation: androidDeviceAdbInvocation(serial, args, serverPort), options: rest }; } /** - * Addresses `args` for `serial`, carrying the chosen adb server on the target and nowhere else. A - * managed transport re-reads the argv through the shared managed rules, so the payload it spawns - * is the parsed command by reference. A caller-chosen server rewrites addressing too, and an - * ambient transport keeps the caller's argv as the emitted form, because ambient adb lets a later - * `-s` win. + * Addresses `args` for `serial`, carrying the per-call adb server on the target and nowhere else. + * A private server re-reads the argv through the shared private-server rules, so the payload it + * spawns is the parsed command by reference. An ambient transport keeps the caller's argv as the + * emitted form, because ambient adb lets a later `-s` win. */ function androidDeviceAdbInvocation( serial: string, @@ -265,8 +242,6 @@ function androidDeviceAdbInvocation( if (port === undefined) { return androidAdbInvocation(selector, parsed.command, ['-s', serial, ...args]); } - // A private adb server makes this a managed transport, whoever named the port: the rules over - // what such a transport may be asked for are the same ones the lease is held to. const managed = applyManagedAndroidAdbServer(parsed, { port }); return androidAdbInvocation( requireManagedAndroidAdbSerial(managed.target, serial), @@ -274,37 +249,11 @@ function androidDeviceAdbInvocation( ); } -/** - * The adb server a device-scoped route runs against, from the one channel that may name it. - * - * Under a lease that is the lease's private server and nothing else: a route constructed for - * another port, or a call asking for one, is refused rather than followed. Without a lease the - * owner's port wins over a per-call request, which is what the route was built to answer for. - */ -function deviceServerPort( - serial: string, - installed: number | undefined, - requested: number | undefined, -): number | undefined { - const scope = androidAdbProviderScope.getStore(); - if (scope) requireScopedSerial(scope, { kind: 'serial', serial }); - if (scope?.serverPort !== undefined) { - // A lease's server is the only one this route may answer for; naming another, at construction - // or per call, is refused rather than followed. - requireSameAndroidAdbServer(installed, scope.serverPort); - return requireSameAndroidAdbServer(requested, scope.serverPort); - } - return installed ?? requested; -} - -export function createLocalAndroidAdbProvider( - device: DeviceInfo, - options: Readonly<{ serverPort?: number }> = {}, -): AndroidAdbProvider { - const exec = createDeviceAdbExecutor(device, options); +export function createLocalAndroidAdbProvider(device: DeviceInfo): AndroidAdbProvider { + const exec = createDeviceAdbExecutor(device); return { exec, - spawn: createSerialAdbSpawner(device.id, options.serverPort), + spawn: createSerialAdbSpawner(device.id), reverse: createExecAndroidPortReverseProvider(exec), pull: async (remotePath, localPath, options) => await exec(['pull', remotePath, localPath], options), @@ -319,7 +268,7 @@ export function resolveAndroidAdbExecutor( device: DeviceInfo, executor?: AndroidAdbExecutor, ): AndroidAdbExecutor { - const scoped = scopeForDevice(device); + const scoped = androidAdbProviderScope.getStore(); if (executor) return guardDeviceShell(executor); if (scoped?.serial === device.id) return guardDeviceShell(scoped.provider.exec); return createDeviceAdbExecutor(device); @@ -329,7 +278,7 @@ export function resolveAndroidAdbProvider( device: DeviceInfo, provider?: AndroidAdbProvider | AndroidAdbExecutor, ): AndroidAdbProvider { - const scoped = scopeForDevice(device); + const scoped = androidAdbProviderScope.getStore(); if (provider) return guardProviderDeviceShell(normalizeAndroidAdbProvider(provider)); return guardProviderDeviceShell( scoped?.serial === device.id @@ -346,7 +295,7 @@ export function resolveAndroidAdbProvider( export function resolveScopedAndroidAdbBackgroundTransport( device: DeviceInfo, ): ScopedAndroidAdbBackgroundTransport { - const scoped = scopeForDevice(device); + const scoped = androidAdbProviderScope.getStore(); if (scoped?.serial !== device.id) return { mode: 'local' }; return { mode: 'transport-composed', @@ -355,12 +304,12 @@ export function resolveScopedAndroidAdbBackgroundTransport( } export function resolveAndroidTextInjector(device: DeviceInfo): AndroidTextInjector | undefined { - const scoped = scopeForDevice(device); + const scoped = androidAdbProviderScope.getStore(); return scoped?.serial === device.id ? scoped.provider.text : undefined; } export function resolveAndroidTouchProvider(device: DeviceInfo): AndroidTouchProvider | undefined { - const scoped = scopeForDevice(device); + const scoped = androidAdbProviderScope.getStore(); return scoped?.serial === device.id && scoped.provider.touch ? scoped.provider : undefined; } @@ -386,21 +335,11 @@ export async function withAndroidAdbProvider( // command-executor override and direct resolveAndroidAdb* lookups — gets // classified failure hints on exec and the semantic provider methods alike. const enriched = normalizeAndroidAdbProvider(provider); - const scope = { - provider: enriched, - serial: options.serial, - ...(options.serverPort === undefined ? {} : { serverPort: options.serverPort }), - }; + const scope = { provider: enriched, serial: options.serial }; const override = createAndroidCommandExecutorOverride(scope); - const run = async () => - await androidAdbProviderScope.run( - scope, - async () => await requireAndroidAdbHost().withAdbCommandExecutorOverride(override, fn), - ); - if (options.serverPort === undefined) return await run(); - return await withAndroidHostAdbTransport( - createScopedHostTransport(scope, options.serverPort), - run, + return await androidAdbProviderScope.run( + scope, + async () => await requireAndroidAdbHost().withAdbCommandExecutorOverride(override, fn), ); } @@ -409,87 +348,18 @@ function createAndroidCommandExecutorOverride( ): AndroidAdbCommandExecutorOverride { const exec = guardDeviceShell(scope.provider.exec); return (cmd, args, options) => { - if (!isAdbCommand(cmd)) return undefined; - if (scope.serverPort === undefined && cmd !== 'adb') return undefined; - const invocation = parseAndroidAdbArgv(args); - requireScopedSerial(scope, invocation.target.selector); - if (invocation.target.selector.kind === 'serial') { - if (invocation.target.selector.serial !== scope.serial) return undefined; - // Under a private adb server the provider cannot restate a caller's host globals, and a - // `-P` naming another server would reach adb through a provider that never sees addressing, - // so both are refused here. Without a lease the caller's own adb invocation is what runs, as - // it always has. - if (scope.serverPort !== undefined) { - requireManagedAndroidAdbAddressing(invocation.target, scope.serverPort); - } - // The provider contract is argv-shaped, so it receives the caller's request with this - // scope's own `-s` pair removed — readiness tokens and transport globals left where the - // caller put them, and never a rebuild with the scope's serial stitched back in. - const payload = androidAdbPayloadWithoutSerial(args, scope.serial); - if (payload === undefined) return undefined; - return requireAndroidAdbHost().withoutAdbCommandExecutorOverride( - async () => await exec(payload, options), - ); - } - const port = scope.serverPort; - if (port === undefined) return undefined; - return execOnScopedTransport(scope, port, invocation, options); - }; -} - -function createScopedHostTransport( - scope: AndroidAdbProviderScope, - port: number, -): AndroidAdbHostTransport { - return async (invocation, options) => { - requireScopedSerial(scope, invocation.target.selector); - return execOnScopedTransport(scope, port, invocation, options); + if (cmd !== 'adb') return undefined; + // The provider contract is argv-shaped, so it receives the caller's request with this + // scope's own `-s` pair removed — readiness tokens and transport globals left where the + // caller put them, and never a rebuild with the scope's serial stitched back in. + const payload = androidAdbPayloadWithoutSerial(args, scope.serial); + if (payload === undefined) return undefined; + return requireAndroidAdbHost().withoutAdbCommandExecutorOverride( + async () => await exec(payload, options), + ); }; } -/** - * Answers one request on the lease's own transport: the scope's serial, and its server carried in - * the addressing rather than in the options, which is what leaves a caller no second way to name - * another adb server. A call that names one anyway is refused, not quietly restated. - */ -async function execOnScopedTransport( - scope: AndroidAdbProviderScope, - port: number, - invocation: AndroidAdbInvocation, - options: AndroidAdbExecutorOptions | undefined, -): Promise { - requireSameAndroidAdbServer(port, options?.serverPort); - const { serverPort: _omitted, ...rest } = options ?? {}; - const serialTarget = requireManagedAndroidAdbSerial(invocation.target, scope.serial); - const scoped = applyManagedAndroidAdbServer( - androidAdbInvocation(serialTarget, invocation.command), - { port }, - ); - const host = requireAndroidAdbHost(); - return await host.withoutAdbCommandExecutorOverride( - async () => await host.execAdb(scoped, { ...rest, allowFailure: true }), - ); -} - -function scopeForDevice(device: DeviceInfo): AndroidAdbProviderScope | undefined { - const scoped = androidAdbProviderScope.getStore(); - if (scoped) requireScopedSerial(scoped, { kind: 'serial', serial: device.id }); - return scoped; -} - -/** Under a private server, a request that names another device is not this scope's to answer. */ -function requireScopedSerial( - scope: AndroidAdbProviderScope | undefined, - selector: AndroidAdbSelector, -): void { - if (scope?.serverPort === undefined) return; - requireUnconflictedAndroidAdbSelector(selector, scope.serial); -} - -function isAdbCommand(command: string): boolean { - return deviceShellExecutableOf(command) === 'adb'; -} - /** Runs `adb shell ` through an executor; every word is quoted for the device shell. */ export async function runAdbShell( adb: AndroidAdbExecutor, diff --git a/packages/platform-android/src/adb-transport.test.ts b/packages/platform-android/src/adb-transport.test.ts index 58fa3c34ab..633881ab63 100644 --- a/packages/platform-android/src/adb-transport.test.ts +++ b/packages/platform-android/src/adb-transport.test.ts @@ -9,6 +9,7 @@ import { androidAdbPayloadWithoutSerial, androidAdbSerialTarget, type AndroidAdbExecutorOptions, + type AndroidAdbTarget, androidManagedAdbEnvironment, lowerAndroidAdbInvocation, requireSameAndroidAdbServer, @@ -21,6 +22,10 @@ import { serializeAndroidAdbInvocation, } from './adb-transport.ts'; +function privateServerTarget(serial: string, port: number): AndroidAdbTarget { + return { ...androidAdbSerialTarget(serial), server: { kind: 'port', port } }; +} + describe('adb argv grammar', () => { it('carries the arity adb documents for every global option', () => { expect(ADB_GLOBAL_OPTIONS).toEqual({ @@ -204,7 +209,7 @@ describe('lowerAndroidAdbInvocation', () => { it('carries the owned server into argv and environment, and drops the option channel', () => { const options: AndroidAdbExecutorOptions = { serverPort: 15_037, timeoutMs: 5_000 }; const lowered = lowerAndroidAdbInvocation( - androidAdbInvocation(androidAdbSerialTarget('emulator-5554', 15_037), ['shell', 'id']), + androidAdbInvocation(privateServerTarget('emulator-5554', 15_037), ['shell', 'id']), options, { ANDROID_ADB_SERVER_PORT: '5037', ADB_SERVER_SOCKET: 'tcp:elsewhere:5037' }, ); @@ -235,12 +240,12 @@ describe('lowerAndroidAdbInvocation', () => { describe('requireAndroidAdbServerPort', () => { it('answers from the port the addressing owns', () => { - const invocation = androidAdbInvocation(androidAdbSerialTarget('A', 15_037), ['shell', 'id']); + const invocation = androidAdbInvocation(privateServerTarget('A', 15_037), ['shell', 'id']); expect(requireAndroidAdbServerPort(invocation, { serverPort: 15_037 })).toBe(15_037); }); it('refuses a per-call port that would move an owned server', () => { - const invocation = androidAdbInvocation(androidAdbSerialTarget('A', 15_037), ['shell', 'id']); + const invocation = androidAdbInvocation(privateServerTarget('A', 15_037), ['shell', 'id']); expect(() => requireAndroidAdbServerPort(invocation, { serverPort: 9_999 })).toThrowError( expect.objectContaining({ code: 'COMMAND_FAILED', @@ -400,7 +405,7 @@ describe('requireManagedAndroidAdbCommand', () => { describe('androidManagedAdbEnvironment', () => { it('points a private server at loopback and clears a socket', () => { const environment = androidManagedAdbEnvironment( - androidAdbSerialTarget('A', 5039), + privateServerTarget('A', 5039), { ANDROID_ADB_SERVER_PORT: '5037', ADB_SERVER_SOCKET: 'tcp:5037' }, { ...process.env, PATH: '/bin' }, ); diff --git a/packages/platform-android/src/adb-transport.ts b/packages/platform-android/src/adb-transport.ts index 744ec9e618..753ba53d44 100644 --- a/packages/platform-android/src/adb-transport.ts +++ b/packages/platform-android/src/adb-transport.ts @@ -194,7 +194,6 @@ export type AndroidAdbProvider = AndroidAdbProviderBase & AndroidTouchCapabiliti export type AndroidAdbProviderScopeOptions = { serial: string; - serverPort?: number; }; export type ScopedAndroidAdbBackgroundTransport = @@ -313,14 +312,8 @@ export type AndroidAdbInvocation = Readonly<{ rawArgv?: readonly string[]; }>; -export function androidAdbSerialTarget( - serial: string, - serverPort?: number, -): Readonly { - return { - selector: { kind: 'serial', serial }, - server: serverPort === undefined ? { kind: 'ambient' } : { kind: 'port', port: serverPort }, - }; +export function androidAdbSerialTarget(serial: string): Readonly { + return { selector: { kind: 'serial', serial }, server: { kind: 'ambient' } }; } /** Addressing for a server-level command: it selects no device and names no private adb server. */ @@ -485,7 +478,7 @@ function androidAdbOwnedServerPort(invocation: AndroidAdbInvocation): number | u /** * Reconciles the two channels an adb server port arrives on: the addressing this layer owns, and * the per-call option an SDK caller can still pass. An owned port is not overridable — a caller - * cannot move a managed lease onto another adb server by naming one. + * cannot move an addressed invocation onto another adb server by naming one. */ export function requireAndroidAdbServerPort( invocation: AndroidAdbInvocation, @@ -539,7 +532,7 @@ export type AndroidManagedAdbServer = Readonly<{ port: number }>; /** * Adopts `invocation` for one managed adb server. The command travels by reference, so a payload * authored upstream is the same array that reaches the spawn boundary. Addressing is rewritten - * wherever the lease adds something the caller left unsaid, and refused wherever the caller named + * wherever the private server adds something the caller left unsaid, and refused wherever the caller named * a server of their own — see {@link requireManagedAndroidAdbAddressing}. */ export function applyManagedAndroidAdbServer( @@ -569,10 +562,7 @@ export function requireManagedAndroidAdbSerial( } /** A selection naming another device is a conflict; a request that names none is not. */ -export function requireUnconflictedAndroidAdbSelector( - selector: AndroidAdbSelector, - serial: string, -): void { +function requireUnconflictedAndroidAdbSelector(selector: AndroidAdbSelector, serial: string): void { if (selector.kind === 'serial' && selector.serial !== serial) throw transportMismatch('device'); } @@ -581,14 +571,11 @@ export function requireUnconflictedAndroidAdbSelector( * server but the one it holds. * * A `-P` naming a different server is refused here, before anything is dispatched, rather than - * rewritten onto the lease's server. A caller who asked for 5037 and got 15038 would otherwise + * rewritten onto the private server. A caller who asked for 5037 and got 15038 would otherwise * read a successful exit as evidence about 5037, which is the one answer a private server must not * give. A `-P` naming this server, or none at all, is the ordinary case. */ -export function requireManagedAndroidAdbAddressing( - target: AndroidAdbTarget, - managedPort: number, -): void { +function requireManagedAndroidAdbAddressing(target: AndroidAdbTarget, managedPort: number): void { if (target.hostGlobals) throw transportMismatch('target'); requireSameAndroidAdbServer( managedPort, diff --git a/src/daemon/device/device-claim-rule.ts b/src/daemon/device/device-claim-rule.ts index bbfae8977e..0dd3f189e7 100644 --- a/src/daemon/device/device-claim-rule.ts +++ b/src/daemon/device/device-claim-rule.ts @@ -5,8 +5,9 @@ import type { RuntimeOwnerRef } from '@agent-device/contracts/platform-runtime'; * The device-claim rule: what a runtime owner's kind selects at a claim gate. * `ordinary` acquires a host-local device claim, and `none` leaves the claim * store alone because the provider lease already excludes peers. The rule - * follows the admitted runtime owner and lease, never request metadata: a `macos-app` lease holds one app, not the host Mac - * (ADR 0007), so the lease is all the exclusion it needs. + * follows the admitted runtime owner and lease, never request metadata: a + * `macos-app` lease holds one app, not the host Mac (ADR 0007), so the lease + * is all the exclusion it needs. */ export type DeviceClaimRule = 'ordinary' | 'none'; diff --git a/src/platform-runtime-android-adb-host.test.ts b/src/platform-runtime-android-adb-host.test.ts index fa69c2ea64..cff89dad52 100644 --- a/src/platform-runtime-android-adb-host.test.ts +++ b/src/platform-runtime-android-adb-host.test.ts @@ -129,10 +129,13 @@ test.skipIf(process.platform === 'win32')( 'process.stdout.write(JSON.stringify(call));', ].join('\n'), async () => { - const provider = createLocalAndroidAdbProvider(ANDROID_EMULATOR, { - serverPort: 15_037, - }); - const adb = provider.exec; + const local = createLocalAndroidAdbProvider(ANDROID_EMULATOR); + const serverPort = 15_037; + const adb: typeof local.exec = async (args, options) => + await local.exec(args, { ...options, serverPort }); + const provider = { + spawn: (args: readonly string[]) => local.spawn?.(args, { serverPort }), + }; const serial = JSON.parse( (await adb(deviceShellArgv('adb', 'shell', ['id']))).stdout, ) as { From 0f2361a01af4c6baa4adbeeccb374aaa0b9d5a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 10 Oct 2026 19:10:38 +0200 Subject: [PATCH 2/3] docs(adr): say the SDK serverPort works only through an executor that honors it Co-Authored-By: Claude Opus 5.5 --- docs/adr/0021-host-simlock-managed-device-allocation.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0021-host-simlock-managed-device-allocation.md b/docs/adr/0021-host-simlock-managed-device-allocation.md index 6f4e2be2d0..6daa2c3dec 100644 --- a/docs/adr/0021-host-simlock-managed-device-allocation.md +++ b/docs/adr/0021-host-simlock-managed-device-allocation.md @@ -62,8 +62,10 @@ sandbox" model applies: agent-device does not verify that a device is leased. - Remote access to a leased device uses Simlock's own gateway or HTTP API, or agent-device's existing `proxy` and remote daemon; neither depends on the other. - The CLI and daemon take no per-request Android adb server port: one daemon talks to one adb - server, so agents holding leases from different Simlock daemons use separate state dirs. Node SDK - callers can instead pass `serverPort` in `AndroidAdbExecutorOptions` (`agent-device/android-adb`). + server, so agents holding leases from different Simlock daemons use separate state dirs. The Node + SDK's `agent-device/android-adb` functions run the caller's own adb executor, so the caller points + that executor at Simlock's server; the per-call `serverPort` in `AndroidAdbExecutorOptions` takes + effect only when that executor honors it. ## 4. Withdrawn design From e37d01ca745398cf99da7f1eb6b9d80f0ef40268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 10 Oct 2026 19:20:23 +0200 Subject: [PATCH 3/3] chore(gates): drop scripts/ export exemptions the production gate no longer reads The production-exports config hides every scripts/ finding through ignoreFindings, so the per-symbol entries for resolveVitestMaxWorkers, readTestScope, ownedTestFiles and the help-conformance *_SAMPLE constants were redundant. The base check:fallow run does not need them either. Co-Authored-By: Claude Opus 5.5 --- .fallowrc.json | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/.fallowrc.json b/.fallowrc.json index a472347b5d..575941efea 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -337,30 +337,15 @@ "GatedKeysAreResolverKeys" ] }, - { - // resolveVitestMaxWorkers is consumed by vitest.config.ts — a tool config outside --production analysis. - "file": "scripts/lib/vitest-concurrency.ts", - "exports": ["resolveVitestMaxWorkers"] - }, - { - // Mutation-lane seam: readTestScope is consumed by vitest.mutation.config.ts, a tool config outside --production analysis. - "file": "scripts/mutation/test-scope.ts", - "exports": ["readTestScope"] - }, - { - // Mutation-lane completeness gate: ownedTestFiles enumerates the whole derived ownership map, which only the assertion in ownership.test.ts consumes — a production consumer would defeat the point. - "file": "scripts/mutation/ownership.ts", - "exports": ["ownedTestFiles"] - }, { // ADR 0010 shared-hint pin: the facade re-export's only consumer is src/__tests__/is-argument-surface-parity.test.ts, which asserts the daemon and CLI `is` surfaces raise the SAME hint text. The constant is live inside the package (predicates.ts raises it); a production consumer of the re-export would defeat the point, since it exists so neither surface copies the string. "file": "packages/selectors/src/index.ts", "exports": ["IS_PREDICATE_USAGE_HINT"] }, { - // Help-benchmark conformance seams: helpTopicIds feeds the topic-coverage gate, and the *_SAMPLE constants feed the bench sample producers (scripts/__tests__/help-conformance-sample-producers.ts); every consumer is a test-tree file outside --production analysis. - "file": "{src/cli/parser/cli-help.ts,scripts/help-conformance-sample-outputs.mjs}", - "exports": ["helpTopicIds", "PRIVATE_AX_RECOVERY_SAMPLE", "DEVICE_CLAIM_IN_USE_SAMPLE"] + // Help-benchmark conformance seam: helpTopicIds feeds the topic-coverage gate, whose consumer is a test-tree file outside --production analysis. + "file": "src/cli/parser/cli-help.ts", + "exports": ["helpTopicIds"] }, { // #1458: getSessionCommandKind's former production consumer (the session.ts if-chain) was replaced by SESSION_COMMAND_HANDLER_IMPLS, which encodes command-kind groupings directly via shared handler references instead of a kind lookup. The accessor and its SessionCommandKind classification remain covered by daemon-command-registry.test.ts, a test-tree file outside --production analysis.