From bbc51ba9000437da05e50bdff64a71fca775afac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 15 Oct 2026 12:55:43 +0200 Subject: [PATCH 1/2] refactor(daemon): require composed router capabilities --- .../support/human-control-router-fixture.ts | 4 ++ .../support/test-device-runtime-gateway.ts | 14 +++++- .../__tests__/request-router-open.test.ts | 49 +------------------ .../daemon/src/server/http/daemon-runtime.ts | 2 +- packages/daemon/src/server/request-router.ts | 41 ++-------------- 5 files changed, 22 insertions(+), 88 deletions(-) diff --git a/packages/daemon/src/__tests__/support/human-control-router-fixture.ts b/packages/daemon/src/__tests__/support/human-control-router-fixture.ts index 54ed1c1295..3c56a98a2d 100644 --- a/packages/daemon/src/__tests__/support/human-control-router-fixture.ts +++ b/packages/daemon/src/__tests__/support/human-control-router-fixture.ts @@ -13,6 +13,8 @@ import { tenantScopedSessionName } from '../../core/session-tenant-scope.ts'; import { lifecycleDeviceRuntimeGateway } from './test-device-runtime-gateway.ts'; import { HUMAN_CONTROL_LEASE_REQUEST, humanControlRequest } from './human-control-fixtures.ts'; import { fakeCreateCommandSurface } from './command-surface-fake.ts'; +import { clearAndroidObservationFixture } from './android-observation-fixture.ts'; +import { platformResourceCleanupFixture } from './platform-resource-cleanup-fixture.ts'; export function createHumanControlHarness() { const registry = new LeaseRegistry(); @@ -36,6 +38,8 @@ export function createHumanControlHarness() { providerCredentials: providerCredentialsFixture({}, '/tmp'), deviceInventoryGateways: createTestDeviceInventoryGateways(), deviceRuntimeGateway: lifecycleDeviceRuntimeGateway, + androidObservation: clearAndroidObservationFixture, + platformResourceCleanup: platformResourceCleanupFixture(), trackDownloadableArtifact: () => 'artifact-1', }); return { registry, lease, sessionStore, sessionName, handleRequest }; diff --git a/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts b/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts index 13ea6b5f15..2cbb83f8c4 100644 --- a/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts +++ b/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts @@ -210,12 +210,22 @@ async function gestureBinding(device: DeviceInfo) { export function createRequestHandler( deps: Omit< RequestRouterDeps, - 'deviceRuntimeGateway' | 'providerCredentials' | 'platformServices' | 'claimRecoveryGateway' + | 'deviceRuntimeGateway' + | 'providerCredentials' + | 'platformServices' + | 'claimRecoveryGateway' + | 'androidObservation' + | 'platformResourceCleanup' > & Partial< Pick< RequestRouterDeps, - 'deviceRuntimeGateway' | 'providerCredentials' | 'platformServices' | 'claimRecoveryGateway' + | 'deviceRuntimeGateway' + | 'providerCredentials' + | 'platformServices' + | 'claimRecoveryGateway' + | 'androidObservation' + | 'platformResourceCleanup' > >, ) { diff --git a/packages/daemon/src/server/__tests__/request-router-open.test.ts b/packages/daemon/src/server/__tests__/request-router-open.test.ts index 93b728c47f..5f7a2e6aff 100644 --- a/packages/daemon/src/server/__tests__/request-router-open.test.ts +++ b/packages/daemon/src/server/__tests__/request-router-open.test.ts @@ -1,9 +1,5 @@ import { createTestDeviceInventoryGateways } from '@agent-device/device-selection/inventory-gateways-fixtures'; -import { - daemonPlatformServicesFixture, - daemonClaimRecoveryGatewayFixture, - observeBootTimeMsFixture, -} from '../../__tests__/support/platform-services-fixture.ts'; +import { observeBootTimeMsFixture } from '../../__tests__/support/platform-services-fixture.ts'; import { legacyDispatchCapture } from '../../__tests__/support/legacy-snapshot-capture-fixture.ts'; import { test, expect, vi, beforeEach } from 'vitest'; import fs from 'node:fs'; @@ -67,8 +63,6 @@ import { createRequestHandler, lifecycleDeviceRuntimeGateway, } from '../../__tests__/support/test-device-runtime-gateway.ts'; -import { createRequestHandler as createProductionRequestHandler } from '../request-router.ts'; -import { providerCredentialsFixture } from '../../__tests__/support/provider-credentials-fixture.ts'; import { resolveRequestExecutionLockPlan } from '../request-binding.ts'; import { LeaseRegistry } from '../../resources/lease-registry.ts'; import { ensureDeviceReady } from '../../core/device/device-ready.ts'; @@ -454,47 +448,6 @@ test.each([ ).toBe(lease.leaseId); }); -test('close fails synchronously when root composition omits platform resource cleanup', async () => { - const sessionStore = makeSessionStore('agent-device-router-open-'); - sessionStore.publish('default', { - name: 'default', - device: makeIosDevice('SIM-CLOSE-MISSING-CLEANUP'), - createdAt: Date.now(), - actions: [], - }); - const handler = createProductionRequestHandler({ - createCommandSurface: fakeCreateCommandSurface, - logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), - platformServices: daemonPlatformServicesFixture(), - claimRecoveryGateway: daemonClaimRecoveryGatewayFixture(), - token: 'test-token', - sessionStore, - leaseRegistry: new LeaseRegistry(), - providerCredentials: providerCredentialsFixture({}, '/tmp'), - deviceRuntimeGateway: lifecycleDeviceRuntimeGateway, - deviceInventoryGateways: createTestDeviceInventoryGateways(), - trackDownloadableArtifact: () => 'artifact-id', - }); - - const response = await handler({ - token: 'test-token', - session: 'default', - command: 'close', - positionals: [], - meta: { requestId: 'req-close-missing-cleanup' }, - }); - - expect(response.ok).toBe(false); - if (!response.ok) { - expect(response.error).toMatchObject({ - code: 'INTERNAL_ERROR', - message: 'Platform resource cleanup was not supplied by root runtime composition', - }); - } - expect(sessionStore.get('default')).toBeDefined(); - expect(legacyDispatchCapture).not.toHaveBeenCalled(); -}); - test('close rejects a different client before cleanup', async () => { const sessionStore = makeSessionStore('agent-device-router-open-'); const leaseRegistry = new LeaseRegistry(); diff --git a/packages/daemon/src/server/http/daemon-runtime.ts b/packages/daemon/src/server/http/daemon-runtime.ts index c0052a548d..3de749dc9a 100644 --- a/packages/daemon/src/server/http/daemon-runtime.ts +++ b/packages/daemon/src/server/http/daemon-runtime.ts @@ -145,7 +145,7 @@ export type DaemonRuntimeDeps = Readonly<{ /** The runtime gateway every lifecycle and recovery step in this server runs through. */ deviceRuntimeGateway: RequestRouterDeps['deviceRuntimeGateway']; /** The resource-cleanup surface the session teardowns run through. */ - platformResourceCleanup: NonNullable; + platformResourceCleanup: RequestRouterDeps['platformResourceCleanup']; /** The owner-scoped gateway factory the startup claim reconciliation composes. */ claimRecoveryGateway: ClaimRecoveryGatewayFactory; /** diff --git a/packages/daemon/src/server/request-router.ts b/packages/daemon/src/server/request-router.ts index 6a5ba7037c..5ac97fae82 100644 --- a/packages/daemon/src/server/request-router.ts +++ b/packages/daemon/src/server/request-router.ts @@ -137,49 +137,16 @@ export type RequestRouterDeps = { /** Ends the sessions opened under a lease the tenant just released, and clears their claims. */ endSessionsForLease?: (leaseId: string) => Promise; providerAppCatalog?: ProviderAppCatalog; - androidObservation?: AndroidObservationAdapter; + androidObservation: AndroidObservationAdapter; /** Builds the per-request command surface the device-facing runtimes execute through. */ createCommandSurface: CreateDaemonCommandSurface; - platformResourceCleanup?: PlatformResourceCleanup; + platformResourceCleanup: PlatformResourceCleanup; providerDeviceRuntimeScope?: (task: () => Promise) => Promise; /** ADR 0029: the daemon policy every admitted request, including nested steps, obeys. */ daemonPolicy?: DaemonPolicy; trackDownloadableArtifact: TrackDownloadableArtifact; }; -const unavailableAndroidObservation = new Proxy({} as AndroidObservationAdapter, { - get() { - return async () => { - throw new AppError( - 'INTERNAL_ERROR', - 'Android observation was not supplied by root runtime composition', - ); - }; - }, -}); - -function missingPlatformResourceCleanup(): AppError { - return new AppError( - 'INTERNAL_ERROR', - 'Platform resource cleanup was not supplied by root runtime composition', - ); -} - -const unavailablePlatformResourceCleanup: PlatformResourceCleanup = Object.freeze({ - stopSnapshotHelper: async () => { - throw missingPlatformResourceCleanup(); - }, - closeManagedBrowser: async () => { - throw missingPlatformResourceCleanup(); - }, - cleanupSessionlessExecutionHost: async () => { - throw missingPlatformResourceCleanup(); - }, - retainExecutionHostAfterClose: () => { - throw missingPlatformResourceCleanup(); - }, -}); - export function createRequestHandler(deps: RequestRouterDeps): DaemonInvokeFn { const { logPath, @@ -200,9 +167,9 @@ export function createRequestHandler(deps: RequestRouterDeps): DaemonInvokeFn { cloudArtifactProvider, endSessionsForLease, providerAppCatalog, - androidObservation = unavailableAndroidObservation, + androidObservation, createCommandSurface, - platformResourceCleanup = unavailablePlatformResourceCleanup, + platformResourceCleanup, providerDeviceRuntimeScope, trackDownloadableArtifact, daemonPolicy, From 6b69d4b21e91b5c5438ad29d7197f020618f8bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 15 Oct 2026 15:57:08 +0200 Subject: [PATCH 2/2] fix(test): preserve router defaults for undefined capabilities --- .../__tests__/support/test-device-runtime-gateway.ts | 6 ++++-- .../src/server/__tests__/request-router-open.test.ts | 12 ++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts b/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts index 2cbb83f8c4..6c2036efc7 100644 --- a/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts +++ b/packages/daemon/src/__tests__/support/test-device-runtime-gateway.ts @@ -234,12 +234,14 @@ export function createRequestHandler( providerCredentials = providerCredentialsFixture({}, path.dirname(deps.logPath)), platformServices = daemonPlatformServicesFixture(), claimRecoveryGateway = daemonClaimRecoveryGatewayFixture(), + androidObservation = clearAndroidObservationFixture, + platformResourceCleanup = platformResourceCleanupFixture(), ...rest } = deps; const handle = createProductionRequestHandler({ - androidObservation: clearAndroidObservationFixture, - platformResourceCleanup: platformResourceCleanupFixture(), ...rest, + androidObservation, + platformResourceCleanup, platformServices, claimRecoveryGateway, deviceRuntimeGateway, diff --git a/packages/daemon/src/server/__tests__/request-router-open.test.ts b/packages/daemon/src/server/__tests__/request-router-open.test.ts index 5f7a2e6aff..b49ab29844 100644 --- a/packages/daemon/src/server/__tests__/request-router-open.test.ts +++ b/packages/daemon/src/server/__tests__/request-router-open.test.ts @@ -87,6 +87,10 @@ const mockAwaitFixtureReadiness = vi.mocked(awaitFixtureReadiness); function createOpenHandler( sessionStore: ReturnType, leaseRegistry = new LeaseRegistry(), + capabilities: Pick< + Parameters[0], + 'androidObservation' | 'platformResourceCleanup' + > = {}, ) { return createRequestHandler({ createCommandSurface: fakeCreateCommandSurface, @@ -97,6 +101,7 @@ function createOpenHandler( deviceRuntimeGateway: lifecycleDeviceRuntimeGateway, deviceInventoryGateways: createTestDeviceInventoryGateways(), trackDownloadableArtifact: () => 'artifact-id', + ...capabilities, }); } @@ -364,7 +369,10 @@ test('proxy open without required lease metadata fails before device resolution' expect(legacyDispatchCapture).not.toHaveBeenCalled(); }); -test('close releases the session lease', async () => { +test.each([ + ['omitted', {}], + ['explicit undefined', { androidObservation: undefined, platformResourceCleanup: undefined }], +])('close releases the session lease with %s fixture capabilities', async (_case, capabilities) => { const sessionStore = makeSessionStore('agent-device-router-open-'); const leaseRegistry = new LeaseRegistry(); const lease = leaseRegistry.allocateLease({ @@ -385,7 +393,7 @@ test('close releases the session lease', async () => { clientId: 'client-a', }, }); - const handler = createOpenHandler(sessionStore, leaseRegistry); + const handler = createOpenHandler(sessionStore, leaseRegistry, capabilities); const response = await handler({ token: 'test-token',