diff --git a/test/helpers/coding-cli/real-session-contract-harness.ts b/test/helpers/coding-cli/real-session-contract-harness.ts index cd0def13..eae6a59b 100644 --- a/test/helpers/coding-cli/real-session-contract-harness.ts +++ b/test/helpers/coding-cli/real-session-contract-harness.ts @@ -822,6 +822,54 @@ export async function startCodexAppServer( } } +export async function startOpencodeServe( + workspace: ProbeWorkspace, + opencodePath: string, + runEnv: NodeJS.ProcessEnv, + healthPath: string, +): Promise<{ + baseUrl: string + port: number + process: TrackedProcess + health: unknown +}> { + const endpoint = await allocateLocalhostPort() + const baseUrl = `http://${endpoint.hostname}:${endpoint.port}` + const processHandle = await workspace.spawnProcess( + opencodePath, + ['serve', '--hostname', endpoint.hostname, '--port', String(endpoint.port)], + { + env: runEnv, + }, + ) + + // Fail fast (with the serve process's own error) if it cannot bind the port, + // rather than silently attaching to a stranger already listening on it or + // waiting out the full health-check timeout. + const health = await waitFor(`OpenCode serve ${baseUrl}`, async () => { + const stderr = processHandle.stderr() + if (/ServeError|Failed to start server|EADDRINUSE/i.test(stderr)) { + throw new Error(`OpenCode serve could not start on ${baseUrl}: ${stderr.trim()}`) + } + try { + const response = await fetchWithTimeout(`${baseUrl}${healthPath}`) + if (!response.ok) { + return undefined + } + return await response.json() + } catch { + return undefined + } + }, 30_000, 200) + + return { + baseUrl, + port: endpoint.port, + process: processHandle, + health, + } +} + export class CodexRpcProbeClient { private readonly socket: WebSocket private readonly pendingRequests = new Map() diff --git a/test/integration/real/coding-cli-session-contract.test.ts b/test/integration/real/coding-cli-session-contract.test.ts index ca8ec9ea..a3adea83 100644 --- a/test/integration/real/coding-cli-session-contract.test.ts +++ b/test/integration/real/coding-cli-session-contract.test.ts @@ -22,11 +22,11 @@ import { seedCodexHome, seedOpencodeHomes, startCodexAppServer, + startOpencodeServe, waitForCodexSessionArtifact, waitForCodexShellSnapshot, waitForFileSizeIncrease, waitForAnyHttpBusyStatus, - waitForHttpHealthy, waitForJsonResponse, waitForJsonLine, waitForOpencodeDbSession, @@ -528,18 +528,14 @@ describe.sequential('coding cli real provider session contract', () => { const firstDbRow = await waitForOpencodeDbSession(homes.dbPath, firstSessionId) expect(firstDbRow.id).toBe(firstSessionId) - const servePort = 46123 - const serve = await workspace.spawnProcess( - opencodePath, - ['serve', '--hostname', '127.0.0.1', '--port', String(servePort)], - { - env: runEnv, - }, + const { baseUrl, process: serve, health } = await startOpencodeServe( + workspace, + opencodePath, + runEnv, + note.providers.opencode.globalHealthPath, ) - const healthUrl = `http://127.0.0.1:${servePort}${note.providers.opencode.globalHealthPath}` - const statusUrl = `http://127.0.0.1:${servePort}${note.providers.opencode.sessionStatusPath}` - const health = await waitForHttpHealthy(healthUrl) + const statusUrl = `${baseUrl}${note.providers.opencode.sessionStatusPath}` expect(health).toEqual({ healthy: true, version: opencodeBinary.version, @@ -555,7 +551,7 @@ describe.sequential('coding cli real provider session contract', () => { 'json', '--dangerously-skip-permissions', '--attach', - `http://127.0.0.1:${servePort}`, + baseUrl, ], { env: runEnv, diff --git a/test/unit/client/components/TerminalView.osc52.test.tsx b/test/unit/client/components/TerminalView.osc52.test.tsx index aeb1b5c6..5e89300b 100644 --- a/test/unit/client/components/TerminalView.osc52.test.tsx +++ b/test/unit/client/components/TerminalView.osc52.test.tsx @@ -281,7 +281,7 @@ describe('TerminalView OSC52 policy handling', () => { messageHandler!({ type: 'terminal.output', terminalId, seqStart: 1, seqEnd: 1, data: `before${OSC52_COPY}after` }) await waitFor(() => { - expect(terminalInstances[0].write).toHaveBeenCalledWith('beforeafter', undefined) + expect(terminalInstances[0].write.mock.calls.some((call) => call[0] === 'beforeafter')).toBe(true) }) expect(clipboardMocks.copyText).toHaveBeenCalledWith('copy') expect(screen.queryByRole('dialog', { name: 'Clipboard access request' })).not.toBeInTheDocument() @@ -398,7 +398,7 @@ describe('TerminalView OSC52 policy handling', () => { messageHandler!({ type: 'terminal.output', terminalId, seqStart: 1, seqEnd: 1, data: `before${OSC52_COPY}after` }) await waitFor(() => { - expect(terminalInstances[0].write).toHaveBeenCalledWith('beforeafter', undefined) + expect(terminalInstances[0].write.mock.calls.some((call) => call[0] === 'beforeafter')).toBe(true) }) expect(clipboardMocks.copyText).not.toHaveBeenCalled() expect(screen.queryByRole('dialog', { name: 'Clipboard access request' })).not.toBeInTheDocument() diff --git a/test/unit/client/components/TerminalView.renderer.test.tsx b/test/unit/client/components/TerminalView.renderer.test.tsx index 182bc09c..ce26f637 100644 --- a/test/unit/client/components/TerminalView.renderer.test.tsx +++ b/test/unit/client/components/TerminalView.renderer.test.tsx @@ -363,7 +363,7 @@ describe('TerminalView renderer mode', () => { data: 'still works', }) await waitFor(() => { - expect(terminalInstances[0].write).toHaveBeenCalledWith('still works', undefined) + expect(terminalInstances[0].write.mock.calls.some((call) => call[0] === 'still works')).toBe(true) }) }) })