// Codex tests cover auth profile runtime contract plugin behavior. import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { abortAndDrainAgentHarnessRun, type EmbeddedRunAttemptParams, } from "openclaw/plugin-sdk/agent-harness"; import { AUTH_PROFILE_RUNTIME_CONTRACT } from "openclaw/plugin-sdk/agent-runtime-test-contracts"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { CodexAppServerClientFactory } from "./client-factory.js"; import { runCodexAppServerAttempt as runCodexAppServerAttemptImpl } from "./run-attempt.js"; import { readCodexAppServerBinding, writeCodexAppServerBinding as writeRawCodexAppServerBinding, } from "./session-binding.js"; import { createCodexTestModel } from "./test-support.js"; let codexAppServerClientFactoryForTest: CodexAppServerClientFactory | undefined; type RunCodexAppServerAttemptOptions = NonNullable< Parameters[1] >; function setCodexAppServerClientFactoryForTest(factory: CodexAppServerClientFactory): void { codexAppServerClientFactoryForTest = factory; } function resetCodexAppServerClientFactoryForTest(): void { codexAppServerClientFactoryForTest = undefined; } function runCodexAppServerAttempt( params: EmbeddedRunAttemptParams, options: RunCodexAppServerAttemptOptions = {}, ) { const clientFactory = options.clientFactory ?? codexAppServerClientFactoryForTest; return runCodexAppServerAttemptImpl( params, clientFactory ? { ...options, clientFactory } : options, ); } function createParams(sessionFile: string, workspaceDir: string): EmbeddedRunAttemptParams { return { prompt: AUTH_PROFILE_RUNTIME_CONTRACT.workspacePrompt, sessionId: AUTH_PROFILE_RUNTIME_CONTRACT.sessionId, sessionKey: AUTH_PROFILE_RUNTIME_CONTRACT.sessionKey, sessionFile, workspaceDir, runId: AUTH_PROFILE_RUNTIME_CONTRACT.runId, provider: AUTH_PROFILE_RUNTIME_CONTRACT.codexHarnessProvider, modelId: "gpt-5.4-codex", model: createCodexTestModel(AUTH_PROFILE_RUNTIME_CONTRACT.codexHarnessProvider), thinkLevel: "medium", disableTools: true, timeoutMs: 5_000, authStorage: {} as never, authProfileStore: { version: 1, profiles: {} }, modelRegistry: {} as never, } as EmbeddedRunAttemptParams; } const DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT = JSON.stringify({ "features.standalone_web_search": false, web_search: "disabled", }); const APP_SERVER_START_WAIT = { interval: 1, timeout: 5_000 } as const; function writeCodexAppServerBinding(...args: Parameters) { const [sessionFile, binding, lookup] = args; return writeRawCodexAppServerBinding( sessionFile, { webSearchThreadConfigFingerprint: DISABLED_CODEX_WEB_SEARCH_THREAD_CONFIG_FINGERPRINT, ...binding, }, lookup, ); } function threadStartResult(threadId = "thread-auth-contract") { return { thread: { id: threadId, sessionId: "session-1", forkedFromId: null, preview: "", ephemeral: false, modelProvider: "openai", createdAt: 1, updatedAt: 1, status: { type: "idle" }, path: null, cwd: "", cliVersion: "0.125.0", source: "unknown", agentNickname: null, agentRole: null, gitInfo: null, name: null, turns: [], }, model: "gpt-5.4-codex", modelProvider: "openai", serviceTier: null, cwd: "", instructionSources: [], approvalPolicy: "never", approvalsReviewer: "user", sandbox: { type: "dangerFullAccess" }, permissionProfile: null, reasoningEffort: null, }; } function turnStartResult(turnId = "turn-auth-contract") { return { turn: { id: turnId, status: "inProgress", items: [], error: null, startedAt: null, completedAt: null, durationMs: null, }, }; } function getMockServerVersion() { return "0.132.0"; } function getMockRuntimeIdentity() { return { serverVersion: getMockServerVersion() }; } function mockClientRuntimeMethods() { return { getRuntimeIdentity: getMockRuntimeIdentity, getServerVersion: getMockServerVersion, }; } function createCodexAuthProfileHarness(params: { startMethod: "thread/start" | "thread/resume" }) { const seenAuthProfileIds: Array = []; const seenAgentDirs: Array = []; const requests: Array<{ method: string; params: unknown }> = []; let notify: (notification: unknown) => Promise = async () => undefined; setCodexAppServerClientFactoryForTest(async (_startOptions, authProfileId, agentDir) => { seenAuthProfileIds.push(authProfileId); seenAgentDirs.push(agentDir); return { ...mockClientRuntimeMethods(), request: vi.fn(async (method: string, requestParams?: unknown) => { requests.push({ method, params: requestParams }); if (method === params.startMethod) { return threadStartResult(); } if (method === "turn/start") { return turnStartResult(); } throw new Error(`unexpected method: ${method}`); }), addNotificationHandler: (handler: (notification: unknown) => Promise) => { notify = handler; return () => undefined; }, addRequestHandler: () => () => undefined, } as never; }); return { seenAuthProfileIds, seenAgentDirs, async waitForMethod(method: string) { await vi.waitFor(() => expect(requests.map((entry) => entry.method)).toContain(method), { ...APP_SERVER_START_WAIT, }); }, async completeTurn() { await notify({ method: "turn/completed", params: { threadId: "thread-auth-contract", turnId: "turn-auth-contract", turn: { id: "turn-auth-contract", status: "completed" }, }, }); }, }; } describe("Auth profile runtime contract - Codex app-server adapter", () => { let tmpDir: string; beforeEach(async () => { vi.useRealTimers(); tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-auth-contract-")); }); afterEach(async () => { vi.useRealTimers(); await abortAndDrainAgentHarnessRun({ sessionId: AUTH_PROFILE_RUNTIME_CONTRACT.sessionId, sessionKey: AUTH_PROFILE_RUNTIME_CONTRACT.sessionKey, settleMs: 1_000, forceClear: true, reason: "test_cleanup", }); resetCodexAppServerClientFactoryForTest(); await fs.rm(tmpDir, { recursive: true, force: true }); }); it("passes the exact OpenAI Codex auth profile into app-server startup", async () => { const harness = createCodexAuthProfileHarness({ startMethod: "thread/start" }); const sessionFile = path.join(tmpDir, "session.jsonl"); const params = createParams(sessionFile, tmpDir); params.authProfileId = AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId; params.agentDir = tmpDir; const run = runCodexAppServerAttempt(params); await vi.waitFor( () => expect(harness.seenAuthProfileIds).toEqual([ AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId, ]), APP_SERVER_START_WAIT, ); expect(harness.seenAgentDirs).toEqual([tmpDir]); await harness.waitForMethod("turn/start"); await harness.completeTurn(); await run; }); it("reuses a bound OpenAI Codex auth profile when resume params omit authProfileId", async () => { const harness = createCodexAuthProfileHarness({ startMethod: "thread/resume" }); const sessionFile = path.join(tmpDir, "session.jsonl"); await writeCodexAppServerBinding(sessionFile, { threadId: "thread-auth-contract", cwd: tmpDir, authProfileId: AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId, dynamicToolsFingerprint: "[]", }); // authProfileId is intentionally omitted to exercise the resume-bound profile path. const params = createParams(sessionFile, tmpDir); const run = runCodexAppServerAttempt(params); await vi.waitFor( () => expect(harness.seenAuthProfileIds).toEqual([ AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId, ]), APP_SERVER_START_WAIT, ); await harness.waitForMethod("turn/start"); await harness.completeTurn(); await run; }); it("prefers an explicit runtime auth profile over a stale persisted binding", async () => { const harness = createCodexAuthProfileHarness({ startMethod: "thread/resume" }); const sessionFile = path.join(tmpDir, "session.jsonl"); await writeCodexAppServerBinding(sessionFile, { threadId: "thread-auth-contract", cwd: tmpDir, authProfileId: "openai:stale", dynamicToolsFingerprint: "[]", }); const params = createParams(sessionFile, tmpDir); params.authProfileId = AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId; const run = runCodexAppServerAttempt(params); await vi.waitFor( () => expect(harness.seenAuthProfileIds).toEqual([ AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId, ]), APP_SERVER_START_WAIT, ); await harness.waitForMethod("turn/start"); await harness.completeTurn(); await run; const binding = await readCodexAppServerBinding(sessionFile); expect(binding?.authProfileId).toBe(AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId); }); });