Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ import { storeSessionForTest } from '../../../__tests__/support/store-factory.ts
* `caution`/`manual` divergence is no longer a dead end.
*/
import { test, expect, vi, beforeEach } from 'vitest';
import { mkdtempForTestSync } from '@agent-device/test-kit';
import {
bottomTabsRealCaptureFixture,
buildNodes,
mkdtempForTestSync,
recordArticleEvidence,
} from '@agent-device/test-kit';
import { computeTargetEvidence } from '@agent-device/selectors/target-evidence';

vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => {
const actual =
Expand Down Expand Up @@ -52,11 +58,6 @@ import {
writeReplayFile,
} from '../replay-runtime/session-replay-runtime.fixtures.ts';
import { freshEvidence, makeRecordingReplayInvoke } from './session-replay-repair.fixtures.ts';
import {
bottomTabsRealCaptureFixture,
recordArticleEvidence,
toSnapshotNodes,
} from '@agent-device/replay-port/target-classification-fixtures';

/** Repair-transaction status, or `undefined` outside a repair publication. */
function sessionRepairStatus(session: SessionState | undefined) {
Expand Down Expand Up @@ -90,7 +91,7 @@ test('a record-and-heal divergence on the LAST step resumes with an empty tail a
sessionName,
makeIosSession(sessionName, { appBundleId: 'com.example.app' }),
);
const evidence = await recordArticleEvidence();
const evidence = recordArticleEvidence(computeTargetEvidence);
const filePath = writeReplayFile(root, [
'open "Demo" --relaunch',
`# agent-device:target-v1 ${JSON.stringify(evidence)}`,
Expand Down Expand Up @@ -561,7 +562,7 @@ test('an unauthorized --from one past the plan end is rejected on an ARMED sessi
sessionName,
makeIosSession(sessionName, { appBundleId: 'com.example.app' }),
);
const evidence = await recordArticleEvidence();
const evidence = recordArticleEvidence(computeTargetEvidence);
const filePath = writeReplayFile(root, [
'open "Demo" --relaunch',
`# agent-device:target-v1 ${JSON.stringify(evidence)}`,
Expand All @@ -574,7 +575,7 @@ test('an unauthorized --from one past the plan end is rejected on an ARMED sessi
// `state-repair` (the app-state sub-flow), never `record-and-heal`. No
// `pendingRecordAndHeal` watermark is ever stamped for this session.
mockDispatchCommand.mockResolvedValue({
nodes: toSnapshotNodes([
nodes: buildNodes([
{
index: 0,
type: 'Application',
Expand Down Expand Up @@ -647,7 +648,7 @@ test('a stale --plan-digest on an empty-tail resume is rejected WITHOUT consumin
sessionName,
makeIosSession(sessionName, { appBundleId: 'com.example.app' }),
);
const evidence = await recordArticleEvidence();
const evidence = recordArticleEvidence(computeTargetEvidence);
const filePath = writeReplayFile(root, [
'open "Demo" --relaunch',
`# agent-device:target-v1 ${JSON.stringify(evidence)}`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
* zero-match selector) instead of returning it.
*/
import { test, expect, vi, beforeEach } from 'vitest';
import { mkdtempForTestSync } from '@agent-device/test-kit';
import {
bottomTabsRealCaptureFixture,
mkdtempForTestSync,
recordArticleEvidence,
} from '@agent-device/test-kit';
import { computeTargetEvidence } from '@agent-device/selectors/target-evidence';

vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => {
const actual =
Expand All @@ -42,10 +47,6 @@ import {
baseReplayRequest as baseReq,
writeReplayFile,
} from '../replay-runtime/session-replay-runtime.fixtures.ts';
import {
bottomTabsRealCaptureFixture,
recordArticleEvidence,
} from '@agent-device/replay-port/target-classification-fixtures';
import {
captureSnapshotThroughLegacyDispatchFixture,
legacyDispatchCapture,
Expand Down Expand Up @@ -111,7 +112,7 @@ function assertDivergenceShape(
test('(a) an ANNOTATED press whose dispatch throws a selector-miss yields REPLAY_DIVERGENCE, not COMMAND_FAILED', async () => {
const root = mkdtempForTestSync('agent-device-replay-dispatch-miss-annotated-');
const { sessionStore, sessionName } = setupSession(root);
const evidence = await recordArticleEvidence();
const evidence = recordArticleEvidence(computeTargetEvidence);
const filePath = writeReplayFile(root, [
`# agent-device:target-v1 ${JSON.stringify(evidence)}`,
'click id="article"',
Expand Down
7 changes: 3 additions & 4 deletions packages/replay-port/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,6 @@
"types": "./src/daemon-port/target-classification.ts",
"default": "./src/daemon-port/target-classification.ts"
},
"./target-classification-fixtures": {
"types": "./src/target-classification.fixtures.ts",
"default": "./src/target-classification.fixtures.ts"
},
"./target-token": {
"types": "./src/daemon-port/target-token.ts",
"default": "./src/daemon-port/target-token.ts"
Expand All @@ -144,5 +140,8 @@
"types": "./src/replay-script-source.fixtures.ts",
"default": "./src/replay-script-source.fixtures.ts"
}
},
"devDependencies": {
"@agent-device/test-kit": "workspace:*"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
import {
bottomTabsRealCaptureFixture,
recordArticleEvidence,
toSnapshotNodes,
} from '@agent-device/replay-port/target-classification-fixtures';
buildNodes,
} from '@agent-device/test-kit';

/** Verified outcomes carry the verified member + matchCount (for the post-resolution guard). */
function assertVerified(
Expand All @@ -33,7 +33,7 @@ function assertVerified(
const PLATFORM = 'ios' as const;

test('classifyReplayTarget: real-capture fixture verifies by @ref when the tree is unchanged', async () => {
const recorded = await recordArticleEvidence();
const recorded = recordArticleEvidence(computeTargetEvidence);
const replayNodes = bottomTabsRealCaptureFixture();
const winner = replayNodes.find((node) => node.label === 'Article, unselected');
assert.ok(winner);
Expand All @@ -50,7 +50,7 @@ test('classifyReplayTarget: real-capture fixture verifies by @ref when the tree
});

test('classifyReplayTarget: real-capture fixture — a relabeled node is identity-mismatch (path 3)', async () => {
const recorded = await recordArticleEvidence();
const recorded = recordArticleEvidence(computeTargetEvidence);
const replayNodes = bottomTabsRealCaptureFixture();
const winner = replayNodes.find((node) => node.label === 'Article, unselected');
assert.ok(winner);
Expand Down Expand Up @@ -99,7 +99,7 @@ function saveButtonRecorded(overrides: Partial<TargetAnnotationV1> = {}): Target
}

function saveButtonTree(): SnapshotNode[] {
return toSnapshotNodes([
return buildNodes([
{ index: 0, type: 'Window', depth: 0 },
{ index: 1, type: 'Toolbar', label: 'Editor', depth: 1, parentIndex: 0 },
{
Expand All @@ -116,7 +116,7 @@ function saveButtonTree(): SnapshotNode[] {

test('classifyReplayTarget path 2: selector-miss when the recorded target is gone', () => {
const recorded = saveButtonRecorded();
const nodes = toSnapshotNodes([
const nodes = buildNodes([
{ index: 0, type: 'Window', depth: 0 },
{ index: 1, type: 'Toolbar', label: 'Editor', depth: 1, parentIndex: 0 },
]);
Expand Down Expand Up @@ -152,7 +152,7 @@ test('classifyReplayTarget path 4: verified via @ref on an unchanged tree', () =
});

test('classifyReplayTarget uses the later chain alternative that resolution selected after an earlier tie', () => {
const nodes = toSnapshotNodes([
const nodes = buildNodes([
{
index: 0,
type: 'Button',
Expand Down Expand Up @@ -231,7 +231,7 @@ test('classifyReplayTarget path 5: a unique-but-wrong rebind is caught even when
// Two "Go back" buttons at different depths: the label-only selector
// matches both, but only one carries the recorded id. The disambiguation
// heuristic (deepest-then-smallest-area) prefers the decoy.
const nodes = toSnapshotNodes([
const nodes = buildNodes([
{ index: 0, type: 'Window', rect: { x: 0, y: 0, width: 400, height: 800 }, depth: 0 },
{
index: 1,
Expand Down Expand Up @@ -279,7 +279,7 @@ test('classifyReplayTarget path 5: a unique-but-wrong rebind is caught even when
// ---------------------------------------------------------------------------

function duplicateRowTree(): SnapshotNode[] {
return toSnapshotNodes([
return buildNodes([
{ index: 0, type: 'Window', depth: 0 },
{ index: 1, type: 'ScrollView', identifier: 'list', depth: 1, parentIndex: 0 },
// Two anonymous section wrappers (role only, no label) — a real
Expand Down Expand Up @@ -439,7 +439,7 @@ test('classifyReplayTarget: document-order determinism for equal rect centers',
// tie-break were nondeterministic or reversed, the winner (e3) would not
// match `orderedRegion[0]` and this would report a mismatch instead of
// verified.
const nodes = toSnapshotNodes([
const nodes = buildNodes([
{ index: 0, type: 'ScrollView', identifier: 'list', depth: 0 },
{ index: 1, type: 'Other', depth: 1, parentIndex: 0 },
{ index: 2, type: 'Other', depth: 1, parentIndex: 0 },
Expand Down Expand Up @@ -508,7 +508,7 @@ function androidSharedIdListFixture(
parentIndex: wrapperIndex,
});
});
return toSnapshotNodes(raw);
return buildNodes(raw);
}

test('#1269 e2e: a demoted shared-id row rebinds by role+label after the shared-id rows reorder on replay', () => {
Expand Down Expand Up @@ -663,7 +663,7 @@ test('#1280 e2e: a retargeted press on a row container rebinds its labeled desce
// ---------------------------------------------------------------------------

test('#2498 e2e: a read recorded on a collapsed wrapper chain verifies on the same tree', () => {
const nodes = toSnapshotNodes(UNVERIFIED_HITTABILITY_WRAPPER_CHAIN_NODES);
const nodes = buildNodes(UNVERIFIED_HITTABILITY_WRAPPER_CHAIN_NODES);
const reports = nodes.filter((node) => node.identifier === 'scoring_home_button');
const control = resolveElementReportedTwice(nodes, reports);
assert.ok(control, 'dispatch resolves the toolbar button, not the item host that reports it');
Expand All @@ -684,7 +684,7 @@ test('#2498 e2e: a read recorded on a collapsed wrapper chain verifies on the sa
});

test('#2498 e2e: matches in distinct subtrees stay a refusal, not a collapse to one of them', () => {
const nodes = toSnapshotNodes(ELEMENT14_DISTINCT_SUBTREE_NODES);
const nodes = buildNodes(ELEMENT14_DISTINCT_SUBTREE_NODES);
const recordedWinner = nodes.find((node) => node.type === 'XCUIElementTypeButton');
assert.ok(recordedWinner);
const recorded = computeTargetEvidence({ node: recordedWinner, preActionNodes: nodes });
Expand Down
3 changes: 2 additions & 1 deletion packages/replay-port/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
{ "path": "../maestro" },
{ "path": "../replay-test" },
{ "path": "../selectors" },
{ "path": "../session-journal" }
{ "path": "../session-journal" },
{ "path": "../test-kit" }
]
}
4 changes: 4 additions & 0 deletions packages/test-kit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export {
unavailableDeploymentSnapshotAndShutdownOperationFacts,
} from './runtime-operation-facts.ts';
export { buildNodes } from './snapshot-builders.ts';
export {
bottomTabsRealCaptureFixture,
recordArticleEvidence,
} from './target-classification-fixtures.ts';
export { mkdtempForTest, mkdtempForTestSync } from './tmp-dir.ts';
export { likelyPlayableWebmContainer } from './video-fixtures.ts';
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import assert from 'node:assert/strict';
import type { RawSnapshotNode, SnapshotNode } from '@agent-device/kernel/snapshot';
import type { SnapshotNode } from '@agent-device/kernel/snapshot';
import type { TargetAnnotationV1 } from '@agent-device/contracts/replay';

export function toSnapshotNodes(raw: RawSnapshotNode[]): SnapshotNode[] {
return raw.map((node, position) => ({ ...node, ref: `e${position + 1}` }));
}
import { buildNodes } from './snapshot-builders.ts';

export function bottomTabsRealCaptureFixture(): SnapshotNode[] {
return toSnapshotNodes([
return buildNodes([
{
index: 0,
type: 'Application',
Expand Down Expand Up @@ -80,8 +77,12 @@ export function bottomTabsRealCaptureFixture(): SnapshotNode[] {
]);
}

export async function recordArticleEvidence(): Promise<TargetAnnotationV1> {
const { computeTargetEvidence } = await import('@agent-device/selectors/target-evidence');
export function recordArticleEvidence(
computeTargetEvidence: (capture: {
node: SnapshotNode;
preActionNodes: SnapshotNode[];
}) => TargetAnnotationV1 | undefined,
): TargetAnnotationV1 {
const nodes = bottomTabsRealCaptureFixture();
const winner = nodes.find((node) => node.label === 'Article, unselected');
if (!winner) throw new Error('fixture missing Article tab');
Expand Down
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading