mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
6c0fcb64a1
* fix: reject distinct ambiguous mutation targets * fix(ios): scope the raw-match rejection to mutating dispatches `RunnerTests+Interaction.findElement` applied the new fail-closed classification to `querySelector` as well as press/type, because the read call site takes the default `allowNonHittableFallback: false`. With one visible/hittable match and one non-hittable same-selector duplicate the query started returning AMBIGUOUS_MATCH where it previously selected the hittable element, and `queryDirectIosSelectorOrFallback` preserves that error for read callers — so `get`, `is`, and `wait` surfaced an error instead of their prior answer. `classifyDirectSelectorCandidates` now takes a `rawMatchPolicy`. Mutations keep `.rejectDistinctMatches` (the default, so no mutation call site changes); `queryElement` passes `.preferHittableMatch`, restoring the prior read rule: prefer the single hittable match, ambiguous only when hittable matches compete, and never adopt the Maestro coordinate fallback. The Maestro expected-point path is untouched. Covers the one-hittable + one-non-hittable read, competing hittable reads, and the non-hittable-only read. ADR 0011's amendment now states the scope. * test(ios): execute selector read ambiguity regression --------- Co-authored-by: Claude <noreply@anthropic.com>
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'vitest';
|
|
import { ref, selector } from '../../../src/commands/interaction/runtime/selector-read-utils.ts';
|
|
import { equivalentWrapperChainSnapshot } from './fixtures.ts';
|
|
import { createContractDevice } from './runtime-harness.ts';
|
|
|
|
// ADR 0012 mutation contract, daemon half: diagnosticRef tokens are not refs.
|
|
// The MCP half (never ref-issued/pinned) lives in src/mcp/__tests__/command-tools.test.ts.
|
|
|
|
test('resolution mutation contract: a diagnosticRef is not a resolvable @ref target', async () => {
|
|
const device = createContractDevice(equivalentWrapperChainSnapshot(), {
|
|
tap: async () => ({ ok: true }),
|
|
});
|
|
|
|
const result = await device.interactions.click(selector('label=Chat'), {
|
|
session: 'default',
|
|
});
|
|
|
|
assert.equal(result.kind, 'selector');
|
|
const resolution = result.resolution;
|
|
assert.equal(resolution?.kind, 'disambiguated');
|
|
if (resolution?.kind !== 'disambiguated') return;
|
|
const alternativeDiagnosticRef = resolution.alternatives[0]?.diagnosticRef;
|
|
assert.ok(
|
|
alternativeDiagnosticRef,
|
|
'the disambiguated resolution must carry a losing alternative',
|
|
);
|
|
|
|
// Acting on it without a fresh snapshot gets the ordinary unknown-ref refusal.
|
|
await assert.rejects(
|
|
() => device.interactions.press(ref(`@${alternativeDiagnosticRef}`), { session: 'default' }),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof Error);
|
|
assert.equal((error as { code?: string }).code, 'COMMAND_FAILED');
|
|
assert.match(error.message, new RegExp(`Ref @${alternativeDiagnosticRef} not found`));
|
|
const details = (error as { details?: Record<string, unknown> }).details;
|
|
assert.match(String(details?.hint), /refs expire/i);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test('resolution mutation contract: the winner diagnosticRef is also not a resolvable @ref target', async () => {
|
|
const device = createContractDevice(equivalentWrapperChainSnapshot(), {
|
|
tap: async () => ({ ok: true }),
|
|
});
|
|
|
|
const result = await device.interactions.click(selector('label=Chat'), {
|
|
session: 'default',
|
|
});
|
|
|
|
assert.equal(result.kind, 'selector');
|
|
const resolution = result.resolution;
|
|
assert.equal(resolution?.kind, 'disambiguated');
|
|
if (resolution?.kind !== 'disambiguated') return;
|
|
const winnerDiagnosticRef = resolution.winnerDiagnostic.diagnosticRef;
|
|
|
|
// The winner's token is equally non-actionable.
|
|
await assert.rejects(
|
|
() => device.interactions.press(ref(`@${winnerDiagnosticRef}`), { session: 'default' }),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof Error);
|
|
assert.equal((error as { code?: string }).code, 'COMMAND_FAILED');
|
|
return true;
|
|
},
|
|
);
|
|
});
|