From 11f67d8a9d97db6aaa2140fe90b5d2f93a11491f Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Mon, 7 Sep 2026 22:21:10 +0900 Subject: [PATCH 1/2] fix(codex): restore Windows shim caller token environment (cherry picked from commit 06829fa566c5e9d688a934fd31d481227450828b) --- .../docs/ja/reference/cli/lifecycle.md | 6 ++ .../docs/ko/reference/cli/lifecycle.md | 6 ++ .../content/docs/reference/cli/lifecycle.md | 6 ++ .../docs/ru/reference/cli/lifecycle.md | 6 ++ .../docs/zh-cn/reference/cli/lifecycle.md | 6 ++ src/codex/shim.ts | 11 ++- tests/codex-integration/codex-shim.test.ts | 84 ++++++++++++++++++- 7 files changed, 123 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md index 5afed8454e..b7952b5c28 100644 --- a/docs-site/src/content/docs/ja/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ja/reference/cli/lifecycle.md @@ -212,6 +212,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows のトークン環境] +新しく生成される Windows CMD と PowerShell のシムは、実行後に呼び出し元の `OPENCODEX_API_AUTH_TOKEN` を元の状態に戻します。Codex とその子プロセスには、引き続きトークンが継承される可能性があります。 + +OpenCodex の更新後、既存の Windows シムにこの動作を適用するには、`ocx codex-shim uninstall`、続いて `ocx codex-shim install` を実行して再作成してください。通常の更新では、正常な Windows シムは書き換えられません。 +::: + :::tip[サービス vs シム] 常時オンのバックグラウンド プロキシには `ocx service` を使用します (推奨)。デーモンを使用しない軽量のオンデマンド起動には、`ocx codex-shim` を使用します。プロキシは、`codex` が起動された場合にのみ起動します。 ::: diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index 74bff28244..d4c19ebd46 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -293,6 +293,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows 토큰 환경] +새로 생성된 Windows CMD 및 PowerShell shim은 실행 후 호출자의 `OPENCODEX_API_AUTH_TOKEN`을 원래 상태로 복원합니다. Codex와 자식 프로세스는 여전히 토큰을 상속할 수 있습니다. + +OpenCodex를 업데이트한 뒤 기존 Windows shim에 이 동작을 적용하려면 `ocx codex-shim uninstall`을 실행한 다음 `ocx codex-shim install`로 다시 설치하세요. 일반 업데이트는 정상인 Windows shim을 다시 작성하지 않습니다. +::: + :::tip[서비스와 shim] 항상 켜져 있는 백그라운드 프록시에는 `ocx service`를 사용합니다(권장). 데몬 없이 가볍게 필요할 때만 시작하려면 `ocx codex-shim`을 사용합니다. 이 경우 프록시는 `codex`를 실행할 때만 시작됩니다. diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index a0cb47ac92..a7d61a0bee 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -440,6 +440,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows token environment] +Newly generated Windows CMD and PowerShell shims restore the caller's `OPENCODEX_API_AUTH_TOKEN` after execution. Codex and its child processes can still inherit the token. + +After updating OpenCodex, recreate an existing Windows shim with `ocx codex-shim uninstall` followed by `ocx codex-shim install` to obtain this behavior. An ordinary update does not rewrite a healthy Windows shim. +::: + :::tip[Service vs Shim] Use `ocx service` for an always-on background proxy (recommended). Use `ocx codex-shim` for lightweight, on-demand startup without a daemon — the proxy starts only when `codex` is launched. diff --git a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md index fa3f74f4c7..f743f5f468 100644 --- a/docs-site/src/content/docs/ru/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ru/reference/cli/lifecycle.md @@ -313,6 +313,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Окружение токена в Windows] +Новые обёртки для Windows CMD и PowerShell восстанавливают исходное состояние `OPENCODEX_API_AUTH_TOKEN` в вызывающей оболочке после выполнения. Codex и его дочерние процессы по-прежнему могут унаследовать токен. + +После обновления OpenCodex пересоздайте существующую обёртку Windows командами `ocx codex-shim uninstall`, затем `ocx codex-shim install`, чтобы применить это поведение. Обычное обновление не перезаписывает исправную обёртку Windows. +::: + :::tip[Service vs Shim] Используйте `ocx service` для всегда работающего фонового прокси (рекомендуется). Используйте `ocx codex-shim` для лёгкого on-demand запуска без демона — в этом случае прокси стартует только diff --git a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md index d822d21666..5977c734de 100644 --- a/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/zh-cn/reference/cli/lifecycle.md @@ -209,6 +209,12 @@ ocx codex-shim status ocx codex-shim uninstall ``` +:::note[Windows 令牌环境] +新生成的 Windows CMD 和 PowerShell shim 会在执行后恢复调用方原有的 `OPENCODEX_API_AUTH_TOKEN` 状态。Codex 及其子进程仍可能继承令牌。 + +更新 OpenCodex 后,如需让现有 Windows shim 采用此行为,请先运行 `ocx codex-shim uninstall`,再运行 `ocx codex-shim install` 重新安装。常规更新不会重写正常的 Windows shim。 +::: + :::tip[Service vs Shim] 将 `ocx service` 用于始终在线的后台代理(推荐)。将 `ocx codex-shim` 用于无需守护进程的轻量按需启动——代理只会在启动 `codex` 时运行。 ::: diff --git a/src/codex/shim.ts b/src/codex/shim.ts index a63c48fb7a..57f46cfb0d 100644 --- a/src/codex/shim.ts +++ b/src/codex/shim.ts @@ -1069,6 +1069,7 @@ export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cl const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n"); return `@echo off\r rem ${SHIM_MARKER}\r +setlocal\r ${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r ${windowsBatchSet("OCX_BUN", bunPath)}\r ${windowsBatchSet("OCX_CLI", cliPath)}\r @@ -1115,6 +1116,9 @@ export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: const tokenFile = serviceApiTokenFilePath(); return `#!/usr/bin/env pwsh # ${SHIM_MARKER} +$hadApiAuthToken = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN +$priorApiAuthToken = $env:OPENCODEX_API_AUTH_TOKEN +try { if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) { $env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim() } @@ -1147,7 +1151,12 @@ if (-not $skipEnsure) { } } & ${psString(realCodexPath)} @args -exit $LASTEXITCODE +$codexExitCode = $LASTEXITCODE +} finally { + if ($hadApiAuthToken) { $env:OPENCODEX_API_AUTH_TOKEN = $priorApiAuthToken } + else { Remove-Item Env:\\OPENCODEX_API_AUTH_TOKEN -ErrorAction SilentlyContinue } +} +exit $codexExitCode `; } diff --git a/tests/codex-integration/codex-shim.test.ts b/tests/codex-integration/codex-shim.test.ts index d9ec5d17d5..30885ccabe 100644 --- a/tests/codex-integration/codex-shim.test.ts +++ b/tests/codex-integration/codex-shim.test.ts @@ -6,7 +6,7 @@ import { tmpdir } from "node:os"; import { autoRestoreCodexShim, buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim, diagnoseCodexShim, findCodexOnPath, inspectCodexShimBackingForCommand, installCodexShim, isLocalAbsoluteInspectionPath, isVersionManagerOwnedCodexPath, isWindowsInteropDir, lastCodexDiscoveryError, setCodexShimFreshWriteHookForTests, setCodexShimGuardedWriteHookForTests, setCodexShimProbeHookForTests, setCodexShimProbeObservationMsForTests, setCodexShimProbeShellForTests, setCodexShimRollbackRestoreHookForTests, uninstallCodexShim } from "../../src/codex/shim"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { repoPath, repoRoot } from "../helpers/repo-root"; -import { INTERNAL_DEADLINE_MS } from "../helpers/test-budget"; +import { INTERNAL_DEADLINE_MS, SPAWN_BUDGET_MS } from "../helpers/test-budget"; const SHIM_MARKER = "opencodex codex autostart shim"; const UNIX_SHIM_REVISION_MARKER = "opencodex unix codex shim revision 2"; @@ -1296,6 +1296,88 @@ printf '%s\\n' child-codex } }); + for (const shell of ["cmd", "powershell", "pwsh"] as const) { + for (const callerToken of shell === "cmd" ? [undefined, "caller-token"] : [undefined, "", "caller-token"]) { + test.skipIf(process.platform !== "win32")(`Windows ${shell} shim restores the caller token (${callerToken === undefined ? "missing" : callerToken === "" ? "empty" : "explicit token, bypass"})`, () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-scope-")); + const oldHome = process.env.OPENCODEX_HOME; + try { + process.env.OPENCODEX_HOME = dir; + const extension = shell === "cmd" ? "cmd" : "ps1"; + const realPath = join(dir, `codex-real.${extension}`); + const wrapperPath = join(dir, `codex.${extension}`); + const driverPath = join(dir, `driver.${extension}`); + const ensurePath = join(dir, "ensure.ts"); + const ensureLog = join(dir, "ensure.log"); + writeFileSync(join(dir, "service-api-token"), "file-token\n"); + writeFileSync(ensurePath, `import { writeFileSync } from "node:fs"; writeFileSync(${JSON.stringify(ensureLog)}, "ensure"); process.exit(19);`); + if (shell === "cmd") { + writeFileSync(realPath, "@echo off\r\necho child:%OPENCODEX_API_AUTH_TOKEN%\r\nexit /b 37\r\n"); + writeFileSync(wrapperPath, buildWindowsCodexShim(realPath, process.execPath, ensurePath, "process")); + writeFileSync(driverPath, `@echo off\r\ncall "${wrapperPath}" exec "arg value"\r\nset "result=%ERRORLEVEL%"\r\necho after:%OPENCODEX_API_AUTH_TOKEN%\r\necho result:%result%\r\nexit /b 0\r\n`); + } else { + writeFileSync(realPath, '"child:$env:OPENCODEX_API_AUTH_TOKEN"\nexit 37\n'); + writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, process.execPath, ensurePath, "process")}`); + const emptyToken = callerToken === "" ? "$env:OPENCODEX_API_AUTH_TOKEN = ''\n" : ""; + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n${emptyToken}$beforePresence = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN\n& '${wrapperPath.replace(/'/g, "''")}' exec 'arg value'\n$result = $LASTEXITCODE\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n"result:$result"\n"presence-preserved:$($beforePresence -eq (Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN))"\n`); + } + const env = shimChildEnv({ + OPENCODEX_HOME: dir, + OPENCODEX_API_AUTH_TOKEN: callerToken ?? "", + OCX_SHIM_BYPASS: callerToken ? "1" : "", + }); + if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN; + const result = shell === "cmd" + ? spawnSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/c", "driver.cmd"], { cwd: dir, env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true }) + : spawnSync(`${shell}.exe`, ["-NoProfile", "-NonInteractive", "-File", driverPath], { env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim().split(/\r?\n/)).toEqual([ + `child:${callerToken || "file-token"}`, + `after:${callerToken ?? ""}`, + "result:37", + ...(shell === "cmd" ? [] : ["presence-preserved:True"]), + ]); + expect(existsSync(ensureLog)).toBe(!callerToken); + } finally { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + removeTreeWithRetry(dir); + } + }, SPAWN_BUDGET_MS); + } + } + + for (const failurePhase of ["ensure", "Codex"]) { + test.skipIf(process.platform !== "win32")(`Windows PowerShell shim restores the caller token when ${failurePhase} throws`, () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-error-")); + const oldHome = process.env.OPENCODEX_HOME; + try { + process.env.OPENCODEX_HOME = dir; + const wrapperPath = join(dir, "codex.ps1"); + const ensurePath = join(dir, "throw.ps1"); + const driverPath = join(dir, "driver.ps1"); + const realPath = join(dir, "codex-real.ps1"); + writeFileSync(join(dir, "service-api-token"), "file-token\n"); + writeFileSync(ensurePath, failurePhase === "ensure" ? "throw 'fixture ensure failure'\n" : "exit 19\n"); + writeFileSync(realPath, "throw 'fixture Codex failure'\n"); + writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, ensurePath, "unused.ts", "process")}`); + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\ntry { & '${wrapperPath.replace(/'/g, "''")}' exec } catch { "error:$($_.Exception.Message)" }\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n`); + for (const executable of ["powershell.exe", "pwsh.exe"]) { + const result = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], { + env: shimChildEnv({ OPENCODEX_HOME: dir, OPENCODEX_API_AUTH_TOKEN: "", OCX_SHIM_BYPASS: "" }), + encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true, + }); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim().split(/\r?\n/)).toEqual([`error:fixture ${failurePhase} failure`, "after:"]); + } + } finally { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + removeTreeWithRetry(dir); + } + }, SPAWN_BUDGET_MS); + } + test("Unix shim skips ocx startup only for Codex management commands", () => { if (process.platform === "win32") return; From d6f9eca6999500b953de82afd0710f16c8941d0a Mon Sep 17 00:00:00 2001 From: t Date: Tue, 8 Sep 2026 07:25:34 +0900 Subject: [PATCH 2/2] test(codex): cover explicit and exceptional Windows shim token restoration Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> --- tests/codex-integration/codex-shim.test.ts | 86 ++++++++++++++-------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/tests/codex-integration/codex-shim.test.ts b/tests/codex-integration/codex-shim.test.ts index 30885ccabe..3178a7ed3a 100644 --- a/tests/codex-integration/codex-shim.test.ts +++ b/tests/codex-integration/codex-shim.test.ts @@ -1297,8 +1297,14 @@ printf '%s\\n' child-codex }); for (const shell of ["cmd", "powershell", "pwsh"] as const) { - for (const callerToken of shell === "cmd" ? [undefined, "caller-token"] : [undefined, "", "caller-token"]) { - test.skipIf(process.platform !== "win32")(`Windows ${shell} shim restores the caller token (${callerToken === undefined ? "missing" : callerToken === "" ? "empty" : "explicit token, bypass"})`, () => { + const cases = [ + { callerToken: undefined, bypass: false, label: "missing" }, + ...(shell === "cmd" ? [] : [{ callerToken: "", bypass: false, label: "empty" }]), + { callerToken: "caller-token", bypass: false, label: "explicit token, ensure" }, + { callerToken: "caller-token", bypass: true, label: "explicit token, bypass" }, + ]; + for (const { callerToken, bypass, label } of cases) { + test.skipIf(process.platform !== "win32")(`Windows ${shell} shim restores the caller token (${label})`, () => { const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-scope-")); const oldHome = process.env.OPENCODEX_HOME; try { @@ -1324,7 +1330,7 @@ printf '%s\\n' child-codex const env = shimChildEnv({ OPENCODEX_HOME: dir, OPENCODEX_API_AUTH_TOKEN: callerToken ?? "", - OCX_SHIM_BYPASS: callerToken ? "1" : "", + OCX_SHIM_BYPASS: bypass ? "1" : "", }); if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN; const result = shell === "cmd" @@ -1337,7 +1343,7 @@ printf '%s\\n' child-codex "result:37", ...(shell === "cmd" ? [] : ["presence-preserved:True"]), ]); - expect(existsSync(ensureLog)).toBe(!callerToken); + expect(existsSync(ensureLog)).toBe(!bypass); } finally { if (oldHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = oldHome; @@ -1348,34 +1354,52 @@ printf '%s\\n' child-codex } for (const failurePhase of ["ensure", "Codex"]) { - test.skipIf(process.platform !== "win32")(`Windows PowerShell shim restores the caller token when ${failurePhase} throws`, () => { - const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-error-")); - const oldHome = process.env.OPENCODEX_HOME; - try { - process.env.OPENCODEX_HOME = dir; - const wrapperPath = join(dir, "codex.ps1"); - const ensurePath = join(dir, "throw.ps1"); - const driverPath = join(dir, "driver.ps1"); - const realPath = join(dir, "codex-real.ps1"); - writeFileSync(join(dir, "service-api-token"), "file-token\n"); - writeFileSync(ensurePath, failurePhase === "ensure" ? "throw 'fixture ensure failure'\n" : "exit 19\n"); - writeFileSync(realPath, "throw 'fixture Codex failure'\n"); - writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, ensurePath, "unused.ts", "process")}`); - writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\ntry { & '${wrapperPath.replace(/'/g, "''")}' exec } catch { "error:$($_.Exception.Message)" }\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n`); - for (const executable of ["powershell.exe", "pwsh.exe"]) { - const result = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], { - env: shimChildEnv({ OPENCODEX_HOME: dir, OPENCODEX_API_AUTH_TOKEN: "", OCX_SHIM_BYPASS: "" }), - encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true, - }); - expect(result.status, result.stderr).toBe(0); - expect(result.stdout.trim().split(/\r?\n/)).toEqual([`error:fixture ${failurePhase} failure`, "after:"]); - } - } finally { - if (oldHome === undefined) delete process.env.OPENCODEX_HOME; - else process.env.OPENCODEX_HOME = oldHome; - removeTreeWithRetry(dir); + for (const executable of ["powershell.exe", "pwsh.exe"]) { + for (const callerToken of [undefined, "", "caller-token"]) { + test.skipIf(process.platform !== "win32")(`Windows ${executable} shim restores ${callerToken === undefined ? "missing" : callerToken === "" ? "empty" : "explicit"} caller token when ${failurePhase} throws`, () => { + const dir = mkdtempSync(join(tmpdir(), "ocx-shim-token-error-")); + const oldHome = process.env.OPENCODEX_HOME; + try { + process.env.OPENCODEX_HOME = dir; + const wrapperPath = join(dir, "codex.ps1"); + const ensurePath = join(dir, "throw.ps1"); + const driverPath = join(dir, "driver.ps1"); + const realPath = join(dir, "codex-real.ps1"); + writeFileSync(join(dir, "service-api-token"), "file-token\n"); + writeFileSync(ensurePath, failurePhase === "ensure" ? "throw 'fixture ensure failure'\n" : "exit 19\n"); + writeFileSync(realPath, "throw 'fixture Codex failure'\n"); + writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realPath, ensurePath, "unused.ts", "process")}`); + const emptyToken = callerToken === "" ? "$env:OPENCODEX_API_AUTH_TOKEN = ''\n" : ""; + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n${emptyToken}$beforePresence = Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN\ntry { & '${wrapperPath.replace(/'/g, "''")}' exec } catch { "error:$($_.Exception.Message)" }\n"after:$env:OPENCODEX_API_AUTH_TOKEN"\n"presence-preserved:$($beforePresence -eq (Test-Path Env:\\OPENCODEX_API_AUTH_TOKEN))"\n`); + const env = shimChildEnv({ OPENCODEX_HOME: dir, OPENCODEX_API_AUTH_TOKEN: callerToken ?? "", OCX_SHIM_BYPASS: "" }); + if (callerToken === undefined) delete env.OPENCODEX_API_AUTH_TOKEN; + const result = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], { + env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true, + }); + expect(result.error).toBeUndefined(); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout.trim().split(/\r?\n/)).toEqual([ + `error:fixture ${failurePhase} failure`, `after:${callerToken ?? ""}`, "presence-preserved:True", + ]); + + // A failed process must complete, rather than satisfy the check through a timeout. + writeFileSync(driverPath, `\uFEFF$ErrorActionPreference = 'Stop'\n& '${wrapperPath.replace(/'/g, "''")}' exec\n`); + const uncaught = spawnSync(executable, ["-NoProfile", "-NonInteractive", "-File", driverPath], { + env, encoding: "utf8", timeout: INTERNAL_DEADLINE_MS, windowsHide: true, + }); + expect(uncaught.error).toBeUndefined(); + expect(uncaught.signal).toBeNull(); + expect(typeof uncaught.status, uncaught.stderr).toBe("number"); + expect(uncaught.status, uncaught.stderr).not.toBe(0); + expect(uncaught.stderr).toContain(`fixture ${failurePhase} failure`); + } finally { + if (oldHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = oldHome; + removeTreeWithRetry(dir); + } + }, SPAWN_BUDGET_MS); } - }, SPAWN_BUDGET_MS); + } } test("Unix shim skips ocx startup only for Codex management commands", () => {