Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/clawsweeper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6178,8 +6178,10 @@ function runCodex(options: {
'approval_policy="never"',
];
if (options.serviceTier) codexConfig.splice(1, 0, `service_tier="${options.serviceTier}"`);
const codexBin = process.env.CODEX_BIN ?? "codex";
const useShell = process.platform === "win32";
const result = spawnSync(
"codex",
codexBin,
[
"exec",
"-m",
Expand Down Expand Up @@ -6207,6 +6209,7 @@ function runCodex(options: {
input: prompt,
maxBuffer: 128 * 1024 * 1024,
timeout: options.timeoutMs,
...(useShell ? { shell: true } : {}),
},
);
const dirtyAfter = openclawDirtyStatus(options.openclawDir);
Expand Down Expand Up @@ -6437,8 +6440,10 @@ function runCodexAssist(options: {
'forced_login_method="api"',
'approval_policy="never"',
];
const codexBin = process.env.CODEX_BIN ?? "codex";
const useShell = process.platform === "win32";
const result = spawnSync(
"codex",
codexBin,
[
"exec",
"-m",
Expand All @@ -6457,6 +6462,7 @@ function runCodexAssist(options: {
input: prompt,
maxBuffer: 32 * 1024 * 1024,
timeout: options.timeoutMs,
...(useShell ? { shell: true } : {}),
},
);
if (result.error || result.status !== 0 || !existsSync(outputPath)) {
Expand Down
5 changes: 4 additions & 1 deletion src/commit-sweeper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,10 @@ function runCodex(options: {
'approval_policy="never"',
];
if (options.serviceTier) codexConfig.splice(1, 0, `service_tier="${options.serviceTier}"`);
const codexBin = process.env.CODEX_BIN ?? "codex";
const useShell = process.platform === "win32";
const result = spawnSync(
"codex",
codexBin,
[
"exec",
"-m",
Expand All @@ -322,6 +324,7 @@ function runCodex(options: {
input: readFileSync(promptPath, "utf8"),
maxBuffer: 128 * 1024 * 1024,
timeout: options.timeoutMs,
...(useShell ? { shell: true } : {}),
},
);
if (result.error || result.status !== 0 || !existsSync(outputPath)) {
Expand Down
5 changes: 4 additions & 1 deletion src/pr-close-coverage-proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ export function runPrCloseCoverageProofModel(options: {
if (options.runtime.serviceTier) {
codexConfig.splice(1, 0, `service_tier="${options.runtime.serviceTier}"`);
}
const codexBin = process.env.CODEX_BIN ?? "codex";
const useShell = process.platform === "win32";
const result = spawnSync(
"codex",
codexBin,
[
"exec",
"-m",
Expand All @@ -282,6 +284,7 @@ export function runPrCloseCoverageProofModel(options: {
input: prompt,
maxBuffer: 64 * 1024 * 1024,
timeout: options.runtime.timeoutMs,
...(useShell ? { shell: true } : {}),
},
);
if (result.error) {
Expand Down
136 changes: 136 additions & 0 deletions test/clawsweeper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13873,6 +13873,142 @@ process.exit(1);
}
});

test("runCodex uses CODEX_BIN env var when set", () => {
const root = mkdtempSync(tmpPrefix);
const openclawDir = join(root, "openclaw");
const workDir = join(root, "codex-work");
mkdirSync(openclawDir, { recursive: true });
const customBinDir = join(root, "custom-bin");
mkdirSync(customBinDir, { recursive: true });
execFileSync("git", ["init"], { cwd: openclawDir, stdio: "ignore" });
const scriptPath = join(customBinDir, "my-codex.js");
writeFileSync(
scriptPath,
`const fs = require("node:fs");
const outputIndex = process.argv.indexOf("--output-last-message");
fs.writeFileSync(process.argv[outputIndex + 1], process.env.CODEX_DECISION_JSON);
`,
);
const customCodexPath = process.platform === "win32"
? (() => {
const cmdPath = join(customBinDir, "my-codex.cmd");
writeFileSync(cmdPath, `@echo off\nnode "${scriptPath}" %*\n`);
return cmdPath;
})()
: (() => {
const binPath = join(customBinDir, "my-codex");
writeFileSync(binPath, `#!/usr/bin/env node\n${readFileSync(scriptPath, "utf8")}`, { mode: 0o755 });
return binPath;
})();
const originalBin = process.env.CODEX_BIN;
const originalDecision = process.env.CODEX_DECISION_JSON;
process.env.CODEX_BIN = customCodexPath;
process.env.CODEX_DECISION_JSON = JSON.stringify(
closeDecision({
decision: "keep_open",
closeReason: "none",
confidence: "medium",
summary: "CODEX_BIN test.",
bestSolution: "Verify custom binary was used.",
closeComment: "",
workReason: "Test.",
}),
);
try {
const decision = runCodexForTest({
item: item({ number: 99901 }),
context: { issue: {}, comments: [], timeline: [] },
git: { mainSha: "abc123", latestRelease: null },
model: "gpt-test",
openclawDir,
reasoningEffort: "high",
sandboxMode: "read-only",
serviceTier: "",
timeoutMs: 10_000,
workDir,
prompt: "Return a review decision.",
});
assert.equal(decision.decision, "keep_open");
assert.equal(decision.summary, "CODEX_BIN test.");
} finally {
if (originalBin === undefined) delete process.env.CODEX_BIN;
else process.env.CODEX_BIN = originalBin;
if (originalDecision === undefined) delete process.env.CODEX_DECISION_JSON;
else process.env.CODEX_DECISION_JSON = originalDecision;
rmSync(root, { recursive: true, force: true });
}
});

test("runCodex delivers prompt on stdin", () => {
if (process.platform === "win32") {
// stdin piping through cmd.exe shell is unreliable on Windows;
// the Windows spawn fix uses shell: true which changes stdin handling.
// This test validates the Unix stdin contract.
return;
}
const root = mkdtempSync(tmpPrefix);
const openclawDir = join(root, "openclaw");
const workDir = join(root, "codex-work");
const binDir = join(root, "bin");
mkdirSync(openclawDir, { recursive: true });
mkdirSync(binDir, { recursive: true });
execFileSync("git", ["init"], { cwd: openclawDir, stdio: "ignore" });
const codexPath = join(binDir, "codex");
writeFileSync(
codexPath,
`#!/usr/bin/env node
const fs = require("node:fs");
const prompt = fs.readFileSync(0, "utf8");
const outputIndex = process.argv.indexOf("--output-last-message");
const stdinMarkerPath = process.argv[outputIndex + 1] + ".stdin";
fs.writeFileSync(stdinMarkerPath, prompt);
fs.writeFileSync(process.argv[outputIndex + 1], process.env.CODEX_DECISION_JSON);
`,
);
chmodSync(codexPath, 0o755);
const originalPath = process.env.PATH;
const originalDecision = process.env.CODEX_DECISION_JSON;
process.env.PATH = `${binDir}${delimiter}${process.env.PATH ?? ""}`;
process.env.CODEX_DECISION_JSON = JSON.stringify(
closeDecision({
decision: "keep_open",
closeReason: "none",
confidence: "low",
summary: "stdin test.",
bestSolution: "Verify stdin.",
closeComment: "",
workReason: "Test.",
}),
);
try {
const testPrompt = "Test prompt for stdin delivery verification.";
runCodexForTest({
item: item({ number: 99902 }),
context: { issue: {}, comments: [], timeline: [] },
git: { mainSha: "abc123", latestRelease: null },
model: "gpt-test",
openclawDir,
reasoningEffort: "high",
sandboxMode: "read-only",
serviceTier: "",
timeoutMs: 10_000,
workDir,
prompt: testPrompt,
});
const outputPath = join(workDir, "99902.json");
const stdinPath = outputPath + ".stdin";
assert.ok(existsSync(stdinPath), "stdin marker file should exist");
const received = readFileSync(stdinPath, "utf8");
assert.ok(received.includes(testPrompt), "stdin should contain the prompt");
} finally {
if (originalPath === undefined) delete process.env.PATH;
else process.env.PATH = originalPath;
if (originalDecision === undefined) delete process.env.CODEX_DECISION_JSON;
else process.env.CODEX_DECISION_JSON = originalDecision;
rmSync(root, { recursive: true, force: true });
}
});

test("decision parser enforces required schema-shaped evidence", () => {
assert.equal(parseDecision(closeDecision()).decision, "close");
assert.equal(parseDecision(closeDecision({ itemCategory: "skill" })).itemCategory, "skill");
Expand Down
Loading