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..1f85e6cb8 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 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', + 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 ? {