Files
codex-action/test/runCodexExec.test.mjs
Michael Bolin 52fe01ec70 fix: keep permission profile helper backward compatible (#116)
## Why

PR #113 added `--permission-profile` to the internal `run-codex-exec`
helper as a required Commander option. Existing callers that invoke the
helper with the pre-#113 argument set now fail during option parsing,
before the existing permission-selection fallback can preserve the
legacy `workspace-write` behavior.

The established `--sandbox` helper option was already required. Making
both helper options optional, as proposed in #115, would broaden the
internal interface unnecessarily; only the newly introduced option needs
a backward-compatible default.

## What changed

- Make `--permission-profile` optional with an empty-string default.
- Keep `--sandbox` required so malformed helper invocations still fail
early.
- Add regression coverage for omitting `--permission-profile` while
preserving the legacy `workspace-write` fallback.
- Rebuild the checked-in `dist/main.js` bundle.

## Testing

- `corepack pnpm test`
- `corepack pnpm run check`
2026-07-04 09:36:55 -07:00

191 lines
5.5 KiB
JavaScript

import assert from "node:assert/strict";
import {
chmodSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { test } from "node:test";
import { fileURLToPath } from "node:url";
const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
/**
* Runs the bundled `run-codex-exec` command with a generated fake `codex` executable first on
* `PATH`. The fake captures its arguments and writes a final-message file without making an API
* request, so successful cases exercise command construction, process spawning, and output
* handling without an API key. Validation-error cases assert that the fake was never spawned.
*/
function runCodexExecWithFakeCodex({
sandbox = "",
permissionProfile = "",
extraArgs = "",
safetyStrategy = "drop-sudo",
} = {}) {
const tempDir = mkdtempSync(path.join(tmpdir(), "codex-action-permissions-"));
const capturePath = path.join(tempDir, "args.json");
const outputPath = path.join(tempDir, "output.txt");
const fakeCodexPath = path.join(tempDir, "codex.mjs");
writeFileSync(
fakeCodexPath,
`import { writeFileSync } from "node:fs";
const args = process.argv.slice(2);
writeFileSync(process.env.CODEX_CAPTURE_ARGS, JSON.stringify(args));
const outputIndex = args.indexOf("--output-last-message");
if (outputIndex < 0 || outputIndex + 1 >= args.length) {
throw new Error("missing --output-last-message");
}
writeFileSync(args[outputIndex + 1], "fake final message\\n");
`,
"utf8"
);
const posixLauncher = path.join(tempDir, "codex");
writeFileSync(
posixLauncher,
`#!/bin/sh\nexec node "${fakeCodexPath}" "$@"\n`,
"utf8"
);
chmodSync(posixLauncher, 0o755);
writeFileSync(
path.join(tempDir, "codex.cmd"),
`@node "${fakeCodexPath}" %*\r\n`,
"utf8"
);
const result = spawnSync(
process.execPath,
[
mainPath,
"run-codex-exec",
"--prompt",
"test prompt",
"--prompt-file",
"",
"--codex-home",
"",
"--cd",
tempDir,
"--extra-args",
extraArgs,
"--output-file",
outputPath,
"--output-schema-file",
"",
"--output-schema",
"",
"--sandbox",
sandbox,
...(permissionProfile == null
? []
: ["--permission-profile", permissionProfile]),
"--model",
"",
"--effort",
"",
"--safety-strategy",
safetyStrategy,
"--codex-user",
"",
],
{
encoding: "utf8",
env: {
...process.env,
PATH: `${tempDir}${path.delimiter}${process.env.PATH ?? ""}`,
CODEX_CAPTURE_ARGS: capturePath,
},
}
);
let capturedArgs = null;
try {
capturedArgs = JSON.parse(readFileSync(capturePath, "utf8"));
} catch {
// Expected when argument validation rejects the invocation before spawning Codex.
}
rmSync(tempDir, { recursive: true, force: true });
return { result, capturedArgs };
}
test("preserves workspace-write as the default legacy sandbox", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex();
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(capturedArgs.slice(-2), ["--sandbox", "workspace-write"]);
});
test("allows permission-profile to be omitted", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex({
permissionProfile: null,
});
assert.equal(result.status, 0, result.stderr);
assert.deepEqual(capturedArgs.slice(-2), ["--sandbox", "workspace-write"]);
});
test("selects a permission profile without passing --sandbox", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex({
permissionProfile: "public-review",
});
assert.equal(result.status, 0, result.stderr);
assert.equal(capturedArgs.includes("--sandbox"), false);
assert.deepEqual(capturedArgs.slice(-2), [
"--config",
'default_permissions="public-review"',
]);
});
test("rejects permission-profile with sandbox", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex({
permissionProfile: "public-review",
sandbox: "read-only",
});
assert.notEqual(result.status, 0);
assert.equal(capturedArgs, null);
assert.match(result.stderr, /mutually exclusive/);
});
test("rejects permission-profile with the read-only safety strategy", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex({
permissionProfile: "public-review",
safetyStrategy: "read-only",
});
assert.notEqual(result.status, 0);
assert.equal(capturedArgs, null);
assert.match(result.stderr, /forces the legacy read-only sandbox/);
});
test("rejects permission-profile with a sandbox in codex-args", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex({
permissionProfile: "public-review",
extraArgs: '["--sandbox", "read-only"]',
});
assert.notEqual(result.status, 0);
assert.equal(capturedArgs, null);
assert.match(result.stderr, /sandbox override in `codex-args`/);
});
for (const extraArgs of [
'["--config", "sandbox_workspace_write.network_access=true"]',
'["--config=sandbox_workspace_write.network_access=true"]',
]) {
test(`rejects permission-profile with ${extraArgs} in codex-args`, () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex({
permissionProfile: "public-review",
extraArgs,
});
assert.notEqual(result.status, 0);
assert.equal(capturedArgs, null);
assert.match(result.stderr, /sandbox override in `codex-args`/);
});
}