diff --git a/src/main/wsl/wsl-orcad-chat-provider-fixture.ts b/src/main/wsl/wsl-orcad-chat-provider-fixture.ts index 1ba3bfb2d92..e9db3c21be9 100644 --- a/src/main/wsl/wsl-orcad-chat-provider-fixture.ts +++ b/src/main/wsl/wsl-orcad-chat-provider-fixture.ts @@ -1,3 +1,4 @@ +import { vi } from 'vitest' import { z } from 'zod' import { folderWorkspaceKey } from '../../shared/workspace-scope' import { readFileSync } from 'node:fs' @@ -126,3 +127,34 @@ export const WslChatProviderReport = z.object({ controlRequests: z.array(z.object({ request: z.object({ subtype: z.string() }) })), controlResponses: z.array(z.object({ response: z.object({ response: z.unknown().optional() }) })) }) + +const PROVIDER_REPORT_TIMEOUT_MS = 30_000 + +/** Scripted providers rewrite their report in place after each act, so one read can see a torn file or a stale one. */ +export async function waitForProviderReport( + host: WslChatHost, + path: string, + schema: z.ZodType, + settled: (report: T) => boolean, + awaiting: string +): Promise { + let lastRead = '' + try { + return await vi.waitFor( + async () => { + lastRead = await host.guest(`cat ${quotePosixShell(path)} 2>/dev/null || true`) + const report = schema.parse(JSON.parse(lastRead)) + if (!settled(report)) { + throw new Error(`report does not show ${awaiting} yet`) + } + return report + }, + { timeout: PROVIDER_REPORT_TIMEOUT_MS, interval: 250 } + ) + } catch (error) { + throw new Error( + `Provider report ${path} did not show ${awaiting} within ${PROVIDER_REPORT_TIMEOUT_MS} ms; last read ${JSON.stringify(lastRead.slice(-600))}`, + { cause: error } + ) + } +} diff --git a/src/main/wsl/wsl-orcad-scripted-provider-chat.ts b/src/main/wsl/wsl-orcad-scripted-provider-chat.ts index 67be18a25f7..610c80f9558 100644 --- a/src/main/wsl/wsl-orcad-scripted-provider-chat.ts +++ b/src/main/wsl/wsl-orcad-scripted-provider-chat.ts @@ -6,7 +6,10 @@ import type { AgentSessionSubscribeEvent } from '../../shared/agent-session-wire import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../shared/electron-remote-runtime-client-capabilities' import { subscribeRemoteRuntimeRequest } from '../../shared/remote-runtime/remote-runtime-client' import { quotePosixShell } from '../../shared/shell-argument-quoting' -import type { WslScriptedProviderInstall } from './wsl-orcad-chat-provider-fixture' +import { + waitForProviderReport, + type WslScriptedProviderInstall +} from './wsl-orcad-chat-provider-fixture' import { assistantText } from './wsl-orcad-chat-assistant-text' import { envelope } from './wsl-orcad-chat-envelope' import type { ChatLane } from './wsl-orcad-structured-chat-contract' @@ -167,8 +170,13 @@ export async function exerciseScriptedProviderChat( }, { timeout: 30_000 } ) - const report = await readReport() - expect(report.methods).toContain(provider.stopMethod) + const report = await waitForProviderReport( + lane, + install.reportPath, + ScriptedProviderReport, + (written) => written.methods.includes(provider.stopMethod), + provider.stopMethod + ) expect(report.cwd).toBe(cwd) // The guest's own runtime ran the guest install found on its login PATH: never a /mnt/ copy. expect(report.execPath).toMatch(/^\/(?!mnt\/)/u) diff --git a/src/main/wsl/wsl-orcad-structured-chat-contract.ts b/src/main/wsl/wsl-orcad-structured-chat-contract.ts index 0bab9c7ef6d..ed3a9e7553d 100644 --- a/src/main/wsl/wsl-orcad-structured-chat-contract.ts +++ b/src/main/wsl/wsl-orcad-structured-chat-contract.ts @@ -28,6 +28,7 @@ import { import { installWslChatFixture, WslChatProviderReport, + waitForProviderReport, writeChatFixtureFile, type WslChatHost } from './wsl-orcad-chat-provider-fixture' @@ -180,8 +181,12 @@ async function exerciseChat( envelope: envelope(sessionId, 'agentSession.cancel', {}, fence) }) ).toMatchObject({ ok: true, value: { cancelled: true } }) - const report = WslChatProviderReport.parse( - JSON.parse(await lane.guest(`cat ${quotePosixShell(reportPath)}`)) + const report = await waitForProviderReport( + lane, + reportPath, + WslChatProviderReport, + (written) => written.controlRequests.some((entry) => entry.request.subtype === 'interrupt'), + 'the Stop interrupt' ) expect(report.cwd).toBe(cwd) expect(report.execPath).toMatch(/^\//u) @@ -192,7 +197,6 @@ async function exerciseChat( ORCA_AGENT_SESSION_ID: sessionId, ORCA_STRUCTURED_SESSION: '1' }) - expect(report.controlRequests.some((entry) => entry.request.subtype === 'interrupt')).toBe(true) expect(report.controlResponses).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -257,11 +261,16 @@ async function exerciseChat( }, { timeout: 30_000 } ) - const resumed = WslChatProviderReport.parse( - JSON.parse(await lane.guest(`cat ${quotePosixShell(reportPath)}`)) + const resumedArgv = new RegExp(`--resume(?:=| )${providerId}`) + const resumed = await waitForProviderReport( + lane, + reportPath, + WslChatProviderReport, + (written) => resumedArgv.test(written.argv.join(' ')) && written.userMessages.length > 0, + 'the resumed turn' ) expect(resumed.cwd).toBe(cwd) - expect(resumed.argv.join(' ')).toMatch(new RegExp(`--resume(?:=| )${providerId}`)) + expect(resumed.argv.join(' ')).toMatch(resumedArgv) expect(resumed.userMessages).toHaveLength(1) return { sessionId,