From c7da1e81e5c250a66f55c7fc27c80d469e1d2db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 3 Oct 2026 12:08:49 +0200 Subject: [PATCH] fix: make retired touch observations consistent --- .../selector-capture-runtime.test.ts | 4 -- .../interaction-touch-response.test.ts | 54 ++++++++++++++++++- .../internal/find-target-capture.ts | 4 +- src/daemon/interaction/internal/find.ts | 1 - .../internal/interaction-touch-press.ts | 1 - .../internal/interaction-touch-response.ts | 19 ++++--- src/daemon/selector-capture-runtime.ts | 1 - src/daemon/selector-runtime-backend.ts | 1 - .../session-open-execution-runtime.test.ts | 2 +- 9 files changed, 64 insertions(+), 23 deletions(-) diff --git a/src/daemon/__tests__/selector-capture-runtime.test.ts b/src/daemon/__tests__/selector-capture-runtime.test.ts index 9230151055..5173d75ad5 100644 --- a/src/daemon/__tests__/selector-capture-runtime.test.ts +++ b/src/daemon/__tests__/selector-capture-runtime.test.ts @@ -54,7 +54,6 @@ test('selector capture cache is keyed by scoped presentation options', async () const runtime = createSelectorCaptureRuntime({ ref: sessionStore.lookup(sessionName), device: session.device, - session, sessionStore, sessionName, capture: boundCapture, @@ -228,7 +227,6 @@ function proofRuntime(params: { const runtime = createSelectorCaptureRuntime({ ref: sessionStore.lookup(params.sessionName), device: session.device, - session, sessionStore, sessionName: params.sessionName, consumedSnapshot, @@ -322,7 +320,6 @@ function makeCaptureRuntime(sessionName: string) { const runtime = createSelectorCaptureRuntime({ ref: sessionStore.lookup(sessionName), device: session.device, - session, sessionStore, sessionName, capture: boundCapture, @@ -356,7 +353,6 @@ test('a held selector capture updates the matching rebuilt record without restor const runtime = createSelectorCaptureRuntime({ ref, device: ref.session.device, - session: ref.session, sessionStore, sessionName: address, capture: boundCapture, diff --git a/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts b/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts index c72da0e3eb..0dbc8a9b27 100644 --- a/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts +++ b/src/daemon/interaction/internal/__tests__/interaction-touch-response.test.ts @@ -101,6 +101,59 @@ function selectorResult(readiness?: { polls: number; waitedMs: number }): PressC const WAITED_SELECTOR_RESULT = selectorResult({ polls: 3, waitedMs: 400 }); +test.each([false, true])('selector-touch observation after retirement=%s', async (retired) => { + const sessionStore = makeSessionStore(); + const address = 'cwd:selector-touch-response:default'; + const session = makeSession('default'); + session.snapshot = { + nodes: attachRefs([{ index: 0, type: 'Button', label: 'Continue' }]), + createdAt: Date.now(), + backend: 'xctest', + }; + session.snapshotGeneration = 3; + const ref = sessionStore.publish(address, session); + const result: PressCommandResult = { + ...selectorResult(), + settle: { + settled: true, + waitedMs: 25, + captures: 2, + quietMs: 25, + timeoutMs: 2000, + diff: { + summary: { additions: 1, removals: 0, unchanged: 0 }, + lines: [{ kind: 'added', text: 'Continue', ref: 'e1' }], + }, + }, + }; + let successor; + if (retired) { + sessionStore.retire(ref); + successor = sessionStore.publish(address, makeSession('default')); + } + const payloads = await buildTargetedTouchResponsePayloads({ + params: { + req: { token: 't', command: 'press', positionals: ['@e1'], session: 'default' }, + sessionName: address, + sessionRef: ref, + sessionStore, + contextFromFlags, + captureSnapshotForSession: vi.fn(), + }, + result, + staleRefsWarning: undefined, + extra: {}, + }); + if (retired) { + expect(payloads.responseData.settle).toBeUndefined(); + expect(payloads.result.settle).toBeUndefined(); + expect(sessionStore.lookup(address)).toEqual(successor); + expect(successor?.session.snapshot).toBeUndefined(); + } else { + expect(payloads.responseData.settle).toMatchObject({ refsGeneration: 3, settled: true }); + } +}); + test.each([false, true])( 'point-touch publication after a held frame probe, retired=%s', async (retired) => { @@ -157,7 +210,6 @@ test.each([false, true])( return sessionStore.requireCurrent(ref).snapshot!; }, }, - session, result, staleRefsWarning: undefined, extra: {}, diff --git a/src/daemon/interaction/internal/find-target-capture.ts b/src/daemon/interaction/internal/find-target-capture.ts index 9160a93e85..3109598692 100644 --- a/src/daemon/interaction/internal/find-target-capture.ts +++ b/src/daemon/interaction/internal/find-target-capture.ts @@ -21,7 +21,6 @@ export function createFindTargetCapture( params: Readonly<{ ref: SessionRef; device: SessionState['device']; - session: SessionState; req: DaemonRequest; logPath: string; locator: FindLocator; @@ -36,11 +35,10 @@ export function createFindTargetCapture( captureProof: RequestCaptureProof; }>, ): () => Promise { - const { device, session, req, logPath, locator, query, sessionStore, sessionName } = params; + const { device, req, logPath, locator, query, sessionStore, sessionName } = params; const captureRuntime = createSelectorCaptureRuntime({ ref: params.ref, device, - session, sessionStore, sessionName, req, diff --git a/src/daemon/interaction/internal/find.ts b/src/daemon/interaction/internal/find.ts index 92c8e04ae7..1057e97804 100644 --- a/src/daemon/interaction/internal/find.ts +++ b/src/daemon/interaction/internal/find.ts @@ -134,7 +134,6 @@ export async function handleFindCommands(params: FindRouteInput): Promise; extra: Record; }): Promise { const { params: handlerParams, result, publicData, extra } = params; - const referenceFrame = + const probedFrame = result.kind === 'point' ? await resolveDirectTouchReferenceFrameSafely({ ref: handlerParams.sessionRef!, @@ -305,13 +303,14 @@ export async function buildTargetedTouchResponsePayloads(params: { captureSnapshotForSession: handlerParams.captureSnapshotForSession, observation: handlerParams.androidObservation, }) - : readSnapshotNodesReferenceFrame( - handlerParams.sessionStore.requireCurrent(handlerParams.sessionRef!).snapshot?.nodes ?? - [], - ); - const currentResult = handlerParams.sessionStore.resolveCurrent(handlerParams.sessionRef!) - ? result - : { ...result, settle: undefined }; + : undefined; + const current = handlerParams.sessionStore.resolveCurrent(handlerParams.sessionRef!); + const referenceFrame = current + ? result.kind === 'point' + ? probedFrame + : readSnapshotNodesReferenceFrame(current.snapshot?.nodes ?? []) + : undefined; + const currentResult = current ? result : { ...result, settle: undefined }; return buildInteractionResponseData({ source: { kind: 'runtime', result: currentResult, publicData }, referenceFrame, diff --git a/src/daemon/selector-capture-runtime.ts b/src/daemon/selector-capture-runtime.ts index 508ffca20b..6bd2a92089 100644 --- a/src/daemon/selector-capture-runtime.ts +++ b/src/daemon/selector-capture-runtime.ts @@ -26,7 +26,6 @@ const SELECTOR_CAPTURE_CACHE_TTL_MS = 750; export type SelectorCaptureRuntimeParams = { ref: SessionRef | undefined; device: SessionState['device']; - session: SessionState | undefined; sessionStore: SessionStore; sessionName: string; req: DaemonRequest; diff --git a/src/daemon/selector-runtime-backend.ts b/src/daemon/selector-runtime-backend.ts index 1e87c6c47f..24ad3eeb8d 100644 --- a/src/daemon/selector-runtime-backend.ts +++ b/src/daemon/selector-runtime-backend.ts @@ -181,7 +181,6 @@ function createSelectorBackend(params: SelectorRuntimeDeviceParams): AgentDevice : createSelectorCaptureRuntime({ ref: params.ref, device, - session, sessionStore, sessionName, req, diff --git a/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts b/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts index d7d41a58ef..f5d2f37a62 100644 --- a/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts +++ b/src/daemon/session-lifecycle/internal/__tests__/session-open-execution-runtime.test.ts @@ -605,7 +605,7 @@ test('fresh open does not publish when its artifact directory cannot be created' fs.writeFileSync(path.dirname(store.resolveSessionDir('cwd:held-open:default')), 'blocked'); await expect( invokeHeldOpen(store, { runtime: { metroHost: 'new-host', metroPort: 9000 } }), - ).rejects.toThrow(); + ).rejects.toMatchObject({ code: 'ENOTDIR' }); expect(mockDispatch).toHaveBeenCalled(); expect(store.get('cwd:held-open:default')).toBeUndefined(); expect(store.getRuntimeHints('cwd:held-open:default')).toBeUndefined();