From 3608baa853c01318da65944175e79df4cc3d2cce Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Jul 2026 09:27:44 -0700 Subject: [PATCH] test(codex): deduplicate run-attempt scenarios (#113076) * test(codex): deduplicate run attempt scenarios * test(codex): satisfy run-attempt lint --- .../codex/src/app-server/run-attempt.test.ts | 2366 +++++------------ 1 file changed, 676 insertions(+), 1690 deletions(-) diff --git a/extensions/codex/src/app-server/run-attempt.test.ts b/extensions/codex/src/app-server/run-attempt.test.ts index f424b15ab527..52ee0bbade96 100644 --- a/extensions/codex/src/app-server/run-attempt.test.ts +++ b/extensions/codex/src/app-server/run-attempt.test.ts @@ -578,13 +578,395 @@ function buildEmptyCodexToolTelemetry(): CodexAppServerToolTelemetry { }; } +function createRunPaths() { + return { + sessionFile: path.join(tempDir, "session.jsonl"), + workspaceDir: path.join(tempDir, "workspace"), + agentDir: path.join(tempDir, "agent"), + }; +} + +function createRunParams() { + const { sessionFile, workspaceDir } = createRunPaths(); + return createParams(sessionFile, workspaceDir); +} + +const GOOGLE_CALENDAR_PLUGIN_CONFIG = { + codexPlugins: { + enabled: true, + plugins: { + "google-calendar": { + marketplaceName: "openai-curated", + pluginName: "google-calendar", + }, + }, + }, +} as const; + +type GoogleCalendarCacheKeyInput = { + appServer: ReturnType; + agentDir: string; +}; + +function googleCalendarAppListResult(isEnabled: boolean) { + return { + data: [ + { + id: "google-calendar-app", + name: "Google Calendar", + description: null, + logoUrl: null, + logoUrlDark: null, + distributionChannel: null, + branding: null, + appMetadata: null, + labels: null, + installUrl: null, + isAccessible: true, + isEnabled, + pluginDisplayNames: [], + }, + ], + nextCursor: null, + }; +} + +const GOOGLE_CALENDAR_PLUGIN_LIST_RESULT = { + marketplaces: [ + { + name: "openai-curated", + path: "/marketplaces/openai-curated", + interface: null, + plugins: [ + { + id: "google-calendar", + name: "google-calendar", + source: { type: "remote" }, + installed: true, + enabled: true, + installPolicy: "AVAILABLE", + authPolicy: "ON_USE", + availability: "AVAILABLE", + interface: null, + }, + ], + }, + ], + marketplaceLoadErrors: [], + featuredPluginIds: [], +} as const; + +const GOOGLE_CALENDAR_PLUGIN_READ_RESULT = { + plugin: { + marketplaceName: "openai-curated", + marketplacePath: "/marketplaces/openai-curated", + summary: { + id: "google-calendar", + name: "google-calendar", + source: { type: "remote" }, + installed: true, + enabled: true, + installPolicy: "AVAILABLE", + authPolicy: "ON_USE", + availability: "AVAILABLE", + interface: null, + }, + description: null, + skills: [], + apps: [ + { + id: "google-calendar-app", + name: "Google Calendar", + description: null, + installUrl: null, + needsAuth: false, + }, + ], + mcpServers: ["google-calendar"], + }, +} as const; + +function createGoogleCalendarRequest(appList?: () => unknown) { + return vi.fn(async (method: string) => { + if (method === "app/list" && appList) { + return appList(); + } + if (method === "plugin/list") { + return GOOGLE_CALENDAR_PLUGIN_LIST_RESULT; + } + if (method === "plugin/read") { + return GOOGLE_CALENDAR_PLUGIN_READ_RESULT; + } + if (method === "thread/start") { + return threadStartResult("thread-1"); + } + if (method === "turn/start") { + return turnStartResult("turn-1", "inProgress"); + } + return undefined; + }); +} + +async function primeGoogleCalendarAppInventory(key: string, isEnabled: boolean): Promise { + defaultCodexAppInventoryCache.clear(); + await defaultCodexAppInventoryCache.refreshNow({ + key, + request: async () => googleCalendarAppListResult(isEnabled), + }); +} + +async function writeTokenPressureState( + sessionFile: string, + agentDir: string, + info: Record, +): Promise { + await fs.writeFile( + path.join(path.dirname(sessionFile), "sessions.json"), + JSON.stringify({ + "agent:main:session-1": { + sessionFile, + totalTokens: 12_000, + }, + }), + ); + const rolloutDir = path.join(agentDir, "codex-home", "sessions"); + await fs.mkdir(rolloutDir, { recursive: true }); + await fs.writeFile( + path.join(rolloutDir, "rollout-thread-existing.jsonl"), + `${JSON.stringify({ payload: { type: "token_count", info } })}\n`, + ); +} + +function installFailingThreadStartClient(onThreadStart: () => unknown) { + const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent"); + clearSpy.mockClear(); + const state: { failedClient?: unknown } = {}; + setCodexAppServerClientFactoryForTest(async () => { + const client = { + ...mockClientRuntimeMethods(), + request: vi.fn(async (method: string) => { + if (method === "thread/start") { + return await onThreadStart(); + } + return {}; + }), + addNotificationHandler: vi.fn(() => () => undefined), + addRequestHandler: vi.fn(() => () => undefined), + }; + state.failedClient = client; + return client as never; + }); + return { clearSpy, state }; +} + +async function runSharedClientRestartTest(closeCount: number) { + const { sessionFile, workspaceDir } = createRunPaths(); + await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); + const requests: string[][] = []; + let starts = 0; + const state: { + notify: (notification: CodexServerNotification) => Promise; + } = { notify: async () => undefined }; + setCodexAppServerClientFactoryForTest(async () => { + const startIndex = starts++; + const methods: string[] = []; + requests.push(methods); + return { + ...mockClientRuntimeMethods(), + request: vi.fn(async (method: string) => { + methods.push(method); + if (method === "thread/resume" && startIndex < closeCount) { + throw new Error("codex app-server client is closed"); + } + if (method === "thread/resume") { + return threadStartResult("thread-existing"); + } + if (method === "turn/start") { + return turnStartResult(); + } + return {}; + }), + addNotificationHandler: (handler: typeof state.notify) => { + state.notify = handler; + return () => undefined; + }, + addRequestHandler: () => () => undefined, + } as never; + }); + const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); + await vi.waitFor(() => expect(requests[closeCount]).toContain("turn/start"), fastWait); + await state.notify({ + method: "turn/completed", + params: { + threadId: "thread-existing", + turnId: "turn-1", + turn: { id: "turn-1", status: "completed" }, + }, + }); + return { result: await run, requests }; +} + +async function createSandboxReleaseFixture( + handleRequest: (method: string, params?: unknown) => unknown, +) { + const params = createRunParams(); + params.disableTools = false; + params.runtimePlan = createCodexRuntimePlanFixture(); + const appServer = { + ...createThreadLifecycleAppServerOptions(), + sandbox: "danger-full-access" as const, + }; + const sandbox = createSandboxContext({ + runShellCommand: async () => ({ + stdout: Buffer.alloc(0), + stderr: Buffer.alloc(0), + code: 0, + }), + }); + const request = vi.fn(async (method: string, requestParams?: unknown) => + handleRequest(method, requestParams), + ); + const client = { ...mockClientRuntimeMethods(), request }; + const environment = await ensureCodexSandboxExecServerEnvironment({ + client: client as never, + sandbox, + appServerStartOptions: appServer.start, + }); + if (!environment) { + throw new Error("expected sandbox exec-server environment"); + } + return { appServer, client, environment, params, request, sandbox }; +} + +async function startFastAutoProgressTest( + options: { + fastModeAuto?: boolean; + fastModeAutoProgressState?: EmbeddedRunAttemptParams["fastModeAutoProgressState"]; + reportAgentEvents?: boolean; + verbose?: boolean; + } = {}, +) { + const now = vi.spyOn(Date, "now").mockReturnValue(1_000); + const onToolResult = vi.fn(); + const onAgentEvent = vi.fn(); + const { sessionFile, workspaceDir } = createRunPaths(); + const harness = createStartedThreadHarness(); + const params = createParams(sessionFile, workspaceDir); + if (options.verbose !== false) { + params.verboseLevel = "full"; + } + params.fastModeAuto = options.fastModeAuto ?? true; + params.fastModeStartedAtMs = 1_000; + params.fastModeAutoOnSeconds = 30; + if (options.fastModeAutoProgressState) { + params.fastModeAutoProgressState = options.fastModeAutoProgressState; + } + params.onToolResult = onToolResult; + if (options.reportAgentEvents !== false) { + params.onAgentEvent = onAgentEvent; + } + const run = runCodexAppServerAttempt(params); + await harness.waitForMethod("turn/start"); + return { harness, now, onAgentEvent, onToolResult, params, run, workspaceDir }; +} + +function fastProgressEventSummaries(onAgentEvent: ReturnType) { + return onAgentEvent.mock.calls + .map(([event]) => event) + .filter((event) => event.stream === "item" && event.data?.title === "Fast") + .map((event) => event.data?.summary); +} + +type ElicitationRequestHandler = (request: { + id: string; + method: string; + params?: unknown; +}) => Promise; + +function installElicitationClient(request: ReturnType) { + const state: { + handleRequest?: ElicitationRequestHandler; + notify: (notification: CodexServerNotification) => Promise; + } = { notify: async () => undefined }; + setCodexAppServerClientFactoryForTest( + async () => + ({ + ...mockClientRuntimeMethods(), + request, + addNotificationHandler: (handler: typeof state.notify) => { + state.notify = handler; + return () => undefined; + }, + addRequestHandler: (handler: ElicitationRequestHandler) => { + state.handleRequest = handler; + return () => undefined; + }, + }) as never, + ); + return state; +} + +async function completeStartedRun( + run: Promise, + waitForMethod: ReturnType["waitForMethod"], + completeTurn: ReturnType["completeTurn"], + threadId = "thread-1", +): Promise { + await waitForMethod("turn/start"); + await completeTurn({ threadId, turnId: "turn-1" }); + await run; +} + +function installCleanupTrackingClient(turnStartError?: Error) { + const retireSpy = vi.spyOn( + sharedClientModule, + "clearSharedCodexAppServerClientIfCurrentAndUnclaimed", + ); + retireSpy.mockReturnValue({ found: true, activeLeases: 0, pendingAcquires: 0, closed: true }); + const events: string[] = []; + const closeAndWait = vi.fn(async () => { + events.push("closeAndWait"); + return true; + }); + const state: { + client?: unknown; + notify?: (notification: CodexServerNotification) => Promise; + } = {}; + setCodexAppServerClientFactoryForTest(async () => { + const client = { + ...mockClientRuntimeMethods(), + request: vi.fn(async (method: string) => { + events.push(`request:${method}`); + if (method === "thread/start") { + return threadStartResult(); + } + if (method === "turn/start") { + if (turnStartError) { + throw turnStartError; + } + return turnStartResult(); + } + return {}; + }), + addNotificationHandler: vi.fn((handler) => { + state.notify = handler; + return () => undefined; + }), + addRequestHandler: vi.fn(() => () => undefined), + addCloseHandler: vi.fn(() => () => undefined), + closeAndWait, + }; + state.client = client; + return client as never; + }); + return { closeAndWait, events, retireSpy, state }; +} + setupRunAttemptTestHooks(); describe("runCodexAppServerAttempt", () => { it("executes and reports the same materialized SecretRef credential", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); const authProfileId = "openai:work"; const authProfileStore: EmbeddedRunAttemptParams["authProfileStore"] = { version: 1, @@ -632,12 +1014,10 @@ describe("runCodexAppServerAttempt", () => { agentDir, config, }); - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - expect(result.authBindingFingerprint).toBe(expected?.fingerprint); expect(clientOptions?.authBindingFingerprint).toBe(expected?.fingerprint); expect(clientOptions?.authProfileStore?.profiles[authProfileId]).toEqual({ @@ -647,15 +1027,13 @@ describe("runCodexAppServerAttempt", () => { }); expect(authProfileStore.profiles[authProfileId]).toHaveProperty("keyRef"); }); - it("starts active OpenClaw sandbox threads with Codex native execution disabled", async () => { testing.setOpenClawCodingToolsFactoryForTests(() => [ createRuntimeDynamicTool("exec"), createRuntimeDynamicTool("process"), createRuntimeDynamicTool("message"), ]); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const params = createParams(sessionFile, workspaceDir); params.disableTools = false; setCodexTestModelSupportsTools(params, true); @@ -687,7 +1065,6 @@ describe("runCodexAppServerAttempt", () => { } throw new Error(`unexpected method: ${method}`); }); - await startOrResumeThread({ client: { request } as never, params, @@ -699,7 +1076,6 @@ describe("runCodexAppServerAttempt", () => { userMcpServersEnabled: nativeToolSurfaceEnabled, environmentSelection: [], }); - const startRequest = request.mock.calls.find(([method]) => method === "thread/start"); const startParams = startRequest?.[1] as Record | undefined; const startConfig = startParams?.config as Record | undefined; @@ -800,7 +1176,6 @@ describe("runCodexAppServerAttempt", () => { throw new Error("expected sandbox exec-server environment"); } const environmentSelection = [environment]; - await startOrResumeThread({ client: client as never, params, @@ -812,7 +1187,6 @@ describe("runCodexAppServerAttempt", () => { userMcpServersEnabled: nativeToolSurfaceEnabled, environmentSelection, }); - const turnParams = buildTurnStartParams(params, { threadId: "thread-1", cwd: environment.cwd, @@ -820,7 +1194,6 @@ describe("runCodexAppServerAttempt", () => { sandboxPolicy: { type: "externalSandbox", networkAccess: "enabled" }, environmentSelection, }); - const environmentAdd = request.mock.calls.find(([method]) => method === "environment/add"); const environmentAddParams = environmentAdd?.[1] as | { environmentId?: string; execServerUrl?: string } @@ -839,7 +1212,6 @@ describe("runCodexAppServerAttempt", () => { }; } | undefined; - expect(nativeToolSurfaceEnabled).toBe(true); expect(environmentAddParams?.environmentId).toMatch(/^openclaw-sandbox-/); expect(environmentAddParams?.execServerUrl).toMatch(/^ws:\/\/127\.0\.0\.1:/); @@ -862,51 +1234,22 @@ describe("runCodexAppServerAttempt", () => { await releaseCodexSandboxExecServerEnvironment(sandbox as never); } }); - it("closes the sandbox exec-server release path used by turn/start failure cleanup", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const params = createParams(sessionFile, workspaceDir); - params.disableTools = false; - params.runtimePlan = createCodexRuntimePlanFixture(); - const appServer = { - ...createThreadLifecycleAppServerOptions(), - sandbox: "danger-full-access", - }; - const sandbox = createSandboxContext({ - runShellCommand: async () => ({ - stdout: Buffer.alloc(0), - stderr: Buffer.alloc(0), - code: 0, - }), - }); - const request = vi.fn(async (method: string, _params?: unknown) => { - if (method === "environment/add") { - return {}; - } - if (method === "thread/start") { - return threadStartResult(); - } - if (method === "turn/start") { - throw new Error("turn start failed"); - } - throw new Error(`unexpected method: ${method}`); - }); - const client = { - ...mockClientRuntimeMethods(), - request, - }; - try { - const environment = await ensureCodexSandboxExecServerEnvironment({ - client: client as never, - sandbox, - appServerStartOptions: appServer.start, + const { appServer, client, environment, params, request, sandbox } = + await createSandboxReleaseFixture((method) => { + if (method === "environment/add") { + return {}; + } + if (method === "thread/start") { + return threadStartResult(); + } + if (method === "turn/start") { + throw new Error("turn start failed"); + } + throw new Error(`unexpected method: ${method}`); }); - if (!environment) { - throw new Error("expected sandbox exec-server environment"); - } + try { const environmentSelection = [environment]; - const thread = await startOrResumeThread({ client: client as never, params, @@ -918,7 +1261,6 @@ describe("runCodexAppServerAttempt", () => { userMcpServersEnabled: false, environmentSelection, }); - const turnParams = buildTurnStartParams(params, { threadId: thread.threadId, cwd: environment.cwd, @@ -926,14 +1268,12 @@ describe("runCodexAppServerAttempt", () => { sandboxPolicy: { type: "externalSandbox", networkAccess: "enabled" }, environmentSelection, }); - await expect( client.request("turn/start", turnParams).catch(async (error: unknown) => { await releaseCodexSandboxExecServerEnvironment(sandbox); throw error; }), ).rejects.toThrow("turn start failed"); - const environmentAdd = request.mock.calls.find(([method]) => method === "environment/add"); const environmentAddParams = environmentAdd?.[1] as { execServerUrl?: string } | undefined; expect(environmentAddParams?.execServerUrl).toMatch(/^ws:\/\/127\.0\.0\.1:/); @@ -944,46 +1284,18 @@ describe("runCodexAppServerAttempt", () => { }); it("closes the sandbox exec-server release path used by context-engine retry setup cleanup", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const params = createParams(sessionFile, workspaceDir); - params.disableTools = false; - params.runtimePlan = createCodexRuntimePlanFixture(); - const appServer = { - ...createThreadLifecycleAppServerOptions(), - sandbox: "danger-full-access", - }; - const sandbox = createSandboxContext({ - runShellCommand: async () => ({ - stdout: Buffer.alloc(0), - stderr: Buffer.alloc(0), - code: 0, - }), - }); - const request = vi.fn(async (method: string, _params?: unknown) => { - if (method === "environment/add") { - return {}; - } - if (method === "thread/start") { - throw new Error("retry setup failed"); - } - throw new Error(`unexpected method: ${method}`); - }); - const client = { - ...mockClientRuntimeMethods(), - request, - }; - try { - const environment = await ensureCodexSandboxExecServerEnvironment({ - client: client as never, - sandbox, - appServerStartOptions: appServer.start, + const { appServer, client, environment, params, request, sandbox } = + await createSandboxReleaseFixture((method) => { + if (method === "environment/add") { + return {}; + } + if (method === "thread/start") { + throw new Error("retry setup failed"); + } + throw new Error(`unexpected method: ${method}`); }); - if (!environment) { - throw new Error("expected sandbox exec-server environment"); - } + try { const environmentSelection = [environment]; - await expect( startOrResumeThread({ client: client as never, @@ -1000,7 +1312,6 @@ describe("runCodexAppServerAttempt", () => { throw error; }), ).rejects.toThrow("retry setup failed"); - const environmentAdd = request.mock.calls.find(([method]) => method === "environment/add"); const environmentAddParams = environmentAdd?.[1] as { execServerUrl?: string } | undefined; expect(environmentAddParams?.execServerUrl).toMatch(/^ws:\/\/127\.0\.0\.1:/); @@ -1009,39 +1320,14 @@ describe("runCodexAppServerAttempt", () => { await releaseCodexSandboxExecServerEnvironment(sandbox); } }); - it("closes the sandbox exec-server release path used by startup timeout cleanup", async () => { - const appServer = { - ...createThreadLifecycleAppServerOptions(), - sandbox: "danger-full-access", - }; - const sandbox = createSandboxContext({ - runShellCommand: async () => ({ - stdout: Buffer.alloc(0), - stderr: Buffer.alloc(0), - code: 0, - }), - }); - const request = vi.fn(async (method: string, _params?: unknown) => { + const { request, sandbox } = await createSandboxReleaseFixture((method) => { if (method === "environment/add") { return {}; } throw new Error(`unexpected method: ${method}`); }); - const client = { - ...mockClientRuntimeMethods(), - request, - }; try { - const environment = await ensureCodexSandboxExecServerEnvironment({ - client: client as never, - sandbox, - appServerStartOptions: appServer.start, - }); - if (!environment) { - throw new Error("expected sandbox exec-server environment"); - } - await expect( testing.withCodexStartupTimeout({ timeoutMs: 5, @@ -1052,7 +1338,6 @@ describe("runCodexAppServerAttempt", () => { operation: async () => new Promise(() => {}), }), ).rejects.toThrow("codex app-server startup timed out"); - const environmentAdd = request.mock.calls.find(([method]) => method === "environment/add"); const environmentAddParams = environmentAdd?.[1] as { execServerUrl?: string } | undefined; expect(environmentAddParams?.execServerUrl).toMatch(/^ws:\/\/127\.0\.0\.1:/); @@ -1063,8 +1348,7 @@ describe("runCodexAppServerAttempt", () => { }); it("starts Codex threads without duplicate OpenClaw workspace tools by default", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const appServer = createThreadLifecycleAppServerOptions(); const request = vi.fn(async (method: string, _params: unknown) => { if (method === "thread/start") { @@ -1090,7 +1374,6 @@ describe("runCodexAppServerAttempt", () => { ].map(createNamedDynamicTool), {}, ); - await startOrResumeThread({ client: { request } as never, params: createParams(sessionFile, workspaceDir), @@ -1098,13 +1381,11 @@ describe("runCodexAppServerAttempt", () => { dynamicTools, appServer, }); - const startRequest = request.mock.calls.find(([method]) => method === "thread/start"); const dynamicToolNames = specNames( (startRequest?.[1] as { dynamicTools?: CodexDynamicToolSpec[] } | undefined)?.dynamicTools ?? [], ); - expect(dynamicToolNames).toContain("message"); expect(dynamicToolNames).toContain("web_search"); for (const toolName of [ @@ -1123,17 +1404,14 @@ describe("runCodexAppServerAttempt", () => { expect(dynamicToolNames).not.toContain(toolName); } }); - it("passes MCP server config through to Codex thread/start", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const request = vi.fn(async (method: string, _params: unknown) => { if (method === "thread/start") { return threadStartResult(); } throw new Error(`unexpected method: ${method}`); }); - await startOrResumeThread({ client: { request } as never, params: createParams(sessionFile, workspaceDir), @@ -1150,7 +1428,6 @@ describe("runCodexAppServerAttempt", () => { mcpServersFingerprint: "mcp-v1", mcpServersFingerprintEvaluated: true, }); - const startRequest = request.mock.calls.find(([method]) => method === "thread/start"); expect((startRequest?.[1] as { config?: unknown } | undefined)?.config).toMatchObject({ mcp_servers: { @@ -1167,8 +1444,7 @@ describe("runCodexAppServerAttempt", () => { }); it("starts a new Codex thread when the MCP server fingerprint changes", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeCodexAppServerBinding(sessionFile, { threadId: "old-thread", cwd: workspaceDir, @@ -1181,7 +1457,6 @@ describe("runCodexAppServerAttempt", () => { } throw new Error(`unexpected method: ${method}`); }); - const binding = await startOrResumeThread({ client: { request } as never, params: createParams(sessionFile, workspaceDir), @@ -1191,15 +1466,12 @@ describe("runCodexAppServerAttempt", () => { mcpServersFingerprint: "mcp-v2", mcpServersFingerprintEvaluated: true, }); - expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]); expect(binding.threadId).toBe("new-thread"); expect(binding.mcpServersFingerprint).toBe("mcp-v2"); }); - it("uses task cwd for Codex app-server requests while keeping bootstrap workspace separate", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const taskCwd = path.join(tempDir, "task-repo"); await fs.mkdir(workspaceDir, { recursive: true }); await fs.mkdir(taskCwd, { recursive: true }); @@ -1208,7 +1480,6 @@ describe("runCodexAppServerAttempt", () => { const appServer = createThreadLifecycleAppServerOptions(); const params = createParams(sessionFile, workspaceDir); const requests: Array<{ method: string; params: unknown }> = []; - await startOrResumeThread({ client: { ...mockClientRuntimeMethods(), @@ -1228,7 +1499,6 @@ describe("runCodexAppServerAttempt", () => { }); const threadStart = requests.find((request) => request.method === "thread/start"); expect((threadStart?.params as { cwd?: string } | undefined)?.cwd).toBe(taskCwd); - const turnStart = buildTurnStartParams(params, { threadId: "thread-1", cwd: taskCwd, @@ -1238,8 +1508,7 @@ describe("runCodexAppServerAttempt", () => { }); it("starts a no-MCP Codex thread when MCP config is evaluated empty", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeCodexAppServerBinding(sessionFile, { threadId: "old-thread", cwd: workspaceDir, @@ -1252,7 +1521,6 @@ describe("runCodexAppServerAttempt", () => { } throw new Error(`unexpected method: ${method}`); }); - const binding = await startOrResumeThread({ client: { request } as never, params: createParams(sessionFile, workspaceDir), @@ -1261,24 +1529,20 @@ describe("runCodexAppServerAttempt", () => { appServer: createThreadLifecycleAppServerOptions(), mcpServersFingerprintEvaluated: true, }); - expect(request.mock.calls.map(([method]) => method)).toEqual(["thread/start"]); expect(binding.threadId).toBe("new-thread"); expect(binding.mcpServersFingerprint).toBeUndefined(); expect((await readCodexAppServerBinding(sessionFile))?.mcpServersFingerprint).toBeUndefined(); }); - it("scopes Codex developer reply instructions to message-tool-only delivery", () => { const workspaceDir = path.join(tempDir, "workspace"); const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); params.sourceReplyDeliveryMode = "message_tool_only"; - expect( testing.buildDeveloperInstructions(params, { dynamicTools: [createMessageDynamicTool("Message test tool")], }), ).toContain("Visible source replies are not automatically delivered for this run."); - const withoutMessageToolInstructions = testing.buildDeveloperInstructions(params, { dynamicTools: [], }); @@ -1287,7 +1551,6 @@ describe("runCodexAppServerAttempt", () => { ); expect(withoutMessageToolInstructions).not.toContain("message(action=send)"); expect(withoutMessageToolInstructions).not.toContain("Use `message`"); - params.sourceReplyDeliveryMode = "automatic"; const automaticInstructions = testing.buildDeveloperInstructions(params); expect(automaticInstructions).toContain("reply normally in your final assistant message"); @@ -1316,23 +1579,19 @@ describe("runCodexAppServerAttempt", () => { }); const workspaceDir = path.join(tempDir, "workspace"); const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir); - const instructions = testing.buildDeveloperInstructions(params); - expect(instructions).toContain("Codex app-server command guidance."); expect(instructions).not.toContain("Legacy global command guidance."); expect(instructions).not.toContain("Unscoped structured command guidance."); expect(instructions).not.toContain("OpenClaw main command guidance."); }); - it("passes OpenClaw skills as turn collaboration developer instructions", async () => { const llmInput = vi.fn(); initializeGlobalHookRunner( createMockPluginRegistry([{ hookName: "llm_input", handler: llmInput }]), ); vi.stubEnv("OPENCLAW_TRAJECTORY", "1"); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const harness = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); const trajectoryEvents: Array<{ @@ -1352,7 +1611,6 @@ describe("runCodexAppServerAttempt", () => { prompt: "demo", skills: [], }; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -1360,11 +1618,9 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - const threadStart = harness.requests.find((request) => request.method === "thread/start"); const threadStartParams = threadStart?.params as { developerInstructions?: string }; expect(threadStartParams.developerInstructions).not.toContain(""); - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; @@ -1404,10 +1660,8 @@ describe("runCodexAppServerAttempt", () => { const onRunAgentEvent = vi.fn(); params.timeoutMs = 60_000; params.onAgentEvent = onRunAgentEvent; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); - const request = { id: "request-tool-1", method: "item/tool/call", @@ -1431,7 +1685,6 @@ describe("runCodexAppServerAttempt", () => { expect(replayedResponse).toEqual(firstResponse); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - expect(onRunAgentEvent).toHaveBeenCalledWith({ stream: "tool", data: { @@ -1474,7 +1727,6 @@ describe("runCodexAppServerAttempt", () => { .map((event) => event.data?.phase); expect(toolPhases).toEqual(["start", "result"]); }); - it("keeps leading delivery hints out of the Codex current user request", async () => { for (const [index, deliveryHint] of MESSAGE_TOOL_DELIVERY_HINTS.entries()) { // Bindings are keyed by session identity, so the previous iteration's @@ -1489,12 +1741,10 @@ describe("runCodexAppServerAttempt", () => { prompt: "demo", skills: [], }; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; @@ -1520,7 +1770,6 @@ describe("runCodexAppServerAttempt", () => { params.prompt = "external channel prompt"; const onUserMessagePersisted = vi.fn(); params.onUserMessagePersisted = onUserMessagePersisted; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await vi.waitFor(async () => { @@ -1541,18 +1790,14 @@ describe("runCodexAppServerAttempt", () => { }), ); }); - const messagesBeforeCompletion = await readTranscriptMessagesByIdentity(params); expect(messagesBeforeCompletion.some((message) => message.role === "assistant")).toBe(false); - await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const messagesAfterCompletion = await readTranscriptMessagesByIdentity(params); expect(messagesAfterCompletion.filter((message) => message.role === "user")).toHaveLength(1); expect(onUserMessagePersisted).toHaveBeenCalledTimes(1); }); - it("does not mirror the Codex prompt early when user message persistence is suppressed", async () => { const sessionFile = path.join(tempDir, "session-suppressed-early-prompt.jsonl"); const storePath = path.join(tempDir, "sessions-suppressed-early-prompt.json"); @@ -1562,7 +1807,6 @@ describe("runCodexAppServerAttempt", () => { attachSqliteSessionTarget(params, storePath, "session-suppressed-early-prompt"); params.prompt = "already persisted prompt"; params.suppressNextUserMessagePersistence = true; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await expect( @@ -1588,10 +1832,8 @@ describe("runCodexAppServerAttempt", () => { idempotencyKey: "codex-app-server:thread-1:turn-1:prompt", }), ); - await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const messagesAfterCompletion = await readTranscriptMessagesByIdentity(params); expect(messagesAfterCompletion).not.toContainEqual( expect.objectContaining({ @@ -1611,7 +1853,6 @@ describe("runCodexAppServerAttempt", () => { path.join(tempDir, "session-orphan-tool.jsonl"), path.join(tempDir, "workspace-orphan-tool"), ); - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.notify({ @@ -1655,9 +1896,7 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - const result = await run; - expect(result.promptError).toBeNull(); expect(result.lastToolError).toMatchObject({ toolName: "bash", @@ -1677,7 +1916,6 @@ describe("runCodexAppServerAttempt", () => { expect(snapshotJson).toContain('"isError":true'); expect(snapshotJson).toContain("without a matching tool.result"); }); - it("keeps OpenClaw control-path tools direct when code-mode-only is enabled", () => { const tools = [ createRuntimeDynamicTool("message"), @@ -1692,7 +1930,6 @@ describe("runCodexAppServerAttempt", () => { signal: new AbortController().signal, directToolNames: ["message"], }); - const specs = flattenSpecsWithNamespace(toolBridge.specs); const message = specs.find((tool) => tool.name === "message"); const webSearch = specs.find((tool) => tool.name === "web_search"); @@ -1700,7 +1937,6 @@ describe("runCodexAppServerAttempt", () => { const agentsList = specs.find((tool) => tool.name === "agents_list"); const sessionsSpawn = specs.find((tool) => tool.name === "sessions_spawn"); const sessionsYield = specs.find((tool) => tool.name === "sessions_yield"); - expect(message).not.toHaveProperty("namespace"); expect(message).not.toHaveProperty("deferLoading"); expect(webSearch?.namespace).toBe("openclaw"); @@ -1722,9 +1958,8 @@ describe("runCodexAppServerAttempt", () => { ? [createRuntimeDynamicTool("heartbeat_respond")] : []), ]); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const createRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => { + const { sessionFile, workspaceDir } = createRunPaths(); + const createHeartbeatRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => { const params = createParams(sessionFile, workspaceDir); params.disableTools = false; params.runtimePlan = createCodexRuntimePlanFixture(); @@ -1734,20 +1969,19 @@ describe("runCodexAppServerAttempt", () => { } return params; }; - const registeredTools = [ createRuntimeDynamicTool("message"), createRuntimeDynamicTool("heartbeat_respond"), ]; const normalBridge = createCodexToolBridgeForTest( - createRunParams(), + createHeartbeatRunParams(), [createRuntimeDynamicTool("message")], registeredTools, ); - const normalInstructions = testing.buildDeveloperInstructions(createRunParams(), { + const normalInstructions = testing.buildDeveloperInstructions(createHeartbeatRunParams(), { dynamicTools: normalBridge.availableSpecs, }); - const heartbeatParams = createRunParams("heartbeat"); + const heartbeatParams = createHeartbeatRunParams("heartbeat"); const heartbeatBridge = createCodexToolBridgeForTest( heartbeatParams, [createRuntimeDynamicTool("message"), createRuntimeDynamicTool("heartbeat_respond")], @@ -1756,14 +1990,13 @@ describe("runCodexAppServerAttempt", () => { const heartbeatInstructions = testing.buildDeveloperInstructions(heartbeatParams, { dynamicTools: heartbeatBridge.availableSpecs, }); - const nextNormalParams = createRunParams(); + const nextNormalParams = createHeartbeatRunParams(); const nextNormalBridge = createCodexToolBridgeForTest( nextNormalParams, [createRuntimeDynamicTool("message")], registeredTools, ); const registeredToolNames = specNames(normalBridge.specs); - expect(registeredToolNames).toContain("message"); expect(registeredToolNames).toContain("heartbeat_respond"); expect(normalInstructions).not.toContain( @@ -1785,7 +2018,6 @@ describe("runCodexAppServerAttempt", () => { expect(codexDynamicToolsFingerprint(nextNormalBridge.specs)).toBe( codexDynamicToolsFingerprint(normalBridge.specs), ); - let startedThreadId: string | undefined; const request = vi.fn(async (method: string) => { if (method === "thread/start") { @@ -1811,23 +2043,19 @@ describe("runCodexAppServerAttempt", () => { appServer: createThreadLifecycleAppServerOptions(), }); } - expect(request.mock.calls.map(([method]) => method)).toEqual([ "thread/start", "thread/resume", "thread/resume", ]); }); - it("keeps message in the registered schema when disabled for an internal turn", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const params = createParams(sessionFile, workspaceDir); params.disableTools = false; params.disableMessageTool = true; params.sourceReplyDeliveryMode = "message_tool_only"; params.runtimePlan = createCodexRuntimePlanFixture(); - const availableTools: RuntimeDynamicToolForTest[] = []; const registeredTools = [createRuntimeDynamicTool("message")]; const bridge = createCodexToolBridgeForTest(params, availableTools, registeredTools); @@ -1842,7 +2070,6 @@ describe("runCodexAppServerAttempt", () => { normalTools, normalRegisteredTools, ); - expect(bridge.availableSpecs.map((tool) => tool.name)).not.toContain("message"); expect(bridge.specs.map((tool) => tool.name)).toContain("message"); expect(codexDynamicToolsFingerprint(bridge.specs)).toBe( @@ -1876,9 +2103,8 @@ describe("runCodexAppServerAttempt", () => { ? [createRuntimeDynamicTool("heartbeat_respond")] : []), ]); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const createRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => { + const { sessionFile, workspaceDir } = createRunPaths(); + const createHeartbeatRunParams = (trigger?: EmbeddedRunAttemptParams["trigger"]) => { const params = createParams(sessionFile, workspaceDir); params.disableTools = false; const runtimePlan = createCodexRuntimePlanFixture(); @@ -1903,40 +2129,34 @@ describe("runCodexAppServerAttempt", () => { createRuntimeDynamicTool("heartbeat_respond"), ]; const normalBridge = createCodexToolBridgeForTest( - createRunParams(), + createHeartbeatRunParams(), registeredTools, registeredTools, ); const heartbeatBridge = createCodexToolBridgeForTest( - createRunParams("heartbeat"), + createHeartbeatRunParams("heartbeat"), [createRuntimeDynamicTool("heartbeat_respond")], registeredTools, ); const nextNormalBridge = createCodexToolBridgeForTest( - createRunParams(), + createHeartbeatRunParams(), registeredTools, registeredTools, ); - expect(specNames(heartbeatBridge.availableSpecs)).toEqual(["heartbeat_respond"]); expect(specNames(heartbeatBridge.specs)).toEqual(specNames(normalBridge.specs)); expect(specNames(nextNormalBridge.specs)).toEqual(specNames(normalBridge.specs)); }); - it("disables Codex native tool surfaces when runtime toolsAllow is empty", async () => { testing.setOpenClawCodingToolsFactoryForTests(() => [ createRuntimeDynamicTool("message"), createRuntimeDynamicTool("web_search"), ]); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.disableTools = false; params.runtimePlan = createCodexRuntimePlanFixture(); params.toolsAllow = []; params.extraSystemPrompt = "Tool and file actions are disabled for this sender by chat policy."; - const { request, nativeToolSurfaceEnabled } = await startThreadWithDisabledNativeSurfaceForTest( params, { @@ -1955,7 +2175,6 @@ describe("runCodexAppServerAttempt", () => { developerInstructions: params.extraSystemPrompt, }, ); - const startRequest = request.mock.calls.find(([method]) => method === "thread/start"); const startParams = startRequest?.[1] as | { @@ -1972,7 +2191,6 @@ describe("runCodexAppServerAttempt", () => { }; } | undefined; - expect(nativeToolSurfaceEnabled).toBe(false); expect(startParams?.dynamicTools).toEqual([]); expect(startParams?.environments).toEqual([]); @@ -1992,18 +2210,13 @@ describe("runCodexAppServerAttempt", () => { it("fails closed for Codex app defaults when restricted native tools have no plugin config", async () => { testing.setOpenClawCodingToolsFactoryForTests(() => [createRuntimeDynamicTool("message")]); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.disableTools = false; params.runtimePlan = createCodexRuntimePlanFixture(); params.toolsAllow = []; - const { request } = await startThreadWithDisabledNativeSurfaceForTest(params, { pluginConfig: { appServer: { mode: "yolo" } }, }); - const startRequest = request.mock.calls.find(([method]) => method === "thread/start"); const startParams = startRequest?.[1] as | { @@ -2015,7 +2228,6 @@ describe("runCodexAppServerAttempt", () => { }; } | undefined; - expect(startParams?.config?.apps?.["_default"]).toEqual({ enabled: false, destructive_enabled: false, @@ -2023,57 +2235,17 @@ describe("runCodexAppServerAttempt", () => { }); expect(request.mock.calls.map(([method]) => method)).not.toContain("app/list"); }); - it("retires the shared Codex app-server client after one-shot cleanup turns", async () => { - const retireSpy = vi.spyOn( - sharedClientModule, - "clearSharedCodexAppServerClientIfCurrentAndUnclaimed", - ); - retireSpy.mockReturnValue({ found: true, activeLeases: 0, pendingAcquires: 0, closed: true }); - const events: string[] = []; - const closeAndWait = vi.fn(async () => { - events.push("closeAndWait"); - return true; - }); - let startedClient: unknown; - let notify: ((notification: CodexServerNotification) => Promise) | undefined; - setCodexAppServerClientFactoryForTest(async () => { - const client = { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - events.push(`request:${method}`); - if (method === "thread/start") { - return threadStartResult(); - } - if (method === "turn/start") { - return turnStartResult(); - } - return {}; - }), - addNotificationHandler: vi.fn((handler) => { - notify = handler; - return () => undefined; - }), - addRequestHandler: vi.fn(() => () => undefined), - addCloseHandler: vi.fn(() => () => undefined), - closeAndWait, - }; - startedClient = client; - return client as never; - }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const { closeAndWait, events, retireSpy, state } = installCleanupTrackingClient(); + const params = createRunParams(); params.cleanupBundleMcpOnRunEnd = true; - const run = runCodexAppServerAttempt(params); // The keyed router only delivers turn/completed after the turn is bound. await vi.waitFor(() => expect(events).toContain("request:turn/start"), fastWait); - if (!notify) { + if (!state.notify) { throw new Error("expected turn notification handler"); } - await notify({ + await state.notify({ method: "turn/completed", params: { threadId: "thread-1", @@ -2082,8 +2254,7 @@ describe("runCodexAppServerAttempt", () => { }, }); await run; - - expect(retireSpy).toHaveBeenCalledWith(startedClient); + expect(retireSpy).toHaveBeenCalledWith(state.client); expect(closeAndWait).toHaveBeenCalledWith({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 }); expect(events.indexOf("request:thread/unsubscribe")).toBeGreaterThan(-1); expect(events.indexOf("closeAndWait")).toBeGreaterThan( @@ -2092,54 +2263,19 @@ describe("runCodexAppServerAttempt", () => { }); it("retires the shared Codex app-server client after one-shot turn start failures", async () => { - const retireSpy = vi.spyOn( - sharedClientModule, - "clearSharedCodexAppServerClientIfCurrentAndUnclaimed", - ); - retireSpy.mockReturnValue({ found: true, activeLeases: 0, pendingAcquires: 0, closed: true }); - const events: string[] = []; - const closeAndWait = vi.fn(async () => { - events.push("closeAndWait"); - return true; - }); - let startedClient: unknown; - setCodexAppServerClientFactoryForTest(async () => { - const client = { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - events.push(`request:${method}`); - if (method === "thread/start") { - return threadStartResult(); - } - if (method === "turn/start") { - throw new Error("turn start failed"); - } - return {}; - }), - addNotificationHandler: vi.fn(() => () => undefined), - addRequestHandler: vi.fn(() => () => undefined), - addCloseHandler: vi.fn(() => () => undefined), - closeAndWait, - }; - startedClient = client; - return client as never; - }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), + const { closeAndWait, events, retireSpy, state } = installCleanupTrackingClient( + new Error("turn start failed"), ); + const params = createRunParams(); params.cleanupBundleMcpOnRunEnd = true; - await expect(runCodexAppServerAttempt(params)).rejects.toThrow("turn start failed"); - - expect(retireSpy).toHaveBeenCalledWith(startedClient); + expect(retireSpy).toHaveBeenCalledWith(state.client); expect(closeAndWait).toHaveBeenCalledWith({ exitTimeoutMs: 2_000, forceKillDelayMs: 250 }); expect(events.indexOf("request:thread/unsubscribe")).toBeGreaterThan(-1); expect(events.indexOf("closeAndWait")).toBeGreaterThan( events.indexOf("request:thread/unsubscribe"), ); }); - it("keeps the shared Codex app-server client warm without one-shot cleanup", async () => { const retireSpy = vi.spyOn( sharedClientModule, @@ -2170,11 +2306,7 @@ describe("runCodexAppServerAttempt", () => { closeAndWait, }) as never, ); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); - + const params = createRunParams(); const run = runCodexAppServerAttempt(params); // The keyed router only delivers turn/completed after the turn is bound. await vi.waitFor( @@ -2193,16 +2325,12 @@ describe("runCodexAppServerAttempt", () => { }, }); await run; - expect(retireSpy).not.toHaveBeenCalled(); expect(closeAndWait).not.toHaveBeenCalled(); }); it("keeps searchable Codex dynamic tools canonical in mirrored transcript snapshots", async () => { - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); const projector = new CodexAppServerEventProjector(params, "thread-1", "turn-1"); projector.recordDynamicToolCall({ callId: "call-wiki-status-1", @@ -2217,7 +2345,6 @@ describe("runCodexAppServerAttempt", () => { contentItems: [{ type: "inputText", text: "wiki_status done" }], }); const result = projector.buildResult(buildEmptyCodexToolTelemetry()); - expect(result.messagesSnapshot.map((message) => message.role)).toEqual([ "user", "assistant", @@ -2259,7 +2386,6 @@ describe("runCodexAppServerAttempt", () => { expect(JSON.stringify(result.messagesSnapshot)).not.toContain("tool_search"); expect(JSON.stringify(result.messagesSnapshot)).not.toContain("function_call_output"); }); - it("applies before_prompt_build to Codex developer instructions and turn input", async () => { const llmInput = vi.fn(); const beforePromptBuild = vi.fn(async () => ({ @@ -2275,12 +2401,10 @@ describe("runCodexAppServerAttempt", () => { { hookName: "llm_input", handler: llmInput }, ]), ); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const sessionManager = SessionManager.open(sessionFile); sessionManager.appendMessage(assistantMessage("previous turn", Date.now())); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -2288,7 +2412,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - expect(beforePromptBuild).toHaveBeenCalledOnce(); const [hookInput, hookContext] = mockCall(beforePromptBuild, "before_prompt_build") as [ { @@ -2331,8 +2454,7 @@ describe("runCodexAppServerAttempt", () => { }); it("projects bounded continuity when starting Codex without a native thread binding", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const sessionManager = SessionManager.open(sessionFile); sessionManager.appendMessage( userMessage( @@ -2353,7 +2475,6 @@ describe("runCodexAppServerAttempt", () => { const harness = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); params.prompt = "make the default webpage openclaw"; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -2361,12 +2482,10 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const inputText = (turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ?? ""; - expect(inputText).toContain("OpenClaw assembled context for this turn:"); expect(inputText).toContain("older next-step anchor: keep the handoff checklist"); expect(inputText).toContain("we are fixing the Opik default project"); @@ -2374,10 +2493,8 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).toContain("Current user request:"); expect(inputText).toContain("make the default webpage openclaw"); }); - it("keeps large fresh-thread continuity under the Codex turn/start input limit", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const sessionManager = SessionManager.open(sessionFile); sessionManager.appendMessage( userMessage( @@ -2400,17 +2517,14 @@ describe("runCodexAppServerAttempt", () => { const params = createParams(sessionFile, workspaceDir); params.contextTokenBudget = 300_000; params.prompt = `current prompt survives ${"p".repeat(80_000)}`; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const inputText = (turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ?? ""; - expect(inputText.length).toBeLessThanOrEqual(1 << 20); expect(inputText).toContain("OpenClaw assembled context for this turn:"); expect(inputText).toContain("recent continuity anchor: resume the database migration"); @@ -2438,13 +2552,11 @@ describe("runCodexAppServerAttempt", () => { initializeGlobalHookRunner( createMockPluginRegistry([{ hookName: "before_prompt_build", handler: beforePromptBuild }]), ); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const sessionManager = SessionManager.open(sessionFile); sessionManager.appendMessage(userMessage("prior visible context", Date.now())); sessionManager.appendMessage(assistantMessage("prior assistant context", Date.now() + 1)); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -2452,7 +2564,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - expect(beforePromptBuild).toHaveBeenCalledTimes(2); const [, secondHookInput] = beforePromptBuild.mock.calls.map( ([event]) => event as HookInputForTest, @@ -2476,10 +2587,8 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).toContain("prior visible context"); expect(inputText).not.toContain("hook-side mutation"); }); - it("does not replay mirrored history already covered by an existing Codex binding", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const binding = await readCodexAppServerBinding(sessionFile); const bindingUpdatedAt = Date.parse(binding?.historyCoveredThrough ?? ""); @@ -2496,7 +2605,6 @@ describe("runCodexAppServerAttempt", () => { const harness = createResumeHarness(); const params = createParams(sessionFile, workspaceDir); params.prompt = "is the previous message trustworthy?"; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -2504,13 +2612,11 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - expect(harness.requests.map((request) => request.method)).toContain("thread/resume"); const turnStart = harness.requests.find((request) => request.method === "turn/start"); const inputText = (turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ?? ""; - expect(inputText).not.toContain("OpenClaw assembled context for this turn:"); expect(inputText).not.toContain("we were discussing the Sonnet leak screenshots"); expect(inputText).not.toContain("David Ondrej was mentioned in that prior thread"); @@ -2519,8 +2625,7 @@ describe("runCodexAppServerAttempt", () => { }); it("keeps resumed native web-search outcomes unknown without raw events", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const harness = createResumeHarness(); const diagnosticEvents: DiagnosticEventPayload[] = []; @@ -2529,7 +2634,6 @@ describe("runCodexAppServerAttempt", () => { diagnosticEvents.push(event); } }); - try { const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); @@ -2553,7 +2657,6 @@ describe("runCodexAppServerAttempt", () => { } finally { stopDiagnostics(); } - expect(diagnosticEvents.map((event) => event.type)).toEqual([ "tool.execution.started", "tool.execution.error", @@ -2564,10 +2667,8 @@ describe("runCodexAppServerAttempt", () => { }); expect(JSON.stringify(diagnosticEvents)).not.toContain("sensitive resumed query"); }); - it("uses retained raw web-search status on resumed threads", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const harness = createResumeHarness(); const diagnosticEvents: DiagnosticEventPayload[] = []; @@ -2576,7 +2677,6 @@ describe("runCodexAppServerAttempt", () => { diagnosticEvents.push(event); } }); - try { const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); @@ -2613,7 +2713,6 @@ describe("runCodexAppServerAttempt", () => { } finally { stopDiagnostics(); } - expect(diagnosticEvents.map((event) => event.type)).toEqual([ "tool.execution.started", "tool.execution.completed", @@ -2622,8 +2721,7 @@ describe("runCodexAppServerAttempt", () => { }); it("projects only newer visible history when a resumed Codex binding is stale", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const binding = await readCodexAppServerBinding(sessionFile); const bindingUpdatedAt = Date.parse(binding?.historyCoveredThrough ?? ""); @@ -2646,7 +2744,6 @@ describe("runCodexAppServerAttempt", () => { const harness = createResumeHarness(); const params = createParams(sessionFile, workspaceDir); params.prompt = "is the previous message trustworthy?"; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -2654,13 +2751,11 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - expect(harness.requests.map((request) => request.method)).toContain("thread/resume"); const turnStart = harness.requests.find((request) => request.method === "turn/start"); const inputText = (turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ?? ""; - expect(inputText).toContain("OpenClaw assembled context for this turn:"); expect(inputText).not.toContain("old native-owned context"); expect(inputText).toContain("we were discussing the Sonnet leak screenshots"); @@ -2669,10 +2764,8 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).toContain("Current user request:"); expect(inputText).toContain("is the previous message trustworthy?"); }); - it("does not project Codex mirrored transcript echoes as stale binding continuity", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const binding = await readCodexAppServerBinding(sessionFile); const bindingUpdatedAt = Date.parse(binding?.historyCoveredThrough ?? ""); @@ -2697,7 +2790,6 @@ describe("runCodexAppServerAttempt", () => { const harness = createResumeHarness(); const params = createParams(sessionFile, workspaceDir); params.prompt = "continue from the real user message"; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -2705,12 +2797,10 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const inputText = (turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ?? ""; - expect(inputText).not.toContain("OpenClaw assembled context for this turn:"); expect(inputText).not.toContain("codex mirrored user echo"); expect(inputText).not.toContain("codex mirrored assistant echo"); @@ -2718,8 +2808,7 @@ describe("runCodexAppServerAttempt", () => { }); it("does not replay messages persisted during an active native Codex turn", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const originalBindingUpdatedAt = Date.now() - 60_000; const originalBinding = await readCodexAppServerBinding(sessionFile); @@ -2741,7 +2830,6 @@ describe("runCodexAppServerAttempt", () => { expect(Date.parse(completedBinding?.historyCoveredThrough ?? "")).toBeGreaterThan( originalBindingUpdatedAt, ); - const secondHarness = createResumeHarness(); const secondParams = createParams(sessionFile, workspaceDir); secondParams.prompt = "continue after steering"; @@ -2749,7 +2837,6 @@ describe("runCodexAppServerAttempt", () => { await secondHarness.waitForMethod("turn/start"); await secondHarness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await secondRun; - const turnStart = secondHarness.requests.find((request) => request.method === "turn/start"); const inputText = (turnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0]?.text ?? @@ -2758,10 +2845,8 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).not.toContain("steered into active native turn"); expect(inputText).toContain("continue after steering"); }); - it("does not project mirrored messages on consecutive resumes", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const oldBindingUpdatedAt = Date.now() - 60_000; const oldBinding = await readCodexAppServerBinding(sessionFile); @@ -2782,7 +2867,6 @@ describe("runCodexAppServerAttempt", () => { oldBindingUpdatedAt + 2_000, ), ); - const firstHarness = createResumeHarness(); const firstParams = createParams(sessionFile, workspaceDir); firstParams.prompt = "is the previous message trustworthy?"; @@ -2790,7 +2874,6 @@ describe("runCodexAppServerAttempt", () => { await firstHarness.waitForMethod("turn/start"); await firstHarness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await firstRun; - const firstTurnStart = firstHarness.requests.find((request) => request.method === "turn/start"); const firstInputText = (firstTurnStart?.params as { input?: Array<{ text?: string }> } | undefined)?.input?.[0] @@ -2798,7 +2881,6 @@ describe("runCodexAppServerAttempt", () => { expect(firstInputText).toContain("OpenClaw assembled context for this turn:"); expect(firstInputText).toContain("we were discussing the Sonnet leak screenshots"); expect(firstInputText).toContain("is the previous message trustworthy?"); - const secondHarness = createResumeHarness(); const secondParams = createParams(sessionFile, workspaceDir); secondParams.prompt = "continue from there"; @@ -2806,7 +2888,6 @@ describe("runCodexAppServerAttempt", () => { await secondHarness.waitForMethod("turn/start"); await secondHarness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await secondRun; - const secondTurnStart = secondHarness.requests.find( (request) => request.method === "turn/start", ); @@ -2820,8 +2901,7 @@ describe("runCodexAppServerAttempt", () => { }); it("passes stable workspace files as Codex developer instructions and routes MEMORY.md through tools", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const agentsGuidance = "Follow AGENTS guidance."; const soulGuidance = "Soul voice goes here."; const identityGuidance = "Identity guidance goes here."; @@ -2851,7 +2931,6 @@ describe("runCodexAppServerAttempt", () => { systemPromptReport, threadDeveloperInstructions, } = await buildCodexTurnContextForTest(params, workspaceDir); - expect(threadDeveloperInstructions).toContain("OpenClaw Workspace Instructions"); expect(threadDeveloperInstructions).not.toContain(soulGuidance); expect(threadDeveloperInstructions).not.toContain(identityGuidance); @@ -2860,7 +2939,6 @@ describe("runCodexAppServerAttempt", () => { expect(threadDeveloperInstructions).not.toContain(memorySummary); expect(threadDeveloperInstructions).not.toContain("Codex loads AGENTS.md natively"); expect(threadDeveloperInstructions).not.toContain(agentsGuidance); - expect(collaborationInstructions).toContain("# Collaboration Mode: Default"); expect(collaborationInstructions).toContain("request_user_input availability"); expect(collaborationInstructions).toContain("OpenClaw Agent Soul"); @@ -2903,7 +2981,6 @@ describe("runCodexAppServerAttempt", () => { expect(systemPromptReport.systemPrompt.chars).toBe( [threadDeveloperInstructions, collaborationInstructions].join("\n\n").length, ); - const fileStats = new Map( systemPromptReport.injectedWorkspaceFiles.map((file) => [file.name, file]), ); @@ -2938,10 +3015,8 @@ describe("runCodexAppServerAttempt", () => { truncated: false, }); }); - it("adds memory recall guidance when dated memory notes exist without root MEMORY.md", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const datedMemory = "User avoids Chase cards while over 5/24."; await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true }); await fs.writeFile(path.join(workspaceDir, "memory/2026-06-09.md"), datedMemory); @@ -2955,12 +3030,10 @@ describe("runCodexAppServerAttempt", () => { setCodexTestModelSupportsTools(params, true); params.runtimePlan = createCodexRuntimePlanFixture(); setAgentWorkspaceForTest(params, workspaceDir); - const { collaborationInstructions, inputText } = await buildCodexTurnContextForTest( params, workspaceDir, ); - expect(collaborationInstructions).toContain("## Memory Recall"); expect(collaborationInstructions).toContain("MEMORY.md + memory/*.md"); expect(collaborationInstructions).toContain("memory_search"); @@ -2972,8 +3045,7 @@ describe("runCodexAppServerAttempt", () => { }); it("does not synthesize memory recall guidance without a registered memory prompt builder", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const memorySummary = "User avoids Chase cards while over 5/24."; await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary); @@ -2986,12 +3058,10 @@ describe("runCodexAppServerAttempt", () => { setCodexTestModelSupportsTools(params, true); params.runtimePlan = createCodexRuntimePlanFixture(); setAgentWorkspaceForTest(params, workspaceDir); - const { collaborationInstructions, inputText } = await buildCodexTurnContextForTest( params, workspaceDir, ); - expect(collaborationInstructions).not.toContain("## Memory Recall"); expect(collaborationInstructions).toContain("OpenClaw Workspace Memory"); expect(collaborationInstructions).not.toContain("Use `tool_search` first"); @@ -2999,10 +3069,8 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).toBe("hello"); expect(inputText).not.toContain(memorySummary); }); - it("sends workspace bootstrap instructions through Codex app-server payloads", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const agentsGuidance = "Follow AGENTS guidance."; const soulGuidance = "Soul voice goes here."; const identityGuidance = "Identity guidance goes here."; @@ -3017,7 +3085,6 @@ describe("runCodexAppServerAttempt", () => { const harness = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); setAgentWorkspaceForTest(params, workspaceDir); - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3025,7 +3092,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - const threadStart = harness.requests.find((request) => request.method === "thread/start"); const threadStartParams = threadStart?.params as { config?: { instructions?: string }; @@ -3038,7 +3104,6 @@ describe("runCodexAppServerAttempt", () => { expect(threadStartParams.developerInstructions).not.toContain(soulGuidance); expect(threadStartParams.developerInstructions).not.toContain(identityGuidance); expect(threadStartParams.developerInstructions).not.toContain(userProfile); - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; @@ -3057,7 +3122,6 @@ describe("runCodexAppServerAttempt", () => { expect(collaborationInstructions).toContain(identityGuidance); expect(collaborationInstructions).toContain(userProfile); expect(collaborationInstructions).not.toContain(toolGuidance); - const inputText = turnStartParams.input?.[0]?.text ?? ""; expect(inputText).toBe("hello"); expect(inputText).not.toContain(agentsGuidance); @@ -3068,13 +3132,11 @@ describe("runCodexAppServerAttempt", () => { }); it("injects bounded MEMORY.md when memory tools are unavailable", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const memorySummary = "Memory summary goes here."; await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3082,7 +3144,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; @@ -3091,7 +3152,6 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).not.toContain("OpenClaw Workspace Memory"); expect(inputText).not.toContain("memory_search"); expect(inputText).toContain(memorySummary); - const fileStats = new Map( result.systemPromptReport?.injectedWorkspaceFiles.map((file) => [file.name, file]) ?? [], ); @@ -3101,10 +3161,8 @@ describe("runCodexAppServerAttempt", () => { truncated: false, }); }); - it("routes MEMORY.md through memory_get when search is unavailable", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const memorySummary = "Memory summary goes here."; await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary); @@ -3115,7 +3173,6 @@ describe("runCodexAppServerAttempt", () => { setCodexTestModelSupportsTools(params, true); params.runtimePlan = createCodexRuntimePlanFixture(); setAgentWorkspaceForTest(params, workspaceDir); - const { collaborationInstructions, inputText, systemPromptReport } = await buildCodexTurnContextForTest(params, workspaceDir); expect(inputText).not.toContain("OpenClaw Workspace Memory"); @@ -3127,7 +3184,6 @@ describe("runCodexAppServerAttempt", () => { expect(collaborationInstructions).toContain("memory_get"); expect(collaborationInstructions).not.toContain("memory_search"); expect(collaborationInstructions).not.toContain(memorySummary); - const fileStats = new Map( systemPromptReport.injectedWorkspaceFiles.map((file) => [file.name, file]), ); @@ -3139,8 +3195,7 @@ describe("runCodexAppServerAttempt", () => { }); it("reports MEMORY.md as truncated when no-tool fallback exceeds the bootstrap budget", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const soulGuidance = "Soul guidance ".repeat(80); const memorySummary = "Memory summary goes here."; await fs.mkdir(workspaceDir, { recursive: true }); @@ -3156,7 +3211,6 @@ describe("runCodexAppServerAttempt", () => { }, }, } as EmbeddedRunAttemptParams["config"]; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3164,7 +3218,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - const fileStats = new Map( result.systemPromptReport?.injectedWorkspaceFiles.map((file) => [file.name, file]) ?? [], ); @@ -3174,10 +3227,8 @@ describe("runCodexAppServerAttempt", () => { truncated: true, }); }); - it("keeps MEMORY.md out of the Codex workspace context budget", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const memorySummary = "Memory summary ".repeat(300); const hookContext = "Hook context survives the memory budget."; const hookPath = path.join(workspaceDir, "ZZZ.md"); @@ -3214,7 +3265,6 @@ describe("runCodexAppServerAttempt", () => { createRuntimeDynamicTool("memory_search"), createRuntimeDynamicTool("memory_get"), ]); - const { collaborationInstructions, inputText, systemPromptReport } = await buildCodexTurnContextForTest(params, workspaceDir); expect(inputText).not.toContain("OpenClaw Workspace Memory"); @@ -3222,7 +3272,6 @@ describe("runCodexAppServerAttempt", () => { expect(inputText).toContain(hookContext); expect(collaborationInstructions).toContain("OpenClaw Workspace Memory"); expect(collaborationInstructions).not.toContain(memorySummary); - const fileStats = new Map( systemPromptReport.injectedWorkspaceFiles.map((file) => [file.name, file]), ); @@ -3239,8 +3288,7 @@ describe("runCodexAppServerAttempt", () => { }); it("keeps extra MEMORY.md bootstrap files in Codex workspace context", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const rootMemory = "Root memory should stay tool-routed."; const nestedMemory = "Nested package memory remains prompt context."; const nestedMemoryPath = path.join(workspaceDir, "packages/pkg/MEMORY.md"); @@ -3270,7 +3318,6 @@ describe("runCodexAppServerAttempt", () => { setCodexTestModelSupportsTools(params, true); params.runtimePlan = createCodexRuntimePlanFixture(); setAgentWorkspaceForTest(params, workspaceDir); - const { collaborationInstructions, inputText, systemPromptReport } = await buildCodexTurnContextForTest(params, workspaceDir); expect(inputText).not.toContain("OpenClaw Workspace Memory"); @@ -3279,7 +3326,6 @@ describe("runCodexAppServerAttempt", () => { expect(collaborationInstructions).toContain("OpenClaw Workspace Memory"); expect(collaborationInstructions).not.toContain(rootMemory); expect(collaborationInstructions).not.toContain(nestedMemory); - const files = systemPromptReport.injectedWorkspaceFiles; const rootMemoryStats = files.find( (file) => file.path === path.join(workspaceDir, "MEMORY.md"), @@ -3296,10 +3342,8 @@ describe("runCodexAppServerAttempt", () => { truncated: false, }); }); - it("injects MEMORY.md when active workspace is not the memory tool workspace", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const memorySummary = "Memory summary goes here."; await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), memorySummary); @@ -3312,14 +3356,12 @@ describe("runCodexAppServerAttempt", () => { params.disableTools = false; params.runtimePlan = createCodexRuntimePlanFixture(); setAgentWorkspaceForTest(params, path.join(tempDir, "memory-workspace")); - const { collaborationInstructions, inputText, systemPromptReport } = await buildCodexTurnContextForTest(params, workspaceDir); expect(collaborationInstructions).not.toContain("## Memory Recall"); expect(collaborationInstructions).not.toContain("OpenClaw Workspace Memory"); expect(inputText).not.toContain("OpenClaw Workspace Memory"); expect(inputText).toContain(memorySummary); - const fileStats = new Map( systemPromptReport.injectedWorkspaceFiles.map((file) => [file.name, file]), ); @@ -3331,8 +3373,7 @@ describe("runCodexAppServerAttempt", () => { }); it("reports hook-supplied bootstrap files that only expose path and content", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const soulPath = path.join(workspaceDir, "SOUL.md"); const soulGuidance = "Hook supplied soul guidance."; await fs.mkdir(workspaceDir, { recursive: true }); @@ -3349,7 +3390,6 @@ describe("runCodexAppServerAttempt", () => { ]; }); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3357,7 +3397,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - expect(result.systemPromptReport?.injectedWorkspaceFiles).toEqual([ expect.objectContaining({ name: "SOUL.md", @@ -3368,10 +3407,8 @@ describe("runCodexAppServerAttempt", () => { }), ]); }); - it("points heartbeat Codex turns at HEARTBEAT.md without injecting its contents", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const heartbeatPath = path.join(workspaceDir, "HEARTBEAT.md"); await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile(heartbeatPath, "Heartbeat checklist goes here."); @@ -3380,7 +3417,6 @@ describe("runCodexAppServerAttempt", () => { params.trigger = "heartbeat"; params.bootstrapContextMode = "lightweight"; params.bootstrapContextRunKind = "heartbeat"; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3388,13 +3424,11 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const threadStart = harness.requests.find((request) => request.method === "thread/start"); const threadStartParams = threadStart?.params as { developerInstructions?: string; }; expect(threadStartParams.developerInstructions).not.toContain("Heartbeat checklist goes here."); - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; @@ -3407,7 +3441,6 @@ describe("runCodexAppServerAttempt", () => { const inputText = turnStartParams.input?.[0]?.text ?? ""; const collaborationInstructions = turnStartParams.collaborationMode?.settings?.developer_instructions ?? ""; - expect(inputText).not.toContain("Heartbeat checklist goes here."); expect(collaborationInstructions).toContain("HEARTBEAT.md exists"); expect(collaborationInstructions).toContain("Read it before proceeding with this heartbeat"); @@ -3416,8 +3449,7 @@ describe("runCodexAppServerAttempt", () => { }); it("omits heartbeat Codex workspace pointers for empty HEARTBEAT.md files", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await fs.mkdir(workspaceDir, { recursive: true }); await fs.writeFile(path.join(workspaceDir, "HEARTBEAT.md"), "\n\n"); const harness = createStartedThreadHarness(); @@ -3425,7 +3457,6 @@ describe("runCodexAppServerAttempt", () => { params.trigger = "heartbeat"; params.bootstrapContextMode = "lightweight"; params.bootstrapContextRunKind = "heartbeat"; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3433,7 +3464,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { collaborationMode?: { @@ -3444,14 +3474,11 @@ describe("runCodexAppServerAttempt", () => { }; const collaborationInstructions = turnStartParams.collaborationMode?.settings?.developer_instructions ?? ""; - expect(collaborationInstructions).toContain("This is an OpenClaw heartbeat turn"); expect(collaborationInstructions).not.toContain("HEARTBEAT.md exists"); }); - it("keeps lightweight cron Codex turns out of OpenClaw bootstrap context", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const exactCommand = "cd /Users/phaedrus/Projects/openclaw && /Users/phaedrus/clawd/scripts/clawsweeper-related-scan.py"; await fs.mkdir(workspaceDir, { recursive: true }); @@ -3467,7 +3494,6 @@ describe("runCodexAppServerAttempt", () => { prompt: "demo", skills: [], }; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3475,7 +3501,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); const result = await run; - const threadStart = harness.requests.find((request) => request.method === "thread/start"); const threadStartParams = threadStart?.params as { developerInstructions?: string; @@ -3485,7 +3510,6 @@ describe("runCodexAppServerAttempt", () => { expect(threadStartParams.developerInstructions).not.toContain("Soul voice goes here."); expect(threadStartParams.developerInstructions).not.toContain("Follow AGENTS guidance."); expect(threadStartParams.developerInstructions).not.toContain(""); - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; @@ -3512,7 +3536,6 @@ describe("runCodexAppServerAttempt", () => { prompt: "demo", skills: [], }; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await new Promise((resolve) => { @@ -3520,23 +3543,19 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const turnStart = harness.requests.find((request) => request.method === "turn/start"); const turnStartParams = turnStart?.params as { input?: Array<{ text?: string }>; }; expect(turnStartParams.input?.[0]?.text).toBe(exactPrompt); }); - it("forwards Codex app-server verbose tool summaries and completed output", async () => { const onToolResult = vi.fn(); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const harness = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); params.verboseLevel = "full"; params.onToolResult = onToolResult; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.notify({ @@ -3577,7 +3596,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - expect(onToolResult).toHaveBeenCalledTimes(2); expect(onToolResult).toHaveBeenNthCalledWith(1, { text: "📖 Read: `from README.md`", @@ -3596,7 +3614,6 @@ describe("runCodexAppServerAttempt", () => { const params = createParams(sessionFile, workspaceDir); attachSqliteSessionTarget(params, storePath, sessionId); params.prompt = "Send the update to Alice."; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.notify({ @@ -3640,9 +3657,7 @@ describe("runCodexAppServerAttempt", () => { }, }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - const result = await run; - expect(result.settledTurnFinalizationContext).toMatchObject({ source: "openclaw-transcript", messages: [ @@ -3653,15 +3668,12 @@ describe("runCodexAppServerAttempt", () => { }); expect(Object.isFrozen(result.settledTurnFinalizationContext?.messages)).toBe(true); }); - it("preserves every command failure from official app-server events", async () => { const sessionFile = path.join(tempDir, "session-multi-command-failure.jsonl"); const workspaceDir = path.join(tempDir, "workspace-multi-command-failure"); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); - for (const [id, status, exitCode] of [ ["command-failed-1", "failed", 1], ["command-succeeded", "completed", 0], @@ -3700,7 +3712,6 @@ describe("runCodexAppServerAttempt", () => { }); } await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - const result = await run; expect(result.toolMetas).toHaveLength(3); expect(result.toolMetas.filter((meta) => meta.isError === true)).toHaveLength(2); @@ -3711,15 +3722,12 @@ describe("runCodexAppServerAttempt", () => { createMockPluginRegistry([{ hookName: "before_tool_call", handler: vi.fn() }]), ); const info = vi.spyOn(embeddedAgentLog, "info").mockImplementation(() => undefined); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const startRequest = harness.requests.find((request) => request.method === "thread/start"); const startParams = startRequest?.params as Record | undefined; expect(startParams?.approvalPolicy).toBe("untrusted"); @@ -3734,22 +3742,18 @@ describe("runCodexAppServerAttempt", () => { }, ); }); - it("keeps explicit Codex yolo mode unpromoted when OpenClaw tool policy exists", async () => { initializeGlobalHookRunner( createMockPluginRegistry([{ hookName: "before_tool_call", handler: vi.fn() }]), ); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), { pluginConfig: { appServer: { mode: "yolo" } }, }); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const startRequest = harness.requests.find((request) => request.method === "thread/start"); const startParams = startRequest?.params as Record | undefined; expect(startParams?.approvalPolicy).toBe("never"); @@ -3757,21 +3761,18 @@ describe("runCodexAppServerAttempt", () => { }); it("applies stored session permissions to resumed harness turns", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { approvalPolicy: "never", sandbox: "danger-full-access", }); const harness = createResumeHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), { pluginConfig: { appServer: { mode: "guardian" } }, }); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - const resumeParams = harness.requests.find((request) => request.method === "thread/resume") ?.params as Record | undefined; const turnParams = harness.requests.find((request) => request.method === "turn/start") @@ -3781,22 +3782,18 @@ describe("runCodexAppServerAttempt", () => { expect(turnParams?.approvalPolicy).toBe("never"); expect(turnParams?.sandboxPolicy).toEqual({ type: "dangerFullAccess" }); }); - it("keeps normalized full exec mode unpromoted when OpenClaw tool policy exists", async () => { initializeGlobalHookRunner( createMockPluginRegistry([{ hookName: "before_tool_call", handler: vi.fn() }]), ); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const harness = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); params.config = { tools: { exec: { mode: "full" } } } as never; - const run = runCodexAppServerAttempt(params); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const startRequest = harness.requests.find((request) => request.method === "thread/start"); const startParams = startRequest?.params as Record | undefined; expect(startParams?.approvalPolicy).toBe("never"); @@ -3809,23 +3806,18 @@ describe("runCodexAppServerAttempt", () => { ); vi.stubEnv("OPENCLAW_CODEX_APP_SERVER_MODE", " "); vi.stubEnv("OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY", "always"); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const startRequest = harness.requests.find((request) => request.method === "thread/start"); const startParams = startRequest?.params as Record | undefined; expect(startParams?.approvalPolicy).toBe("untrusted"); }); - it("preserves a healthy binding when invalid image cleanup hits a transient thread", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: JSON.stringify([{ name: "message" }]), }); @@ -3838,11 +3830,9 @@ describe("runCodexAppServerAttempt", () => { } return undefined; }); - await expect(runCodexAppServerAttempt(createParams(sessionFile, workspaceDir))).rejects.toThrow( "invalid image_url base64 payload", ); - expect(harness.requests.map((request) => request.method)).toEqual([ "thread/start", "turn/start", @@ -3853,8 +3843,7 @@ describe("runCodexAppServerAttempt", () => { }); it("preserves a healthy binding when the server rejects unsupported image input", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const harness = createAppServerHarness(async (method) => { if (method === "thread/resume") { @@ -3865,11 +3854,9 @@ describe("runCodexAppServerAttempt", () => { } return {}; }); - await expect(runCodexAppServerAttempt(createParams(sessionFile, workspaceDir))).rejects.toThrow( "unsupported image input", ); - expect(harness.requests.map((request) => request.method)).toEqual([ "thread/resume", "turn/start", @@ -3878,10 +3865,8 @@ describe("runCodexAppServerAttempt", () => { const binding = await readCodexAppServerBinding(sessionFile); expect(binding?.threadId).toBe("thread-existing"); }); - it("retries turn/start after a native compact turn finishes", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); let turnStartCalls = 0; const harnessRef: { current?: ReturnType } = {}; @@ -3921,7 +3906,6 @@ describe("runCodexAppServerAttempt", () => { return {}; }); harnessRef.current = harness; - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await vi.waitFor( () => @@ -3932,7 +3916,6 @@ describe("runCodexAppServerAttempt", () => { ); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - expect(harness.requests.map((request) => request.method)).toEqual([ "thread/resume", "turn/start", @@ -3942,8 +3925,7 @@ describe("runCodexAppServerAttempt", () => { }); it("waits for an already-active native turn before starting a resumed thread turn", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const harness = createAppServerHarness(async (method) => { if (method === "thread/resume") { @@ -3962,14 +3944,12 @@ describe("runCodexAppServerAttempt", () => { } return {}; }); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); await harness.waitForMethod("thread/resume"); await new Promise((resolve) => { setTimeout(resolve, 20); }); expect(harness.requests.map((request) => request.method)).not.toContain("turn/start"); - await harness.notify({ method: "turn/completed", params: { @@ -3980,17 +3960,14 @@ describe("runCodexAppServerAttempt", () => { await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - expect(harness.requests.map((request) => request.method)).toEqual([ "thread/resume", "turn/start", "thread/unsubscribe", ]); }); - it("does not retry turn/start for non-compact active turns", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const harness = createAppServerHarness(async (method) => { if (method === "thread/resume") { @@ -4013,11 +3990,9 @@ describe("runCodexAppServerAttempt", () => { } return {}; }); - await expect(runCodexAppServerAttempt(createParams(sessionFile, workspaceDir))).rejects.toThrow( "cannot steer a review turn", ); - expect(harness.requests.map((request) => request.method)).toEqual([ "thread/resume", "turn/start", @@ -4038,16 +4013,11 @@ describe("runCodexAppServerAttempt", () => { } }); const abortController = new AbortController(); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.abortSignal = abortController.signal; - const run = runCodexAppServerAttempt(params); await waitForMethod("turn/start"); abortController.abort("shutdown"); - const result = await run; expect(result.aborted).toBe(true); await new Promise((resolve) => { @@ -4058,13 +4028,9 @@ describe("runCodexAppServerAttempt", () => { process.off("unhandledRejection", onUnhandledRejection); } }); - it("forwards image attachments to the app-server turn input", async () => { const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); const pngBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="; params.model = createCodexTestModel("codex", ["text", "image"]); @@ -4076,12 +4042,8 @@ describe("runCodexAppServerAttempt", () => { data: pngBase64, }, ]; - const run = runCodexAppServerAttempt(params); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); const turnStart = requests.find((entry) => entry.method === "turn/start"); const turnStartParams = turnStart?.params as | { input?: Array<{ text?: string; text_elements?: unknown[]; type?: string; url?: string }> } @@ -4105,14 +4067,10 @@ describe("runCodexAppServerAttempt", () => { return {}; }, ); - - const result = await runCodexAppServerAttempt( - createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), - ); + const result = await runCodexAppServerAttempt(createRunParams()); expect(result.aborted).toBe(false); expect(result.timedOut).toBe(false); }); - it("does not fail when a buffered terminal notification is followed by client close", async () => { let resolveBufferedTerminal!: () => void; const bufferedTerminal = new Promise((resolve) => { @@ -4139,17 +4097,12 @@ describe("runCodexAppServerAttempt", () => { return {}; }, ); - - const run = runCodexAppServerAttempt( - createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), - { turnTerminalIdleTimeoutMs: 60_000 }, - ); + const run = runCodexAppServerAttempt(createRunParams(), { turnTerminalIdleTimeoutMs: 60_000 }); await bufferedTerminal; await new Promise((resolve) => { setImmediate(resolve); }); harness.close(); - const result = await run; expect(result.promptError ?? undefined).toBeUndefined(); expect(result.aborted).toBe(false); @@ -4176,12 +4129,8 @@ describe("runCodexAppServerAttempt", () => { return {}; }, ); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.timeoutMs = 60_000; - const run = runCodexAppServerAttempt(params, { turnCompletionIdleTimeoutMs: 5, turnTerminalIdleTimeoutMs: 60_000, @@ -4192,12 +4141,10 @@ describe("runCodexAppServerAttempt", () => { }); expect(harness.request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - const result = await run; expect(result.aborted).toBe(false); expect(result.timedOut).toBe(false); }); - it("completes when turn/start returns a terminal turn without a follow-up notification", async () => { const harness = createAppServerHarness(async (method) => { if (method === "thread/start") { @@ -4214,11 +4161,7 @@ describe("runCodexAppServerAttempt", () => { } return {}; }); - - const result = await runCodexAppServerAttempt( - createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), - ); - + const result = await runCodexAppServerAttempt(createRunParams()); expect(harness.requests.map((entry) => entry.method)).toContain("turn/start"); expect(result.assistantTexts).toEqual(["done from response"]); expect(result.aborted).toBe(false); @@ -4251,27 +4194,19 @@ describe("runCodexAppServerAttempt", () => { } return {}; }); - - const result = await runCodexAppServerAttempt( - createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), - ); - + const result = await runCodexAppServerAttempt(createRunParams()); expect(harness.requests.map((entry) => entry.method)).toContain("turn/start"); expect(result.assistantTexts).toEqual([]); expect(result.toolMediaUrls).toEqual([savedPath]); expect(result.hostOwnedToolMediaUrls).toEqual([savedPath]); }); - it("does not complete on unscoped turn/completed notifications", async () => { const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt( - createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), - ); + const run = runCodexAppServerAttempt(createRunParams()); let resolved = false; void run.then(() => { resolved = true; }); - await harness.waitForMethod("turn/start"); await harness.notify({ method: "turn/completed", @@ -4287,7 +4222,6 @@ describe("runCodexAppServerAttempt", () => { setImmediate(resolve); }); expect(resolved).toBe(false); - await harness.notify({ method: "turn/completed", params: { @@ -4299,7 +4233,6 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - const result = await run; expect(result.assistantTexts).toEqual(["final completion"]); expect(result.aborted).toBe(false); @@ -4309,14 +4242,11 @@ describe("runCodexAppServerAttempt", () => { it("ignores turn/completed notifications for other subscribed threads", async () => { const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined); const harness = createStartedThreadHarness(); - const run = runCodexAppServerAttempt( - createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")), - ); + const run = runCodexAppServerAttempt(createRunParams()); let resolved = false; void run.then(() => { resolved = true; }); - await harness.waitForMethod("turn/start"); await harness.notify({ method: "turn/completed", @@ -4338,7 +4268,6 @@ describe("runCodexAppServerAttempt", () => { message.includes("turn/completed did not match active turn"), ), ).toBe(false); - await harness.notify({ method: "turn/completed", params: { @@ -4350,18 +4279,12 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - const result = await run; expect(result.assistantTexts).toEqual(["final completion"]); expect(result.aborted).toBe(false); expect(result.timedOut).toBe(false); }); - it("routes Computer Use MCP elicitations through the native bridge", async () => { - let notify: (notification: CodexServerNotification) => Promise = async () => undefined; - let handleRequest: - | ((request: { id: string; method: string; params?: unknown }) => Promise) - | undefined; const bridgeSpy = vi .spyOn(elicitationBridge, "handleCodexAppServerElicitationRequest") .mockResolvedValue({ @@ -4437,32 +4360,8 @@ describe("runCodexAppServerAttempt", () => { } return {}; }); - setCodexAppServerClientFactoryForTest( - async () => - ({ - ...mockClientRuntimeMethods(), - request, - addNotificationHandler: (handler: typeof notify) => { - notify = handler; - return () => undefined; - }, - addRequestHandler: ( - handler: (request: { - id: string; - method: string; - params?: unknown; - }) => Promise, - ) => { - handleRequest = handler; - return () => undefined; - }, - }) as never, - ); - - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const elicitation = installElicitationClient(request); + const params = createRunParams(); attachSqliteSessionTarget(params, path.join(tempDir, "sessions.json"), "session-computer-use"); const run = runCodexAppServerAttempt(params, { pluginConfig: { @@ -4473,13 +4372,12 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function")); + await vi.waitFor(() => expect(elicitation.handleRequest).toBeTypeOf("function")); // The keyed router only accepts turn-scoped requests once the turn is bound. await vi.waitFor(() => expect(request.mock.calls.map(([method]) => method)).toContain("turn/start"), ); - - const result = await handleRequest?.({ + const result = await elicitation.handleRequest?.({ id: "request-elicitation-1", method: "mcpServer/elicitation/request", params: { @@ -4489,7 +4387,6 @@ describe("runCodexAppServerAttempt", () => { mode: "form", }, }); - expect(result).toEqual({ action: "accept", content: { approve: true }, @@ -4513,8 +4410,7 @@ describe("runCodexAppServerAttempt", () => { | { approvalPolicy?: { granular?: { mcp_elicitations?: boolean } } } | undefined; expect(turnStartParams?.approvalPolicy?.granular?.mcp_elicitations).toBe(true); - - await notify({ + await elicitation.notify({ method: "turn/completed", params: { threadId: "thread-1", @@ -4526,55 +4422,19 @@ describe("runCodexAppServerAttempt", () => { }); it("passes session plugin app policy context to elicitation handling", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); - const pluginConfig = { - codexPlugins: { - enabled: true, - plugins: { - "google-calendar": { - marketplaceName: "openai-curated", - pluginName: "google-calendar", - }, - }, - }, - }; + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); + const pluginConfig = GOOGLE_CALENDAR_PLUGIN_CONFIG; const appServer = resolveCodexAppServerRuntimeOptions({ pluginConfig: readCodexPluginConfig(pluginConfig), }); - defaultCodexAppInventoryCache.clear(); - await defaultCodexAppInventoryCache.refreshNow({ - key: buildCodexPluginAppCacheKey({ + await primeGoogleCalendarAppInventory( + buildCodexPluginAppCacheKey({ appServer, agentDir, runtimeIdentity: getMockRuntimeIdentity(), }), - request: async () => ({ - data: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - logoUrl: null, - logoUrlDark: null, - distributionChannel: null, - branding: null, - appMetadata: null, - labels: null, - installUrl: null, - isAccessible: true, - isEnabled: true, - pluginDisplayNames: [], - }, - ], - nextCursor: null, - }), - }); - let notify: (notification: CodexServerNotification) => Promise = async () => undefined; - let handleRequest: - | ((request: { id: string; method: string; params?: unknown }) => Promise) - | undefined; + true, + ); const bridgeSpy = vi .spyOn(elicitationBridge, "handleCodexAppServerElicitationRequest") .mockResolvedValue({ @@ -4582,104 +4442,17 @@ describe("runCodexAppServerAttempt", () => { content: null, _meta: null, }); - const request = vi.fn(async (method: string) => { - if (method === "plugin/list") { - return { - marketplaces: [ - { - name: "openai-curated", - path: "/marketplaces/openai-curated", - interface: null, - plugins: [ - { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - ], - }, - ], - marketplaceLoadErrors: [], - featuredPluginIds: [], - }; - } - if (method === "plugin/read") { - return { - plugin: { - marketplaceName: "openai-curated", - marketplacePath: "/marketplaces/openai-curated", - summary: { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - description: null, - skills: [], - apps: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - installUrl: null, - needsAuth: false, - }, - ], - mcpServers: ["google-calendar"], - }, - }; - } - if (method === "thread/start") { - return threadStartResult("thread-1"); - } - if (method === "turn/start") { - return turnStartResult("turn-1", "inProgress"); - } - return {}; - }); - setCodexAppServerClientFactoryForTest( - async () => - ({ - ...mockClientRuntimeMethods(), - request, - addNotificationHandler: (handler: typeof notify) => { - notify = handler; - return () => undefined; - }, - addRequestHandler: ( - handler: (request: { - id: string; - method: string; - params?: unknown; - }) => Promise, - ) => { - handleRequest = handler; - return () => undefined; - }, - }) as never, - ); - + const request = createGoogleCalendarRequest(); + const elicitation = installElicitationClient(request); const params = createParams(sessionFile, workspaceDir); params.agentDir = agentDir; const run = runCodexAppServerAttempt(params, { pluginConfig }); - await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function")); + await vi.waitFor(() => expect(elicitation.handleRequest).toBeTypeOf("function")); // The keyed router only accepts turn-scoped requests once the turn is bound. await vi.waitFor(() => expect(request.mock.calls.map(([method]) => method)).toContain("turn/start"), ); - - const result = await handleRequest?.({ + const result = await elicitation.handleRequest?.({ id: "request-elicitation-1", method: "mcpServer/elicitation/request", params: { @@ -4689,7 +4462,6 @@ describe("runCodexAppServerAttempt", () => { mode: "form", }, }); - expect(result).toEqual({ action: "decline", content: null, @@ -4720,8 +4492,7 @@ describe("runCodexAppServerAttempt", () => { | { approvalPolicy?: { granular?: { mcp_elicitations?: boolean } } } | undefined; expect(turnStartParams?.approvalPolicy?.granular?.mcp_elicitations).toBe(true); - - await notify({ + await elicitation.notify({ method: "turn/completed", params: { threadId: "thread-1", @@ -4731,439 +4502,110 @@ describe("runCodexAppServerAttempt", () => { }); await run; }); - - it("keys plugin app inventory by the resolved Codex account", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); - const authProfileId = "openai:work"; - const pluginConfig = { - codexPlugins: { - enabled: true, - plugins: { - "google-calendar": { - marketplaceName: "openai-curated", - pluginName: "google-calendar", - }, - }, - }, - }; - const appServer = resolveCodexAppServerRuntimeOptions({ - pluginConfig: readCodexPluginConfig(pluginConfig), - }); - defaultCodexAppInventoryCache.clear(); - await defaultCodexAppInventoryCache.refreshNow({ - key: buildCodexPluginAppCacheKey({ - appServer, - agentDir, - authProfileId, - accountId: "account-work", - runtimeIdentity: getMockRuntimeIdentity(), - }), - request: async () => ({ - data: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - logoUrl: null, - logoUrlDark: null, - distributionChannel: null, - branding: null, - appMetadata: null, - labels: null, - installUrl: null, - isAccessible: true, - isEnabled: true, - pluginDisplayNames: [], - }, - ], - nextCursor: null, - }), - }); - const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(async (method) => { - if (method === "plugin/list") { - return { - marketplaces: [ - { - name: "openai-curated", - path: "/marketplaces/openai-curated", - interface: null, - plugins: [ - { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - ], - }, - ], - marketplaceLoadErrors: [], - featuredPluginIds: [], - }; - } - if (method === "plugin/read") { - return { - plugin: { - marketplaceName: "openai-curated", - marketplacePath: "/marketplaces/openai-curated", - summary: { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - description: null, - skills: [], - apps: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - installUrl: null, - needsAuth: false, - }, - ], - mcpServers: ["google-calendar"], - }, - }; - } - if (method === "app/list") { - throw new Error("app/list should use the account-keyed cache entry"); - } - return undefined; - }); - const params = createParams(sessionFile, workspaceDir); - params.agentDir = agentDir; - params.authProfileId = authProfileId; - params.authProfileStore = { - version: 1, - profiles: { - [authProfileId]: { - type: "oauth", - provider: "openai", - access: "access-token", - refresh: "refresh-token", - expires: Date.now() + 60_000, + it.each([ + { + name: "keys plugin app inventory by the resolved Codex account", + cachedEnabled: true, + cacheKey: ({ appServer, agentDir }: GoogleCalendarCacheKeyInput) => + buildCodexPluginAppCacheKey({ + appServer, + agentDir, + authProfileId: "openai:work", accountId: "account-work", - email: "work@example.test", - }, - }, - }; - - const run = runCodexAppServerAttempt(params, { pluginConfig }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - - const threadStart = requests.find((entry) => entry.method === "thread/start"); - const threadStartParams = threadStart?.params as - | { config?: { apps?: Record } } - | undefined; - expect(threadStartParams?.config?.apps?.["google-calendar-app"]?.enabled).toBe(true); - expect(requests.map((entry) => entry.method)).not.toContain("app/list"); - }); - - it("sends a thread/start app enable override when app/list cached the app as disabled", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); - const pluginConfig = { - codexPlugins: { - enabled: true, - plugins: { - "google-calendar": { - marketplaceName: "openai-curated", - pluginName: "google-calendar", - }, - }, - }, - }; - const appServer = resolveCodexAppServerRuntimeOptions({ - pluginConfig: readCodexPluginConfig(pluginConfig), - }); - defaultCodexAppInventoryCache.clear(); - await defaultCodexAppInventoryCache.refreshNow({ - key: buildCodexPluginAppCacheKey({ - appServer, - agentDir, - runtimeIdentity: getMockRuntimeIdentity(), - }), - request: async () => ({ - data: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - logoUrl: null, - logoUrlDark: null, - distributionChannel: null, - branding: null, - appMetadata: null, - labels: null, - installUrl: null, - isAccessible: true, - isEnabled: false, - pluginDisplayNames: [], - }, - ], - nextCursor: null, - }), - }); - const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(async (method) => { - if (method === "plugin/list") { - return { - marketplaces: [ - { - name: "openai-curated", - path: "/marketplaces/openai-curated", - interface: null, - plugins: [ - { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - ], - }, - ], - marketplaceLoadErrors: [], - featuredPluginIds: [], - }; - } - if (method === "plugin/read") { - return { - plugin: { - marketplaceName: "openai-curated", - marketplacePath: "/marketplaces/openai-curated", - summary: { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - description: null, - skills: [], - apps: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - installUrl: null, - needsAuth: false, - }, - ], - mcpServers: ["google-calendar"], - }, - }; - } - if (method === "app/list") { - throw new Error("app/list should use the cached inventory entry"); - } - return undefined; - }); - const params = createParams(sessionFile, workspaceDir); - params.agentDir = agentDir; - - const run = runCodexAppServerAttempt(params, { pluginConfig }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - - const threadStart = requests.find((entry) => entry.method === "thread/start"); - const threadStartParams = threadStart?.params as - | { config?: { apps?: Record } } - | undefined; - expect(threadStartParams?.config?.apps?.["google-calendar-app"]?.enabled).toBe(true); - expect(requests.map((entry) => entry.method)).not.toContain("app/list"); - }); - - it("keys plugin app inventory by inherited API key fallback credentials", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); - const pluginConfig = { - codexPlugins: { - enabled: true, - plugins: { - "google-calendar": { - marketplaceName: "openai-curated", - pluginName: "google-calendar", - }, - }, - }, - }; - const appServer = resolveCodexAppServerRuntimeOptions({ - pluginConfig: readCodexPluginConfig(pluginConfig), - }); - defaultCodexAppInventoryCache.clear(); - await defaultCodexAppInventoryCache.refreshNow({ - key: buildCodexPluginAppCacheKey({ - appServer, - agentDir, - envApiKeyFingerprint: resolveCodexAppServerFallbackApiKeyCacheKey({ - startOptions: appServer.start, - baseEnv: { CODEX_API_KEY: "old-codex-env-key" }, + runtimeIdentity: getMockRuntimeIdentity(), }), - runtimeIdentity: getMockRuntimeIdentity(), - }), - request: async () => ({ - data: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - logoUrl: null, - logoUrlDark: null, - distributionChannel: null, - branding: null, - appMetadata: null, - labels: null, - installUrl: null, - isAccessible: true, - isEnabled: true, - pluginDisplayNames: [], - }, - ], - nextCursor: null, - }), - }); - vi.stubEnv("CODEX_API_KEY", "new-codex-env-key"); - vi.stubEnv("OPENAI_API_KEY", ""); - const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(async (method) => { - if (method === "app/list") { - return { - data: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - logoUrl: null, - logoUrlDark: null, - distributionChannel: null, - branding: null, - appMetadata: null, - labels: null, - installUrl: null, - isAccessible: true, - isEnabled: true, - pluginDisplayNames: [], + appList: () => { + throw new Error("app/list should use the account-keyed cache entry"); + }, + configure: (params: EmbeddedRunAttemptParams) => { + params.authProfileId = "openai:work"; + params.authProfileStore = { + version: 1, + profiles: { + "openai:work": { + type: "oauth", + provider: "openai", + access: "access-token", + refresh: "refresh-token", + expires: Date.now() + 60_000, + accountId: "account-work", + email: "work@example.test", }, - ], - nextCursor: null, - }; - } - if (method === "plugin/list") { - return { - marketplaces: [ - { - name: "openai-curated", - path: "/marketplaces/openai-curated", - interface: null, - plugins: [ - { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - ], - }, - ], - marketplaceLoadErrors: [], - featuredPluginIds: [], - }; - } - if (method === "plugin/read") { - return { - plugin: { - marketplaceName: "openai-curated", - marketplacePath: "/marketplaces/openai-curated", - summary: { - id: "google-calendar", - name: "google-calendar", - source: { type: "remote" }, - installed: true, - enabled: true, - installPolicy: "AVAILABLE", - authPolicy: "ON_USE", - availability: "AVAILABLE", - interface: null, - }, - description: null, - skills: [], - apps: [ - { - id: "google-calendar-app", - name: "Google Calendar", - description: null, - installUrl: null, - needsAuth: false, - }, - ], - mcpServers: ["google-calendar"], }, }; - } - return undefined; + }, + expectsAppList: false, + }, + { + name: "sends a thread/start app enable override when app/list cached the app as disabled", + cachedEnabled: false, + cacheKey: ({ appServer, agentDir }: GoogleCalendarCacheKeyInput) => + buildCodexPluginAppCacheKey({ + appServer, + agentDir, + runtimeIdentity: getMockRuntimeIdentity(), + }), + appList: () => { + throw new Error("app/list should use the cached inventory entry"); + }, + expectsAppList: false, + }, + { + name: "keys plugin app inventory by inherited API key fallback credentials", + cachedEnabled: true, + cacheKey: ({ appServer, agentDir }: GoogleCalendarCacheKeyInput) => + buildCodexPluginAppCacheKey({ + appServer, + agentDir, + envApiKeyFingerprint: resolveCodexAppServerFallbackApiKeyCacheKey({ + startOptions: appServer.start, + baseEnv: { CODEX_API_KEY: "old-codex-env-key" }, + }), + runtimeIdentity: getMockRuntimeIdentity(), + }), + appList: () => googleCalendarAppListResult(true), + configure: () => { + vi.stubEnv("CODEX_API_KEY", "new-codex-env-key"); + vi.stubEnv("OPENAI_API_KEY", ""); + }, + expectsAppList: true, + }, + ])("$name", async ({ cachedEnabled, cacheKey, appList, configure, expectsAppList }) => { + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); + const pluginConfig = GOOGLE_CALENDAR_PLUGIN_CONFIG; + const appServer = resolveCodexAppServerRuntimeOptions({ + pluginConfig: readCodexPluginConfig(pluginConfig), }); + await primeGoogleCalendarAppInventory(cacheKey({ appServer, agentDir }), cachedEnabled); + const { requests, waitForMethod, completeTurn } = createStartedThreadHarness( + createGoogleCalendarRequest(appList), + ); const params = createParams(sessionFile, workspaceDir); params.agentDir = agentDir; - + configure?.(params); const run = runCodexAppServerAttempt(params, { pluginConfig }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - - expect(requests.map((entry) => entry.method)).toContain("app/list"); + await completeStartedRun(run, waitForMethod, completeTurn); const threadStart = requests.find((entry) => entry.method === "thread/start"); const threadStartParams = threadStart?.params as | { config?: { apps?: Record } } | undefined; expect(threadStartParams?.config?.apps?.["google-calendar-app"]?.enabled).toBe(true); + if (expectsAppList) { + expect(requests.map((entry) => entry.method)).toContain("app/list"); + } else { + expect(requests.map((entry) => entry.method)).not.toContain("app/list"); + } }); it("times out app-server startup before thread setup can hang forever", async () => { setCodexAppServerClientFactoryForTest(() => new Promise(() => {})); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.timeoutMs = 1; - await expect(runCodexAppServerAttempt(params, { startupTimeoutFloorMs: 1 })).rejects.toThrow( "codex app-server startup timed out", ); expect(queueActiveRunMessageForTest("session-1", "after timeout")).toBe(false); }); - it("passes the selected auth profile into app-server startup", async () => { const seenAuthProfileIds: Array = []; const seenAgentDirs: Array = []; @@ -5173,13 +4615,9 @@ describe("runCodexAppServerAttempt", () => { seenAgentDirs.push(agentDir); }, }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.authProfileId = "openai:work"; params.agentDir = path.join(tempDir, "agent"); - const run = runCodexAppServerAttempt(params); await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai:work"]), { interval: 1, @@ -5190,7 +4628,6 @@ describe("runCodexAppServerAttempt", () => { }); await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - expect(seenAuthProfileIds).toEqual(["openai:work"]); expect(seenAgentDirs).toEqual([path.join(tempDir, "agent")]); expect(requests.map((entry) => entry.method)).toContain("turn/start"); @@ -5225,19 +4662,14 @@ describe("runCodexAppServerAttempt", () => { addRequestHandler: () => () => undefined, }) as never, ); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.timeoutMs = 1; params.config = { diagnostics: { enabled: true, otel: { enabled: true, traces: true } }, } as never; - try { await expect(runCodexAppServerAttempt(params)).rejects.toThrow("turn/start timed out"); await flushDiagnosticEvents(); - const errorEvent = diagnosticEvents.find((event) => event.type === "model.call.error") as | ({ failureKind?: string; errorCategory?: string } & DiagnosticEventPayload) | undefined; @@ -5248,7 +4680,6 @@ describe("runCodexAppServerAttempt", () => { stopDiagnostics(); } }); - it("does not install an active run handle when turn start resolves after abort", async () => { let resolveTurnStart: ((value: ReturnType) => void) | undefined; const request = vi.fn(async (method: string) => { @@ -5272,12 +4703,8 @@ describe("runCodexAppServerAttempt", () => { }) as never, ); const abortController = new AbortController(); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.abortSignal = abortController.signal; - const run = runCodexAppServerAttempt(params); await vi.waitFor( () => expect(request.mock.calls.map(([method]) => method)).toContain("turn/start"), @@ -5285,24 +4712,18 @@ describe("runCodexAppServerAttempt", () => { ); abortController.abort("test_abort"); resolveTurnStart?.(turnStartResult()); - await expect(run).rejects.toThrow("test_abort"); expect(queueActiveRunMessageForTest("session-1", "after abort")).toBe(false); }); it("keeps extended history enabled when resuming a bound Codex thread", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const { requests, waitForMethod, completeTurn } = createResumeHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), { pluginConfig: { appServer: { mode: "yolo" } }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); expectResumeRequest(requests, { threadId: "thread-existing", model: "gpt-5.4-codex", @@ -5314,36 +4735,12 @@ describe("runCodexAppServerAttempt", () => { const resumeRequestParams = resumeRequest?.params as Record | undefined; expect(resumeRequestParams?.developerInstructions).not.toContain(CODEX_GPT5_BEHAVIOR_CONTRACT); }); - it("starts a fresh Codex thread before resume when the native rollout reaches the fallback fuse", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); - await fs.writeFile( - path.join(path.dirname(sessionFile), "sessions.json"), - JSON.stringify({ - "agent:main:session-1": { - sessionFile, - totalTokens: 12_000, - }, - }), - ); - const rolloutDir = path.join(agentDir, "codex-home", "sessions"); - await fs.mkdir(rolloutDir, { recursive: true }); - await fs.writeFile( - path.join(rolloutDir, "rollout-thread-existing.jsonl"), - `${JSON.stringify({ - payload: { - type: "token_count", - info: { - total_token_usage: { - total_tokens: 300_000, - }, - }, - }, - })}\n`, - ); + await writeTokenPressureState(sessionFile, agentDir, { + total_token_usage: { total_tokens: 300_000 }, + }); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); params.agentDir = agentDir; @@ -5357,14 +4754,10 @@ describe("runCodexAppServerAttempt", () => { }, }, } as never; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { mode: "yolo" } }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); expect(requests.map((entry) => entry.method)).toContain("thread/start"); expect(requests.map((entry) => entry.method)).not.toContain("thread/resume"); const savedBinding = await readCodexAppServerBinding(sessionFile); @@ -5372,57 +4765,27 @@ describe("runCodexAppServerAttempt", () => { }); it("starts a fresh Codex thread before turn/start when the next prompt would exhaust native headroom", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); - await fs.writeFile( - path.join(path.dirname(sessionFile), "sessions.json"), - JSON.stringify({ - "agent:main:session-1": { - sessionFile, - totalTokens: 12_000, - }, - }), - ); - const rolloutDir = path.join(agentDir, "codex-home", "sessions"); - await fs.mkdir(rolloutDir, { recursive: true }); - await fs.writeFile( - path.join(rolloutDir, "rollout-thread-existing.jsonl"), - `${JSON.stringify({ - payload: { - type: "token_count", - info: { - last_token_usage: { - total_tokens: 220_000, - }, - model_context_window: 258_400, - }, - }, - })}\n`, - ); + await writeTokenPressureState(sessionFile, agentDir, { + last_token_usage: { total_tokens: 220_000 }, + model_context_window: 258_400, + }); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); params.agentDir = agentDir; params.prompt = "large prompt ".repeat(12_000); - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { mode: "yolo" } }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); expect(requests.map((entry) => entry.method)).toContain("thread/start"); expect(requests.map((entry) => entry.method)).not.toContain("thread/resume"); const savedBinding = await readCodexAppServerBinding(sessionFile); expect(savedBinding?.threadId).toBe("thread-1"); }); - it("preserves stale-binding continuity when token pressure forces a fresh Codex thread", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); const binding = await readCodexAppServerBinding(sessionFile); const bindingUpdatedAt = Date.parse(binding?.historyCoveredThrough ?? ""); @@ -5453,43 +4816,18 @@ describe("runCodexAppServerAttempt", () => { ), ); } - await fs.writeFile( - path.join(path.dirname(sessionFile), "sessions.json"), - JSON.stringify({ - "agent:main:session-1": { - sessionFile, - totalTokens: 12_000, - }, - }), - ); - const rolloutDir = path.join(agentDir, "codex-home", "sessions"); - await fs.mkdir(rolloutDir, { recursive: true }); - await fs.writeFile( - path.join(rolloutDir, "rollout-thread-existing.jsonl"), - `${JSON.stringify({ - payload: { - type: "token_count", - info: { - last_token_usage: { - total_tokens: 220_000, - }, - model_context_window: 258_400, - }, - }, - })}\n`, - ); + await writeTokenPressureState(sessionFile, agentDir, { + last_token_usage: { total_tokens: 220_000 }, + model_context_window: 258_400, + }); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); params.agentDir = agentDir; params.prompt = "large prompt ".repeat(12_000); - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { mode: "yolo" } }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); expect(requests.map((entry) => entry.method)).toContain("thread/start"); expect(requests.map((entry) => entry.method)).not.toContain("thread/resume"); const turnStart = requests.find((request) => request.method === "turn/start"); @@ -5504,37 +4842,14 @@ describe("runCodexAppServerAttempt", () => { }); it("preserves bound auth when rotating a fallback-fuse native rollout", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const agentDir = path.join(tempDir, "agent"); + const { sessionFile, workspaceDir, agentDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { authProfileId: "openai:work", dynamicToolsFingerprint: "[]", }); - await fs.writeFile( - path.join(path.dirname(sessionFile), "sessions.json"), - JSON.stringify({ - "agent:main:session-1": { - sessionFile, - totalTokens: 12_000, - }, - }), - ); - const rolloutDir = path.join(agentDir, "codex-home", "sessions"); - await fs.mkdir(rolloutDir, { recursive: true }); - await fs.writeFile( - path.join(rolloutDir, "rollout-thread-existing.jsonl"), - `${JSON.stringify({ - payload: { - type: "token_count", - info: { - total_token_usage: { - total_tokens: 300_000, - }, - }, - }, - })}\n`, - ); + await writeTokenPressureState(sessionFile, agentDir, { + total_token_usage: { total_tokens: 300_000 }, + }); const seenAuthProfileIds: Array = []; const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(undefined, { onStart: (authProfileId) => { @@ -5555,17 +4870,13 @@ describe("runCodexAppServerAttempt", () => { }, }, } as never; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { mode: "yolo" } }, }); await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai:work"]), { interval: 1, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); expect(requests.map((entry) => entry.method)).toContain("thread/start"); expect(requests.map((entry) => entry.method)).not.toContain("thread/resume"); expect(seenAuthProfileIds).toEqual(["openai:work"]); @@ -5573,53 +4884,8 @@ describe("runCodexAppServerAttempt", () => { expect(savedBinding?.authProfileId).toBe("openai:work"); expect(savedBinding?.threadId).toBe("thread-1"); }); - it("restarts the app-server once when a shared client closes during startup", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); - const requests: string[][] = []; - let starts = 0; - let notify: (notification: CodexServerNotification) => Promise = async () => undefined; - setCodexAppServerClientFactoryForTest(async () => { - const startIndex = starts++; - const methods: string[] = []; - requests.push(methods); - return { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - methods.push(method); - if (method === "thread/resume" && startIndex === 0) { - throw new Error("codex app-server client is closed"); - } - if (method === "thread/resume") { - return threadStartResult("thread-existing"); - } - if (method === "turn/start") { - return turnStartResult(); - } - return {}; - }), - addNotificationHandler: (handler: typeof notify) => { - notify = handler; - return () => undefined; - }, - addRequestHandler: () => () => undefined, - } as never; - }); - - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); - await vi.waitFor(() => expect(requests[1]).toContain("turn/start"), fastWait); - await notify({ - method: "turn/completed", - params: { - threadId: "thread-existing", - turnId: "turn-1", - turn: { id: "turn-1", status: "completed" }, - }, - }); - - const result = await run; + const { result, requests } = await runSharedClientRestartTest(1); expect(result.aborted).toBe(false); expect(requests).toEqual([ ["thread/resume"], @@ -5628,51 +4894,7 @@ describe("runCodexAppServerAttempt", () => { }); it("tolerates a second app-server close while retrying startup", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - await writeExistingBinding(sessionFile, workspaceDir, { dynamicToolsFingerprint: "[]" }); - const requests: string[][] = []; - let starts = 0; - let notify: (notification: CodexServerNotification) => Promise = async () => undefined; - setCodexAppServerClientFactoryForTest(async () => { - const startIndex = starts++; - const methods: string[] = []; - requests.push(methods); - return { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - methods.push(method); - if (method === "thread/resume" && startIndex < 2) { - throw new Error("codex app-server client is closed"); - } - if (method === "thread/resume") { - return threadStartResult("thread-existing"); - } - if (method === "turn/start") { - return turnStartResult(); - } - return {}; - }), - addNotificationHandler: (handler: typeof notify) => { - notify = handler; - return () => undefined; - }, - addRequestHandler: () => () => undefined, - } as never; - }); - - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir)); - await vi.waitFor(() => expect(requests[2]).toContain("turn/start"), fastWait); - await notify({ - method: "turn/completed", - params: { - threadId: "thread-existing", - turnId: "turn-1", - turn: { id: "turn-1", status: "completed" }, - }, - }); - - const result = await run; + const { result, requests } = await runSharedClientRestartTest(2); expect(result.aborted).toBe(false); expect(requests).toEqual([ ["thread/resume"], @@ -5680,142 +4902,60 @@ describe("runCodexAppServerAttempt", () => { ["thread/resume", "turn/start", "thread/unsubscribe"], ]); }); - it("does not retire the shared Codex client when a spawned helper run fails with a logical thread/start error", async () => { - const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent"); - clearSpy.mockClear(); - let failedClient: unknown; - setCodexAppServerClientFactoryForTest(async () => { - const c = { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - if (method === "thread/start") { - throw new CodexAppServerRpcError( - { message: "401 authentication_error: Invalid bearer token" }, - "thread/start", - ); - } - return {}; - }), - addNotificationHandler: vi.fn(() => () => undefined), - addRequestHandler: vi.fn(() => () => undefined), - }; - failedClient = c; - return c as never; + const { clearSpy, state } = installFailingThreadStartClient(() => { + throw new CodexAppServerRpcError( + { message: "401 authentication_error: Invalid bearer token" }, + "thread/start", + ); }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.spawnedBy = "agent:main:session-parent"; - await expect(runCodexAppServerAttempt(params)).rejects.toThrow("Invalid bearer token"); - const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient); + const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === state.failedClient); expect(calledWithFailedClient).toBe(false); clearSpy.mockRestore(); }); it("retires the shared Codex client when a spawned helper run times out during thread/start", async () => { - const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent"); - clearSpy.mockClear(); - let failedClient: unknown; - setCodexAppServerClientFactoryForTest(async () => { - const c = { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - if (method === "thread/start") { - return await new Promise(() => {}); - } - return {}; - }), - addNotificationHandler: vi.fn(() => () => undefined), - addRequestHandler: vi.fn(() => () => undefined), - }; - failedClient = c; - return c as never; - }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const { clearSpy, state } = installFailingThreadStartClient(() => new Promise(() => {})); + const params = createRunParams(); params.spawnedBy = "agent:main:session-parent"; params.timeoutMs = 1; - await expect(runCodexAppServerAttempt(params, { startupTimeoutFloorMs: 1 })).rejects.toThrow( "codex app-server startup timed out", ); - const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient); + const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === state.failedClient); expect(calledWithFailedClient).toBe(true); clearSpy.mockRestore(); }); - it("retires the shared Codex client when a spawned helper hits a thread/start write failure", async () => { - const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent"); - clearSpy.mockClear(); - let failedClient: unknown; - setCodexAppServerClientFactoryForTest(async () => { - const c = { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - if (method === "thread/start") { - throw Object.assign(new Error("write EPIPE"), { code: "EPIPE" }); - } - return {}; - }), - addNotificationHandler: vi.fn(() => () => undefined), - addRequestHandler: vi.fn(() => () => undefined), - }; - failedClient = c; - return c as never; + const { clearSpy, state } = installFailingThreadStartClient(() => { + throw Object.assign(new Error("write EPIPE"), { code: "EPIPE" }); }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); + const params = createRunParams(); params.spawnedBy = "agent:main:session-parent"; - await expect(runCodexAppServerAttempt(params)).rejects.toThrow("write EPIPE"); - const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient); + const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === state.failedClient); expect(calledWithFailedClient).toBe(true); clearSpy.mockRestore(); }); it("retires the shared Codex client when a top-level run fails with a logical thread/start error", async () => { - const clearSpy = vi.spyOn(sharedClientModule, "clearSharedCodexAppServerClientIfCurrent"); - clearSpy.mockClear(); - let failedClient: unknown; - setCodexAppServerClientFactoryForTest(async () => { - const c = { - ...mockClientRuntimeMethods(), - request: vi.fn(async (method: string) => { - if (method === "thread/start") { - throw new CodexAppServerRpcError( - { message: "401 authentication_error: Invalid bearer token" }, - "thread/start", - ); - } - return {}; - }), - addNotificationHandler: vi.fn(() => () => undefined), - addRequestHandler: vi.fn(() => () => undefined), - }; - failedClient = c; - return c as never; + const { clearSpy, state } = installFailingThreadStartClient(() => { + throw new CodexAppServerRpcError( + { message: "401 authentication_error: Invalid bearer token" }, + "thread/start", + ); }); - const params = createParams( - path.join(tempDir, "session.jsonl"), - path.join(tempDir, "workspace"), - ); - + const params = createRunParams(); await expect(runCodexAppServerAttempt(params)).rejects.toThrow("Invalid bearer token"); - const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === failedClient); + const calledWithFailedClient = clearSpy.mock.calls.some(([arg]) => arg === state.failedClient); expect(calledWithFailedClient).toBe(true); clearSpy.mockRestore(); }); - it("passes configured app-server policy, sandbox, service tier, and model on resume", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { model: "gpt-5.2" }); const { requests, waitForMethod, completeTurn } = createResumeHarness(); const params = createParams(sessionFile, workspaceDir); @@ -5834,7 +4974,6 @@ describe("runCodexAppServerAttempt", () => { }, }, }; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { @@ -5845,10 +4984,7 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); expectResumeRequest(requests, { threadId: "thread-existing", model: "gpt-5.4-codex", @@ -5875,11 +5011,9 @@ describe("runCodexAppServerAttempt", () => { }); it("passes current Codex service tier request values through app-server resume and turn requests", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { model: "gpt-5.2" }); const { requests, waitForMethod, completeTurn } = createResumeHarness(); - const run = runCodexAppServerAttempt(createParams(sessionFile, workspaceDir), { pluginConfig: { appServer: { @@ -5889,10 +5023,7 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); const resumeRequest = requests.find((request) => request.method === "thread/resume"); const resumeRequestParams = resumeRequest?.params as Record | undefined; expect(resumeRequestParams?.serviceTier).toBe("priority"); @@ -5900,10 +5031,8 @@ describe("runCodexAppServerAttempt", () => { const turnRequestParams = turnRequest?.params as Record | undefined; expect(turnRequestParams?.serviceTier).toBe("priority"); }); - it("uses human approval instead of Guardian for auto exec on custom model providers", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); const params = { ...createParams(sessionFile, workspaceDir), @@ -5918,19 +5047,14 @@ describe("runCodexAppServerAttempt", () => { }, }, } as EmbeddedRunAttemptParams; - const run = runCodexAppServerAttempt(params, { pluginConfig: {} }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); const startRequest = requests.find((request) => request.method === "thread/start"); const startRequestParams = startRequest?.params as Record | undefined; expect(startRequestParams?.modelProvider).toBe("lmstudio"); expect(startRequestParams?.approvalPolicy).toBe("on-request"); expect(startRequestParams?.approvalsReviewer).toBe("user"); expect(startRequestParams?.sandbox).toBe("workspace-write"); - const turnRequest = requests.find((request) => request.method === "turn/start"); const turnRequestParams = turnRequest?.params as Record | undefined; expect(turnRequestParams?.approvalPolicy).toBe("on-request"); @@ -5938,11 +5062,9 @@ describe("runCodexAppServerAttempt", () => { }); it("enables Guardian on the first turn after a fresh thread confirms the OpenAI provider", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); const params = createParams(sessionFile, workspaceDir); - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { @@ -5950,22 +5072,16 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); const startRequest = requests.find((request) => request.method === "thread/start"); const startRequestParams = startRequest?.params as Record | undefined; expect(startRequestParams?.approvalsReviewer).toBe("user"); - const turnRequest = requests.find((request) => request.method === "turn/start"); const turnRequestParams = turnRequest?.params as Record | undefined; expect(turnRequestParams?.approvalsReviewer).toBe("auto_review"); }); - it("uses human approval instead of Guardian for custom OpenAI-compatible endpoints", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(); const params = { ...createParams(sessionFile, workspaceDir), @@ -5987,26 +5103,20 @@ describe("runCodexAppServerAttempt", () => { }, }, } as EmbeddedRunAttemptParams; - const run = runCodexAppServerAttempt(params, { pluginConfig: {} }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); const startRequest = requests.find((request) => request.method === "thread/start"); const startRequestParams = startRequest?.params as Record | undefined; expect(startRequestParams?.modelProvider).toBe("openai"); expect(startRequestParams?.approvalPolicy).toBe("on-request"); expect(startRequestParams?.approvalsReviewer).toBe("user"); - const turnRequest = requests.find((request) => request.method === "turn/start"); const turnRequestParams = turnRequest?.params as Record | undefined; expect(turnRequestParams?.approvalsReviewer).toBe("user"); }); it("keeps Codex code-mode-only while disabling Guardian for provider-qualified local models", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const { requests, waitForMethod, completeTurn } = createStartedThreadHarness(async (method) => { if (method === "thread/start") { const response = threadStartResult(); @@ -6034,7 +5144,6 @@ describe("runCodexAppServerAttempt", () => { }, }, } as EmbeddedRunAttemptParams; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { @@ -6042,10 +5151,7 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-1", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn); const startRequest = requests.find((request) => request.method === "thread/start"); const startRequestParams = startRequest?.params as Record | undefined; const startConfig = startRequestParams?.config as Record | undefined; @@ -6055,7 +5161,6 @@ describe("runCodexAppServerAttempt", () => { expect(startRequestParams?.approvalsReviewer).toBe("user"); expect(startConfig?.["features.code_mode"]).toBe(true); expect(startConfig?.["features.code_mode_only"]).toBe(true); - const turnRequest = requests.find((request) => request.method === "turn/start"); const turnRequestParams = turnRequest?.params as Record | undefined; const collaborationMode = turnRequestParams?.collaborationMode as @@ -6065,10 +5170,8 @@ describe("runCodexAppServerAttempt", () => { expect(collaborationMode?.settings?.model).toBe("local-model"); expect(turnRequestParams?.approvalsReviewer).toBe("user"); }); - it("uses bound local model providers when disabling Guardian on resumed threads", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { authProfileId: "openai-profile", model: "local-model", @@ -6092,7 +5195,6 @@ describe("runCodexAppServerAttempt", () => { }, }, }; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { @@ -6101,10 +5203,7 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); const resumeRequest = requests.find((request) => request.method === "thread/resume"); const resumeRequestParams = resumeRequest?.params as Record | undefined; expect(resumeRequestParams?.modelProvider).toBe("lmstudio"); @@ -6115,8 +5214,7 @@ describe("runCodexAppServerAttempt", () => { }); it("uses a supervised native model for review policy despite an outer Anthropic default", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { connectionScope: "supervision", supervisionSourceThreadId: "thread-existing", @@ -6152,7 +5250,6 @@ describe("runCodexAppServerAttempt", () => { ...params.config, tools: { ...params.config?.tools, exec: { mode: "auto" } }, } as EmbeddedRunAttemptParams["config"]; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { mode: "guardian" }, @@ -6163,7 +5260,6 @@ describe("runCodexAppServerAttempt", () => { await harness.waitForMethod("turn/start"); await harness.completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - expect(clientFactory).toHaveBeenCalledWith( expect.objectContaining({ authProfileId: null, @@ -6181,10 +5277,8 @@ describe("runCodexAppServerAttempt", () => { expect(turnParams).not.toHaveProperty("modelProvider"); expect(turnParams?.approvalsReviewer).toBe("auto_review"); }); - it("fails before client startup when a successor generation hides a private supervision binding", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); const sessionKey = "agent:main:supervised-stale-generation"; registerCodexTestSessionIdentity(sessionFile, "session-previous", sessionKey); await writeExistingBinding(sessionFile, workspaceDir, { @@ -6211,7 +5305,6 @@ describe("runCodexAppServerAttempt", () => { const clientFactory = vi.fn(async () => { throw new Error("client must not start"); }); - await expect( runCodexAppServerAttempt(params, { pluginConfig: { supervision: { enabled: true } }, @@ -6222,7 +5315,6 @@ describe("runCodexAppServerAttempt", () => { message: "Codex session generation is no longer current: session-current", }); expect(clientFactory).not.toHaveBeenCalled(); - registerCodexTestSessionIdentity(sessionFile, "session-previous", sessionKey); await expect(readCodexAppServerBinding(sessionFile)).resolves.toMatchObject({ threadId: "thread-existing", @@ -6231,8 +5323,7 @@ describe("runCodexAppServerAttempt", () => { }); it("does not inherit a bound local provider for explicit native OpenAI resumed runs", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { authProfileId: "openai-profile", model: "local-model", @@ -6257,7 +5348,6 @@ describe("runCodexAppServerAttempt", () => { }, }, }; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { @@ -6265,20 +5355,15 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); const resumeRequest = requests.find((request) => request.method === "thread/resume"); const resumeRequestParams = resumeRequest?.params as Record | undefined; expect(resumeRequestParams?.model).toBe("gpt-5.5"); expect(resumeRequestParams).not.toHaveProperty("modelProvider"); expect(resumeRequestParams?.approvalsReviewer).toBe("auto_review"); }); - it("does not apply bound local model providers to provider-qualified resumed models", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { model: "local-model", modelProvider: "lmstudio", @@ -6287,7 +5372,6 @@ describe("runCodexAppServerAttempt", () => { const params = createParams(sessionFile, workspaceDir); params.provider = "codex"; params.modelId = "openai/gpt-5.5"; - const run = runCodexAppServerAttempt(params, { pluginConfig: { appServer: { @@ -6296,10 +5380,7 @@ describe("runCodexAppServerAttempt", () => { }, }, }); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); const resumeRequest = requests.find((request) => request.method === "thread/resume"); const resumeRequestParams = resumeRequest?.params as Record | undefined; expect(resumeRequestParams?.model).toBe("gpt-5.5"); @@ -6334,15 +5415,11 @@ describe("runCodexAppServerAttempt", () => { const { requests, waitForMethod, completeTurn } = createResumeHarness(); const params = createParams(sessionFile, workspaceDir); params.fastMode = fastMode; - const options = configuredServiceTier ? { pluginConfig: { appServer: { serviceTier: configuredServiceTier } } } : {}; const run = runCodexAppServerAttempt(params, options); - await waitForMethod("turn/start"); - await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); - await run; - + await completeStartedRun(run, waitForMethod, completeTurn, "thread-existing"); for (const method of ["thread/resume", "turn/start"]) { const request = requests.find((entry) => entry.method === method); const requestParams = request?.params as Record | undefined; @@ -6350,10 +5427,8 @@ describe("runCodexAppServerAttempt", () => { } }, ); - it("reuses the bound auth profile for app-server startup when params omit it", async () => { - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); + const { sessionFile, workspaceDir } = createRunPaths(); await writeExistingBinding(sessionFile, workspaceDir, { authProfileId: "openai:bound", dynamicToolsFingerprint: "[]", @@ -6380,7 +5455,6 @@ describe("runCodexAppServerAttempt", () => { const params = createParams(sessionFile, workspaceDir); delete params.authProfileId; params.agentDir = path.join(tempDir, "agent"); - const run = runCodexAppServerAttempt(params); await vi.waitFor(() => expect(seenAuthProfileIds).toEqual(["openai:bound"]), { interval: 1, @@ -6391,30 +5465,14 @@ describe("runCodexAppServerAttempt", () => { }); await completeTurn({ threadId: "thread-existing", turnId: "turn-1" }); await run; - expect(seenAuthProfileIds).toEqual(["openai:bound"]); expect(seenAgentDirs).toEqual([path.join(tempDir, "agent")]); expect(requests.map((entry) => entry.method)).toContain("turn/start"); }); it("announces Codex app-server fast auto progress after the crossing tool result", async () => { - const now = vi.spyOn(Date, "now").mockReturnValue(1_000); - const onToolResult = vi.fn(); - const onAgentEvent = vi.fn(); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const harness = createStartedThreadHarness(); - const params = createParams(sessionFile, workspaceDir); - params.verboseLevel = "full"; - params.fastModeAuto = true; - params.fastModeStartedAtMs = 1_000; - params.fastModeAutoOnSeconds = 30; - params.onToolResult = onToolResult; - params.onAgentEvent = onAgentEvent; - - const run = runCodexAppServerAttempt(params); - await harness.waitForMethod("turn/start"); - + const { harness, now, onAgentEvent, onToolResult, run, workspaceDir } = + await startFastAutoProgressTest(); const notifyCommand = async (id: string, output: string, nowMs: number) => { await harness.notify({ method: "item/started", @@ -6449,13 +5507,11 @@ describe("runCodexAppServerAttempt", () => { }, }); }; - await notifyCommand("tool-before", "before", 20_000); await notifyCommand("tool-crossing", "crossing", 35_500); await notifyCommand("tool-after", "after", 42_000); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const payloads = onToolResult.mock.calls.map(([payload]) => payload) as Array<{ channelData?: Record; text?: string; @@ -6478,29 +5534,17 @@ describe("runCodexAppServerAttempt", () => { expect(payloads[onIndex]?.channelData).toEqual({ openclawProgressKind: "fast-mode-auto", }); - const fastEvents = onAgentEvent.mock.calls - .map(([event]) => event) - .filter((event) => event.stream === "item" && event.data?.title === "Fast"); - expect(fastEvents.map((event) => event.data?.summary)).toEqual([ + expect(fastProgressEventSummaries(onAgentEvent)).toEqual([ "💨Fast: auto-off(34s>=30s)", "💨Fast: auto-on", ]); }); - it("does not announce Codex fast auto progress for explicit fast mode", async () => { - const now = vi.spyOn(Date, "now").mockReturnValue(1_000); - const onToolResult = vi.fn(); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const harness = createStartedThreadHarness(); - const params = createParams(sessionFile, workspaceDir); - params.fastModeAuto = false; - params.fastModeStartedAtMs = 1_000; - params.fastModeAutoOnSeconds = 30; - params.onToolResult = onToolResult; - - const run = runCodexAppServerAttempt(params); - await harness.waitForMethod("turn/start"); + const { harness, now, onToolResult, run } = await startFastAutoProgressTest({ + fastModeAuto: false, + reportAgentEvents: false, + verbose: false, + }); now.mockReturnValue(35_500); await harness.notify({ method: "rawResponseItem/completed", @@ -6517,32 +5561,16 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const texts = onToolResult.mock.calls.map(([payload]) => payload.text ?? ""); expect(texts.filter((text) => text.startsWith("💨Fast:"))).toEqual([]); }); it("announces Codex app-server fast auto progress for snapshot-only tool results", async () => { - const now = vi.spyOn(Date, "now").mockReturnValue(1_000); - const onToolResult = vi.fn(); - const onAgentEvent = vi.fn(); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const harness = createStartedThreadHarness(); - const params = createParams(sessionFile, workspaceDir); - params.verboseLevel = "full"; - params.fastModeAuto = true; - params.fastModeStartedAtMs = 1_000; - params.fastModeAutoOnSeconds = 30; - params.onToolResult = onToolResult; - params.onAgentEvent = onAgentEvent; - - const run = runCodexAppServerAttempt(params); - await harness.waitForMethod("turn/start"); + const { harness, now, onAgentEvent, onToolResult, run, workspaceDir } = + await startFastAutoProgressTest(); await new Promise((resolve) => { setImmediate(resolve); }); - now.mockReturnValue(35_500); await harness.notify({ method: "turn/completed", @@ -6571,42 +5599,21 @@ describe("runCodexAppServerAttempt", () => { }, }); await run; - const texts = onToolResult.mock.calls.map(([payload]) => payload.text ?? ""); expect(texts.filter((text) => text.startsWith("💨Fast: auto-off"))).toEqual([ "💨Fast: auto-off(34s>=30s)", ]); expect(texts.filter((text) => text === "💨Fast: auto-on")).toHaveLength(1); - const fastEvents = onAgentEvent.mock.calls - .map(([event]) => event) - .filter((event) => event.stream === "item" && event.data?.title === "Fast"); - expect(fastEvents.map((event) => event.data?.summary)).toEqual([ + expect(fastProgressEventSummaries(onAgentEvent)).toEqual([ "💨Fast: auto-off(34s>=30s)", "💨Fast: auto-on", ]); }); - it("announces Codex app-server fast auto progress for raw function call outputs", async () => { - const now = vi.spyOn(Date, "now").mockReturnValue(1_000); - const onToolResult = vi.fn(); - const onAgentEvent = vi.fn(); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const harness = createStartedThreadHarness(); - const params = createParams(sessionFile, workspaceDir); - params.verboseLevel = "full"; - params.fastModeAuto = true; - params.fastModeStartedAtMs = 1_000; - params.fastModeAutoOnSeconds = 30; - params.onToolResult = onToolResult; - params.onAgentEvent = onAgentEvent; - - const run = runCodexAppServerAttempt(params); - await harness.waitForMethod("turn/start"); + const { harness, now, onAgentEvent, onToolResult, run } = await startFastAutoProgressTest(); await new Promise((resolve) => { setImmediate(resolve); }); - now.mockReturnValue(35_500); await harness.notify({ method: "rawResponseItem/completed", @@ -6623,42 +5630,25 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const texts = onToolResult.mock.calls.map(([payload]) => payload.text ?? ""); expect(texts.filter((text) => text.startsWith("💨Fast: auto-off"))).toEqual([ "💨Fast: auto-off(34s>=30s)", ]); expect(texts.filter((text) => text === "💨Fast: auto-on")).toHaveLength(1); - const fastEvents = onAgentEvent.mock.calls - .map(([event]) => event) - .filter((event) => event.stream === "item" && event.data?.title === "Fast"); - expect(fastEvents.map((event) => event.data?.summary)).toEqual([ + expect(fastProgressEventSummaries(onAgentEvent)).toEqual([ "💨Fast: auto-off(34s>=30s)", "💨Fast: auto-on", ]); }); it("does not duplicate Codex app-server fast auto progress already announced by the outer runner", async () => { - const now = vi.spyOn(Date, "now").mockReturnValue(1_000); - const onToolResult = vi.fn(); - const onAgentEvent = vi.fn(); - const sessionFile = path.join(tempDir, "session.jsonl"); - const workspaceDir = path.join(tempDir, "workspace"); - const harness = createStartedThreadHarness(); - const params = createParams(sessionFile, workspaceDir); - params.verboseLevel = "full"; - params.fastModeAuto = true; - params.fastModeStartedAtMs = 1_000; - params.fastModeAutoOnSeconds = 30; - params.fastModeAutoProgressState = { - offAnnounced: true, - resetAnnounced: false, - }; - params.onToolResult = onToolResult; - params.onAgentEvent = onAgentEvent; - - const run = runCodexAppServerAttempt(params); - await harness.waitForMethod("turn/start"); + const { harness, now, onAgentEvent, onToolResult, params, run, workspaceDir } = + await startFastAutoProgressTest({ + fastModeAutoProgressState: { + offAnnounced: true, + resetAnnounced: false, + }, + }); await harness.notify({ method: "item/started", params: { @@ -6693,7 +5683,6 @@ describe("runCodexAppServerAttempt", () => { }); await harness.completeTurn({ threadId: "thread-1", turnId: "turn-1" }); await run; - const texts = onToolResult.mock.calls.map(([payload]) => payload.text ?? ""); expect(texts.filter((text) => text.startsWith("💨Fast: auto-off"))).toEqual([]); expect(texts.filter((text) => text === "💨Fast: auto-on")).toHaveLength(1); @@ -6701,10 +5690,7 @@ describe("runCodexAppServerAttempt", () => { offAnnounced: true, resetAnnounced: true, }); - const fastEvents = onAgentEvent.mock.calls - .map(([event]) => event) - .filter((event) => event.stream === "item" && event.data?.title === "Fast"); - expect(fastEvents.map((event) => event.data?.summary)).toEqual(["💨Fast: auto-on"]); + expect(fastProgressEventSummaries(onAgentEvent)).toEqual(["💨Fast: auto-on"]); }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */