mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
fef0b12cc5
* 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>
115 lines
4.6 KiB
TypeScript
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);
|
|
});
|
|
});
|