Files
Michał Pierzchała fef0b12cc5 chore: hoist shared snapshot/selector test fixtures into a single canonical location (#2419)
* chore: hoist shared snapshot/selector test fixtures into @agent-device/selectors

PR #2397 left two copies of the snapshot-state builder and duplicated
geometry/touch-point arbitraries (root's src/__tests__/test-utils/ and the
package's internal/__tests__/), because packages cannot import root src/.
Move the canonical versions into a new @agent-device/selectors/test-fixtures
subpath and have both root and the selectors package import from it, leaving
buildNodes and the root-only replay/gesture arbitraries in place.

Fixes #2402

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x

* chore: exempt test-fixtures.ts's test-only arbitraries from dead-code check

PROPERTY_RUNS, scrollingContainerTypeArb, distinctRectPairArb, and
interactionTouchPointScenarioArb are consumed only by *.test.ts files, which
Fallow's --production analysis does not see, matching the existing pattern
for other workspace-package symbols reached only from the test tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x

* chore: consolidate makeSnapshotState into capture-kit, rename fixtures file

An adversarial review of the #2402 fixture-hoisting change found a third
copy of makeSnapshotState in packages/capture-kit/src/snapshot-state.fixtures.ts,
predating PR #2397. Since @agent-device/selectors already depends on
capture-kit, make capture-kit's copy canonical (exported as
./snapshot-state-fixtures) and have the selectors package's fixtures module
re-export it instead of duplicating it a third time.

Also rename packages/selectors/src/test-fixtures.ts to
snapshot-geometry.fixtures.ts (subpath ./snapshot-geometry-fixtures) to match
every other test-fixture module's *.fixtures.ts convention in this repo,
which lets it fall under .fallowrc.json's existing blanket **/*.fixtures.ts
dead-code exemption instead of needing a bespoke per-symbol entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01USabYbQjD16A2UkkvMpf5x

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-09 13:56:40 +02:00

115 lines
4.6 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'vitest';
import type { InteractionGuarantee } from '@agent-device/contracts/interaction-guarantees';
import { makeSnapshotState } from '@agent-device/selectors/snapshot-geometry-fixtures';
import { assertRpcError, assertRpcOk } from '../provider-scenarios/assertions.ts';
import { scenarioName } from './coverage-manifest.ts';
import { COORDINATE_COVERAGE } from './coordinate.coverage.ts';
import { settledWelcomeSnapshot, viewportOnlySnapshot } from './fixtures.ts';
import { createContractDevice } from './runtime-harness.ts';
import { runnerTapEntry, runnerTapErrorEntry, withIosContractDaemon } from './daemon-harness.ts';
// ADR 0011 Layer 3, coordinate path: raw x/y tap, intentionally minimal
// semantics. Path forcing is natural: a point target never resolves nodes.
const scenario = (guarantee: InteractionGuarantee): string =>
scenarioName(COORDINATE_COVERAGE, guarantee);
test(scenario('offscreen'), async () => {
const device = createContractDevice(viewportOnlySnapshot(), {
tap: async () => ({ ok: true }),
});
const result = await device.interactions.click(
{ kind: 'point', x: 500, y: 500 },
{ session: 'default' },
);
// Coordinate semantics: the escape hatch forwards the tap but must warn —
// never a silent out-of-viewport no-op, never a refusal.
assert.equal(result.kind, 'point');
assert.match(result.warning ?? '', /outside the last-known viewport \(400x800\)/);
});
test(scenario('verifyEvidence'), async () => {
let captureCount = 0;
const device = createContractDevice(viewportOnlySnapshot(), {
captureSnapshot: async () => {
captureCount += 1;
if (captureCount === 1) return { snapshot: viewportOnlySnapshot() };
return { snapshot: makeSnapshotState([]) };
},
tap: async () => ({ ok: true }),
});
const result = await device.interactions.click(
{ kind: 'point', x: 10, y: 20 },
{ session: 'default', verify: true },
);
assert.equal(result.kind, 'point');
assert.ok(result.evidence);
assert.equal(result.evidence?.changedFromBefore, true);
});
test(scenario('settleObservation'), async () => {
let captureCount = 0;
const device = createContractDevice(viewportOnlySnapshot(), {
// Point targets normally skip captures entirely; --settle opts into the
// evidence-baseline capture (first call) and the settle loop's captures.
captureSnapshot: async () => {
captureCount += 1;
if (captureCount === 1) return { snapshot: viewportOnlySnapshot() };
return { snapshot: settledWelcomeSnapshot() };
},
tap: async () => ({ ok: true }),
});
const result = await device.interactions.press(
{ kind: 'point', x: 10, y: 20 },
{ session: 'default', settle: { quietMs: 25, timeoutMs: 2_000 } },
);
assert.equal(result.kind, 'point');
const settle = result.settle;
assert.ok(settle, 'point press --settle must return a settle observation');
assert.equal(settle.settled, true);
assert.deepEqual(settle.diff?.summary, { additions: 1, removals: 1, unchanged: 0 });
});
test(scenario('errorTaxonomy'), async () => {
await withIosContractDaemon(
[runnerTapErrorEntry(new Error('runner tap crashed'))],
async (daemon) => {
const press = await daemon.callCommand('press', ['100', '200']);
// An unclassified backend failure gets the shared fallback classification
// from normalizeError: stable code, original message, actionable hint.
const error = assertRpcError(press, 'UNKNOWN', /runner tap crashed/);
assert.ok(typeof error.hint === 'string' && error.hint.length > 0);
assert.ok(typeof error.diagnosticId === 'string');
},
);
});
test(scenario('responseConstruction'), async () => {
await withIosContractDaemon([runnerTapEntry({ x: 100, y: 200 })], async (daemon) => {
const press = await daemon.callCommand('press', ['100', '200']);
const data = assertRpcOk(press);
// Canonical point response set: the tapped coordinates, no fabricated
// identity fields (the path has no resolved node by design).
assert.equal(data.x, 100);
assert.equal(data.y, 200);
assert.equal(data.ref, undefined);
assert.equal(data.selector, undefined);
});
});
// Inapplicable cell (ADR 0012), so intentionally absent from the coverage manifest.
test('coordinate resolutionDisclosure: inapplicable — no resolution field is attached', async () => {
await withIosContractDaemon([runnerTapEntry({ x: 100, y: 200 })], async (daemon) => {
const press = await daemon.callCommand('press', ['100', '200']);
const data = assertRpcOk(press);
assert.equal(data.resolution, undefined);
});
});