mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
test(desktop): pin the genuine post-create switch abort during attachment sync (#62805)
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 via8c288760d0+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.
This commit is contained in:
parent
eb2f648628
commit
83333c6cf3
2 changed files with 86 additions and 0 deletions
|
|
@ -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<string | null> = { current: null }
|
||||
const selectedStoredSessionIdRef: MutableRefObject<string | null> = { 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<string, unknown> }[] = []
|
||||
|
||||
const requestGateway = vi.fn(async (method: string, params?: Record<string, unknown>) => {
|
||||
calls.push({ method, params })
|
||||
|
||||
if (method === 'file.attach') {
|
||||
// Block here so the user can switch sessions mid-sync.
|
||||
await new Promise<void>(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(
|
||||
<Harness
|
||||
activeSessionId={null}
|
||||
activeSessionIdRef={activeSessionIdRef}
|
||||
createBackendSessionForSend={createBackendSessionForSend}
|
||||
getRouteToken={() => 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)', () => {
|
||||
|
|
|
|||
1
contributors/emails/floatingrain@yeah.net
Normal file
1
contributors/emails/floatingrain@yeah.net
Normal file
|
|
@ -0,0 +1 @@
|
|||
floatingrain
|
||||
Loading…
Add table
Add a link
Reference in a new issue