From 07f858f4e79c0afca0291a5362687d0daad7bb55 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 7 Oct 2026 02:26:27 -0700 Subject: [PATCH 1/2] test(desktop): pin the journal snapshot before recovery, keep start going when the journal cannot load, and run tmux in the canary E2E --- .github/workflows/desktop-e2e.yml | 4 ++ .../src/main/desktop-executor/service.test.ts | 62 ++++++++++++++++--- .../src/main/desktop-executor/service.ts | 8 +++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/.github/workflows/desktop-e2e.yml b/.github/workflows/desktop-e2e.yml index 2981d76cf48..72642e5b7c3 100644 --- a/.github/workflows/desktop-e2e.yml +++ b/.github/workflows/desktop-e2e.yml @@ -129,6 +129,10 @@ jobs: working-directory: apps/desktop run: bunx playwright install chromium + # The terminal-cancel suite drives a real tmux server; without tmux its tmux scenarios skip. + - name: Install tmux + run: brew install tmux + - name: Run Playwright _electron smoke suite working-directory: apps/desktop run: bunx playwright test diff --git a/apps/desktop/src/main/desktop-executor/service.test.ts b/apps/desktop/src/main/desktop-executor/service.test.ts index 363e418fedf..f90eeacdc88 100644 --- a/apps/desktop/src/main/desktop-executor/service.test.ts +++ b/apps/desktop/src/main/desktop-executor/service.test.ts @@ -6,7 +6,31 @@ import { describe, expect, it, vi } from 'vitest' vi.mock('electron', () => import('@/test/electron-mock')) +/** How many journal loads still fail, like a read that throws. */ +const journalFaults = vi.hoisted(() => ({ loads: 0 })) + +vi.mock('@/main/desktop-executor/journal', async () => { + const actual = await vi.importActual( + '@/main/desktop-executor/journal' + ) + return { + ...actual, + createExecutorJournal: (...args: Parameters) => { + const journal = actual.createExecutorJournal(...args) + return { + ...journal, + load: () => { + if (journalFaults.loads <= 0) return journal.load() + journalFaults.loads -= 1 + return Promise.reject(new Error('The disk went away.')) + }, + } + }, + } +}) + import { net } from 'electron' +import { DesktopExecutor } from '@/main/desktop-executor/executor' import { createExecutorJournal } from '@/main/desktop-executor/journal' import { createDesktopExecutorService, deviceName } from '@/main/desktop-executor/service' @@ -156,18 +180,40 @@ describe('results recovery will hand to the model', () => { executionToken: 't1', completion: { status: 'success', message: 'running', data: { status: 'running' } }, }) - const { sim, desktopExecutor } = await service(1, userData) + // Recovery returns only once Sim has the result and the journal no longer holds it, so a + // snapshot taken any time after recovery started would come back empty. + const recover = DesktopExecutor.prototype.recover + const recovered = vi + .spyOn(DesktopExecutor.prototype, 'recover') + .mockImplementation(async function (this: DesktopExecutor) { + await recover.call(this) + await vi.waitFor(async () => expect(await createExecutorJournal(path).load()).toEqual([])) + }) + try { + const { sim, desktopExecutor } = await service(1, userData) + desktopExecutor.start() + await vi.waitFor(() => expect(sim.registrations).toHaveLength(1)) + sim.registrations[0]?.(true) + await vi.waitFor(() => expect(sim.requests).toContain('POST /api/desktop/tool/complete')) + await vi.waitFor(() => expect(recovered).toHaveBeenCalled()) + + expect([...(await desktopExecutor.pendingResults())]).toEqual(['handed-back']) + await desktopExecutor.signOut() + } finally { + recovered.mockRestore() + } + }) + + it('counts a journal that cannot be loaded at all as holding none, and still starts', async () => { + journalFaults.loads = 1 + const { sim, desktopExecutor } = await service() + + expect([...(await desktopExecutor.pendingResults())]).toEqual([]) desktopExecutor.start() await vi.waitFor(() => expect(sim.registrations).toHaveLength(1)) sim.registrations[0]?.(true) - // Recovery hands the result to Sim and drops it from the journal. - await vi.waitFor(async () => { - expect(sim.requests).toContain('POST /api/desktop/tool/complete') - expect(await createExecutorJournal(path).load()).toEqual([]) - }) - - expect([...(await desktopExecutor.pendingResults())]).toEqual(['handed-back']) + await vi.waitFor(() => expect(sim.requests).toContain('GET /api/desktop/inbox')) await desktopExecutor.signOut() }) }) diff --git a/apps/desktop/src/main/desktop-executor/service.ts b/apps/desktop/src/main/desktop-executor/service.ts index a46ffc29377..a9c7cdd0282 100644 --- a/apps/desktop/src/main/desktop-executor/service.ts +++ b/apps/desktop/src/main/desktop-executor/service.ts @@ -154,6 +154,14 @@ export function createDesktopExecutorService( ) ) ) + // The journal reads an unreadable file as empty, so this is for a load that throws anyway: + // it holds none, and the executor still starts. + .catch((error: unknown) => { + logger.warn('Could not read the executor journal for pending results', { + error: getErrorMessage(error), + }) + return new Set() + }) return pendingSnapshot } /** Bumped on sign-out, so work started for the previous session cannot resume it. */ From 9cece15ca4edd261ec993f8b26ea8cdf17a44e57 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 7 Oct 2026 02:47:19 -0700 Subject: [PATCH 2/2] test(desktop): wait for the journal to empty, not for recovery to be called --- apps/desktop/src/main/desktop-executor/service.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/main/desktop-executor/service.test.ts b/apps/desktop/src/main/desktop-executor/service.test.ts index f90eeacdc88..9542c34ff5e 100644 --- a/apps/desktop/src/main/desktop-executor/service.test.ts +++ b/apps/desktop/src/main/desktop-executor/service.test.ts @@ -195,7 +195,7 @@ describe('results recovery will hand to the model', () => { await vi.waitFor(() => expect(sim.registrations).toHaveLength(1)) sim.registrations[0]?.(true) await vi.waitFor(() => expect(sim.requests).toContain('POST /api/desktop/tool/complete')) - await vi.waitFor(() => expect(recovered).toHaveBeenCalled()) + await vi.waitFor(async () => expect(await createExecutorJournal(path).load()).toEqual([])) expect([...(await desktopExecutor.pendingResults())]).toEqual(['handed-back']) await desktopExecutor.signOut()