diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 0e5304a64..73af9f431 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -1194,7 +1194,10 @@ export async function restoreNativeCodexAsync(): Promise<{ success: boolean; message: string; }> { - const inline = restoreNativeCodex({ skipHistory: true }); + const inline = restoreNativeCodexCore({ skipHistory: true }); + if (!inline.restoreHistory) { + return { success: inline.success, message: inline.message }; + } const outcome = await runCodexHistoryJob({ ...resolveCodexHistoryJobTarget(), operation: deriveCodexHistoryOperation({ @@ -1217,9 +1220,10 @@ export async function restoreNativeCodexAsync(): Promise<{ return { success: inline.success, message: `${inline.message}${historyMsg}` }; } -export function restoreNativeCodex(options: { skipHistory?: boolean } = {}): { +function restoreNativeCodexCore(options: { skipHistory?: boolean } = {}): { success: boolean; message: string; + restoreHistory: boolean; } { const activeProvider = currentExternalCodexModelProvider(); if (activeProvider) { @@ -1227,6 +1231,7 @@ export function restoreNativeCodex(options: { skipHistory?: boolean } = {}): { return { success: true, message: `External Codex provider ${tomlString(activeProvider)} preserved; no native restore was needed.`, + restoreHistory: false, }; } const journal = restoreJournalState(); @@ -1272,7 +1277,15 @@ export function restoreNativeCodex(options: { skipHistory?: boolean } = {}): { : history.ejectedRows ? ` ${history.ejectedRows} opencodex history thread(s) were ejected to openai so native Codex can resume them.` : ""; - return { success: cfg.success, message: `${msg}${historyMsg}` }; + return { success: cfg.success, message: `${msg}${historyMsg}`, restoreHistory: true }; +} + +export function restoreNativeCodex(options: { skipHistory?: boolean } = {}): { + success: boolean; + message: string; +} { + const result = restoreNativeCodexCore(options); + return { success: result.success, message: result.message }; } export function getCodexConfigPath(): string { diff --git a/tests/codex-history-job.test.ts b/tests/codex-history-job.test.ts index 0c9846198..8fb3a3f24 100644 --- a/tests/codex-history-job.test.ts +++ b/tests/codex-history-job.test.ts @@ -147,7 +147,7 @@ test("an overrun Worker returns a typed timeout rather than hanging", async () = */ test("the synchronous restore body is gated on skipHistory", () => { const source = readFileSync(join(import.meta.dir, "..", "src", "codex", "inject.ts"), "utf8"); - const body = source.slice(source.indexOf("export function restoreNativeCodex(")); + const body = source.slice(source.indexOf("function restoreNativeCodexCore(")); const historyCall = body.indexOf("syncCodexHistoryProvider(\"openai\""); expect(historyCall).toBeGreaterThan(-1); @@ -157,5 +157,5 @@ test("the synchronous restore body is gated on skipHistory", () => { expect(gate).toBeLessThan(historyCall); // And the async wrapper is the thing that sets it. - expect(source).toContain("restoreNativeCodex({ skipHistory: true })"); + expect(source).toContain("restoreNativeCodexCore({ skipHistory: true })"); }); diff --git a/tests/codex-inject-integration.test.ts b/tests/codex-inject-integration.test.ts index 0b73f1e26..7e45e080c 100644 --- a/tests/codex-inject-integration.test.ts +++ b/tests/codex-inject-integration.test.ts @@ -42,6 +42,19 @@ function runRestore(codexHome: string, ocxHome: string): { stdout: string; statu return { stdout: result.stdout?.trim() ?? "", status: result.status ?? 1 }; } +function runRestoreAsync(codexHome: string, ocxHome: string): { stdout: string; status: number } { + const script = ` + const { restoreNativeCodexAsync } = require("./src/codex/inject"); + restoreNativeCodexAsync().then(result => console.log(JSON.stringify(result))); + `; + const result = spawnSync(process.execPath, ["--eval", script], { + cwd: repoRoot, + env: { ...process.env, CODEX_HOME: codexHome, OPENCODEX_HOME: ocxHome }, + encoding: "utf8", + }); + return { stdout: result.stdout?.trim() ?? "", status: result.status ?? 1 }; +} + describe("injectCodexConfig integration (Design B)", () => { let codexHome: string; let ocxHome: string; @@ -505,6 +518,35 @@ describe("injectCodexConfig integration (Design B)", () => { expect(existsSync(journalPath)).toBe(false); }); + test("async restore preserves external provider history", () => { + const configPath = join(codexHome, "config.toml"); + writeFileSync(configPath, 'model_provider = "custom"\n', "utf8"); + const rolloutPath = join(codexHome, "rollout-custom.jsonl"); + const rollout = `${JSON.stringify({ + type: "session_meta", + payload: { id: "thread-custom", model_provider: "opencodex", source: "cli" }, + })}\n`; + writeFileSync(rolloutPath, rollout, "utf8"); + const dbPath = join(codexHome, "state_5.sqlite"); + const db = new Database(dbPath); + db.run(`CREATE TABLE threads ( + id TEXT PRIMARY KEY, rollout_path TEXT NOT NULL, model_provider TEXT NOT NULL, + source TEXT NOT NULL, first_user_message TEXT NOT NULL, has_user_event INTEGER NOT NULL + )`); + db.run(`INSERT INTO threads VALUES ('thread-custom', ?, 'opencodex', 'cli', 'hello', 1)`, rolloutPath); + db.close(); + + const result = runRestoreAsync(codexHome, ocxHome); + + expect(result.status).toBe(0); + expect(JSON.parse(result.stdout).message).toContain('External Codex provider "custom" preserved'); + const restored = new Database(dbPath, { readonly: true }); + expect(restored.query("SELECT model_provider FROM threads WHERE id = 'thread-custom'").get()) + .toEqual({ model_provider: "opencodex" }); + restored.close(); + expect(readFileSync(rolloutPath, "utf8")).toBe(rollout); + }); + test("provider selected through a legacy root profile is also preserved", () => { const original = [ 'profile = "work"',