Files
callstack__agent-device/test/integration/interaction-contract/coverage-manifest.ts
Michał Pierzchała f3e07ff236 test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3) (#1092)
* fix: preserve the runner's non-hittable fallback marker through direct selector press

The direct iOS selector press handler spread successText('Tapped <selector>')
after the runner payload, clobbering the 'tapped via non-hittable coordinate
fallback' message that directIosSelectorFallbackDetails keys on — so
maestroNonHittableCoordinateFallbackUsed could never be true end-to-end (the
existing unit test passes because it mocks dispatchCommand above this layer).
Found by the ADR 0011 Layer-3 maestro-fallback contract scenario.

Share the marker string as MAESTRO_NON_HITTABLE_FALLBACK_MESSAGE and keep it
as the success message when the runner reports fallback usage.

* test: interaction contract suite with registry-driven coverage gate (ADR 0011 Layer 3)

test/integration/interaction-contract/ holds one scenario file per dispatch
path, each with a sibling .coverage.ts manifest declaring which guarantee
matrix cells it proves (scenario strings double as the vitest test titles).
index.ts aggregates the manifests statically, and a new Layer-3 gate in
src/contracts/__tests__/interaction-contract-coverage.test.ts fails when any
enforced (runtime/runner/delegated) cell lacks a scenario or when a scenario
claims a waived/inapplicable cell — coverage of the matrix is by
construction in both directions.

Path forcing is natural (no test-only env switch needed): selector/ref
targets take the runtime path, a tapTarget backend takes the native-ref fast
path, x/y takes the coordinate path, and simple-selector clicks on an iOS
provider transcript take the direct runner path (the transcript itself
proves which path ran via assertComplete).

Fixtures are the permanent Bluesky shapes: closed drawer, drawer + visible
twin, edge-grazing container, covered button, non-hittable cell.

Refs #1081
2026-07-04 16:02:41 +02:00

56 lines
1.9 KiB
TypeScript

import type {
InteractionGuarantee,
InteractionPathId,
} from '../../../src/contracts/interaction-guarantees.ts';
/**
* ADR 0011 Layer 3: the machine-readable claim of which interaction guarantee
* matrix cells a scenario file proves. Every `<path>.contract.test.ts` has a
* sibling `<path>.coverage.ts` exporting one of these manifests (kept out of
* the test file so the coverage gate can import it without re-registering the
* scenarios), and `index.ts` aggregates them statically for the gate test in
* `src/contracts/__tests__/interaction-contract-coverage.test.ts`.
*
* Scenario strings double as the vitest test titles — the test files derive
* their titles from the manifest (via `scenarioName`/`scenarioNames`), so a
* claim without a matching scenario cannot be written by accident.
*/
export type ContractCoverageEntry = {
path: InteractionPathId;
guarantee: InteractionGuarantee;
scenario: string;
};
export function definePathCoverage(
path: InteractionPathId,
scenarios: Partial<Record<InteractionGuarantee, string | readonly string[]>>,
): readonly ContractCoverageEntry[] {
return Object.entries(scenarios).flatMap(([guarantee, names]) =>
(typeof names === 'string' ? [names] : (names ?? [])).map((scenario) => ({
path,
guarantee: guarantee as InteractionGuarantee,
scenario,
})),
);
}
export function scenarioNames(
coverage: readonly ContractCoverageEntry[],
guarantee: InteractionGuarantee,
): string[] {
const names = coverage
.filter((entry) => entry.guarantee === guarantee)
.map((entry) => entry.scenario);
if (names.length === 0) {
throw new Error(`no contract scenario declared for guarantee "${guarantee}"`);
}
return names;
}
export function scenarioName(
coverage: readonly ContractCoverageEntry[],
guarantee: InteractionGuarantee,
): string {
return scenarioNames(coverage, guarantee)[0]!;
}