From 83333c6cf3b0610e993a8edfdec866a36539dba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E5=AD=90=E8=81=AA?= Date: Fri, 24 Jul 2026 18:39:25 -0700 Subject: [PATCH] test(desktop): pin the genuine post-create switch abort during attachment sync (#62805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvaged from PR #62805 (@floatingrain), whose diagnosis of the deterministic frontend self-abort (createBackendSessionForSend mutating the selected ref + route, then the caller's drift guard reading its own re-home as a user switch) was correct — and correct about the sweeper misread that closed it: the sweeper's implemented_on_main verdict cited submit.ts:245, which was inside the session.resume block, while the raw post-create guard at the createBackendSessionForSend site was still aborting every new chat on the then-current main (4281151ae8). The mechanism itself has since landed via 8c288760d0 + 1bdd478efa, so what remains distinct is this regression: after the pipeline adopts the created chat as its pinned target, a GENUINE user switch (selection and route both moving to another chat during the attachment-sync await) must still abort instead of being masked by the re-baseline. Part of the #63078 fix branch. --- .../hooks/use-prompt-actions/index.test.tsx | 85 +++++++++++++++++++ contributors/emails/floatingrain@yeah.net | 1 + 2 files changed, 86 insertions(+) create mode 100644 contributors/emails/floatingrain@yeah.net diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx index 3b946a19f587..acd3d0e00952 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/index.test.tsx @@ -2973,6 +2973,91 @@ describe('usePromptActions new-chat first-send delivery (#63078)', () => { expect(selectedStoredSessionIdRef.current).toBe(NEW_STORED_ID) expect(calls.some(call => call.method === 'prompt.submit')).toBe(false) }) + + it('still aborts when the user genuinely switches chats after create, during attachment sync (#62805)', async () => { + // The post-create re-baseline (adopting the created chat as the pinned + // target) must not mask a REAL switch later in the pipeline: the user + // clicks a different session after createBackendSessionForSend lands but + // before attachment sync settles — selection AND route both move to the + // other chat, and the drift check at the post-attachments boundary must + // abort rather than deliver the text into whichever chat won the race. + const activeSessionIdRef: MutableRefObject = { current: null } + const selectedStoredSessionIdRef: MutableRefObject = { current: null } + let routeToken = '/::' + + let releaseFileAttach: () => void = () => {} + + $connection.set({ mode: 'remote' } as never) + Object.defineProperty(window, 'hermesDesktop', { + configurable: true, + value: { readFileDataUrl: vi.fn(async () => 'data:application/pdf;base64,JVBERi0=') } + }) + + const createBackendSessionForSend = vi.fn(async () => { + activeSessionIdRef.current = NEW_RUNTIME_ID + selectedStoredSessionIdRef.current = NEW_STORED_ID + routeToken = `/${NEW_STORED_ID}::` + + return NEW_RUNTIME_ID + }) + + const calls: { method: string; params?: Record }[] = [] + + const requestGateway = vi.fn(async (method: string, params?: Record) => { + calls.push({ method, params }) + + if (method === 'file.attach') { + // Block here so the user can switch sessions mid-sync. + await new Promise(resolve => { + releaseFileAttach = resolve + }) + + return { + attached: true, + ref_text: '@file:.hermes/desktop-attachments/test.pdf', + uploaded: true + } as never + } + + return {} as never + }) + + const attachment: ComposerAttachment = { + id: 'file:test', + kind: 'file', + label: 'test.pdf', + path: '/abs/test.pdf', + refText: '@file:`/abs/test.pdf`' + } + + let handle: HarnessHandle | null = null + render( + routeToken} + onReady={h => (handle = h)} + refreshSessions={async () => undefined} + requestGateway={requestGateway} + selectedStoredSessionIdRef={selectedStoredSessionIdRef} + storedSessionId={null} + /> + ) + await waitFor(() => expect(handle).not.toBeNull()) + + const submitting = handle!.submitText('message before the switch', { attachments: [attachment] }) + await waitFor(() => expect(calls.some(c => c.method === 'file.attach')).toBe(true)) + + // Simulate a user switching to a different session after the new session + // was created and the sync phase started. + selectedStoredSessionIdRef.current = 'stored-other-session' + routeToken = '/stored-other-session::' + releaseFileAttach() + + expect(await submitting).toBe(false) + expect(calls.some(c => c.method === 'prompt.submit')).toBe(false) + }) }) describe('usePromptActions eager attachment upload (drop-time)', () => { diff --git a/contributors/emails/floatingrain@yeah.net b/contributors/emails/floatingrain@yeah.net new file mode 100644 index 000000000000..80165203d5b1 --- /dev/null +++ b/contributors/emails/floatingrain@yeah.net @@ -0,0 +1 @@ +floatingrain