diff --git a/plugins/provider-codex/src/bridge/bridge.resume-hydration.test.ts b/plugins/provider-codex/src/bridge/bridge.resume-hydration.test.ts new file mode 100644 index 000000000..9adc7664a --- /dev/null +++ b/plugins/provider-codex/src/bridge/bridge.resume-hydration.test.ts @@ -0,0 +1,75 @@ +import { + mkdtempSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { experimental_createBridgeJsonRpcTestHarness as createBridgeJsonRpcTestHarness } from "@get-bb/plugin-sdk/provider-bridge/testing"; +import { handleLine } from "./bridge.js"; + +const THREAD_ID = "thr_resume_hydration"; +const PROVIDER_THREAD_ID = "codex-resume-hydration"; +const fakeAppServerPath = fileURLToPath( + new URL("./fake-codex-app-server.mjs", import.meta.url), +); +const sessionOptions = { + permissionMode: "full", + permissionScope: "full", + approvalReviewer: null, + permissionEscalation: null, +} as const; + +let harness: ReturnType; +let workspaceDir: string; +let requestLogPath: string; + +beforeEach(() => { + workspaceDir = mkdtempSync(join(tmpdir(), "bb-codex-resume-hydration-")); + requestLogPath = join(workspaceDir, "requests.jsonl"); + const scriptPath = join(workspaceDir, "script.json"); + writeFileSync(scriptPath, JSON.stringify({ requestLogPath }), "utf8"); + vi.stubEnv("BB_CODEX_BRIDGE_APP_SERVER_COMMAND", process.execPath); + vi.stubEnv( + "BB_CODEX_BRIDGE_APP_SERVER_ARGS", + JSON.stringify([fakeAppServerPath, scriptPath]), + ); + harness = createBridgeJsonRpcTestHarness(handleLine); +}); + +afterEach(async () => { + const cleanupId = 993_001; + harness.sendRequest(cleanupId, "thread/stop", { + threadId: THREAD_ID, + providerThreadId: PROVIDER_THREAD_ID, + intent: "release", + activeTurnId: null, + }); + await harness.waitForResponse(cleanupId).catch(() => undefined); + harness.restore(); + vi.unstubAllEnvs(); + rmSync(workspaceDir, { recursive: true, force: true }); +}); + +it("excludes turn history when it resumes a Codex thread", async () => { + harness.sendRequest(1, "thread/resume", { + threadId: THREAD_ID, + providerThreadId: PROVIDER_THREAD_ID, + cwd: workspaceDir, + instructionMode: "append", + options: { ...sessionOptions }, + }); + expect((await harness.waitForResponse(1)).error).toBeUndefined(); + + const requests = readFileSync(requestLogPath, "utf8") + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + expect(requests).toContainEqual({ + method: "thread/resume", + params: expect.objectContaining({ excludeTurns: true }), + }); +}); diff --git a/plugins/provider-codex/src/bridge/fake-codex-app-server.mjs b/plugins/provider-codex/src/bridge/fake-codex-app-server.mjs index 74793f9e6..46a5f2d7b 100644 --- a/plugins/provider-codex/src/bridge/fake-codex-app-server.mjs +++ b/plugins/provider-codex/src/bridge/fake-codex-app-server.mjs @@ -141,6 +141,7 @@ function runScriptedTurn(threadId) { const scriptPath = process.argv[2]; const script = scriptPath ? JSON.parse(readFileSync(scriptPath, "utf8")) : null; const scriptedTurns = script?.turns ?? null; +const requestLogPath = script?.requestLogPath ?? null; const modelListFailOnceMarkerPath = script?.modelListFailOnceMarkerPath ?? null; /** * `archiveStatePath`: a JSON file of archived thread ids shared by every fake @@ -309,6 +310,9 @@ function replayLastTurnUsage(threadId) { async function handleRequest(message) { const { id, method } = message; const params = message.params ?? {}; + if (requestLogPath !== null) { + appendFileSync(requestLogPath, `${JSON.stringify({ method, params })}\n`); + } switch (method) { case "initialize": respond(id, {});