Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/daemon/__tests__/request-handler-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ async function runCatalogCommandThroughHandlerChain(
sessionStore,
leaseRegistry,
invoke: async () => ({ ok: true, data: {} }),
androidAdbExecutor: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
providerScope: {
androidAdbExecutor: async () => ({ stdout: '', stderr: '', exitCode: 0 }),
},
bindDevice: unavailableBindDevice,
bindExactDevice: unavailableBindExactDevice,
reconcileOrphanedDeviceClaim: async () => ({
Expand Down
96 changes: 96 additions & 0 deletions src/daemon/__tests__/request-handler-chain-provider-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Proves the exact androidAdbExecutor reference reaches the session handler
// through the neutral providerScope, and that an empty scope forwards none.
import assert from 'node:assert/strict';
import { test, vi } from 'vitest';

const handleSessionCommandsMock = vi.fn(async (_params: unknown) => ({ ok: true, data: {} }));
vi.mock('../handlers/session.ts', () => ({
handleSessionCommands: handleSessionCommandsMock,
}));

import { INTERNAL_COMMANDS } from '../../command-catalog.ts';
import { LeaseRegistry } from '../lease-registry.ts';
import { runRequestHandlerChain } from '../request-handler-chain.ts';
import { makeIosSession } from '../../__tests__/test-utils/index.ts';
import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts';
import {
unavailableBindDevice,
unavailableBindExactDevice,
} from './test-device-runtime-gateway.ts';
import { createScreenRecordingAdmissionLedger } from '../screen-recording-admission-ledger.ts';
import type { DaemonRequest } from '../types.ts';
import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts';

function makeRequest(command: string, sessionName: string): DaemonRequest {
return {
command,
token: 'test-token',
session: sessionName,
positionals: [],
flags: {},
meta: { requestId: `req-${command}` },
};
}

function baseChainParams(sessionName: string) {
const sessionStore = makeSessionStore('agent-device-provider-scope-');
sessionStore.set(sessionName, makeIosSession(sessionName));
return {
req: makeRequest(INTERNAL_COMMANDS.runtime, sessionName),
sessionName,
logPath: '/tmp/agent-device-provider-scope.log',
sessionStore,
leaseRegistry: new LeaseRegistry(),
invoke: async () => ({ ok: true, data: {} }) as const,
bindDevice: unavailableBindDevice,
bindExactDevice: unavailableBindExactDevice,
reconcileOrphanedDeviceClaim: async () => ({
status: 'retained' as const,
reason: 'test-no-recovery',
}),
screenRecordingAdmissionLedger: createScreenRecordingAdmissionLedger(),
requestScope: {
signal: new AbortController().signal,
diagnostics: { emit: () => {} },
progress: { report: () => {} },
},
retainDeviceExecutionLock: async () => {},
throwIfCanceled: () => {},
contextFromFlags: () => ({ logPath: '/tmp/agent-device-provider-scope.log' }),
};
}

test('the android adb executor from the generic provider scope reaches the session handler by the exact same reference', async () => {
handleSessionCommandsMock.mockClear();
const androidAdbExecutor: AndroidAdbExecutor = async () => ({
stdout: '',
stderr: '',
exitCode: 0,
});

await runRequestHandlerChain({
...baseChainParams('provider-scope-test'),
providerScope: { androidAdbExecutor },
});

assert.equal(handleSessionCommandsMock.mock.calls.length, 1);
const forwardedParams = handleSessionCommandsMock.mock.calls[0]?.[0] as {
androidAdbExecutor?: AndroidAdbExecutor;
};
assert.equal(forwardedParams.androidAdbExecutor, androidAdbExecutor);
});

test('an empty provider scope forwards no android adb executor to the session handler', async () => {
handleSessionCommandsMock.mockClear();

await runRequestHandlerChain({
...baseChainParams('provider-scope-empty'),
providerScope: {},
});

assert.equal(handleSessionCommandsMock.mock.calls.length, 1);
const forwardedParams = handleSessionCommandsMock.mock.calls[0]?.[0] as {
androidAdbExecutor?: AndroidAdbExecutor;
};
assert.equal(forwardedParams.androidAdbExecutor, undefined);
});
1 change: 1 addition & 0 deletions src/daemon/__tests__/request-handler-chain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function makeChainParams(req: DaemonRequest) {
sessionStore,
leaseRegistry: new LeaseRegistry(),
invoke: async (): Promise<DaemonResponse> => ({ ok: true, data: {} }),
providerScope: {},
bindDevice: unavailableBindDevice,
bindExactDevice: unavailableBindExactDevice,
reconcileOrphanedDeviceClaim: async () => ({
Expand Down
12 changes: 9 additions & 3 deletions src/daemon/request-handler-chain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { CommandFlags } from '@agent-device/contracts/command';
import type { CloudArtifactProvider } from '@agent-device/contracts/observability';
import type { AndroidAdbExecutor } from '../platforms/android/adb-executor.ts';
import { AppError } from '@agent-device/kernel/errors';
import { getDaemonCommandRoute } from './daemon-command-registry.ts';
import * as genericRequestHandlerModule from './request-generic-dispatch.ts';
Expand All @@ -14,6 +13,7 @@ import type { DeviceClaimReconciler } from './device-claims.ts';
import type { AppLogAdmissionLedger } from './app-log-admission-ledger.ts';
import type { ScreenRecordingAdmissionLedger } from './screen-recording-admission-ledger.ts';
import type { PlatformRequestScope } from '@agent-device/contracts/platform';
import type { RequestPlatformProviderScope } from './request-platform-providers.ts';

type RequestHandlerChainParams = {
req: DaemonRequest;
Expand All @@ -27,7 +27,13 @@ type RequestHandlerChainParams = {
cloudArtifactProvider?: CloudArtifactProvider;
invoke: DaemonInvokeFn;
invokeReplayAction?: DaemonInvokeFn;
androidAdbExecutor?: AndroidAdbExecutor;
/**
* Per-request platform-provider injections resolved by the generic
* `withRequestPlatformProviderScope` mechanism. Route handlers pick their own
* platform-specific field back out of this neutral scope instead of the chain
* carrying one named slot per platform (e.g. `androidAdbExecutor`).
*/
providerScope: RequestPlatformProviderScope;
bindDevice: BindDeviceRuntime;
bindExactDevice: BindExactDeviceRuntime;
reconcileOrphanedDeviceClaim: DeviceClaimReconciler;
Expand Down Expand Up @@ -129,7 +135,7 @@ async function runSessionHandler(
leaseLifecycleProvider: params.leaseLifecycleProvider,
invoke: params.invoke,
invokeReplayAction: params.invokeReplayAction,
androidAdbExecutor: params.androidAdbExecutor,
androidAdbExecutor: params.providerScope.androidAdbExecutor,
bindDevice: params.bindDevice,
bindExactDevice: params.bindExactDevice,
reconcileOrphanedDeviceClaim: params.reconcileOrphanedDeviceClaim,
Expand Down
2 changes: 1 addition & 1 deletion src/daemon/request-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export function createRequestHandler(deps: RequestRouterDeps): DaemonInvokeFn {
invokeReplayAction: allowReplayActions
? createReplayScopedActionInvoker(lockedScope, providerScope)
: undefined,
androidAdbExecutor: providerScope.androidAdbExecutor,
providerScope,
bindDevice: lockedScope.bindDevice,
bindExactDevice: lockedScope.bindExactDevice,
reconcileOrphanedDeviceClaim: createDeviceClaimReconciler({
Expand Down
Loading