diff --git a/fallow-baselines/health.json b/fallow-baselines/health.json index de3ecb293b..8604759aee 100644 --- a/fallow-baselines/health.json +++ b/fallow-baselines/health.json @@ -171,7 +171,7 @@ "count": 1 } }, - "src/daemon/handlers/__tests__/session-appstate-input-perf.test.ts": { + "src/daemon/handlers/__tests__/session-appstate-input.test.ts": { "crap_high": { "count": 3 }, @@ -189,7 +189,7 @@ "count": 1 } }, - "src/daemon/handlers/__tests__/session-logs.test.ts": { + "src/daemon/session-observability/internal/__tests__/session-logs.test.ts": { "crap_moderate": { "count": 1 } @@ -230,7 +230,7 @@ "count": 1 } }, - "src/daemon/handlers/session-observability.ts": { + "src/daemon/session-observability/internal/session-observability.ts": { "crap_moderate": { "count": 2 } diff --git a/scripts/layering/architecture-ownership.test.ts b/scripts/layering/architecture-ownership.test.ts index 394bdcdf07..bece2b1a96 100644 --- a/scripts/layering/architecture-ownership.test.ts +++ b/scripts/layering/architecture-ownership.test.ts @@ -6,6 +6,7 @@ import { ARCHITECTURE_OWNERSHIP, matchesDeclaredRoot, SESSION_LIFECYCLE_RETIRED_HANDLER_PATHS, + SESSION_OBSERVABILITY_RETIRED_HANDLER_PATHS, } from './architecture-ownership.ts'; import { readNamedExports } from './facade-exports.ts'; import { resolveImportEdges } from './model.ts'; @@ -66,6 +67,13 @@ test('session lifecycle retires its handler-owned helper paths', () => { } }); +test('session observability retires its handler-owned paths', () => { + const tracked = new Set(listTrackedTypeScriptFiles(repoRoot)); + for (const retiredPath of SESSION_OBSERVABILITY_RETIRED_HANDLER_PATHS) { + assert.equal(tracked.has(retiredPath), false, `retired path was restored: ${retiredPath}`); + } +}); + test('vocabulary roots are exported contract facades', () => { const manifest = JSON.parse( fs.readFileSync(path.join(repoRoot, 'packages/contracts/package.json'), 'utf8'), diff --git a/scripts/layering/architecture-ownership.ts b/scripts/layering/architecture-ownership.ts index 1fa41e8c62..1c0871a2be 100644 --- a/scripts/layering/architecture-ownership.ts +++ b/scripts/layering/architecture-ownership.ts @@ -28,6 +28,11 @@ const DAEMON_SESSION_LIFECYCLE_FACADE = { ], } as const; +const DAEMON_SESSION_OBSERVABILITY_FACADE = { + root: 'src/daemon/session-observability/index.ts', + exports: ['SessionObservabilityCommandInput', 'handleSessionObservabilityCommands'], +} as const; + export const SESSION_LIFECYCLE_RETIRED_HANDLER_PATHS = [ 'src/daemon/handlers/session-device-utils.ts', 'src/daemon/handlers/session-runtime-admission.ts', @@ -60,6 +65,13 @@ const DAEMON_INTERACTION_FACADE = { ], } as const; +export const SESSION_OBSERVABILITY_RETIRED_HANDLER_PATHS = [ + 'src/daemon/handlers/session-observability.ts', + 'src/daemon/handlers/session-perf-runtime.ts', + 'src/daemon/handlers/session-network.ts', + 'src/daemon/handlers/session-audio.ts', +] as const; + export const LOGICAL_MODULE_POLICIES = [ { name: 'ad-replay', @@ -106,6 +118,12 @@ export const LOGICAL_MODULE_POLICIES = [ internalForbiddenTargetRoots: ['src/daemon/handlers/'], facade: DAEMON_INTERACTION_FACADE, }, + { + name: 'daemon-session-observability', + roots: ['src/daemon/session-observability/'], + forbiddenTargetRoots: ['src/daemon/handlers/'], + facade: DAEMON_SESSION_OBSERVABILITY_FACADE, + }, ] as const satisfies readonly LogicalModulePolicy[]; export const ARCHITECTURE_OWNERSHIP = { diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index c87f85aaed..7376d4ee4a 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -77,6 +77,7 @@ import { import { checkDaemonModularityRatchets, checkRetiredSessionLifecyclePaths, + checkRetiredSessionObservabilityPaths, daemonModularitySummary, } from './daemon-modularity.ts'; import { @@ -585,6 +586,7 @@ export const LAYERING_RULES: Readonly> = { 'daemon-modularity-ratchets': (context) => [ ...checkDaemonModularityRatchets(context.edges, context.typeCycleMembers), ...checkRetiredSessionLifecyclePaths(context.sourceFiles), + ...checkRetiredSessionObservabilityPaths(context.sourceFiles), ], 'daemon-platform-boundary': (context) => checkDaemonPlatformBoundary([...context.sources].map(([path, source]) => ({ path, source }))), diff --git a/scripts/layering/daemon-modularity.test.ts b/scripts/layering/daemon-modularity.test.ts index b00a5cb1a8..d9044b885b 100644 --- a/scripts/layering/daemon-modularity.test.ts +++ b/scripts/layering/daemon-modularity.test.ts @@ -3,6 +3,7 @@ import { test } from 'node:test'; import { checkDaemonModularityRatchets, checkRetiredSessionLifecyclePaths, + checkRetiredSessionObservabilityPaths, DAEMON_MODULARITY_BASELINE, TYPE_CYCLE_BASELINE, } from './daemon-modularity.ts'; @@ -370,6 +371,50 @@ test('interaction rejects handler crossings and deep imports around its facade', ); }); +test('session observability rejects handler deep imports in both directions', () => { + const edges = resolveImportEdges( + new Map([ + [ + 'src/daemon/handlers/session.ts', + "import { handleSessionObservabilityCommands } from '../session-observability/internal/session-observability.ts';\nexport function handleSessionCommands() {}", + ], + [ + 'src/daemon/session-observability/internal/session-observability.ts', + "import { handleSessionCommands } from '../../handlers/session.ts';\nexport function handleSessionObservabilityCommands() {}", + ], + [ + 'src/daemon/session-observability/index.ts', + 'export function handleSessionObservabilityCommands() {}', + ], + ]), + ); + + const violations = checkDaemonModularityRatchets( + [...baselineEdges(), ...edges], + baselineTypeCycleMembers(), + ); + assert.deepEqual( + violations.map(({ file, line, message }) => ({ + file, + line, + message: message.replace(/;.*/, ''), + })), + [ + { + file: 'src/daemon/handlers/session.ts', + line: 1, + message: + "src/daemon/handlers/session.ts must not import daemon-session-observability's internal tree (src/daemon/session-observability/internal/session-observability.ts)", + }, + { + file: 'src/daemon/session-observability/internal/session-observability.ts', + line: 1, + message: 'daemon-session-observability must not import src/daemon/handlers/session.ts', + }, + ], + ); +}); + test('session lifecycle rejects restored neutral helper paths', () => { const violations = checkRetiredSessionLifecyclePaths([ 'src/daemon/session-device-resolution.ts', @@ -422,6 +467,30 @@ test('session lifecycle rejects any restored open or close handler path', () => ); }); +test('session observability rejects restored handler paths', () => { + const restoredPaths = [ + 'src/daemon/handlers/session-observability.ts', + 'src/daemon/handlers/session-perf-runtime.ts', + 'src/daemon/handlers/session-network.ts', + 'src/daemon/handlers/session-audio.ts', + 'src/daemon/handlers/session-network-regressed.ts', + 'src/daemon/handlers/session-perf.ts', + 'src/daemon/handlers/session-logs.ts', + 'src/daemon/handlers/session-events.ts', + ] as const; + const violations = checkRetiredSessionObservabilityPaths(restoredPaths); + + assert.deepEqual( + violations.map(({ file, message }) => ({ file, message })), + restoredPaths.map((file) => ({ + file, + message: + `retired session observability path was restored: ${file}. ` + + 'Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.', + })), + ); +}); + test('R9 records zone ceilings and keeps engine files outside the largest component', () => { // One commands file and one engine file traded for two provider-webdriver ones, so the // total stays at the baseline and only the per-zone claims are on trial. diff --git a/scripts/layering/daemon-modularity.ts b/scripts/layering/daemon-modularity.ts index 5898d21058..5e007f8c34 100644 --- a/scripts/layering/daemon-modularity.ts +++ b/scripts/layering/daemon-modularity.ts @@ -3,6 +3,7 @@ import { LOGICAL_MODULE_POLICIES, matchesDeclaredRoot, SESSION_LIFECYCLE_RETIRED_HANDLER_PATHS, + SESSION_OBSERVABILITY_RETIRED_HANDLER_PATHS, type LogicalModulePolicy, } from './architecture-ownership.ts'; import { targetDagZone, type LayeringViolation, type ResolvedImportEdge } from './model.ts'; @@ -57,19 +58,41 @@ export function checkDaemonModularityRatchets( export function checkRetiredSessionLifecyclePaths( sourceFiles: readonly string[], ): LayeringViolation[] { - const restoredPaths = sourceFiles.filter( - (file) => - SESSION_LIFECYCLE_RETIRED_HANDLER_PATHS.some((retiredPath) => retiredPath === file) || - /^src\/daemon\/handlers\/session-(?:open|close)(?:-[^/]+)?\.ts$/.test(file), + return checkRetiredHandlerPaths( + sourceFiles, + SESSION_LIFECYCLE_RETIRED_HANDLER_PATHS, + /^src\/daemon\/handlers\/session-(?:open|close)(?:-[^/]+)?\.ts$/, + 'session lifecycle', ); - return restoredPaths.map((file) => ({ - rule: 'R10 daemon-modularity', - file, - line: 1, - message: - `retired session lifecycle path was restored: ${file}. ` + - 'Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.', - })); +} + +export function checkRetiredSessionObservabilityPaths( + sourceFiles: readonly string[], +): LayeringViolation[] { + return checkRetiredHandlerPaths( + sourceFiles, + SESSION_OBSERVABILITY_RETIRED_HANDLER_PATHS, + /^src\/daemon\/handlers\/session-(?:observability|perf|logs|events|network|audio)(?:-[^/]+)?\.ts$/, + 'session observability', + ); +} + +function checkRetiredHandlerPaths( + sourceFiles: readonly string[], + retiredPaths: readonly string[], + pattern: RegExp, + capability: string, +): LayeringViolation[] { + return sourceFiles + .filter((file) => retiredPaths.includes(file) || pattern.test(file)) + .map((file) => ({ + rule: 'R10 daemon-modularity', + file, + line: 1, + message: + `retired ${capability} path was restored: ${file}. ` + + 'Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.', + })); } function checkSessionStateBaseline(): LayeringViolation[] { diff --git a/scripts/layering/model.test.ts b/scripts/layering/model.test.ts index 4444ab117c..41a09ed3d2 100644 --- a/scripts/layering/model.test.ts +++ b/scripts/layering/model.test.ts @@ -313,7 +313,7 @@ test('session-state writes are found by field, and non-daemon or undeclared name // a runner session outside the daemon is a different type that happens to share a name ['src/platforms/apple/runner-session.ts', 'session.refFrameState = 1;'], // a local that is not a declared SessionState field - ['src/daemon/handlers/session-audio.ts', 'session.somethingElse = 1;'], + ['src/daemon/session-observability/internal/session-audio.ts', 'session.somethingElse = 1;'], // reads and comparisons are not writes ['src/daemon/handlers/find.ts', "if (session.refFrameState === 'active') return;"], // a write into a sub-object is not a write to the field itself diff --git a/src/daemon/handlers/__tests__/session-appstate-input-perf.test.ts b/src/daemon/handlers/__tests__/session-appstate-input.test.ts similarity index 93% rename from src/daemon/handlers/__tests__/session-appstate-input-perf.test.ts rename to src/daemon/handlers/__tests__/session-appstate-input.test.ts index 1518da4de9..970e9a1f42 100644 --- a/src/daemon/handlers/__tests__/session-appstate-input-perf.test.ts +++ b/src/daemon/handlers/__tests__/session-appstate-input.test.ts @@ -272,25 +272,3 @@ test('clipboard rejects unsupported iOS physical devices', async () => { expect(response.error.message).toMatch(/clipboard is not supported on this device/i); } }); - -test('perf requires an active session', async () => { - const sessionStore = makeSessionStore(); - const response = await handleSessionCommands({ - req: { - token: 't', - session: 'default', - command: 'perf', - positionals: [], - flags: {}, - }, - sessionName: 'default', - logPath: path.join(os.tmpdir(), 'daemon.log'), - sessionStore, - invoke: noopInvoke, - }); - expect(response).toBeTruthy(); - expect(response?.ok).toBe(false); - if (response && !response.ok) { - expect(response.error.code).toBe('SESSION_NOT_FOUND'); - } -}); diff --git a/src/daemon/handlers/find.ts b/src/daemon/handlers/find.ts index a56104024f..107431f6df 100644 --- a/src/daemon/handlers/find.ts +++ b/src/daemon/handlers/find.ts @@ -15,7 +15,7 @@ import { contextFromFlags } from '../context.ts'; import { readCommandMessage, successText } from '@agent-device/kernel/success-text'; import { errorResponse, noActiveSessionError } from '../response.ts'; import { withSystemSurfaceDisclosure } from './system-surface-disclosure.ts'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { stripInternalInteractionFlags } from '../interaction-outcome-policy.ts'; import { resolveFindMatch } from './find-match-resolution.ts'; import { executeFocusPoint } from '../focus-runtime.ts'; diff --git a/src/daemon/handlers/handler-utils.ts b/src/daemon/handlers/handler-utils.ts deleted file mode 100644 index 0f89df767e..0000000000 --- a/src/daemon/handlers/handler-utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CommandFlags } from '@agent-device/contracts/command'; -import { SessionStore } from '../session-store.ts'; -import type { DaemonRequest, SessionState } from '../types.ts'; - -/** - * Record a session action if a session is active. No-op when session is undefined. - * - * By default the recorded positionals/flags mirror the request; pass `overrides` to - * record a different set (e.g. resolved positionals or stripped public flags). - */ -export function recordSessionAction( - sessionStore: SessionStore, - session: SessionState | undefined, - req: DaemonRequest, - command: string, - result: Record | undefined, - overrides?: { positionals?: string[]; flags?: CommandFlags }, -): void { - if (!session) return; - sessionStore.recordAction(session, { - command, - positionals: overrides?.positionals ?? req.positionals ?? [], - flags: overrides?.flags ?? ((req.flags ?? {}) as CommandFlags), - result: result ?? {}, - }); -} diff --git a/src/daemon/handlers/record-runtime.ts b/src/daemon/handlers/record-runtime.ts index 7ad07dda98..4f0aa728ca 100644 --- a/src/daemon/handlers/record-runtime.ts +++ b/src/daemon/handlers/record-runtime.ts @@ -24,7 +24,7 @@ import { resolveSessionScope } from '../session-routing.ts'; import type { SessionStore } from '../session-store.ts'; import type { BindDeviceRuntime, BindExactDeviceRuntime } from '../request-runtime-binding.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { missingAppSessionResponse, prepareRecordingRequest, diff --git a/src/daemon/handlers/session-app-deployment.ts b/src/daemon/handlers/session-app-deployment.ts index e95b36552d..634850abc2 100644 --- a/src/daemon/handlers/session-app-deployment.ts +++ b/src/daemon/handlers/session-app-deployment.ts @@ -14,7 +14,7 @@ import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; import { resolvePayloadInput } from '../../utils/payload-input.ts'; import { resolveDeployResultTarget } from '../result-serialization.ts'; import { withSuccessText } from '@agent-device/kernel/success-text'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { errorResponse } from '../response.ts'; import { requireSessionOrExplicitSelector, diff --git a/src/daemon/handlers/session-app-source-deployment.ts b/src/daemon/handlers/session-app-source-deployment.ts index 753685130c..14bc983b9b 100644 --- a/src/daemon/handlers/session-app-source-deployment.ts +++ b/src/daemon/handlers/session-app-source-deployment.ts @@ -18,7 +18,7 @@ import { SessionStore } from '../session-store.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; import { resolveInstallFromSourceResultTarget } from '../result-serialization.ts'; import { withSuccessText } from '@agent-device/kernel/success-text'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { resolveCommandDevice } from '../session-device-resolution.ts'; import { requireRuntimeBinding, diff --git a/src/daemon/handlers/session-clipboard.ts b/src/daemon/handlers/session-clipboard.ts index 797ed10692..3f49758811 100644 --- a/src/daemon/handlers/session-clipboard.ts +++ b/src/daemon/handlers/session-clipboard.ts @@ -18,7 +18,7 @@ import { admitRuntimeUse, type RuntimeAdmissionBindings } from '../runtime-admis import { runtimeExecutionFromContext } from '../snapshot-runtime-capture-input.ts'; import { successText } from '@agent-device/kernel/success-text'; import { errorResponse, type DaemonFailureResponse } from '../response.ts'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { requireSessionOrExplicitSelector, resolveCommandDevice, diff --git a/src/daemon/handlers/session-selector-dispatch.ts b/src/daemon/handlers/session-selector-dispatch.ts index 38bfc8c092..64f57a80e7 100644 --- a/src/daemon/handlers/session-selector-dispatch.ts +++ b/src/daemon/handlers/session-selector-dispatch.ts @@ -8,7 +8,7 @@ import { resolveCommandDevice, } from '../session-device-resolution.ts'; import { errorResponse } from '../response.ts'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { resolveBoundAppEventRuntime } from '../app-event-runtime.ts'; import { resolveBoundKeyboardRuntime } from '../keyboard-runtime.ts'; import { resolveRefFrameEffect } from '../daemon-command-registry.ts'; diff --git a/src/daemon/handlers/session.ts b/src/daemon/handlers/session.ts index bf407d1abe..eb787ed9be 100644 --- a/src/daemon/handlers/session.ts +++ b/src/daemon/handlers/session.ts @@ -10,7 +10,7 @@ import { handleSessionOpenCommands, } from '../session-lifecycle/index.ts'; import { handleSessionStateCommands } from './session-state.ts'; -import { handleSessionObservabilityCommands } from './session-observability.ts'; +import { handleSessionObservabilityCommands } from '../session-observability/index.ts'; import { handleReplayCommand, handleReplayTestCommand } from './session-replay-command.ts'; import { handleSessionScriptPublication } from './session-script-publication.ts'; import { handleSessionClipboardCommand } from './session-clipboard.ts'; @@ -26,6 +26,7 @@ import type { SessionInventoryCommandInput, SessionOpenCommandInput, } from '../session-lifecycle/index.ts'; +import type { SessionObservabilityCommandInput } from '../session-observability/index.ts'; import type { DescriptorSessionRouteCommandName } from '../../core/command-descriptor/registry.ts'; import { LeaseRegistry } from '../lease-registry.ts'; @@ -93,7 +94,7 @@ const handleSessionObservabilityCommandGroup: SessionCommandHandler = async ({ audioProbeAdmissionLedger, perfCaptureAdmissionLedger, throwIfCanceled, - }); + } satisfies SessionObservabilityCommandInput); /** * Descriptor-driven exhaustive dispatch table for the daemon's `session` diff --git a/src/daemon/handlers/trace-runtime.ts b/src/daemon/handlers/trace-runtime.ts index 21a0b62a0b..da9fea818f 100644 --- a/src/daemon/handlers/trace-runtime.ts +++ b/src/daemon/handlers/trace-runtime.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import type { TraceCommandResult } from '@agent-device/contracts/recording'; import { SessionStore } from '../session-store.ts'; import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; -import { recordSessionAction } from './handler-utils.ts'; +import { recordSessionAction } from '../session-action-recorder.ts'; import { errorResponse } from '../response.ts'; export function handleTraceCommand(params: { diff --git a/src/daemon/session-action-recorder.ts b/src/daemon/session-action-recorder.ts index 1daebef6fa..977d0051d8 100644 --- a/src/daemon/session-action-recorder.ts +++ b/src/daemon/session-action-recorder.ts @@ -97,6 +97,31 @@ export function recordActionEntry( return action; } +type SessionActionStore = { recordAction(session: SessionState, entry: RecordActionEntry): void }; + +/** + * Record a session action if a session is active. No-op when session is undefined. + * + * By default the recorded positionals/flags mirror the request; pass `overrides` to + * record a different set (e.g. resolved positionals or stripped public flags). + */ +export function recordSessionAction( + sessionStore: SessionActionStore, + session: SessionState | undefined, + req: DaemonRequest, + command: string, + result: Record | undefined, + overrides?: { positionals?: string[]; flags?: CommandFlags }, +): void { + if (!session) return; + sessionStore.recordAction(session, { + command, + positionals: overrides?.positionals ?? req.positionals ?? [], + flags: overrides?.flags ?? ((req.flags ?? {}) as CommandFlags), + result: result ?? {}, + }); +} + type FillLiteral = { literal: string; placeholder: string }; /** The (literal, placeholder) pair a `fill --record-as` entry carries, or `undefined` for an ordinary fill/other command. */ @@ -269,7 +294,7 @@ function replaceFillText(positionals: string[], placeholder: string): string[] { * absent: it is flow timing/synchronisation, not observation, so it always * records. A mutating `find … click|fill|focus|type` never reaches a caller of * `isInteractiveObservation` (it records through `recordSessionAction`, - * `handlers/handler-utils.ts`), so `find` here always means a read-only + * `session-action-recorder.ts`), so `find` here always means a read-only * sub-action; `diff` is likewise absent because only `snapshot` is classified * at the snapshot-runtime call site. */ diff --git a/src/daemon/session-observability/__tests__/application.test.ts b/src/daemon/session-observability/__tests__/application.test.ts new file mode 100644 index 0000000000..411c46095b --- /dev/null +++ b/src/daemon/session-observability/__tests__/application.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, expect, test, vi } from 'vitest'; +import type { DaemonRequest, DaemonResponse } from '../../types.ts'; +import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; + +vi.mock('../index.ts', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + handleSessionObservabilityCommands: vi.fn(actual.handleSessionObservabilityCommands), + }; +}); + +import { handleSessionCommands } from '../../handlers/session.ts'; +import { handleSessionObservabilityCommands } from '../index.ts'; + +const mockHandleSessionObservabilityCommands = vi.mocked(handleSessionObservabilityCommands); + +beforeEach(() => { + mockHandleSessionObservabilityCommands.mockClear(); +}); + +function request(command: DaemonRequest['command']): DaemonRequest { + return { + token: 'test-token', + session: 'default', + command, + positionals: [], + flags: {}, + }; +} + +async function run(command: DaemonRequest['command']): Promise { + return await handleSessionCommands({ + req: request(command), + sessionName: 'default', + logPath: '/tmp/agent-device-session-observability-route.log', + sessionStore: makeSessionStore('agent-device-session-observability-route-'), + invoke: async () => ({ ok: true, data: {} }), + reconcileOrphanedDeviceClaim: async () => ({ + status: 'retained' as const, + reason: 'test-harness', + }), + }); +} + +test('observability commands retain their route response and narrow facade input', async () => { + const expected: DaemonResponse = { ok: true, data: { routed: true } }; + mockHandleSessionObservabilityCommands.mockResolvedValue(expected); + + for (const command of ['perf', 'logs', 'events', 'network', 'audio'] as const) { + await expect(run(command)).resolves.toEqual(expected); + const forwarded = mockHandleSessionObservabilityCommands.mock.calls.at(-1)?.[0]; + expect(Object.keys(forwarded ?? {}).sort()).toEqual( + [ + 'appLogAdmissionLedger', + 'audioProbeAdmissionLedger', + 'bindDevice', + 'inspectFacts', + 'perfCaptureAdmissionLedger', + 'req', + 'sessionName', + 'sessionStore', + 'throwIfCanceled', + ].sort(), + ); + expect(forwarded).toMatchObject({ + req: expect.objectContaining({ command }), + sessionName: 'default', + }); + } +}); diff --git a/src/daemon/handlers/__tests__/session-audio.coverage.ts b/src/daemon/session-observability/__tests__/session-audio.coverage.ts similarity index 84% rename from src/daemon/handlers/__tests__/session-audio.coverage.ts rename to src/daemon/session-observability/__tests__/session-audio.coverage.ts index 94111385ba..16234316a1 100644 --- a/src/daemon/handlers/__tests__/session-audio.coverage.ts +++ b/src/daemon/session-observability/__tests__/session-audio.coverage.ts @@ -2,6 +2,6 @@ import { PUBLIC_COMMANDS as C } from '../../../command-catalog.ts'; export const ANDROID_AUDIO_CONTRACT_EVIDENCE = { commands: [C.audio], - owner: 'daemon/session-audio', + owner: 'daemon/session-observability', testName: 'audio probe starts host helper for Android emulator audio', } as const; diff --git a/src/daemon/session-observability/index.ts b/src/daemon/session-observability/index.ts new file mode 100644 index 0000000000..a584f4b975 --- /dev/null +++ b/src/daemon/session-observability/index.ts @@ -0,0 +1,2 @@ +export { handleSessionObservabilityCommands } from './internal/session-observability.ts'; +export type { SessionObservabilityCommandInput } from './internal/session-observability.ts'; diff --git a/src/daemon/handlers/__tests__/network-runtime-harness.ts b/src/daemon/session-observability/internal/__tests__/network-runtime-harness.ts similarity index 96% rename from src/daemon/handlers/__tests__/network-runtime-harness.ts rename to src/daemon/session-observability/internal/__tests__/network-runtime-harness.ts index 67fd5c8883..f9f2d9ea42 100644 --- a/src/daemon/handlers/__tests__/network-runtime-harness.ts +++ b/src/daemon/session-observability/internal/__tests__/network-runtime-harness.ts @@ -8,9 +8,9 @@ import { narrowDeviceBinding, } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../__tests__/test-utils/runtime-operation-facts.ts'; +import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../../__tests__/test-utils/runtime-operation-facts.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; -import type { BindDeviceRuntime } from '../../request-runtime-binding.ts'; +import type { BindDeviceRuntime } from '../../../request-runtime-binding.ts'; export function createNetworkRuntime( device: DeviceInfo, diff --git a/src/daemon/handlers/__tests__/session-audio.test.ts b/src/daemon/session-observability/internal/__tests__/session-audio.test.ts similarity index 89% rename from src/daemon/handlers/__tests__/session-audio.test.ts rename to src/daemon/session-observability/internal/__tests__/session-audio.test.ts index e47e591bd6..cd7240954d 100644 --- a/src/daemon/handlers/__tests__/session-audio.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-audio.test.ts @@ -13,24 +13,35 @@ import { encodeDurableDescriptor, hostAudioProbeDescriptorCodec, } from '@agent-device/capture-kit'; -import { IOS_DEVICE, WEB_DESKTOP_DEVICE } from '../../../__tests__/test-utils/device-fixtures.ts'; +import { + IOS_DEVICE, + WEB_DESKTOP_DEVICE, +} from '../../../../__tests__/test-utils/device-fixtures.ts'; import { makeAndroidSession, makeIosSession, makeMacOsSession, makeSession, -} from '../../../__tests__/test-utils/session-factories.ts'; -import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; +} from '../../../../__tests__/test-utils/session-factories.ts'; +import { makeSessionStore } from '../../../../__tests__/test-utils/store-factory.ts'; import { unavailableApplicationLifecycleOperationFacts, unavailableDeploymentSnapshotAndShutdownOperationFacts, -} from '../../../__tests__/test-utils/runtime-operation-facts.ts'; -import { createAudioProbeAdmissionLedger } from '../../audio-probe-admission-ledger.ts'; -import { audioProbeDurableResource } from '../../audio-probe-session-resource.ts'; -import type { SessionStore } from '../../session-store.ts'; -import type { DaemonResponse } from '../../types.ts'; -import { handleAudioCommand } from '../session-audio.ts'; -import { ANDROID_AUDIO_CONTRACT_EVIDENCE } from './session-audio.coverage.ts'; +} from '../../../../__tests__/test-utils/runtime-operation-facts.ts'; +import { createAudioProbeAdmissionLedger } from '../../../audio-probe-admission-ledger.ts'; +import { audioProbeDurableResource } from '../../../audio-probe-session-resource.ts'; +import type { SessionStore } from '../../../session-store.ts'; +import type { DaemonResponse } from '../../../types.ts'; +import { handleSessionObservabilityCommands } from '../../index.ts'; +import { ANDROID_AUDIO_CONTRACT_EVIDENCE } from '../../__tests__/session-audio.coverage.ts'; + +async function runAudio( + params: Parameters[0], +): Promise { + const response = await handleSessionObservabilityCommands(params); + assert.ok(response); + return response; +} function ownerFor(device: DeviceInfo) { return localRuntimeOwner(device.platform as Parameters[0]); @@ -196,7 +207,7 @@ test('audio probe validates daemon duration bounds', async () => { }); assertInvalidArgs( - await handleAudioCommand(params as never), + await runAudio(params as never), /duration must be an integer in range 100..120000/, ); assert.equal(bindDevice.mock.calls.length, 0); @@ -211,7 +222,7 @@ test('audio probe validates daemon bucket bounds', async () => { }); assertInvalidArgs( - await handleAudioCommand(params as never), + await runAudio(params as never), /bucket must be an integer in range 100..10000/, ); assert.equal(bindDevice.mock.calls.length, 0); @@ -225,10 +236,7 @@ test('audio probe rejects timing positionals for status', async () => { cells: { capture: false, query: true }, }); - assertInvalidArgs( - await handleAudioCommand(params as never), - /only supported with audio probe start/, - ); + assertInvalidArgs(await runAudio(params as never), /only supported with audio probe start/); assert.equal(bindDevice.mock.calls.length, 0); }); @@ -240,7 +248,7 @@ test('audio refuses with the owner-stated hint when no fact admits it', async () cells: { capture: false, query: false }, }); - const response = await handleAudioCommand(params as never); + const response = await runAudio(params as never); assert.equal(response.ok, false); if (!response.ok) { assert.equal(response.error.code, 'UNSUPPORTED_OPERATION'); @@ -260,7 +268,7 @@ test('audio probe start binds once, adopts the durable handle, and answers from operations: runtime.operations, }); - const response = await handleAudioCommand(params as never); + const response = await runAudio(params as never); assert.ok(response.ok); assert.equal(bindDevice.mock.calls.length, 1); @@ -289,7 +297,7 @@ test('audio probe starts host helper for iOS simulator audio', async () => { operations: runtime.operations, }); - const response = await handleAudioCommand(params as never); + const response = await runAudio(params as never); assert.ok(response.ok); assert.equal(runtime.startCalls.length, 1); @@ -307,7 +315,7 @@ test(ANDROID_AUDIO_CONTRACT_EVIDENCE.testName, async () => { operations: runtime.operations, }); - const response = await handleAudioCommand(params as never); + const response = await runAudio(params as never); assert.ok(response.ok); assert.deepEqual(response.data?.peakDbfs, [-13]); @@ -324,14 +332,14 @@ test('audio probe stop finishes the durable resource and clears the slot', async cells: { capture: true, query: false }, operations: runtime.operations, }); - assert.ok((await handleAudioCommand(start.params as never)).ok); + assert.ok((await runAudio(start.params as never)).ok); const stop = audioParams('macos', sessionStore, session.device, { positionals: ['probe', 'stop'], cells: { capture: true, query: false }, operations: runtime.operations, }); - const response = await handleAudioCommand(stop.params as never); + const response = await runAudio(stop.params as never); assert.ok(response.ok); assert.equal(response.data?.state, 'stopped'); @@ -353,7 +361,7 @@ test('audio probe status without an active probe reports not-started', async () cells: { capture: true, query: false }, }); - const response = await handleAudioCommand(params as never); + const response = await runAudio(params as never); assert.ok(response.ok); assert.equal(response.data?.state, 'stopped'); @@ -373,7 +381,7 @@ test('audio probe forwards daemon millisecond timing to the web query operation' operations: { audioProbeQuery }, }); - const response = await handleAudioCommand(params as never); + const response = await runAudio(params as never); assert.ok(response.ok); assert.equal(bindDevice.mock.calls.length, 1); diff --git a/src/daemon/handlers/__tests__/session-logs.test.ts b/src/daemon/session-observability/internal/__tests__/session-logs.test.ts similarity index 96% rename from src/daemon/handlers/__tests__/session-logs.test.ts rename to src/daemon/session-observability/internal/__tests__/session-logs.test.ts index cd4a4e6829..fb40412fe7 100644 --- a/src/daemon/handlers/__tests__/session-logs.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-logs.test.ts @@ -8,25 +8,25 @@ import { narrowDeviceBinding, } from '@agent-device/contracts/platform-runtime'; import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; -import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../__tests__/test-utils/runtime-operation-facts.ts'; +import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../../__tests__/test-utils/runtime-operation-facts.ts'; import { createAppLogStartResult, createDurableResourceEnvelope } from '@agent-device/capture-kit'; -import { createTestAppLogLiveHandle } from '../../../__tests__/test-utils/app-log-live-handle.ts'; +import { createTestAppLogLiveHandle } from '../../../../__tests__/test-utils/app-log-live-handle.ts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { createAppLogAdmissionLedger, type AppLogAdmissionLedger, -} from '../../app-log-admission-ledger.ts'; -import { handleSessionCommands } from './session-command-harness.ts'; -import { appLogResourceStore } from '../../app-log-resource-store.ts'; -import type { BindDeviceRuntime } from '../../request-runtime-binding.ts'; -import type { SessionStore } from '../../session-store.ts'; +} from '../../../app-log-admission-ledger.ts'; +import { handleSessionCommands } from '../../../handlers/__tests__/session-command-harness.ts'; +import { appLogResourceStore } from '../../../app-log-resource-store.ts'; +import type { BindDeviceRuntime } from '../../../request-runtime-binding.ts'; +import type { SessionStore } from '../../../session-store.ts'; import { makeSession, makeSessionStore, makeTestAppLogResource, noopInvoke, -} from './session-test-harness.ts'; +} from '../../../handlers/__tests__/session-test-harness.ts'; const DEVICE: DeviceInfo = { platform: 'apple', diff --git a/src/daemon/handlers/__tests__/session-network-runtime-parity-fixtures.ts b/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity-fixtures.ts similarity index 100% rename from src/daemon/handlers/__tests__/session-network-runtime-parity-fixtures.ts rename to src/daemon/session-observability/internal/__tests__/session-network-runtime-parity-fixtures.ts diff --git a/src/daemon/handlers/__tests__/session-network-runtime-parity.test.ts b/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts similarity index 96% rename from src/daemon/handlers/__tests__/session-network-runtime-parity.test.ts rename to src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts index 18e0beb895..7f07ef5112 100644 --- a/src/daemon/handlers/__tests__/session-network-runtime-parity.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-network-runtime-parity.test.ts @@ -1,14 +1,14 @@ import path from 'node:path'; import { expect, test } from 'vitest'; import { providerRuntimeOwner } from '@agent-device/contracts/platform-runtime'; -import type { BindDeviceRuntime } from '../../request-runtime-binding.ts'; -import { handleSessionCommands } from './session-command-harness.ts'; +import type { BindDeviceRuntime } from '../../../request-runtime-binding.ts'; +import { handleSessionCommands } from '../../../handlers/__tests__/session-command-harness.ts'; import { makeSession, makeSessionStore, makeTestAppLogResource, noopInvoke, -} from './session-test-harness.ts'; +} from '../../../handlers/__tests__/session-test-harness.ts'; import { NETWORK_RUNTIME_PROJECTION_PARITY } from './session-network-runtime-parity-fixtures.ts'; import { createNetworkRuntime, emptyAppLogResult } from './network-runtime-harness.ts'; diff --git a/src/daemon/handlers/__tests__/session-network.test.ts b/src/daemon/session-observability/internal/__tests__/session-network.test.ts similarity index 89% rename from src/daemon/handlers/__tests__/session-network.test.ts rename to src/daemon/session-observability/internal/__tests__/session-network.test.ts index 3739b046ab..2c54450d96 100644 --- a/src/daemon/handlers/__tests__/session-network.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-network.test.ts @@ -1,8 +1,12 @@ import path from 'node:path'; import { expect, test } from 'vitest'; -import { handleSessionCommands } from './session-command-harness.ts'; +import { handleSessionCommands } from '../../../handlers/__tests__/session-command-harness.ts'; import { createNetworkRuntime, emptyAppLogResult } from './network-runtime-harness.ts'; -import { makeSession, makeSessionStore, noopInvoke } from './session-test-harness.ts'; +import { + makeSession, + makeSessionStore, + noopInvoke, +} from '../../../handlers/__tests__/session-test-harness.ts'; test('network requires an active session before requesting a runtime binding', async () => { const sessionStore = makeSessionStore(); diff --git a/src/daemon/handlers/__tests__/session-observability.test.ts b/src/daemon/session-observability/internal/__tests__/session-observability.test.ts similarity index 93% rename from src/daemon/handlers/__tests__/session-observability.test.ts rename to src/daemon/session-observability/internal/__tests__/session-observability.test.ts index a340146b5b..a4164676e1 100644 --- a/src/daemon/handlers/__tests__/session-observability.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-observability.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict'; import { test } from 'vitest'; -import { makeAndroidSession } from '../../../__tests__/test-utils/session-factories.ts'; -import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; -import { handleSessionObservabilityCommands } from '../session-observability.ts'; +import { makeAndroidSession } from '../../../../__tests__/test-utils/session-factories.ts'; +import { makeSessionStore } from '../../../../__tests__/test-utils/store-factory.ts'; +import { handleSessionObservabilityCommands } from '../../index.ts'; import { createNetworkRuntime, emptyAppLogResult } from './network-runtime-harness.ts'; test('network dump validates include mode directly', async () => { diff --git a/src/daemon/handlers/__tests__/session-perf-runtime.test.ts b/src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts similarity index 90% rename from src/daemon/handlers/__tests__/session-perf-runtime.test.ts rename to src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts index 2da75c089e..731b16bfad 100644 --- a/src/daemon/handlers/__tests__/session-perf-runtime.test.ts +++ b/src/daemon/session-observability/internal/__tests__/session-perf-runtime.test.ts @@ -16,15 +16,15 @@ import { import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; import type { DeviceInfo } from '@agent-device/kernel/device'; import { deviceIdentity } from '@agent-device/kernel/device'; -import { makeAndroidSession } from '../../../__tests__/test-utils/session-factories.ts'; -import { makeSessionStore } from '../../../__tests__/test-utils/store-factory.ts'; -import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../__tests__/test-utils/runtime-operation-facts.ts'; -import { createPerfCaptureAdmissionLedger } from '../../perf-capture-admission-ledger.ts'; +import { makeAndroidSession } from '../../../../__tests__/test-utils/session-factories.ts'; +import { makeSessionStore } from '../../../../__tests__/test-utils/store-factory.ts'; +import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../../../__tests__/test-utils/runtime-operation-facts.ts'; +import { createPerfCaptureAdmissionLedger } from '../../../perf-capture-admission-ledger.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts, -} from '../../request-runtime-binding.ts'; -import { handleSessionObservabilityCommands } from '../session-observability.ts'; +} from '../../../request-runtime-binding.ts'; +import { handleSessionObservabilityCommands } from '../../index.ts'; const available = Object.freeze({ available: true as const }); const unavailable = Object.freeze({ @@ -32,6 +32,27 @@ const unavailable = Object.freeze({ reason: 'owner-capability-missing' as const, }); +test('perf requires an active session', async () => { + const sessionStore = makeSessionStore(); + const response = await handleSessionObservabilityCommands({ + req: { + token: 't', + session: 'default', + command: 'perf', + positionals: [], + flags: {}, + }, + sessionName: 'default', + sessionStore, + }); + + assert.ok(response); + assert.equal(response.ok, false); + if (!response.ok) { + assert.equal(response.error.code, 'SESSION_NOT_FOUND'); + } +}); + test('perf frames admits and binds only the selected runtime operation', async () => { const sessionStore = makeStore(); const perfFrames = vi.fn(async (_input: Parameters[0]) => ({ diff --git a/src/daemon/handlers/session-audio.ts b/src/daemon/session-observability/internal/session-audio.ts similarity index 94% rename from src/daemon/handlers/session-audio.ts rename to src/daemon/session-observability/internal/session-audio.ts index 5f6c23c8cc..6b15b92d63 100644 --- a/src/daemon/handlers/session-audio.ts +++ b/src/daemon/session-observability/internal/session-audio.ts @@ -8,16 +8,19 @@ import { } from '@agent-device/contracts/audio-runtime-plan'; import { emptyAudioProbeResult } from '@agent-device/contracts/audio-probe-result'; import type { RuntimeOperationFact } from '@agent-device/contracts/platform-runtime'; -import type { AudioProbeAdmissionLedger } from '../audio-probe-admission-ledger.ts'; +import type { AudioProbeAdmissionLedger } from '../../audio-probe-admission-ledger.ts'; import { adoptStartedAudioProbe, audioProbeDurableResource, finishLiveAudioProbe, -} from '../audio-probe-session-resource.ts'; -import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; -import type { SessionStore } from '../session-store.ts'; -import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; -import { errorResponse, type DaemonFailureResponse } from '../response.ts'; +} from '../../audio-probe-session-resource.ts'; +import type { + BindDeviceRuntime, + InspectDeviceRuntimeFacts, +} from '../../request-runtime-binding.ts'; +import type { SessionStore } from '../../session-store.ts'; +import type { DaemonRequest, DaemonResponse, SessionState } from '../../types.ts'; +import { errorResponse, type DaemonFailureResponse } from '../../response.ts'; type AudioParams = { req: DaemonRequest; diff --git a/src/daemon/handlers/session-network.ts b/src/daemon/session-observability/internal/session-network.ts similarity index 95% rename from src/daemon/handlers/session-network.ts rename to src/daemon/session-observability/internal/session-network.ts index 6c21b10373..8440ea57fd 100644 --- a/src/daemon/handlers/session-network.ts +++ b/src/daemon/session-observability/internal/session-network.ts @@ -5,10 +5,10 @@ import { } from '@agent-device/contracts/network-runtime-plan'; import { NETWORK_INCLUDE_MODES, type NetworkIncludeMode } from '@agent-device/kernel/contracts'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; -import type { BindDeviceRuntime } from '../request-runtime-binding.ts'; -import type { SessionStore } from '../session-store.ts'; -import type { DaemonRequest, DaemonResponse } from '../types.ts'; -import { errorResponse, type DaemonFailureResponse } from '../response.ts'; +import type { BindDeviceRuntime } from '../../request-runtime-binding.ts'; +import type { SessionStore } from '../../session-store.ts'; +import type { DaemonRequest, DaemonResponse } from '../../types.ts'; +import { errorResponse, type DaemonFailureResponse } from '../../response.ts'; const NETWORK_ACTIONS = ['dump', 'log'] as const; const NETWORK_ACTIONS_MESSAGE = `network requires ${NETWORK_ACTIONS.join(' or ')}`; diff --git a/src/daemon/handlers/session-observability.ts b/src/daemon/session-observability/internal/session-observability.ts similarity index 88% rename from src/daemon/handlers/session-observability.ts rename to src/daemon/session-observability/internal/session-observability.ts index 01eabaa923..c4fb40fcb9 100644 --- a/src/daemon/handlers/session-observability.ts +++ b/src/daemon/session-observability/internal/session-observability.ts @@ -11,28 +11,32 @@ import { import type { RuntimeOwnerRef } from '@agent-device/contracts/platform-runtime'; import { uniqueStrings } from '@agent-device/kernel/collections'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; -import { appendAppLogMarker, clearAppLogFiles, getAppLogPathMetadata } from '../app-log.ts'; -import type { AppLogAdmissionLedger } from '../app-log-admission-ledger.ts'; -import type { AudioProbeAdmissionLedger } from '../audio-probe-admission-ledger.ts'; -import type { PerfCaptureAdmissionLedger } from '../perf-capture-admission-ledger.ts'; -import { appLogResourceStore } from '../app-log-resource-store.ts'; +import { appendAppLogMarker, clearAppLogFiles, getAppLogPathMetadata } from '../../app-log.ts'; +import type { AppLogAdmissionLedger } from '../../app-log-admission-ledger.ts'; +import type { AudioProbeAdmissionLedger } from '../../audio-probe-admission-ledger.ts'; +import type { PerfCaptureAdmissionLedger } from '../../perf-capture-admission-ledger.ts'; +import { appLogResourceStore } from '../../app-log-resource-store.ts'; import { adoptStartedSessionAppLog, clearSessionAppLogFailure, finishSessionAppLog, inspectSessionAppLog, recordSessionAppLogFailure, -} from '../app-log-session-resource.ts'; -import { createNextAppLogFence } from '../app-log-start-preflight.ts'; -import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; -import type { SessionStore } from '../session-store.ts'; -import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts'; -import { errorResponse, type DaemonFailureResponse } from '../response.ts'; + type AppLogSessionSnapshot, +} from '../../app-log-session-resource.ts'; +import { createNextAppLogFence } from '../../app-log-start-preflight.ts'; +import type { + BindDeviceRuntime, + InspectDeviceRuntimeFacts, +} from '../../request-runtime-binding.ts'; +import type { SessionStore } from '../../session-store.ts'; +import type { DaemonRequest, DaemonResponse, SessionState } from '../../types.ts'; +import { errorResponse, type DaemonFailureResponse } from '../../response.ts'; import { handleAudioCommand } from './session-audio.ts'; import { handlePerfRuntimeCommand } from './session-perf-runtime.ts'; import { handleNetworkCommand } from './session-network.ts'; -type ObservabilityParams = { +export type SessionObservabilityCommandInput = { req: DaemonRequest; sessionName: string; sessionStore: SessionStore; @@ -43,7 +47,8 @@ type ObservabilityParams = { perfCaptureAdmissionLedger?: PerfCaptureAdmissionLedger; throwIfCanceled?: () => void; }; -type LogsHandlerParams = Omit & { +type ObservabilityInput = SessionObservabilityCommandInput; +type LogsHandlerParams = Omit & { session: SessionState; bindDevice: BindDeviceRuntime; appLogAdmissionLedger: AppLogAdmissionLedger; @@ -51,16 +56,7 @@ type LogsHandlerParams = Omit | (Extract & { appBundleId: string }); -type SessionLogStatus = { - active: boolean; - state: 'active' | 'recovering' | 'ended' | 'failed' | 'inactive'; - backend: LogBackend; - startedAt?: number; - failureCode?: string; - failureMessage?: string; - hint?: string; - notes?: string[]; -}; +type SessionLogStatus = AppLogSessionSnapshot & { backend: LogBackend; notes?: string[] }; function resolveSessionLogStatus( session: SessionState, @@ -80,19 +76,14 @@ function resolveSessionLogStatus( function buildAppLogFailureNote(failure: AppLogFailure): string { return failure.hint ? `${failure.message} ${failure.hint}` : failure.message; } - function buildAppLogStateNotes(state: SessionLogStatus['state']): string[] | undefined { - if (state === 'failed') { - return [ - 'The app log stream process exited with an error. Run logs doctor for backend diagnostics.', - ]; - } - if (state === 'ended') { - return [ - 'The app log stream process ended. Run logs clear --restart before the next capture window.', - ]; - } - return undefined; + return state === 'failed' + ? ['The app log stream process exited with an error. Run logs doctor for backend diagnostics.'] + : state === 'ended' + ? [ + 'The app log stream process ended. Run logs clear --restart before the next capture window.', + ] + : undefined; } function mergeLogDoctorNotes( @@ -103,7 +94,7 @@ function mergeLogDoctorNotes( } export async function handleSessionObservabilityCommands( - params: ObservabilityParams, + params: SessionObservabilityCommandInput, ): Promise { const { req } = params; @@ -129,7 +120,7 @@ export async function handleSessionObservabilityCommands( return null; } -async function handleEventsCommand(params: ObservabilityParams): Promise { +async function handleEventsCommand(params: ObservabilityInput): Promise { const { req, sessionName, sessionStore } = params; try { await sessionStore.flushEvents(sessionName); @@ -149,7 +140,7 @@ async function handleEventsCommand(params: ObservabilityParams): Promise { +async function handleLogsCommand(params: ObservabilityInput): Promise { const { req, sessionName, sessionStore } = params; const session = sessionStore.get(sessionName); if (!session) { @@ -412,7 +403,7 @@ async function startSessionAppLog( return { ok: false, error: normalized }; } } -function requireAudioSeams(params: ObservabilityParams): Parameters[0] { +function requireAudioSeams(params: ObservabilityInput): Parameters[0] { if (!params.bindDevice || !params.inspectFacts) { throw new AppError('COMMAND_FAILED', 'Device runtime gateway is not configured', { reason: 'runtime-gateway-missing', @@ -435,7 +426,7 @@ function requireAudioSeams(params: ObservabilityParams): Parameters