Skip to content

Commit 848c6ea

Browse files
authored
test: derive unavailable runtime fixtures (#2084)
1 parent 7b48531 commit 848c6ea

7 files changed

Lines changed: 184 additions & 267 deletions
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { localRuntimeOwner, providerRuntimeOwner } from '@agent-device/contracts/platform-runtime';
3+
import { IOS_SIMULATOR } from './device-fixtures.ts';
4+
import { createUnavailableRuntimeFactsForTest } from './runtime-operation-facts.ts';
5+
6+
describe('createUnavailableRuntimeFactsForTest', () => {
7+
it('derives the complete operation surface as unavailable', () => {
8+
const facts = createUnavailableRuntimeFactsForTest(IOS_SIMULATOR, localRuntimeOwner('apple'));
9+
10+
expect(Object.values(facts.operations).every((fact) => !fact.available)).toBe(true);
11+
expect(facts.operations).toMatchObject({
12+
captureScreenshot: { available: false, reason: 'owner-capability-missing' },
13+
typeText: { available: false, reason: 'owner-capability-missing' },
14+
finalizeApplicationClose: { available: false, reason: 'owner-capability-missing' },
15+
});
16+
expect(facts.device.providerMode).toBe('local');
17+
});
18+
19+
it('preserves an exact provider gap across every derived cell', () => {
20+
const unsupportedProvider = {
21+
available: false,
22+
reason: 'unsupported-provider-mode',
23+
} as const;
24+
const facts = createUnavailableRuntimeFactsForTest(
25+
IOS_SIMULATOR,
26+
providerRuntimeOwner('test', 'fixtures'),
27+
unsupportedProvider,
28+
);
29+
30+
expect(
31+
Object.values(facts.operations).every(
32+
(fact) => !fact.available && fact.reason === 'unsupported-provider-mode',
33+
),
34+
).toBe(true);
35+
expect(facts.device.providerMode).toBe('provider-runtime');
36+
});
37+
});

src/__tests__/test-utils/runtime-operation-facts.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import { applicationLifecycleOperationFacts } from '@agent-device/contracts/appl
22
import { audioProbeRuntimeOperationFacts } from '@agent-device/contracts/audio-probe-runtime';
33
import { elementTextRuntimeOperationFacts } from '@agent-device/contracts/element-text-runtime';
44
import { gestureRuntimeOperationFacts } from '@agent-device/contracts/gesture-runtime';
5-
import type { RuntimeOperationFact } from '@agent-device/contracts/platform-runtime';
5+
import type {
6+
RuntimeOperationUnavailability,
7+
RuntimeOwnerRef,
8+
} from '@agent-device/contracts/platform-runtime';
9+
import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable';
10+
import type { DeviceInfo } from '@agent-device/kernel/device';
611
import { screenshotRuntimeOperationFacts } from '@agent-device/contracts/screenshot-runtime';
712
import { scrollRuntimeOperationFacts } from '@agent-device/contracts/scroll-runtime';
813
import { snapshotRuntimeOperationFacts } from '@agent-device/contracts/snapshot-runtime';
914
import { touchRuntimeOperationFacts } from '@agent-device/contracts/touch-runtime';
1015
import { perfRuntimeOperationFacts } from '@agent-device/contracts/perf-runtime';
1116

12-
const unavailable: RuntimeOperationFact = Object.freeze({
17+
const unavailable: RuntimeOperationUnavailability = Object.freeze({
1318
available: false,
1419
reason: 'owner-capability-missing',
1520
});
@@ -95,3 +100,57 @@ export const unavailableApplicationLifecycleOperationFacts = applicationLifecycl
95100
prepareAppleRunner: unavailable,
96101
configureProviderPortReverse: unavailable,
97102
});
103+
104+
/**
105+
* Complete fail-closed facts for tests that exercise only a small runtime surface.
106+
*
107+
* Production owners must classify each required family explicitly. Tests instead start from this
108+
* single complete record and override only the cells whose behavior they are proving.
109+
*/
110+
export function createUnavailableRuntimeFactsForTest(
111+
device: DeviceInfo,
112+
owner: RuntimeOwnerRef,
113+
fact: RuntimeOperationUnavailability = unavailable,
114+
) {
115+
return createUnavailablePlatformRuntimeFacts(device, owner, {
116+
appLog: fact,
117+
network: fact,
118+
screenshot: fact,
119+
viewport: fact,
120+
focus: fact,
121+
gesture: fact,
122+
scroll: fact,
123+
typeText: fact,
124+
touch: fact,
125+
elementText: fact,
126+
back: fact,
127+
home: fact,
128+
orientation: fact,
129+
tvRemote: fact,
130+
keyboardStatus: fact,
131+
keyboardDismiss: fact,
132+
keyboardEnter: fact,
133+
readClipboard: fact,
134+
writeClipboard: fact,
135+
appSwitcher: fact,
136+
triggerAppEvent: fact,
137+
setSetting: fact,
138+
readAlert: fact,
139+
awaitAlert: fact,
140+
acceptAlert: fact,
141+
dismissAlert: fact,
142+
audioProbeCapture: fact,
143+
audioProbeQuery: fact,
144+
lifecycle: applicationLifecycleOperationFacts({
145+
resolveOpenTarget: fact,
146+
prepareApplicationOpen: fact,
147+
openApplication: fact,
148+
applyRuntimeHints: fact,
149+
clearRuntimeHints: fact,
150+
closeApplication: fact,
151+
finalizeApplicationClose: fact,
152+
prepareAppleRunner: fact,
153+
configureProviderPortReverse: fact,
154+
}),
155+
});
156+
}

src/daemon/__tests__/test-device-runtime-gateway.ts

Lines changed: 14 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
import type { BindDeviceRuntime, BindExactDeviceRuntime } from '../request-runtime-binding.ts';
2020
import type { DeviceInfo } from '@agent-device/kernel/device';
2121
import { applicationLifecycleRuntimeFixture } from './application-lifecycle-runtime-fixture.ts';
22-
import { unavailableDeploymentSnapshotAndShutdownOperationFacts } from '../../__tests__/test-utils/runtime-operation-facts.ts';
22+
import { createUnavailableRuntimeFactsForTest } from '../../__tests__/test-utils/runtime-operation-facts.ts';
2323
import { withClientReplayScriptSources } from '../../__tests__/test-utils/replay-script-source.ts';
2424
import type { DaemonInvokeFn } from '../types.ts';
2525
import { clearAndroidObservationFixture } from './android-observation-fixture.ts';
@@ -47,36 +47,14 @@ function lifecycleFactsForTest(device: DeviceInfo): ApplicationLifecycleOperatio
4747

4848
async function lifecycleBindingForTest(device: DeviceInfo) {
4949
const lifecycleFacts = lifecycleFactsForTest(device);
50+
const base = createUnavailableRuntimeFactsForTest(device, localRuntimeOwner(device.platform));
5051
return {
5152
device,
5253
owner: localRuntimeOwner(device.platform),
5354
facts: {
54-
device: {
55-
family: device.platform,
56-
kind: device.kind,
57-
providerMode: 'local' as const,
58-
...(device.appleOs === undefined ? {} : { appleOs: device.appleOs }),
59-
...(device.target === undefined ? {} : { target: device.target }),
60-
...(device.iosPhysicalDeviceBackend === undefined
61-
? {}
62-
: { iosPhysicalDeviceBackend: device.iosPhysicalDeviceBackend }),
63-
},
55+
device: base.device,
6456
operations: {
65-
...unavailableDeploymentSnapshotAndShutdownOperationFacts,
66-
appLogInspect: unavailable,
67-
appLogDoctor: unavailable,
68-
appLogStart: unavailable,
69-
appLogReattach: unavailable,
70-
appLogCleanup: unavailable,
71-
appState: unavailable,
72-
networkDump: unavailable,
73-
screenRecordingStart: unavailable,
74-
screenRecordingReattach: unavailable,
75-
screenRecordingCleanup: unavailable,
76-
ensureReady: unavailable,
77-
bootTarget: unavailable,
78-
bootTargetHeadless: unavailable,
79-
listApps: unavailable,
57+
...base.operations,
8058
...lifecycleFacts,
8159
...admittedGestureFamilyFacts,
8260
...admittedSystemFamilyFacts,
@@ -109,52 +87,16 @@ export const lifecycleDeviceRuntimeGateway: DeviceRuntimeGateway<PlatformRuntime
10987
export const unavailableDeviceRuntimeGateway: DeviceRuntimeGateway<PlatformRuntimeOperations> =
11088
Object.freeze({
11189
inspectFacts: async (device) => (await unavailableBinding(device)).facts,
112-
bind: async ({ device }) => ({
113-
device,
114-
owner: localRuntimeOwner(device.platform),
115-
facts: {
116-
device: {
117-
family: device.platform,
118-
kind: device.kind,
119-
providerMode: 'local',
120-
...(device.appleOs === undefined ? {} : { appleOs: device.appleOs }),
121-
...(device.target === undefined ? {} : { target: device.target }),
122-
...(device.iosPhysicalDeviceBackend === undefined
123-
? {}
124-
: { iosPhysicalDeviceBackend: device.iosPhysicalDeviceBackend }),
125-
},
126-
operations: {
127-
...unavailableDeploymentSnapshotAndShutdownOperationFacts,
128-
appLogInspect: unavailable,
129-
appLogDoctor: unavailable,
130-
appLogStart: unavailable,
131-
appLogReattach: unavailable,
132-
appLogCleanup: unavailable,
133-
appState: unavailable,
134-
networkDump: unavailable,
135-
screenRecordingStart: unavailable,
136-
screenRecordingReattach: unavailable,
137-
screenRecordingCleanup: unavailable,
138-
ensureReady: unavailable,
139-
bootTarget: unavailable,
140-
bootTargetHeadless: unavailable,
141-
listApps: unavailable,
142-
...applicationLifecycleOperationFacts({
143-
resolveOpenTarget: unavailable,
144-
prepareApplicationOpen: unavailable,
145-
openApplication: unavailable,
146-
applyRuntimeHints: unavailable,
147-
clearRuntimeHints: unavailable,
148-
closeApplication: unavailable,
149-
finalizeApplicationClose: unavailable,
150-
prepareAppleRunner: unavailable,
151-
configureProviderPortReverse: unavailable,
152-
}),
153-
},
154-
},
155-
operations: {},
156-
[Symbol.asyncDispose]: async () => {},
157-
}),
90+
bind: async ({ device }) => {
91+
const owner = localRuntimeOwner(device.platform);
92+
return {
93+
device,
94+
owner,
95+
facts: createUnavailableRuntimeFactsForTest(device, owner),
96+
operations: {},
97+
[Symbol.asyncDispose]: async () => {},
98+
};
99+
},
158100
shutdown: async () => {},
159101
});
160102

src/daemon/handlers/__tests__/interaction-get-runtime-fixture.ts

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { vi } from 'vitest';
22
import type { TypeTextBackendResult } from '@agent-device/contracts/interactor-types';
3-
import { applicationLifecycleOperationFacts } from '@agent-device/contracts/application-lifecycle-runtime';
43
import type {
54
ElementTextReadOutcome,
65
ReadTextAtPointInput,
@@ -13,7 +12,6 @@ import {
1312
narrowDeviceBinding,
1413
} from '@agent-device/contracts/platform-runtime';
1514
import type { PlatformRuntimeOperations } from '@agent-device/contracts/platform-runtime-operations';
16-
import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable';
1715
import type { CaptureSnapshotInput } from '@agent-device/contracts/snapshot-runtime';
1816
import type { TypeTextInput } from '@agent-device/contracts/type-text-runtime';
1917
import {
@@ -34,6 +32,7 @@ import type {
3432
} from '../../request-runtime-binding.ts';
3533
import { captureSnapshotWithInteractor } from '../snapshot-interactor-capture.ts';
3634
import { androidObservationFixture } from '../../__tests__/android-observation-fixture.ts';
35+
import { createUnavailableRuntimeFactsForTest } from '../../../__tests__/test-utils/runtime-operation-facts.ts';
3736

3837
/**
3938
* The request-bound runtime seam `get` consumes, faked at `inspectFacts` / `bindDevice` — never
@@ -154,47 +153,7 @@ const unavailable = Object.freeze({
154153
} as const);
155154

156155
function elementReadFacts(device: DeviceInfo): RuntimeFacts<PlatformRuntimeOperations> {
157-
const base = createUnavailablePlatformRuntimeFacts(device, localRuntimeOwner('apple'), {
158-
appLog: unavailable,
159-
network: unavailable,
160-
viewport: unavailable,
161-
focus: unavailable,
162-
gesture: unavailable,
163-
scroll: unavailable,
164-
typeText: unavailable,
165-
touch: unavailable,
166-
elementText: unavailable,
167-
back: unavailable,
168-
home: unavailable,
169-
orientation: unavailable,
170-
tvRemote: unavailable,
171-
keyboardStatus: unavailable,
172-
keyboardDismiss: unavailable,
173-
keyboardEnter: unavailable,
174-
readClipboard: unavailable,
175-
writeClipboard: unavailable,
176-
appSwitcher: unavailable,
177-
triggerAppEvent: unavailable,
178-
setSetting: unavailable,
179-
readAlert: unavailable,
180-
awaitAlert: unavailable,
181-
acceptAlert: unavailable,
182-
dismissAlert: unavailable,
183-
screenshot: unavailable,
184-
audioProbeCapture: unavailable,
185-
audioProbeQuery: unavailable,
186-
lifecycle: applicationLifecycleOperationFacts({
187-
resolveOpenTarget: unavailable,
188-
prepareApplicationOpen: unavailable,
189-
openApplication: unavailable,
190-
applyRuntimeHints: unavailable,
191-
clearRuntimeHints: unavailable,
192-
closeApplication: unavailable,
193-
finalizeApplicationClose: unavailable,
194-
prepareAppleRunner: unavailable,
195-
configureProviderPortReverse: unavailable,
196-
}),
197-
});
156+
const base = createUnavailableRuntimeFactsForTest(device, localRuntimeOwner('apple'));
198157
return Object.freeze({
199158
device: base.device,
200159
operations: {

src/daemon/handlers/__tests__/session-capabilities-install-projection.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,37 @@ test('capabilities fails closed for every fact-owned command when facts inspecti
107107
expect(runtime.uses).toEqual([]);
108108
});
109109

110+
test('capabilities fixture preserves transport mode and owner-specific refusal reasons', async () => {
111+
const device = makeAndroidSession('fixture-semantics').device;
112+
const transport = createCapabilitiesAdmissionRuntime({
113+
appLogAvailable: false,
114+
networkAvailable: false,
115+
providerMode: 'transport-composed',
116+
});
117+
const provider = createCapabilitiesAdmissionRuntime({
118+
appLogAvailable: false,
119+
networkAvailable: false,
120+
providerMode: 'provider-runtime',
121+
});
122+
123+
const transportFacts = await transport.inspectFacts(device);
124+
const providerFacts = await provider.inspectFacts(device);
125+
126+
expect(transportFacts.device.providerMode).toBe('transport-composed');
127+
expect(providerFacts.operations.networkDump).toMatchObject({
128+
available: false,
129+
reason: 'unsupported-provider-mode',
130+
});
131+
expect(providerFacts.operations.tapRef).toEqual({
132+
available: false,
133+
reason: 'owner-capability-missing',
134+
});
135+
expect(providerFacts.operations.finalizeApplicationClose).toEqual({
136+
available: false,
137+
reason: 'owner-capability-missing',
138+
});
139+
});
140+
110141
function createAndroidCapabilitiesSession(suffix: string) {
111142
const sessionName = `android-capabilities-${suffix}`;
112143
const sessionStore = makeSessionStore(`agent-device-capabilities-${suffix}-`);

0 commit comments

Comments
 (0)