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
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,12 +210,22 @@ async function gestureBinding(device: DeviceInfo) {
export function createRequestHandler(
deps: Omit<
RequestRouterDeps,
'deviceRuntimeGateway' | 'providerCredentials' | 'platformServices' | 'claimRecoveryGateway'
| 'deviceRuntimeGateway'
| 'providerCredentials'
| 'platformServices'
| 'claimRecoveryGateway'
| 'androidObservation'
Comment thread
thymikee marked this conversation as resolved.
| 'platformResourceCleanup'
> &
Partial<
Pick<
RequestRouterDeps,
'deviceRuntimeGateway' | 'providerCredentials' | 'platformServices' | 'claimRecoveryGateway'
| 'deviceRuntimeGateway'
| 'providerCredentials'
| 'platformServices'
| 'claimRecoveryGateway'
| 'androidObservation'
| 'platformResourceCleanup'
>
>,
) {
Expand All @@ -224,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,
Expand Down
61 changes: 11 additions & 50 deletions packages/daemon/src/server/__tests__/request-router-open.test.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';
Expand All @@ -93,6 +87,10 @@ const mockAwaitFixtureReadiness = vi.mocked(awaitFixtureReadiness);
function createOpenHandler(
sessionStore: ReturnType<typeof makeSessionStore>,
leaseRegistry = new LeaseRegistry(),
capabilities: Pick<
Parameters<typeof createRequestHandler>[0],
'androidObservation' | 'platformResourceCleanup'
> = {},
) {
return createRequestHandler({
createCommandSurface: fakeCreateCommandSurface,
Expand All @@ -103,6 +101,7 @@ function createOpenHandler(
deviceRuntimeGateway: lifecycleDeviceRuntimeGateway,
deviceInventoryGateways: createTestDeviceInventoryGateways(),
trackDownloadableArtifact: () => 'artifact-id',
...capabilities,
});
}

Expand Down Expand Up @@ -370,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({
Expand All @@ -391,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',
Expand Down Expand Up @@ -454,47 +456,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();
Expand Down
2 changes: 1 addition & 1 deletion packages/daemon/src/server/http/daemon-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<RequestRouterDeps['platformResourceCleanup']>;
platformResourceCleanup: RequestRouterDeps['platformResourceCleanup'];
/** The owner-scoped gateway factory the startup claim reconciliation composes. */
claimRecoveryGateway: ClaimRecoveryGatewayFactory;
/**
Expand Down
41 changes: 4 additions & 37 deletions packages/daemon/src/server/request-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
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?: <T>(task: () => Promise<T>) => Promise<T>;
/** 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,
Expand All @@ -200,9 +167,9 @@ export function createRequestHandler(deps: RequestRouterDeps): DaemonInvokeFn {
cloudArtifactProvider,
endSessionsForLease,
providerAppCatalog,
androidObservation = unavailableAndroidObservation,
androidObservation,
createCommandSurface,
platformResourceCleanup = unavailablePlatformResourceCleanup,
platformResourceCleanup,
providerDeviceRuntimeScope,
trackDownloadableArtifact,
daemonPolicy,
Expand Down
Loading