Skip to content

Commit d950b44

Browse files
committed
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 19dc0af commit d950b44

3 files changed

Lines changed: 87 additions & 9 deletions

File tree

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

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +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';
8-
import { assertCoverageClassificationSummaryWiredToManifest } from './support/coverage-classification.ts';
9+
import {
10+
assertCoverageClassificationSummaryWiredToManifest,
11+
assertLiveCoverageMatchesEvidence,
12+
} from './support/coverage-classification.ts';
13+
import type { MacOsLiveScenario } from './macos-e2e/live-scenarios.ts';
914
import {
1015
MACOS_COVERAGE_GAP_ISSUE,
1116
MACOS_LIVE_SCENARIOS,
@@ -48,6 +53,17 @@ test('macOS coverage report counts every manifest classification', () => {
4853
);
4954
});
5055

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(),
64+
);
65+
});
66+
5167
test('macOS live claims reference commands in existing executable scenarios', () => {
5268
const scenariosById = new Map(MACOS_LIVE_SCENARIOS.map((scenario) => [scenario.id, scenario]));
5369
const claimedCommands: string[] = [];
@@ -119,6 +135,40 @@ test('macOS coverage report persists the manifest rollup and live command list',
119135
);
120136
});
121137

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+
122172
function readEvidence(owner: { path: string; test: string }): string {
123173
const evidencePath = path.resolve(owner.path);
124174
assert.equal(fs.existsSync(evidencePath), true, `${owner.path} does not exist`);

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

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

66
import { mkdtempForTest } from '../../src/__tests__/test-utils/tmp-dir.ts';
77
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
8-
import { assertCoverageClassificationSummaryWiredToManifest } from './support/coverage-classification.ts';
8+
import {
9+
assertCoverageClassificationSummaryWiredToManifest,
10+
assertLiveCoverageMatchesEvidence,
11+
} from './support/coverage-classification.ts';
912
import {
1013
WEB_COVERAGE_GAP_ISSUE,
1114
WEB_PLATFORM_COVERAGE,
@@ -54,7 +57,11 @@ test('web coverage report counts every manifest classification', () => {
5457
test('web live claims are exactly the commands the smoke scenario invokes', () => {
5558
const smokeSource = fs.readFileSync(path.resolve(WEB_SMOKE_EVIDENCE.path), 'utf8');
5659
assert.ok(smokeSource.includes(WEB_SMOKE_TEST_NAME));
57-
assert.deepEqual(invokedWebSmokeCommands(smokeSource), liveCommandsForWebSmoke().sort());
60+
assertLiveCoverageMatchesEvidence(
61+
'web',
62+
WEB_PLATFORM_COVERAGE,
63+
invokedWebSmokeCommands(smokeSource),
64+
);
5865
});
5966

6067
test('web contract claims name existing executable evidence', () => {

‎test/integration/support/coverage-classification.ts‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@ const BUCKET_BY_LEVEL: Readonly<Record<CoverageClassificationLevel, CoverageBuck
2323
* mis-bucketed rollup, and a denominator that stopped matching the catalog.
2424
*
2525
* What it cannot catch is a row classified under the wrong `level`: the recount and the published
26-
* summary read the same rows, so a row moved between buckets moves both and stays consistent. Row
27-
* membership is gated where an independent enumeration of it exists — the Linux lane compares its
28-
* live set against the parsed replay script, the Android lane against each scenario's own command
29-
* declaration, the web lane against the commands its smoke scenario invokes. A platform whose live
30-
* claims have no enumeration behind them, macOS, is gated one way only: each claim must be executed
31-
* by the scenario that owns it, and adding a live claim therefore needs evidence.
26+
* summary read the same rows, so a row moved between buckets moves both and stays consistent. That
27+
* membership is gated by {@link assertLiveCoverageMatchesEvidence}, or by the equivalent enumeration
28+
* a platform already reads — Linux parses its replay script, Android reads each scenario's own
29+
* command declaration.
3230
*/
3331
export function assertCoverageClassificationSummaryWiredToManifest(
3432
platform: string,
@@ -59,6 +57,29 @@ export function assertCoverageClassificationSummaryWiredToManifest(
5957
);
6058
}
6159

60+
/**
61+
* Ties a platform's live bucket to an enumeration of the commands its own evidence executes, read
62+
* from the scenario sources rather than from the manifest's `level` fields. Both directions matter:
63+
* an executed command that is not claimed live means the platform is understating what it proves,
64+
* and a live claim no scenario executes any more means the platform is overstating it. A platform
65+
* with no independent enumeration behind a bucket can only assert the rollup above.
66+
*/
67+
export function assertLiveCoverageMatchesEvidence(
68+
platform: string,
69+
manifest: Readonly<Record<string, { level: CoverageClassificationLevel }>>,
70+
evidenceCommands: Iterable<string>,
71+
): void {
72+
const live = Object.entries(manifest)
73+
.filter(([, entry]) => entry.level === 'live')
74+
.map(([command]) => command)
75+
.sort();
76+
assert.deepEqual(
77+
[...new Set(evidenceCommands)].sort(),
78+
live,
79+
`${platform} live claims are not the commands its own evidence executes`,
80+
);
81+
}
82+
6283
export function buildCoverageClassificationSummary(
6384
entries: readonly { level: CoverageClassificationLevel }[],
6485
): CoverageClassificationSummary {

0 commit comments

Comments
 (0)