Skip to content

Commit 8baa8ea

Browse files
authored
test: derive the command-surface enumerations that restated a declaration (#2713)
* test(coverage): recount the published summaries instead of pinning them Each platform coverage manifest already derives its classification summary from its own rows, and the smoke test that owns the manifest then compared that derived value to a hand-incremented literal. Adding one public command therefore cost five files whose only edit was `54` becoming `55`, while the invariant those files actually protect — every public command is classified, and the published rollup is the manifest's own — was asserted nowhere. `assertCoverageClassificationSummaryDerivedFromManifest` now recounts the manifest independently of the builder, ties `total` to the public catalog's size, and checks the buckets still sum. Parity was verified against the deleted literals before removal, as ADR 0008 requires: macOS 22/15/18/55, web 42/1/12/55, Linux 29/9/17/55, tvOS 17/38/0/55, Android 14/0/41/55, all equal to the derived rollup at this head. The web lane's `liveCommands.length === 12` goes the same way: the loop beneath it already proves each live claim is executed by the smoke scenario, so a count proved only that the list was not empty. A planted mis-bucketed summary and a planted denominator drift both fail, naming the platform. * test(daemon): read the generic route from the descriptor that declares it The test enumerated eleven commands to prove that the daemon's fallthrough route is reached on purpose. Ten of those commands already declare the decision as `daemon.route: 'generic'`, which is where `getDaemonCommandRoute` reads it from, so the list was a second source of truth that every new generic command had to be added to. The expected set is now that trait, projected. What remains hand-written is the case the list was actually guarding — a catalog command that declares no daemon facet at all and lands on the `?? 'generic'` fallback — and it is asserted alone, with a message that names the declaration to add instead. Removing `viewport`'s facet locally fails with `['viewport', 'install-from-source']` against that one expected member. * test(runtime-facts): fill the fail-closed test facts from the owner's helper `createUnavailableRuntimeFactsForTest` restated all twenty-seven fail-closed cells, plus the nine lifecycle cells, to say what `createFullyUnavailablePlatformRuntimeFacts` already builds: every cell unavailable for one reason. The two forms are equal because the input's `network` cell carries the same sentinel the omitted optional cells would have inherited. A new runtime cell therefore costs nothing here, which is the point of the fail-closed default: a test that overrides a cell states its own exception, and one that does not inherits the refusal without anyone remembering to write it down. * test(coverage): name the summary guard for what it guards, and close the web live bucket `buildCoverageClassificationSummary` and the recount in the assertion read the same manifest rows, so a row classified under the wrong `level` moves both sides and stays consistent. The helper therefore guards wiring and denominator, never per-row classification, and it now says so by name (`assertCoverageClassificationSummaryWiredToManifest`) and in its doc comment, which also names where row membership is actually gated: the Linux lane against its parsed replay script, the Android lane against each scenario's own command declaration, the web lane against the commands its smoke scenario invokes, and macOS one way only, since its provider scenarios invoke commands they deliberately do not claim live. The web lane gains that third gate, replacing both the old `liveCommands.length === 12` and the weaker `> 0` this PR first introduced. It parses the smoke scenario's argv heads and compares them to the manifest's live set in both directions, so a command the scenario runs without a live claim, or a live claim the scenario stopped honouring, fails by name. The parsed set equals the claimed set exactly today, so the count the pin carried survives without being hand-incremented. Appending a line containing `['hover',` to the scenario makes the gate fail on `hover`. `localCliCommands()` is dropped from the daemon route test: `CATALOG_COMMANDS` is the public and internal groups, and a `catalog.group: 'local-cli'` descriptor is never projected into either, so the clause could not fire. The reasoning is left where the clause was. * test(coverage): close the macOS live bucket from its scenario sources too The commit before this one recorded macOS as gated one way only, on the finding that its provider scenarios execute `open`, `appstate`, `snapshot`, `screenshot`, `wait` and `click` without claiming them live. That reading was wrong: those commands are claimed live, attributed to the replay scenario, so the mismatch is scenario attribution and not a missing claim. Compared at platform level, the commands the live scenarios execute and the commands the manifest calls live are the same 18. `assertLiveCoverageMatchesEvidence` states that tie for the two platforms whose evidence enumerates its own commands — macOS through `parseReplayScriptDetailed` and the provider call shapes, web through the argv heads its smoke scenario passes — and Linux and Android already read the same kind of enumeration directly. Appending `callCommand('hover', …)` to a macOS live scenario fails the gate with the platform named, and the rollup assertion's doc comment now points here for membership instead of implying it covers classification.
1 parent ec30f3c commit 8baa8ea

8 files changed

Lines changed: 234 additions & 116 deletions

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

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import type {
88
RuntimeOperationUnavailability,
99
RuntimeOwnerRef,
1010
} from '@agent-device/contracts/platform-runtime';
11-
import { createUnavailablePlatformRuntimeFacts } from '@agent-device/contracts/platform-runtime-unavailable';
11+
import {
12+
createFullyUnavailablePlatformRuntimeFacts,
13+
createUnavailablePlatformRuntimeFacts,
14+
} from '@agent-device/contracts/platform-runtime-unavailable';
1215
import type { DeviceInfo } from '@agent-device/kernel/device';
1316
import { screenshotRuntimeOperationFacts } from '@agent-device/contracts/screenshot-runtime';
1417
import { scrollRuntimeOperationFacts } from '@agent-device/contracts/scroll-runtime';
@@ -104,43 +107,9 @@ export function createUnavailableRuntimeFactsForTest(
104107
owner: RuntimeOwnerRef,
105108
fact: RuntimeOperationUnavailability = unavailable,
106109
) {
107-
return createUnavailablePlatformRuntimeFacts(device, owner, {
108-
appLog: fact,
109-
network: fact,
110-
screenshot: fact,
111-
viewport: fact,
112-
focus: fact,
113-
gesture: fact,
114-
scroll: fact,
115-
typeText: fact,
116-
touch: fact,
117-
elementText: fact,
118-
back: fact,
119-
home: fact,
120-
orientation: fact,
121-
tvRemote: fact,
122-
keyboard: fact,
123-
clipboard: fact,
124-
appSwitcher: fact,
125-
actionButton: fact,
126-
triggerAppEvent: fact,
127-
setSetting: fact,
128-
readAlert: fact,
129-
awaitAlert: fact,
130-
acceptAlert: fact,
131-
dismissAlert: fact,
132-
audioProbeCapture: fact,
133-
audioProbeQuery: fact,
134-
lifecycle: applicationLifecycleOperationFacts({
135-
resolveOpenTarget: fact,
136-
prepareApplicationOpen: fact,
137-
openApplication: fact,
138-
applyRuntimeHints: fact,
139-
clearRuntimeHints: fact,
140-
closeApplication: fact,
141-
finalizeApplicationClose: fact,
142-
prepareAppleRunner: fact,
143-
configureProviderPortReverse: fact,
144-
}),
145-
});
110+
return createUnavailablePlatformRuntimeFacts(
111+
device,
112+
owner,
113+
createFullyUnavailablePlatformRuntimeFacts(fact),
114+
);
146115
}

‎src/daemon/__tests__/request-handler-catalog.test.ts‎

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path';
44
import { test } from 'vitest';
55
import { withTestDeviceInventoryProvider as withTargetDeviceResolutionScope } from '../../__tests__/test-utils/device-inventory-gateways.ts';
66
import { INTERNAL_COMMANDS, PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
7+
import { commandDescriptors } from '@agent-device/command-registry/registry';
78
import { isRequestCanceledError, type AppError } from '@agent-device/kernel/errors';
89
import { makeSessionStore } from '../../__tests__/test-utils/store-factory.ts';
910
import { getDaemonCommandRoute, type DaemonCommandRoute } from '../daemon-command-registry.ts';
@@ -52,26 +53,43 @@ test('specialized daemon routes are claimed by their handler chain', async () =>
5253
}
5354
});
5455

56+
// The generic route is the router's fallthrough, so it has two ways in: a descriptor that declares
57+
// `daemon.route: 'generic'`, and a catalog command that declares no daemon facet at all and lands
58+
// on the `?? 'generic'` fallback. The first is a decision read from the declaration; the second is
59+
// what this test exists to keep rare, so it is enumerated alone and by name.
60+
const CATALOG_COMMANDS = [...Object.values(PUBLIC_COMMANDS), ...Object.values(INTERNAL_COMMANDS)];
61+
62+
function declaredDaemonTraitCommands(): string[] {
63+
return commandDescriptors
64+
.filter((descriptor) => 'daemon' in descriptor && descriptor.daemon !== undefined)
65+
.map((descriptor) => descriptor.name);
66+
}
67+
68+
function genericByTraitCommands(): string[] {
69+
return commandDescriptors
70+
.filter((descriptor) => 'daemon' in descriptor && descriptor.daemon?.route === 'generic')
71+
.map((descriptor) => descriptor.name);
72+
}
73+
5574
test('catalog commands use generic routing only when intentionally passthrough or projected', () => {
75+
// Local-CLI commands are excluded by construction: `CATALOG_COMMANDS` holds the public and
76+
// internal groups, which a `catalog.group: 'local-cli'` descriptor is never projected into.
77+
const genericByAbsence = CATALOG_COMMANDS.filter(
78+
(command) => !declaredDaemonTraitCommands().includes(command),
79+
);
80+
assert.deepEqual(
81+
genericByAbsence,
82+
[PUBLIC_COMMANDS.installFromSource],
83+
'a catalog command reaches the generic route by declaring no daemon facet; declare daemon.route instead',
84+
);
85+
5686
const intentionalGenericCatalogCommands = [
57-
PUBLIC_COMMANDS.actionButton,
58-
PUBLIC_COMMANDS.appSwitcher,
59-
PUBLIC_COMMANDS.back,
60-
PUBLIC_COMMANDS.focus,
61-
PUBLIC_COMMANDS.home,
62-
PUBLIC_COMMANDS.installFromSource,
63-
PUBLIC_COMMANDS.orientation,
64-
PUBLIC_COMMANDS.screenshot,
65-
PUBLIC_COMMANDS.scroll,
66-
PUBLIC_COMMANDS.tvRemote,
67-
PUBLIC_COMMANDS.viewport,
87+
...genericByTraitCommands(),
88+
...genericByAbsence,
6889
].sort();
69-
const genericCatalogCommands = [
70-
...Object.values(PUBLIC_COMMANDS),
71-
...Object.values(INTERNAL_COMMANDS),
72-
]
73-
.filter((command) => getDaemonCommandRoute(command) === 'generic')
74-
.sort();
90+
const genericCatalogCommands = CATALOG_COMMANDS.filter(
91+
(command) => getDaemonCommandRoute(command) === 'generic',
92+
).sort();
7593

7694
assert.deepEqual(genericCatalogCommands, intentionalGenericCatalogCommands);
7795
});

‎test/integration/smoke-android-emulator-coverage.test.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from 'node:path';
44
import test from 'node:test';
55

66
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
7+
import { assertCoverageClassificationSummaryWiredToManifest } from './support/coverage-classification.ts';
78
import { ANDROID_EMULATOR_BEHAVIOR_COVERAGE } from './android-emulator-e2e/behavior-coverage.ts';
89
import { ANDROID_PERMISSION_PROMPT_COMMAND } from './android-emulator-e2e/live-lifecycle-scenario.ts';
910
import {
@@ -19,8 +20,9 @@ import {
1920
selectAndroidEmulatorScenarios,
2021
} from './android-emulator-e2e/scenarios.ts';
2122

23+
const publicCommands = Object.values(PUBLIC_COMMANDS).sort();
24+
2225
test('Android emulator coverage exhaustively classifies the public catalog', () => {
23-
const publicCommands = Object.values(PUBLIC_COMMANDS).sort();
2426
assert.deepEqual(Object.keys(ANDROID_EMULATOR_E2E_COVERAGE).sort(), publicCommands);
2527
for (const command of publicCommands) {
2628
const entry = ANDROID_EMULATOR_E2E_COVERAGE[command];
@@ -38,14 +40,12 @@ test('Android emulator coverage exhaustively classifies the public catalog', ()
3840
});
3941

4042
test('Android coverage report summary accounts for every manifest classification', () => {
41-
const summary = ANDROID_EMULATOR_COVERAGE_CLASSIFICATION_SUMMARY;
42-
assert.deepEqual(summary, {
43-
contract: 14,
44-
gap: 0,
45-
live: 41,
46-
total: 55,
47-
});
48-
assert.equal(summary.live + summary.contract + summary.gap, summary.total);
43+
assertCoverageClassificationSummaryWiredToManifest(
44+
'Android emulator',
45+
ANDROID_EMULATOR_E2E_COVERAGE,
46+
ANDROID_EMULATOR_COVERAGE_CLASSIFICATION_SUMMARY,
47+
publicCommands,
48+
);
4949
});
5050

5151
test('Android live command ownership is structural and exhaustive', () => {

‎test/integration/smoke-linux-coverage.test.ts‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import test from 'node:test';
55

66
import { parseReplayScriptDetailed } from '@agent-device/ad-script';
77
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
8+
import { assertCoverageClassificationSummaryWiredToManifest } from './support/coverage-classification.ts';
89
import {
910
LINUX_COVERAGE_GAP_ISSUE,
1011
LINUX_COMMAND_EVIDENCE,
@@ -42,20 +43,15 @@ test('Linux coverage exhaustively classifies the public catalog', () => {
4243
});
4344

4445
test('Linux coverage report has the expected classification counts', () => {
45-
assert.deepEqual(LINUX_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY, {
46-
// focus (#1925), click, and type remain live via the existing replay. The separate
47-
// command-evidence lane adds nine generic-command rows without changing that replay. Artifact
48-
// inventory remains a gap because local Linux screenshot paths are not daemon-downloadable.
49-
// Keyboard, orientation and tv-remote were already fact-owned command-contract rows rather
50-
// than catalog denials; R56 moves app-switcher the same way, for the same reason.
51-
contract: 29,
52-
gap: 9,
53-
live: 17,
54-
total: 55,
55-
});
56-
57-
const { contract, gap, live, total } = LINUX_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY;
58-
assert.equal(contract + gap + live, total);
46+
// Focus (#1925), click, and type are live through the replay; the separate command-evidence
47+
// lane adds generic-command rows without changing it. Artifact inventory stays a gap because
48+
// local Linux screenshot paths are not daemon-downloadable.
49+
assertCoverageClassificationSummaryWiredToManifest(
50+
'Linux',
51+
LINUX_PLATFORM_COVERAGE,
52+
LINUX_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY,
53+
publicCommands,
54+
);
5955
});
6056

6157
test('Linux live claims reference commands in the existing smoke replay', () => {

‎test/integration/smoke-macos-coverage.test.ts‎

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import assert from 'node:assert/strict';
2+
import { parseReplayScriptDetailed } from '@agent-device/ad-script';
23
import fs from 'node:fs';
34
import path from 'node:path';
45
import test from 'node:test';
56

67
import { mkdtempForTest } from '../../src/__tests__/test-utils/tmp-dir.ts';
78
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
9+
import {
10+
assertCoverageClassificationSummaryWiredToManifest,
11+
assertLiveCoverageMatchesEvidence,
12+
} from './support/coverage-classification.ts';
13+
import type { MacOsLiveScenario } from './macos-e2e/live-scenarios.ts';
814
import {
915
MACOS_COVERAGE_GAP_ISSUE,
1016
MACOS_LIVE_SCENARIOS,
@@ -14,8 +20,9 @@ import {
1420
} from './macos-e2e/coverage.ts';
1521
import { writeCoverageReport } from './macos-e2e/coverage-report.ts';
1622

23+
const publicCommands = Object.values(PUBLIC_COMMANDS).sort();
24+
1725
test('macOS coverage exhaustively classifies the public catalog', () => {
18-
const publicCommands = Object.values(PUBLIC_COMMANDS).sort();
1926
assert.deepEqual(Object.keys(MACOS_PLATFORM_COVERAGE).sort(), publicCommands);
2027

2128
for (const command of publicCommands) {
@@ -38,17 +45,22 @@ test('macOS coverage exhaustively classifies the public catalog', () => {
3845
});
3946

4047
test('macOS coverage report counts every manifest classification', () => {
41-
assert.deepEqual(MACOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY, {
42-
contract: 22,
43-
gap: 15,
44-
live: 18,
45-
total: 55,
46-
});
47-
assert.equal(
48-
MACOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY.live +
49-
MACOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY.contract +
50-
MACOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY.gap,
51-
MACOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY.total,
48+
assertCoverageClassificationSummaryWiredToManifest(
49+
'macOS',
50+
MACOS_PLATFORM_COVERAGE,
51+
MACOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY,
52+
publicCommands,
53+
);
54+
});
55+
56+
// The scenario sources say which commands macOS actually runs, independent of what the manifest
57+
// labels them. Provider scenarios run commands whose live claim is attributed to the replay
58+
// scenario, so this closes the platform's live bucket rather than each scenario's partition of it.
59+
test('macOS live claims are exactly the commands its live scenarios execute', () => {
60+
assertLiveCoverageMatchesEvidence(
61+
'macOS',
62+
MACOS_PLATFORM_COVERAGE,
63+
commandsExecutedByMacOsLiveScenarios(),
5264
);
5365
});
5466

@@ -123,6 +135,40 @@ test('macOS coverage report persists the manifest rollup and live command list',
123135
);
124136
});
125137

138+
function commandsExecutedByMacOsLiveScenarios(): string[] {
139+
const catalogCommands: ReadonlySet<string> = new Set(publicCommands);
140+
const executed = new Set<string>();
141+
for (const scenario of MACOS_LIVE_SCENARIOS) {
142+
for (const command of commandsExecutedInScenarioSource(
143+
readEvidence(scenario.owner),
144+
scenario.syntax,
145+
)) {
146+
if (catalogCommands.has(command)) {
147+
executed.add(command);
148+
}
149+
}
150+
}
151+
return [...executed];
152+
}
153+
154+
function commandsExecutedInScenarioSource(
155+
source: string,
156+
syntax: MacOsLiveScenario['syntax'],
157+
): string[] {
158+
const group = (pattern: RegExp): string[] =>
159+
[...source.matchAll(pattern)]
160+
.map((match) => match[1])
161+
.filter((command): command is string => command !== undefined);
162+
switch (syntax) {
163+
case 'replay':
164+
return parseReplayScriptDetailed(source).actions.map((action) => action.command);
165+
case 'provider-command':
166+
return group(/command:\s*'([a-z][a-z-]*)'/g);
167+
case 'provider-call-command':
168+
return group(/callCommand\(\s*'([a-z][a-z-]*)'/g);
169+
}
170+
}
171+
126172
function readEvidence(owner: { path: string; test: string }): string {
127173
const evidencePath = path.resolve(owner.path);
128174
assert.equal(fs.existsSync(evidencePath), true, `${owner.path} does not exist`);

‎test/integration/smoke-tvos-platform-coverage.test.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import test from 'node:test';
55

66
import { TVOS_SIMULATOR } from '../../src/__tests__/test-utils/device-fixtures.ts';
77
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
8+
import { assertCoverageClassificationSummaryWiredToManifest } from './support/coverage-classification.ts';
89
import { createPlatformRuntimeGateway } from '../../src/platform-runtime.ts';
910
import { gestureRefusalMessage } from '@agent-device/contracts/gesture-admission';
1011
import {
@@ -39,12 +40,12 @@ test('tvOS coverage exhaustively classifies the public catalog', () => {
3940
});
4041

4142
test('tvOS coverage report has the expected classification counts', () => {
42-
assert.deepEqual(TVOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY, {
43-
contract: 17,
44-
gap: 38,
45-
live: 0,
46-
total: 55,
47-
});
43+
assertCoverageClassificationSummaryWiredToManifest(
44+
'tvOS',
45+
TVOS_PLATFORM_COVERAGE,
46+
TVOS_PLATFORM_COVERAGE_CLASSIFICATION_SUMMARY,
47+
publicCommands,
48+
);
4849
});
4950

5051
test('tvOS contract claims name existing executable evidence', () => {

0 commit comments

Comments
 (0)