From 4cca76f208f6a240d420aff5cef877b24e11ec5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 5 Sep 2026 18:57:04 +0200 Subject: [PATCH] test(daemon): one typed conformance helper for the daemon runtime suites Extracts the shared exact-owner-unavailable admission mechanics repeated across back/home/app-switcher/focus/viewport/orientation/tv-remote/type/diff runtime test suites into runtime-binding-conformance.ts. Each suite keeps its own expected table (device, unavailable fact, and any owner-specific refusal wording); a new cross-module completeness test checks the shared table against the command registry's runtime-use declarations in both directions, including the newly-registered swipe command. --- .../__tests__/app-switcher-runtime.test.ts | 25 ++ src/daemon/__tests__/back-runtime.test.ts | 23 +- src/daemon/__tests__/focus-runtime.test.ts | 24 +- src/daemon/__tests__/home-runtime.test.ts | 18 +- .../__tests__/orientation-runtime.test.ts | 22 +- ...e-binding-conformance-completeness.test.ts | 73 ++++++ .../__tests__/runtime-binding-conformance.ts | 232 ++++++++++++++++++ .../__tests__/snapshot-diff-runtime.test.ts | 51 ++-- .../__tests__/tv-remote-runtime.test.ts | 55 +---- .../__tests__/type-text-runtime.test.ts | 23 +- src/daemon/__tests__/viewport-runtime.test.ts | 54 +--- 11 files changed, 381 insertions(+), 219 deletions(-) create mode 100644 src/daemon/__tests__/app-switcher-runtime.test.ts create mode 100644 src/daemon/__tests__/runtime-binding-conformance-completeness.test.ts create mode 100644 src/daemon/__tests__/runtime-binding-conformance.ts diff --git a/src/daemon/__tests__/app-switcher-runtime.test.ts b/src/daemon/__tests__/app-switcher-runtime.test.ts new file mode 100644 index 0000000000..8777c7fb86 --- /dev/null +++ b/src/daemon/__tests__/app-switcher-runtime.test.ts @@ -0,0 +1,25 @@ +import { test } from 'vitest'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; + +const appleDevice = { + id: 'app-switcher-runtime-ios-simulator', + name: 'iPhone', + platform: 'apple', + appleOs: 'ios', + kind: 'simulator', + target: 'mobile', + booted: true, +} as const; +const unavailable = Object.freeze({ + available: false, + reason: 'unsupported-device-kind' as const, + hint: 'app-switcher is supported on Apple simulators and physical devices.', +}); + +test('rejects an unavailable exact-owner fact before binding', async () => { + await expectRefusesUnavailableExactOwnerFact({ + command: 'app-switcher', + device: appleDevice, + unavailable, + }); +}); diff --git a/src/daemon/__tests__/back-runtime.test.ts b/src/daemon/__tests__/back-runtime.test.ts index b7d3cdb953..cbd9b59021 100644 --- a/src/daemon/__tests__/back-runtime.test.ts +++ b/src/daemon/__tests__/back-runtime.test.ts @@ -21,6 +21,7 @@ import { activateCompleteRefFrame } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import type { GenericPlatformExecutionParams } from '../request-generic-dispatch.ts'; import { resolveBoundBackRuntime } from '../back-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; // File-scoped id, not the widely shared 'ios-simulator' literal: this owner binding's @@ -125,27 +126,11 @@ test('forwards the requested back mode from the resolved dispatch context', asyn }); test('rejects an unavailable exact-owner fact before binding', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundBackRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'back', device: appleDevice, - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, - }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'back is not supported on this device', - hint: unavailable.hint, - }, - }, + unavailable, }); - expect(harness.inspectFacts).toHaveBeenCalledTimes(1); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); test('request router joins back admission to execution, recording, and ref invalidation', async () => { diff --git a/src/daemon/__tests__/focus-runtime.test.ts b/src/daemon/__tests__/focus-runtime.test.ts index f8e86b0f17..fa328af308 100644 --- a/src/daemon/__tests__/focus-runtime.test.ts +++ b/src/daemon/__tests__/focus-runtime.test.ts @@ -21,6 +21,7 @@ import { activateCompleteRefFrame } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import type { GenericPlatformExecutionParams } from '../request-generic-dispatch.ts'; import { readFocusPoint, resolveBoundFocusRuntime } from '../focus-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; const appleDevice = { @@ -176,28 +177,11 @@ test('parses coordinates exactly as the retired leaf did', () => { }); test('rejects an unavailable exact-owner fact before binding', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundFocusRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'focus', device: appleDevice, - positionals: ['40', '90'], - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, + unavailable, }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'focus is not supported on this device', - hint: unavailable.hint, - }, - }, - }); - expect(harness.inspectFacts).toHaveBeenCalledTimes(1); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); test('request router joins focus admission to execution, recording, and ref invalidation', async () => { diff --git a/src/daemon/__tests__/home-runtime.test.ts b/src/daemon/__tests__/home-runtime.test.ts index 5409ffba04..e3710cecd2 100644 --- a/src/daemon/__tests__/home-runtime.test.ts +++ b/src/daemon/__tests__/home-runtime.test.ts @@ -21,6 +21,7 @@ import { activateCompleteRefFrame } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import type { GenericPlatformExecutionParams } from '../request-generic-dispatch.ts'; import { resolveBoundHomeRuntime } from '../home-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; const macOsDevice = { @@ -101,22 +102,11 @@ test('resolves one admitted binding and drives one home navigation', async () => }); test('rejects an unavailable exact-owner fact before binding (macOS has no springboard home)', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundHomeRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'home', device: macOsDevice, - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, - }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { code: 'UNSUPPORTED_OPERATION', message: 'home is not supported on this device' }, - }, + unavailable, }); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); test('request router joins home admission to execution, recording, and ref invalidation', async () => { diff --git a/src/daemon/__tests__/orientation-runtime.test.ts b/src/daemon/__tests__/orientation-runtime.test.ts index 21c514fae8..00fed20421 100644 --- a/src/daemon/__tests__/orientation-runtime.test.ts +++ b/src/daemon/__tests__/orientation-runtime.test.ts @@ -46,6 +46,7 @@ import { readRequestedOrientation, resolveBoundOrientationRuntime, } from '../orientation-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; import { androidObservationFixture } from './android-observation-fixture.ts'; @@ -175,26 +176,11 @@ test('rejects an invalid rotation before inspection or binding', async () => { }); test('rejects an unavailable exact-owner fact before binding', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundOrientationRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'orientation', device: testDevice, - positionals: ['landscape-left'], - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, + unavailable, }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'orientation is not supported on this device', - }, - }, - }); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); test('request router joins orientation admission to execution and ref invalidation', async () => { diff --git a/src/daemon/__tests__/runtime-binding-conformance-completeness.test.ts b/src/daemon/__tests__/runtime-binding-conformance-completeness.test.ts new file mode 100644 index 0000000000..1ba59080f8 --- /dev/null +++ b/src/daemon/__tests__/runtime-binding-conformance-completeness.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from 'vitest'; +import { + commandDescriptors, + commandRuntimeUseRequirements, +} from '../../core/command-descriptor/registry.ts'; +import { + conformedRuntimeBindings, + refusedRuntimeOperation, + refuseUnavailableExactOwnerFact, + type ConformedRuntimeCommand, +} from './runtime-binding-conformance.ts'; + +/** + * The command registry's `platformExecution` declarations are the production enumeration of + * daemon runtime bindings: every device-runtime descriptor names the uses its route admits, and + * `admitRuntimeOperations` is one seam with no table of its own. The generic and interaction routes + * admit through per-command `src/daemon/-runtime.ts` resolvers, so every single-use + * descriptor on those routes owes the family its conformance entry; the other direction holds any + * entry to a cell its descriptor actually declares. + */ +const CONFORMED_ROUTES = new Set(['generic', 'interaction']); + +function singleUseConformedRouteCommands(): string[] { + return commandDescriptors + .filter((descriptor) => { + const route = 'daemon' in descriptor ? descriptor.daemon?.route : undefined; + return route !== undefined && CONFORMED_ROUTES.has(route); + }) + .map((descriptor) => descriptor.name) + .filter((command) => commandRuntimeUseRequirements(command)?.length === 1) + .sort(); +} + +const conformedCommands = Object.keys(conformedRuntimeBindings) as ConformedRuntimeCommand[]; + +test('every single-use generic or interaction route descriptor has a conformance entry', () => { + const missing = singleUseConformedRouteCommands().filter( + (command) => !conformedCommands.includes(command as ConformedRuntimeCommand), + ); + + expect(missing).toEqual([]); +}); + +test('every conformance entry refuses on a cell its registry descriptor declares', () => { + const undeclared = conformedCommands.filter((command) => { + const declared = commandRuntimeUseRequirements(command)?.flat() ?? []; + return !declared.includes(refusedRuntimeOperation(command)); + }); + + expect(undeclared).toEqual([]); +}); + +const conformanceDevice = { + id: 'runtime-binding-conformance-device', + name: 'Pixel', + platform: 'android', + kind: 'emulator', + target: 'mobile', + booted: true, +} as const; + +test.each(conformedCommands)( + '%s refuses its unavailable declared cell before binding', + async (command) => { + const response = await refuseUnavailableExactOwnerFact({ + command, + device: conformanceDevice, + unavailable: { available: false, reason: 'owner-capability-missing' }, + }); + + expect(response.error.code).toBe('UNSUPPORTED_OPERATION'); + }, +); diff --git a/src/daemon/__tests__/runtime-binding-conformance.ts b/src/daemon/__tests__/runtime-binding-conformance.ts new file mode 100644 index 0000000000..7116646179 --- /dev/null +++ b/src/daemon/__tests__/runtime-binding-conformance.ts @@ -0,0 +1,232 @@ +import { expect, vi } from 'vitest'; +import { + localRuntimeOwner, + type RuntimeFacts, + type RuntimeOperationKey, + type RuntimeOperationUnavailability, +} from '@agent-device/contracts/platform-runtime'; +import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations'; +import type { DeviceInfo } from '@agent-device/kernel/device'; +import { commandRuntimeUseRequirements } from '../../core/command-descriptor/registry.ts'; +import { createUnavailableRuntimeFactsForTest } from '../../__tests__/test-utils/runtime-operation-facts.ts'; +import { makeSession } from '../../__tests__/test-utils/session-factories.ts'; +import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; +import { resolveBoundAppSwitcherRuntime } from '../app-switcher-runtime.ts'; +import { resolveBoundBackRuntime } from '../back-runtime.ts'; +import { resolveBoundFocusRuntime } from '../focus-runtime.ts'; +import { resolveBoundGestureRuntime } from '../gesture-runtime.ts'; +import { resolveBoundHomeRuntime } from '../home-runtime.ts'; +import { resolveBoundOrientationRuntime } from '../orientation-runtime.ts'; +import type { ResolvedGenericExecution } from '../request-generic-dispatch.ts'; +import type { + BindDeviceRuntime, + InspectDeviceRuntimeFacts, + RuntimeAdmissionBindings, +} from '../request-runtime-binding.ts'; +import type { DaemonFailureResponse } from '../response.ts'; +import { dispatchSnapshotDiffViaRuntime } from '../snapshot-diff-runtime.ts'; +import { resolveBoundTvRemoteRuntime } from '../tv-remote-runtime.ts'; +import { resolveBoundTypeTextRuntime } from '../type-text-runtime.ts'; +import { resolveBoundViewportRuntime } from '../viewport-runtime.ts'; + +type RuntimeOperation = RuntimeOperationKey; + +/** What an admission-first route reports: a refusal carries the response and no binding. */ +export type RefusableResolution = + | Readonly<{ ok: false; response: DaemonFailureResponse }> + | Readonly<{ ok: true }>; + +type ConformedRuntimeBinding = Readonly<{ + /** Resolves the route against the given admission seams with the inputs its parser accepts. */ + resolve: ( + device: DeviceInfo, + bindings: Required, + ) => Promise; + /** + * A plan route admits several declared cells in order; name the one this table refuses on and + * the ones the plan admits before reaching it. A single-use route derives its one cell from the + * command registry instead. + */ + plan?: Readonly<{ refused: RuntimeOperation; admitted: readonly RuntimeOperation[] }>; +}>; + +const available = Object.freeze({ available: true } as const); + +/** + * A generic-route resolver's failure branch carries the wire-level `DaemonResponse` union rather + * than the narrower `DaemonFailureResponse` its `ok: false` already guarantees. This proves that + * guarantee once (throwing if a resolver ever broke it) instead of casting it away at each call + * site below. + */ +function refusable(resolved: ResolvedGenericExecution): RefusableResolution { + if (resolved.ok) return { ok: true }; + if (resolved.response.ok) { + throw new Error('a refusal reported an ok response'); + } + return { ok: false, response: resolved.response }; +} + +/** + * Every daemon route whose refusal on an unavailable exact-owner fact is proven through the shared + * conformance helper, keyed by registry command name. `runtime-binding-conformance-completeness` + * checks this table against the registry's runtime-use declarations in both directions. + */ +export const conformedRuntimeBindings = { + back: { + resolve: async (device, bindings) => + refusable(await resolveBoundBackRuntime({ device, ...bindings })), + }, + home: { + resolve: async (device, bindings) => + refusable(await resolveBoundHomeRuntime({ device, ...bindings })), + }, + 'app-switcher': { + resolve: async (device, bindings) => + refusable(await resolveBoundAppSwitcherRuntime({ device, ...bindings })), + }, + focus: { + resolve: async (device, bindings) => + refusable(await resolveBoundFocusRuntime({ device, positionals: ['40', '90'], ...bindings })), + }, + viewport: { + resolve: async (device, bindings) => + refusable( + await resolveBoundViewportRuntime({ device, positionals: ['1280', '900'], ...bindings }), + ), + }, + orientation: { + resolve: async (device, bindings) => + refusable( + await resolveBoundOrientationRuntime({ + device, + positionals: ['landscape-left'], + ...bindings, + }), + ), + }, + 'tv-remote': { + resolve: async (device, bindings) => + refusable(await resolveBoundTvRemoteRuntime({ device, positionals: ['down'], ...bindings })), + }, + type: { + resolve: (device, bindings) => resolveBoundTypeTextRuntime({ device, ...bindings }), + }, + swipe: { + // A swipe always normalizes to the coordinate-fling plan tier (R54), which requires both + // cells in order: `performGesturePlan` admitted, then `captureSnapshot` refused. + plan: { refused: 'captureSnapshot', admitted: ['performGesturePlan'] }, + resolve: (device, bindings) => + resolveBoundGestureRuntime({ + device, + input: { intent: 'fling', from: { x: 10, y: 500 }, to: { x: 10, y: 100 } }, + ...bindings, + }), + }, + diff: { + plan: { refused: 'captureSnapshotWithCustomActions', admitted: ['captureSnapshot'] }, + resolve: async (device, bindings) => { + const sessionStore = makeSessionStore('agent-device-runtime-binding-conformance-'); + const session = makeSession('diff-runtime', { device, appBundleId: 'com.example.app' }); + sessionStore.set(session.name, session); + const response = await dispatchSnapshotDiffViaRuntime({ + req: { + command: 'diff', + positionals: ['snapshot'], + token: 't', + session: session.name, + flags: { snapshotCustomActions: true }, + }, + sessionName: session.name, + logPath: '/tmp/diff-runtime.log', + sessionStore, + ...bindings, + }); + return response.ok ? { ok: true } : { ok: false, response }; + }, + }, +} satisfies Record; + +export type ConformedRuntimeCommand = keyof typeof conformedRuntimeBindings; + +/** The one cell a route refuses on: the plan's named cell, or the registry's single declaration. */ +export function refusedRuntimeOperation(command: ConformedRuntimeCommand): RuntimeOperation { + const binding: ConformedRuntimeBinding = conformedRuntimeBindings[command]; + if (binding.plan) return binding.plan.refused; + const uses = commandRuntimeUseRequirements(command); + const required = uses?.length === 1 ? uses[0] : undefined; + if (required?.length !== 1) { + throw new Error( + `${command} declares no single runtime operation in the command registry; name the refused cell in its conformance plan`, + ); + } + return required[0] as RuntimeOperation; +} + +export type UnavailableExactOwnerFactCase = Readonly<{ + command: ConformedRuntimeCommand; + device: DeviceInfo; + unavailable: RuntimeOperationUnavailability; +}>; + +/** + * Drives the route with exactly one exact-owner cell unavailable and returns the refusal it + * reported. Facts are inspected once and the device is never bound: admission refuses before any + * owner is reached. + */ +export async function refuseUnavailableExactOwnerFact( + testCase: UnavailableExactOwnerFactCase, +): Promise { + const binding: ConformedRuntimeBinding = conformedRuntimeBindings[testCase.command]; + const base = createUnavailableRuntimeFactsForTest( + testCase.device, + localRuntimeOwner(testCase.device.platform), + ); + const admitted = Object.fromEntries( + (binding.plan?.admitted ?? []).map((operation) => [operation, available]), + ); + const facts: RuntimeFacts = { + device: base.device, + operations: { + ...base.operations, + ...admitted, + [refusedRuntimeOperation(testCase.command)]: testCase.unavailable, + }, + }; + const inspectFacts: InspectDeviceRuntimeFacts = vi.fn(async () => facts); + const bindDevice = vi.fn() as unknown as BindDeviceRuntime; + + const resolved = await binding.resolve(testCase.device, { inspectFacts, bindDevice }); + + expect(inspectFacts).toHaveBeenCalledTimes(1); + expect(inspectFacts).toHaveBeenCalledWith(testCase.device); + expect(bindDevice).not.toHaveBeenCalled(); + expect(resolved.ok).toBe(false); + if (resolved.ok) throw new Error(`${testCase.command} admitted an unavailable exact-owner cell`); + return resolved.response; +} + +/** The wording `admitRuntimeOperations` gives every single-use route it refuses. */ +function unsupportedOperationRefusal( + command: ConformedRuntimeCommand, + unavailable: RuntimeOperationUnavailability, +): DaemonFailureResponse['error'] { + return { + code: 'UNSUPPORTED_OPERATION', + message: `${command} is not supported on this device`, + ...(unavailable.hint ? { hint: unavailable.hint } : {}), + }; +} + +/** + * The family conformance check: the route refuses before binding and reports exactly the + * expected error. A route that shapes its own refusal passes it as `refusal`; the rest inherit the + * shared unsupported-operation wording. + */ +export async function expectRefusesUnavailableExactOwnerFact( + testCase: UnavailableExactOwnerFactCase & Readonly<{ refusal?: DaemonFailureResponse['error'] }>, +): Promise { + const response = await refuseUnavailableExactOwnerFact(testCase); + expect(response.error).toEqual( + testCase.refusal ?? unsupportedOperationRefusal(testCase.command, testCase.unavailable), + ); +} diff --git a/src/daemon/__tests__/snapshot-diff-runtime.test.ts b/src/daemon/__tests__/snapshot-diff-runtime.test.ts index 34719247a2..cee7f0201e 100644 --- a/src/daemon/__tests__/snapshot-diff-runtime.test.ts +++ b/src/daemon/__tests__/snapshot-diff-runtime.test.ts @@ -16,6 +16,7 @@ import { makeAndroidSession } from '../../__tests__/test-utils/session-factories import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import { dispatchSnapshotDiffViaRuntime } from '../snapshot-diff-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { unavailableDeviceRuntimeGateway } from './test-device-runtime-gateway.ts'; const available = Object.freeze({ available: true } as const); @@ -37,11 +38,7 @@ type SnapshotOperationName = | 'captureSnapshotWithCustomActions' | 'captureSnapshotWithoutActiveApp'; -async function runtimeHarness(params: { - captures?: readonly SnapshotResult[]; - customActionsAvailable?: boolean; - withoutActiveAppAvailable?: boolean; -}) { +async function runtimeHarness(params: { captures?: readonly SnapshotResult[] }) { const session = makeAndroidSession('diff-runtime'); const device = session.device; const baseFacts = await unavailableDeviceRuntimeGateway.inspectFacts(device); @@ -51,8 +48,8 @@ async function runtimeHarness(params: { ...baseFacts.operations, ...snapshotRuntimeOperationFacts({ capture: available, - customActions: params.customActionsAvailable === false ? unavailable : available, - withoutActiveApp: params.withoutActiveAppAvailable === false ? unavailable : available, + customActions: available, + withoutActiveApp: available, }), }, }; @@ -161,38 +158,18 @@ test.each([ ); test('rejects an unavailable exact-owner fact before binding', async () => { - const harness = await runtimeHarness({ customActionsAvailable: false }); - const sessionStore = makeSessionStore('agent-device-diff-runtime-unavailable-'); - sessionStore.set('diff-runtime', { - ...harness.session, - appBundleId: 'com.example.app', - }); - - const response = await dispatchSnapshotDiffViaRuntime({ - req: { - command: 'diff', - positionals: ['snapshot'], - token: 't', - session: 'diff-runtime', - flags: { snapshotCustomActions: true }, + await expectRefusesUnavailableExactOwnerFact({ + command: 'diff', + device: makeAndroidSession('diff-runtime').device, + unavailable, + refusal: { + code: 'UNSUPPORTED_OPERATION', + message: + '--actions requires an iOS simulator: custom actions are read through the private accessibility snapshot backend, which android/emulator targets do not have.', + hint: 'Re-run without --actions, or target an iOS simulator.', + details: { reason: 'owner-capability-missing' }, }, - sessionName: 'diff-runtime', - logPath: '/tmp/diff-runtime.log', - sessionStore, - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, }); - - expect(response.ok).toBe(false); - expect(harness.inspected).toHaveLength(1); - expect(harness.boundUses).toEqual([]); - expect(response.ok ? undefined : response.error.details?.reason).toBe('owner-capability-missing'); - expect(response.ok ? undefined : response.error.message).toBe( - '--actions requires an iOS simulator: custom actions are read through the private accessibility snapshot backend, which android/emulator targets do not have.', - ); - expect(response.ok ? undefined : response.error.hint).toBe( - 'Re-run without --actions, or target an iOS simulator.', - ); }); test('preserves initialized, unchanged, and changed diff results through one bound capture', async () => { diff --git a/src/daemon/__tests__/tv-remote-runtime.test.ts b/src/daemon/__tests__/tv-remote-runtime.test.ts index a49f25d207..727974449a 100644 --- a/src/daemon/__tests__/tv-remote-runtime.test.ts +++ b/src/daemon/__tests__/tv-remote-runtime.test.ts @@ -21,6 +21,7 @@ import { activateCompleteRefFrame } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import type { GenericPlatformExecutionParams } from '../request-generic-dispatch.ts'; import { resolveBoundTvRemoteRuntime } from '../tv-remote-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; const vegaVvd = { @@ -158,27 +159,11 @@ test('rejects a missing button before inspection or binding', async () => { }); test('rejects an unavailable exact-owner fact before binding', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundTvRemoteRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'tv-remote', device: vegaVvd, - positionals: ['down'], - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, - }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'tv-remote is not supported on this device', - hint: unavailable.hint, - }, - }, + unavailable, }); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); // Pins the wire response for a non-TV target on the two platforms that keep their own capability @@ -216,37 +201,11 @@ test.each([ ])( 'rejects %s with its owner-specific hint, generic message preserved', async (_name, device, hint) => { - const fact: RuntimeOperationFact = Object.freeze({ - available: false, - reason: 'unsupported-device-kind', - hint, - }); - const facts: RuntimeFacts = { - device: { ...deviceShape(device), providerMode: 'local' }, - operations: { tvRemote: fact } as RuntimeFacts['operations'], - }; - const inspectFacts: InspectDeviceRuntimeFacts = vi.fn(async () => facts); - const bindDevice = vi.fn() as unknown as BindDeviceRuntime; - - const resolved = await resolveBoundTvRemoteRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'tv-remote', device, - positionals: ['down'], - inspectFacts, - bindDevice, - }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'tv-remote is not supported on this device', - hint, - }, - }, + unavailable: { available: false, reason: 'unsupported-device-kind', hint }, }); - expect(bindDevice).not.toHaveBeenCalled(); }, ); diff --git a/src/daemon/__tests__/type-text-runtime.test.ts b/src/daemon/__tests__/type-text-runtime.test.ts index a6ff457b32..c0bd09e0f4 100644 --- a/src/daemon/__tests__/type-text-runtime.test.ts +++ b/src/daemon/__tests__/type-text-runtime.test.ts @@ -17,6 +17,7 @@ import { import { deviceShape } from '@agent-device/kernel/device'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import { resolveBoundTypeTextRuntime } from '../type-text-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; const appleDevice = { id: 'ios-simulator', @@ -150,25 +151,9 @@ test('parses exactly as the retired leaf did: refs rejected, spaces joined, dela }); test('rejects an unavailable exact-owner fact before binding, with the owner hint', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundTypeTextRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'type', device: appleDevice, - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, + unavailable, }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'type is not supported on this device', - hint: unavailable.hint, - }, - }, - }); - expect(harness.inspectFacts).toHaveBeenCalledTimes(1); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); diff --git a/src/daemon/__tests__/viewport-runtime.test.ts b/src/daemon/__tests__/viewport-runtime.test.ts index 09170bc407..985a742dcc 100644 --- a/src/daemon/__tests__/viewport-runtime.test.ts +++ b/src/daemon/__tests__/viewport-runtime.test.ts @@ -21,6 +21,7 @@ import { activateCompleteRefFrame } from '../ref-frame.ts'; import type { BindDeviceRuntime, InspectDeviceRuntimeFacts } from '../request-runtime-binding.ts'; import type { GenericPlatformExecutionParams } from '../request-generic-dispatch.ts'; import { resolveBoundViewportRuntime } from '../viewport-runtime.ts'; +import { expectRefusesUnavailableExactOwnerFact } from './runtime-binding-conformance.ts'; import { createRequestHandler } from './test-device-runtime-gateway.ts'; const webDevice = { @@ -140,58 +141,23 @@ test('rejects invalid dimensions before inspection or binding', async () => { }); test('rejects an unavailable exact-owner fact before binding', async () => { - const harness = runtimeHarness(unavailable); - - const resolved = await resolveBoundViewportRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'viewport', device: webDevice, - positionals: ['1280', '900'], - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, + unavailable, }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'viewport is not supported on this device', - hint: unavailable.hint, - }, - }, - }); - expect(harness.inspectFacts).toHaveBeenCalledTimes(1); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); test('preserves the Apple viewport recovery hint through admission', async () => { - const hint = - 'viewport resizes web targets only (--platform web). Apple screen geometry is fixed by the selected simulator or device type — open a different simulator to test another screen size.'; - const harness = runtimeHarness( - { available: false, reason: 'unsupported-platform-leaf', hint }, - appleDevice, - ); - - const resolved = await resolveBoundViewportRuntime({ + await expectRefusesUnavailableExactOwnerFact({ + command: 'viewport', device: appleDevice, - positionals: ['1280', '900'], - inspectFacts: harness.inspectFacts, - bindDevice: harness.bindDevice, - }); - - expect(resolved).toEqual({ - ok: false, - response: { - ok: false, - error: { - code: 'UNSUPPORTED_OPERATION', - message: 'viewport is not supported on this device', - hint, - }, + unavailable: { + available: false, + reason: 'unsupported-platform-leaf', + hint: 'viewport resizes web targets only (--platform web). Apple screen geometry is fixed by the selected simulator or device type — open a different simulator to test another screen size.', }, }); - expect(harness.inspectFacts).toHaveBeenCalledOnce(); - expect(harness.bindDevice).not.toHaveBeenCalled(); }); test('request router joins viewport admission to execution, recording, and ref invalidation', async () => {