Files

77 lines
4.0 KiB
TypeScript
Raw Permalink Normal View History

import { chmodSync, writeFileSync } from "node:fs";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
2026-06-29 12:47:44 +00:00
import { describe, test } from "node:test";
import { expect } from "expect";
import { buildCodexExecArgs, checkCodexAvailability, executeCodexCommandWithFallback, resolveCodexCommand } from "../src/codex-runtime.js";
describe("codex runtime", () => {
test("resolves Codex command from CODEX_BIN only", () => {
expect(resolveCodexCommand({ CODEX_BIN: "codex-custom" })).toBe("codex-custom");
expect(resolveCodexCommand({ OPEN_GAMESTUDIO_CODEX_BIN: "legacy-codex" })).toBe("codex");
expect(resolveCodexCommand({})).toBe("codex");
});
test("builds structured codex exec args with cwd, sandbox, model, reasoning, and stdin prompt", () => {
const args = buildCodexExecArgs({ projectRoot: "/repo", sandbox: "read-only", model: "gpt-5.6-sol", reasoningEffort: "high" });
expect(args).toContain("exec");
2026-06-29 22:04:26 +00:00
expect(args).toContain("--model");
expect(args).toContain("gpt-5.6-sol");
expect(args).toEqual(expect.arrayContaining(["-c", 'model_reasoning_effort="high"']));
expect(args).toContain("--cd");
expect(args).toContain("/repo");
expect(args).toContain("--sandbox");
expect(args).toContain("read-only");
expect(args).toContain("-");
});
test("reports availability failures without throwing", async () => {
const result = await checkCodexAvailability({ codexBin: "/missing/codex" });
expect(result.ok).toBe(false);
expect(result.command).toBe("/missing/codex");
expect(result.reason).toMatch(/not found|unavailable/i);
});
test("reports authenticated stub as available", async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-codex-"));
const stub = path.join(cwd, "codex-stub.mjs");
writeFileSync(stub, "#!/usr/bin/env node\nconsole.log('codex 1.0.0');\n");
chmodSync(stub, 0o755);
const result = await checkCodexAvailability({ codexBin: stub });
expect(result.ok).toBe(true);
expect(result.command).toBe(stub);
});
test("uses the designated fallback only when the primary model is unavailable", async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-codex-fallback-"));
const stub = path.join(cwd, "codex-stub.mjs");
writeFileSync(stub, `#!/usr/bin/env node\nconst model = process.argv[process.argv.indexOf('--model') + 1];\nif (model === 'gpt-5.6-sol') { console.error('model gpt-5.6-sol is not available for this account'); process.exit(1); }\nconsole.log(model);\n`);
chmodSync(stub, 0o755);
const primary = { command: stub, args: buildCodexExecArgs({ projectRoot: cwd, model: "gpt-5.6-sol", reasoningEffort: "high" }) };
const fallback = { command: stub, args: buildCodexExecArgs({ projectRoot: cwd, model: "gpt-5.5", reasoningEffort: "high" }) };
const result = await executeCodexCommandWithFallback({ primary, fallback, primaryModel: "gpt-5.6-sol", fallbackModel: "gpt-5.5" }, "prompt", { cwd });
expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe("gpt-5.5");
expect(result.executedModel).toBe("gpt-5.5");
expect(result.fallbackFromModel).toBe("gpt-5.6-sol");
});
test("does not hide ordinary execution failures behind a fallback", async () => {
const cwd = mkdtempSync(path.join(tmpdir(), "ogs-codex-no-fallback-"));
const stub = path.join(cwd, "codex-stub.mjs");
writeFileSync(stub, "#!/usr/bin/env node\nconsole.error('tests failed'); process.exit(2);\n");
chmodSync(stub, 0o755);
const primary = { command: stub, args: buildCodexExecArgs({ projectRoot: cwd, model: "gpt-5.6-terra", reasoningEffort: "medium" }) };
const fallback = { command: stub, args: buildCodexExecArgs({ projectRoot: cwd, model: "gpt-5.4", reasoningEffort: "medium" }) };
const result = await executeCodexCommandWithFallback({ primary, fallback, primaryModel: "gpt-5.6-terra", fallbackModel: "gpt-5.4" }, "prompt", { cwd });
expect(result.status).toBe(2);
expect(result.executedModel).toBe("gpt-5.6-terra");
expect(result.fallbackFromModel).toBeUndefined();
});
});