diff --git a/docs/dependency-graph-findings.md b/docs/dependency-graph-findings.md index 3454e721c5..159018d69d 100644 --- a/docs/dependency-graph-findings.md +++ b/docs/dependency-graph-findings.md @@ -375,7 +375,8 @@ writing must be removed so the table cannot drift into fiction. to stop core/daemon from BRANCHING on platform." Four facets are live (`appLog`, `recording`, `perf`, provider gating); the branch count says the mechanism is right and unfinished. Worst offenders: `handlers/session-perf.ts` (12), `handlers/session-doctor-device.ts` (6), -`handlers/session-replay-maestro-runtime.ts` (6), `handlers/session-state.ts` (5). Each is a +`handlers/session-replay-maestro-runtime.ts` (6), `handlers/session-state.ts` (5; since deleted — +its commands route straight to their handlers from the `session.ts` table). Each is a candidate facet, and each facet retired is a branch deleted in every command that shares it. ### 5c. Redundant edges: hub concentration, not debt diff --git a/packages/contracts/src/app-state.ts b/packages/contracts/src/app-state.ts index 81d1aa366d..746fcc4078 100644 --- a/packages/contracts/src/app-state.ts +++ b/packages/contracts/src/app-state.ts @@ -3,7 +3,7 @@ import type { SessionSurface } from './session-surface.ts'; /** * Closed result of the `appstate` command, grounded in the daemon handler's - * success returns (src/daemon/handlers/session-state.ts `handleAppStateCommand`). + * success returns (src/daemon/handlers/session-appstate.ts `handleAppStateCommand`). * A discriminated union on `platform`: * - Apple (`ios` / `macos`) session state, with iOS-only device locators that * the previous hand-written mirror omitted; and diff --git a/packages/contracts/src/device.ts b/packages/contracts/src/device.ts index a5fdb37448..5af8cd7d37 100644 --- a/packages/contracts/src/device.ts +++ b/packages/contracts/src/device.ts @@ -8,7 +8,7 @@ import type { TargetShutdownResult } from './target-shutdown-contract.ts'; /** * Closed result of the `boot` command. Mirrors the daemon handler's only - * success return EXACTLY (src/daemon/handlers/session-state.ts) — the fixed + * success return EXACTLY (src/daemon/handlers/session-appstate.ts) — the fixed * object literal `{ platform, target, device, id, kind, booted }` plus the * additive `appleOs` discriminant, emitted only for Apple devices. */ @@ -32,7 +32,7 @@ export type BootCommandResult = { /** * Closed result of the `shutdown` command. Mirrors the daemon handler's success - * return EXACTLY (src/daemon/handlers/session-state.ts) — the fixed object + * return EXACTLY (src/daemon/handlers/session-appstate.ts) — the fixed object * literal `{ platform, target, device, id, kind, shutdown }` plus the additive * `appleOs` discriminant (Apple devices only). The `shutdown` field is the raw * {@link TargetShutdownResult} returned by the bound `shutdownTarget` operation. diff --git a/scripts/layering/daemon-layer-manifest.json b/scripts/layering/daemon-layer-manifest.json index 9256d1b932..cd0fb50c1f 100644 --- a/scripts/layering/daemon-layer-manifest.json +++ b/scripts/layering/daemon-layer-manifest.json @@ -211,7 +211,6 @@ "src/daemon/handlers/session-runtime-port-reverse.ts", "src/daemon/handlers/session-script-publication.ts", "src/daemon/handlers/session-shutdown.ts", - "src/daemon/handlers/session-state.ts", "src/daemon/handlers/session.ts", "src/daemon/handlers/snapshot.ts", "src/daemon/handlers/trace-runtime.ts", diff --git a/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts b/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts index 501631c3e2..c6cd42a31c 100644 --- a/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts +++ b/src/daemon/handlers/__tests__/session-appstate-device-runtime.test.ts @@ -1,7 +1,7 @@ import { daemonPlatformServicesFixture } from '../../__tests__/platform-services-fixture.ts'; import { test, expect, vi } from 'vitest'; -import { handleSessionStateCommands } from '../session-state.ts'; +import { handleAppStateCommand } from '../session-appstate.ts'; import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; import { withTestDeviceInventory } from '../../../__tests__/test-utils/device-inventory-gateways.ts'; import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../__tests__/test-utils/runtime-operation-facts.ts'; @@ -39,7 +39,7 @@ test('appstate rejects web before Android app-state backend dispatch', async () local: async () => [device], }, async () => - await handleSessionStateCommands({ + await handleAppStateCommand({ platformServices: daemonPlatformServicesFixture(), req: { token: 't', @@ -160,7 +160,7 @@ test('appstate rejects a missing readiness fact even when appState is available' const response = await withTestDeviceInventory( { local: async () => [device] }, async () => - await handleSessionStateCommands({ + await handleAppStateCommand({ platformServices: daemonPlatformServicesFixture(), req: { token: 't', @@ -258,7 +258,7 @@ test('sessionless Android appstate inspects once, binds once, and preserves oper const response = await withTestDeviceInventory( { local: async () => [device] }, async () => - await handleSessionStateCommands({ + await handleAppStateCommand({ platformServices: daemonPlatformServicesFixture(), req: { token: 't', diff --git a/src/daemon/handlers/__tests__/session-boot-shutdown-device-claims.test.ts b/src/daemon/handlers/__tests__/session-boot-shutdown-device-claims.test.ts index dbe974659b..f55f8fdcf1 100644 --- a/src/daemon/handlers/__tests__/session-boot-shutdown-device-claims.test.ts +++ b/src/daemon/handlers/__tests__/session-boot-shutdown-device-claims.test.ts @@ -22,7 +22,8 @@ import { LeaseRegistry } from '../../lease-registry.ts'; import { acquireDeviceClaim } from '../../device/device-claims.ts'; import { inspectDeviceClaims } from '../../device/device-claim-inspection.ts'; import { createRequestExecutionScope } from '../../request-execution-scope.ts'; -import { handleSessionStateCommands } from '../session-state.ts'; +import { handleBootCommand } from '../session-boot.ts'; +import { handleShutdownCommand } from '../session-shutdown.ts'; import type { DaemonRequest, DaemonResponse } from '../../daemon-request.ts'; // #1799: `boot`/`shutdown` route through a daemon that need not own the target @@ -57,7 +58,7 @@ async function claimForeignDaemon(root: string): Promise { async function runStateCommand( command: 'boot' | 'shutdown', store: SessionStore, -): Promise { +): Promise { const req: DaemonRequest = { token: 't', session: 'default', @@ -80,14 +81,17 @@ async function runStateCommand( }, }); try { - return await handleSessionStateCommands({ + const handlerParams = { platformServices: daemonPlatformServicesFixture(), req, sessionName: 'default', sessionStore: store, inspectFacts: scope.inspectFacts, bindDevice: scope.bindDevice, - }); + }; + return command === 'boot' + ? await handleBootCommand(handlerParams) + : await handleShutdownCommand(handlerParams); } catch (error) { return { ok: false, error: normalizeError(error) }; } finally { diff --git a/src/daemon/handlers/__tests__/session-boot.test.ts b/src/daemon/handlers/__tests__/session-boot.test.ts index f45f1297ec..607488e3cb 100644 --- a/src/daemon/handlers/__tests__/session-boot.test.ts +++ b/src/daemon/handlers/__tests__/session-boot.test.ts @@ -12,7 +12,7 @@ import { import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable'; import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime'; import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime'; -import { handleSessionStateCommands } from '../session-state.ts'; +import { handleBootCommand } from '../session-boot.ts'; import type { SessionState } from '../../session-state.ts'; import { handleSessionCommands, @@ -456,7 +456,7 @@ test('boot rejects --headless outside Android directly', async () => { booted: false, }; mockResolveTargetDevice.mockResolvedValue(device); - const response = await handleSessionStateCommands({ + const response = await handleBootCommand({ platformServices: daemonPlatformServicesFixture(), req: { token: 't', diff --git a/src/daemon/handlers/__tests__/session-state.test.ts b/src/daemon/handlers/__tests__/session-state.test.ts deleted file mode 100644 index 804b097451..0000000000 --- a/src/daemon/handlers/__tests__/session-state.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { daemonPlatformServicesFixture } from '../../__tests__/platform-services-fixture.ts'; -import { expect, test } from 'vitest'; -import { handleSessionStateCommands } from '../session-state.ts'; -import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; -import type { DaemonRequest } from '../../daemon-request.ts'; - -function request(command: string): DaemonRequest { - return { token: 't', session: 'default', command, positionals: [], flags: {} }; -} - -async function dispatch(command: string) { - return await handleSessionStateCommands({ - platformServices: daemonPlatformServicesFixture(), - req: request(command), - sessionName: 'default', - sessionStore: makeSessionStore('agent-device-session-state-'), - }); -} - -test('the state group stays silent for commands it does not own', async () => { - expect(await dispatch('open')).toBeNull(); -}); - -test('the state group routes each command it owns to its handler', async () => { - // No session and no selector: each owned command reaches its handler's guard rather than - // the group's null, which is what distinguishes routing from silence. - for (const command of ['boot', 'shutdown', 'appstate']) { - const response = await dispatch(command); - if (!response || response.ok) { - throw new Error( - `${command} should reach its handler's guard as an INVALID_ARGS refusal, got ${JSON.stringify(response)}`, - ); - } - expect(response.error.code, command).toBe('INVALID_ARGS'); - } -}); diff --git a/src/daemon/handlers/__tests__/session.test.ts b/src/daemon/handlers/__tests__/session.test.ts new file mode 100644 index 0000000000..3e16b49b23 --- /dev/null +++ b/src/daemon/handlers/__tests__/session.test.ts @@ -0,0 +1,42 @@ +import { daemonPlatformServicesFixture } from '../../__tests__/platform-services-fixture.ts'; +import { expect, test } from 'vitest'; +import * as path from 'node:path'; +import { handleSessionCommands } from './session-command-harness.ts'; +import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; +import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts'; +import { testCreateCommandSurface } from '../../__tests__/command-surface-fixture.ts'; +import type { DaemonRequest } from '../../daemon-request.ts'; + +function request(command: string): DaemonRequest { + return { token: 't', session: 'default', command, positionals: [], flags: {} }; +} + +async function dispatch(command: string) { + return await handleSessionCommands({ + createCommandSurface: testCreateCommandSurface, + platformServices: daemonPlatformServicesFixture(), + req: request(command), + sessionName: 'default', + logPath: path.join(mkdtempForTestSync('daemon'), 'daemon.log'), + sessionStore: makeSessionStore('agent-device-session-router-'), + invoke: async () => ({ ok: true, data: {} }), + }); +} + +test('the session router stays silent for commands its table does not carry', async () => { + expect(await dispatch('not_a_command')).toBeNull(); +}); + +test('the session router maps boot, shutdown and appstate straight to their handlers', async () => { + // No session and no selector: each command reaches its own handler's guard rather than + // the router's null, which is what distinguishes routing from silence. + for (const command of ['boot', 'shutdown', 'appstate']) { + const response = await dispatch(command); + if (!response || response.ok) { + throw new Error( + `${command} should reach its handler's guard as an INVALID_ARGS refusal, got ${JSON.stringify(response)}`, + ); + } + expect(response.error.code, command).toBe('INVALID_ARGS'); + } +}); diff --git a/src/daemon/handlers/session-state.ts b/src/daemon/handlers/session-state.ts deleted file mode 100644 index eca51c67d4..0000000000 --- a/src/daemon/handlers/session-state.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { DaemonResponse } from '../daemon-request.ts'; -import type { RuntimeCommandHandlerParams } from '../session-runtime-admission.ts'; -import { handleAppStateCommand } from './session-appstate.ts'; -import { handleBootCommand } from './session-boot.ts'; -import { handleShutdownCommand } from './session-shutdown.ts'; - -/** - * The routing card for the device-state command group. Each of `boot`, `shutdown` and `appstate` - * answers its own domain question in a dedicated handler; this entry only decides which handler - * speaks, and returns null for a command the group does not own. - */ -export async function handleSessionStateCommands( - params: RuntimeCommandHandlerParams, -): Promise { - if (params.req.command === 'boot') return await handleBootCommand(params); - if (params.req.command === 'shutdown') return await handleShutdownCommand(params); - if (params.req.command === 'appstate') return await handleAppStateCommand(params); - return null; -} diff --git a/src/daemon/handlers/session.ts b/src/daemon/handlers/session.ts index 98e8340837..19334d8029 100644 --- a/src/daemon/handlers/session.ts +++ b/src/daemon/handlers/session.ts @@ -9,7 +9,9 @@ import { handleSessionInventoryCommands, handleSessionOpenCommands, } from '../session-lifecycle/index.ts'; -import { handleSessionStateCommands } from './session-state.ts'; +import { handleBootCommand } from './session-boot.ts'; +import { handleShutdownCommand } from './session-shutdown.ts'; +import { handleAppStateCommand } from './session-appstate.ts'; import { handleSessionObservabilityCommands } from '../session-observability/index.ts'; import { handleReplayCommand, handleReplayTestCommand } from './session-replay-command.ts'; import { handleSessionScriptPublication } from './session-script-publication.ts'; @@ -61,7 +63,7 @@ const handleSessionCloseCommandGroup: SessionCommandHandler = (params) => platformResourceCleanup: params.platformResourceCleanup, } satisfies SessionCloseCommandInput); -const handleSessionStateCommandGroup: SessionCommandHandler = async ({ +const handleBootCommandEntry: SessionCommandHandler = async ({ req, sessionName, sessionStore, @@ -69,7 +71,41 @@ const handleSessionStateCommandGroup: SessionCommandHandler = async ({ bindDevice, platformServices, }) => - await handleSessionStateCommands({ + await handleBootCommand({ + req, + sessionName, + sessionStore, + inspectFacts, + bindDevice, + platformServices, + }); + +const handleShutdownCommandEntry: SessionCommandHandler = async ({ + req, + sessionName, + sessionStore, + inspectFacts, + bindDevice, + platformServices, +}) => + await handleShutdownCommand({ + req, + sessionName, + sessionStore, + inspectFacts, + bindDevice, + platformServices, + }); + +const handleAppStateCommandEntry: SessionCommandHandler = async ({ + req, + sessionName, + sessionStore, + inspectFacts, + bindDevice, + platformServices, +}) => + await handleAppStateCommand({ req, sessionName, sessionStore, @@ -136,9 +172,9 @@ const SESSION_COMMAND_HANDLER_IMPLS = { inspectFacts, bindDevice, }), - boot: handleSessionStateCommandGroup, - shutdown: handleSessionStateCommandGroup, - appstate: handleSessionStateCommandGroup, + boot: handleBootCommandEntry, + shutdown: handleShutdownCommandEntry, + appstate: handleAppStateCommandEntry, session_save_script: async ({ req, sessionName, sessionStore }) => handleSessionScriptPublication({ req, sessionName, sessionStore }), runtime: async ({ diff --git a/src/platform-runtime-gateway.test.ts b/src/platform-runtime-gateway.test.ts index 0d0d2bfaf7..ff2475c626 100644 --- a/src/platform-runtime-gateway.test.ts +++ b/src/platform-runtime-gateway.test.ts @@ -16,7 +16,7 @@ import { viewportRuntimeOperationFacts } from '@agent-device/contracts/viewport- import type { DeviceInfo } from '@agent-device/kernel/device'; import { createLimrunRuntime } from '@agent-device/provider-limrun'; import { describe, expect, test, vi } from 'vitest'; -import { handleSessionStateCommands } from './daemon/handlers/session-state.ts'; +import { handleAppStateCommand } from './daemon/handlers/session-appstate.ts'; import { createRequestRuntimeBindings } from './daemon/request-runtime-binding.ts'; import { createRequestDispatchLedger } from './daemon/request-dispatch-ledger.ts'; import { makeSessionStore } from './__tests__/test-utils/store-factory.ts'; @@ -344,7 +344,7 @@ describe('composed platform runtime gateway', () => { const response = await withTestDeviceInventory( { local: async () => [staleDevice] }, async () => - await handleSessionStateCommands({ + await handleAppStateCommand({ platformServices: daemonPlatformServicesFixture(), req: { token: 't',