From aff15b1f4ab5da9339002ded90c8a9bafe3b97a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 6 Aug 2026 13:03:19 +0200 Subject: [PATCH 1/2] fix(ios): keep the baseline presentation when a tap corroboration has no request flags matchingCaptureFlags dropped the baseline snapshot's scope/depth/raw whenever the incoming request carried no flags, so the post-action corroboration capture ran at the default presentation and could never match a non-default baseline's presentationKey. The corroboration then silently declined to engage, leaking the raw XCTEST_RECORDED_FAILURE it exists to eliminate. --- .../interaction-ios-tap-outcome.test.ts | 47 +++++++++++++++++++ .../handlers/interaction-ios-tap-outcome.ts | 4 +- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts b/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts index 580461eb8..8a477754d 100644 --- a/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts +++ b/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts @@ -4,6 +4,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; +import { buildSnapshotPresentationKey } from '@agent-device/kernel/snapshot'; import { handleInteractionCommands } from '../interaction.ts'; import { handleSnapshotCommands } from '../snapshot.ts'; import { dispatchCommand } from '../../../core/dispatch.ts'; @@ -341,6 +342,52 @@ test('a changed capture with a different presentation keeps the tap failure', as expect(sessionStore.get(sessionName)?.actions).toHaveLength(0); }); +test('corroborates a tap when the request carries no flags and the baseline used a non-default scope', async () => { + const sessionName = 'ios-no-flags-tap-corroboration'; + const sessionStore = makeSessionStore(); + const baseline = snapshot(profileNodes); + baseline.presentationKey = buildSnapshotPresentationKey({ depth: 2, raw: false }); + sessionStore.set( + sessionName, + makeIosSession(sessionName, { + appBundleId: 'com.example.app', + snapshot: baseline, + }), + ); + mockDispatch.mockImplementation(async (_device, command) => { + if (command === 'press') { + throw new AppError( + 'XCTEST_RECORDED_FAILURE', + 'XCTest recorded a failure while executing tap; the action may not have been performed.', + ); + } + if (command === 'snapshot') return snapshotPayload(imageViewerNodes); + return {}; + }); + + // Deliberately built without a `flags` key at all (not `flags: {}`) — this + // mirrors the real production paths (batch steps with no flags, JSON-RPC + // requests that omit the key) that hide the bug this test pins. + const response = await handleInteractionCommands({ + req: { + token: 'test', + session: sessionName, + command: 'click', + positionals: ['id="unfollow"'], + }, + sessionName, + sessionStore, + contextFromFlags, + }); + + expect(response?.ok).toBe(true); + if (response?.ok) { + expect(response.data?.warning).toMatch(/post-action accessibility capture changed/); + expect(response.data?.selector).toBe('id="unfollow"'); + } + expect(sessionStore.get(sessionName)?.actions).toHaveLength(1); +}); + test('a changed capture against a stale baseline keeps the tap failure', async () => { const sessionName = 'ios-stale-baseline-tap-corroboration'; const sessionStore = makeSessionStore(); diff --git a/src/daemon/handlers/interaction-ios-tap-outcome.ts b/src/daemon/handlers/interaction-ios-tap-outcome.ts index 5f5235140..d6138137d 100644 --- a/src/daemon/handlers/interaction-ios-tap-outcome.ts +++ b/src/daemon/handlers/interaction-ios-tap-outcome.ts @@ -247,9 +247,9 @@ function matchingCaptureFlags( flags: CommandFlags | undefined, presentation: SnapshotPresentation | undefined, ): CommandFlags | undefined { - if (!flags) return undefined; + if (!flags && !presentation) return undefined; return { - ...flags, + ...(flags ?? {}), out: undefined, ...(presentation ? { From aae69fd006f2f1e10a11c7b2f53b61262a32a9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Thu, 6 Aug 2026 21:40:36 +0200 Subject: [PATCH 2/2] docs(test): correct tap corroboration reachability --- .../handlers/__tests__/interaction-ios-tap-outcome.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts b/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts index 8a477754d..1f85e6cb8 100644 --- a/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts +++ b/src/daemon/handlers/__tests__/interaction-ios-tap-outcome.test.ts @@ -366,8 +366,8 @@ test('corroborates a tap when the request carries no flags and the baseline used }); // Deliberately built without a `flags` key at all (not `flags: {}`) — this - // mirrors the real production paths (batch steps with no flags, JSON-RPC - // requests that omit the key) that hide the bug this test pins. + // mirrors the raw daemon/JSON-RPC production boundary, which can omit the + // key entirely. CLI and batch paths always materialize a flags object. const response = await handleInteractionCommands({ req: { token: 'test',