e109d2804c
A codex session emits no hooks and rarely calls `ccam stage` on its own, so its card sat at `idle` forever while the amber "detected" badge was the only thing that ever moved. `codex-ingest.js` now advances the declared `stage` from its own written detection — the one place in CCAM where an inference moves the blue `current` ring, and only for `codex` lanes. It stays honest: gated on `recordDetection` having written (so forward-only, declared-wins and the detection TTL are inherited unchanged), passes NO evidence (the node renders `passed-no-evidence` amber, never `done`), and no template's last node carries detect rules, so a lane can never be auto-declared `done`. `setStage` grows a `keepDetection` option — its sole caller — so the detection columns survive the advance and the card keeps showing which command moved it instead of looking like an agent's own claim. Two levers then get the evidence auto-advance cannot supply: - `codex-nudge.js` now nudges on a stage held 2+ min with no evidence, instead of on a detection sitting ahead of the declared stage — after auto-advance the old condition can never fire, since the two now match. - `ccam lanes agents install` on a codex lane writes the marker-delimited `<!-- ccam:stage:start -->` block into the lane's own AGENTS.md — the one instruction channel Codex reads — instead of copying agent templates into `.claude/agents/`, which it never loads. Appended, user prose preserved, reinstall replaces exactly that block. Verified with a real `codex exec` run: stage moved to `watching-pr` with `evidence: null` and the detection signal intact. 1364 server tests pass.
574 lines
23 KiB
JavaScript
574 lines
23 KiB
JavaScript
/**
|
|
* @file Unit tests for Codex stage ingestion: the payload→hook-event mapping,
|
|
* and one full pass over a fixture rollout journal (detection + status mirror +
|
|
* broadcast + the no-new-bytes no-op).
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const os = require("node:os");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
const SUITE_DB_PATH = path.join(
|
|
os.tmpdir(),
|
|
`dashboard-codex-ingest-${Date.now()}-${process.pid}.db`
|
|
);
|
|
process.env.DASHBOARD_DB_PATH = SUITE_DB_PATH;
|
|
|
|
const { describe, it, before, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const codex = require("../lib/codex-ingest");
|
|
const lanes = require("../lib/lanes");
|
|
|
|
/** One rollout line in the real on-disk shape: top-level envelope + payload. */
|
|
function line(payload, type = "response_item") {
|
|
return JSON.stringify({ timestamp: new Date().toISOString(), ordinal: 1, type, payload });
|
|
}
|
|
|
|
describe("codex payload mapping", () => {
|
|
it("maps a function_call exec_command to a Bash event", () => {
|
|
const events = codex.eventsFromPayload({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: "/repo" }),
|
|
});
|
|
assert.deepEqual(events, [
|
|
{ tool_name: "Bash", tool_input: { command: "npm run test:server" } },
|
|
]);
|
|
});
|
|
|
|
it("maps a custom_tool_call exec to a Bash event carrying the raw script", () => {
|
|
const [event] = codex.eventsFromPayload({
|
|
type: "custom_tool_call",
|
|
name: "exec",
|
|
input: 'const r = await tools.exec_command({cmd:"git push origin feat/x"}); text(r.output);',
|
|
});
|
|
assert.equal(event.tool_name, "Bash");
|
|
assert.match(event.tool_input.command, /git push origin feat\/x/);
|
|
});
|
|
|
|
it("maps an apply_patch script to one event per touched file", () => {
|
|
const events = codex.eventsFromPayload({
|
|
type: "custom_tool_call",
|
|
name: "exec",
|
|
input:
|
|
'const patch = "*** Begin Patch\n*** Update File: server/lib/lanes.js\n+x\n' +
|
|
'*** Add File: docs/NEW.md\n+y\n*** End Patch";',
|
|
});
|
|
assert.deepEqual(events, [
|
|
{ tool_name: "Edit", tool_input: { file_path: "server/lib/lanes.js" } },
|
|
{ tool_name: "Write", tool_input: { file_path: "docs/NEW.md" } },
|
|
]);
|
|
});
|
|
|
|
it("maps an apply_patch script whose newlines are still escaped", () => {
|
|
// Verbatim from a real rollout: the patch heredoc lives inside a JS string
|
|
// literal, so after JSON.parse the line breaks are a literal backslash-n,
|
|
// not newlines. Matching only real newlines silently sees no files at all.
|
|
const events = codex.eventsFromPayload({
|
|
type: "custom_tool_call",
|
|
name: "exec",
|
|
input:
|
|
'const patch = "*** Begin Patch\\n*** Add File: config/demo.yml\\n+# demo\\n*** End Patch";',
|
|
});
|
|
assert.deepEqual(events, [
|
|
{ tool_name: "Write", tool_input: { file_path: "config/demo.yml" } },
|
|
]);
|
|
});
|
|
|
|
it("maps update_plan to TaskCreate and spawn_agent to Agent", () => {
|
|
assert.deepEqual(
|
|
codex.eventsFromPayload({ type: "function_call", name: "update_plan", arguments: "{}" }),
|
|
[{ tool_name: "TaskCreate", tool_input: {} }]
|
|
);
|
|
assert.deepEqual(
|
|
codex.eventsFromPayload({
|
|
type: "function_call",
|
|
name: "spawn_agent",
|
|
arguments: JSON.stringify({ task_name: "code_reviewer", message: "…" }),
|
|
}),
|
|
[{ tool_name: "Agent", tool_input: { subagent_type: "code_reviewer" } }]
|
|
);
|
|
});
|
|
|
|
it("ignores anything that is not a tool call", () => {
|
|
assert.deepEqual(codex.eventsFromPayload({ type: "reasoning" }), []);
|
|
assert.deepEqual(
|
|
codex.eventsFromPayload({ type: "custom_tool_call_output", name: "exec" }),
|
|
[]
|
|
);
|
|
assert.deepEqual(codex.eventsFromPayload({ type: "function_call", name: "web_search" }), []);
|
|
assert.deepEqual(codex.eventsFromPayload(null), []);
|
|
});
|
|
});
|
|
|
|
describe("codex ingest pass", () => {
|
|
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), "codex-home-"));
|
|
const DIR = path.join(HOME, "sessions", "2026", "08", "27");
|
|
let prevHome;
|
|
|
|
before(() => {
|
|
prevHome = process.env.CODEX_HOME;
|
|
process.env.CODEX_HOME = HOME;
|
|
fs.mkdirSync(DIR, { recursive: true });
|
|
});
|
|
|
|
after(() => {
|
|
if (prevHome === undefined) delete process.env.CODEX_HOME;
|
|
else process.env.CODEX_HOME = prevHome;
|
|
codex.__resetOffsets();
|
|
try {
|
|
fs.rmSync(HOME, { recursive: true, force: true });
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
});
|
|
|
|
/** A rollout journal for `cwd` with one `npm run test:server` shell call. */
|
|
function writeRollout(name, cwd) {
|
|
const file = path.join(DIR, name);
|
|
fs.writeFileSync(
|
|
file,
|
|
[
|
|
line({ cwd, session_id: "abc" }, "session_meta"),
|
|
line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: cwd }),
|
|
}),
|
|
].join("\n") + "\n"
|
|
);
|
|
return file;
|
|
}
|
|
|
|
it("detects a stage for a codex lane and mirrors its status", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
writeRollout("rollout-2026-08-27T10-00-00-aaa.jsonl", cwd);
|
|
codex.__resetOffsets();
|
|
|
|
const seen = [];
|
|
assert.equal(codex.ingestCodexOnce({ broadcast: (id) => seen.push(id) }), 1);
|
|
const after = lanes.getLane(lane.id);
|
|
assert.equal(after.detected_stage, "tests"); // `npm run test:server` → default template's tests node
|
|
assert.match(after.detected_signal, /test:server/);
|
|
assert.equal(after.status, "running"); // a journal written just now means work is happening
|
|
assert.deepEqual(seen, [lane.id]);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("advances the declared stage from the detection, without evidence", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
writeRollout("rollout-2026-08-27T10-00-10-adv.jsonl", cwd);
|
|
codex.__resetOffsets();
|
|
|
|
codex.ingestCodexOnce({});
|
|
const after = lanes.getLane(lane.id);
|
|
// The ring moves — a codex session has nothing else that would move it…
|
|
assert.equal(after.stage, "tests");
|
|
// …but the node stays amber: an inference sees the tool call, not its result.
|
|
assert.equal(after.stages.tests.evidence, null);
|
|
// …and the badge survives, so the card still says WHY it moved.
|
|
assert.equal(after.detected_stage, "tests");
|
|
assert.match(after.detected_signal, /test:server/);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("never drags an auto-advanced stage backwards", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
lanes.setStage(lane.id, { stage: "review", evidence: "declared by the session" });
|
|
writeRollout("rollout-2026-08-27T10-00-11-back.jsonl", cwd); // detects `tests`, behind `review`
|
|
codex.__resetOffsets();
|
|
|
|
codex.ingestCodexOnce({});
|
|
const after = lanes.getLane(lane.id);
|
|
assert.equal(after.stage, "review"); // declared-wins: recordDetection refused, so no advance
|
|
assert.equal(after.detected_stage, null);
|
|
assert.equal(after.stages.review.evidence, "declared by the session");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("a second pass with no appended bytes changes nothing", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
writeRollout("rollout-2026-08-27T10-00-01-bbb.jsonl", cwd);
|
|
codex.__resetOffsets();
|
|
|
|
codex.ingestCodexOnce({});
|
|
const first = lanes.getLane(lane.id).detected_at;
|
|
assert.equal(codex.ingestCodexOnce({}), 0);
|
|
assert.equal(lanes.getLane(lane.id).detected_at, first);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("never touches a claude lane, even at the journal's own cwd", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "claude-lane-"));
|
|
const lane = lanes.createLane({ title: "cc", cwd, tool: "claude" });
|
|
writeRollout("rollout-2026-08-27T10-00-02-ccc.jsonl", cwd);
|
|
codex.__resetOffsets();
|
|
|
|
assert.equal(codex.ingestCodexOnce({}), 0);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("ignores a journal whose cwd belongs to no lane", () => {
|
|
const orphan = fs.mkdtempSync(path.join(os.tmpdir(), "codex-orphan-"));
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
writeRollout("rollout-2026-08-27T10-00-03-ddd.jsonl", orphan);
|
|
codex.__resetOffsets();
|
|
|
|
assert.equal(codex.ingestCodexOnce({}), 0);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("keeps byte offsets exact when a tail read starts mid multi-byte character", () => {
|
|
// The first sight of a >64 KiB journal reads only its tail, and that offset
|
|
// can land inside a 3-byte UTF-8 sequence. Splitting the decoded STRING and
|
|
// measuring it back with Buffer.byteLength then counts the decoder's U+FFFD
|
|
// replacement instead of the bytes actually consumed, so the next pass
|
|
// starts 1-2 bytes off and every later record is unparseable.
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-2026-08-27T10-00-06-utf8.jsonl");
|
|
const record = (cmd) =>
|
|
line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd, workdir: cwd }),
|
|
});
|
|
|
|
// Grow an all-multi-byte filler line until the tail boundary (size - 64 KiB)
|
|
// falls on a UTF-8 continuation byte.
|
|
const head = line({ cwd, session_id: "utf8" }, "session_meta") + "\n";
|
|
const tailRecord = record("npm run test:server") + "\n";
|
|
let filler = "→".repeat(30_000);
|
|
for (let pad = 0; pad < 8; pad += 1) {
|
|
const body = head + filler + "\n" + tailRecord;
|
|
const buf = Buffer.from(body, "utf8");
|
|
const boundary = buf.length - 64 * 1024;
|
|
if (boundary > 0 && (buf[boundary] & 0xc0) === 0x80) break;
|
|
filler += "x";
|
|
}
|
|
fs.writeFileSync(file, head + filler + "\n" + tailRecord);
|
|
const buf = Buffer.from(fs.readFileSync(file));
|
|
assert.equal(buf[buf.length - 64 * 1024] & 0xc0, 0x80, "fixture must straddle a UTF-8 char");
|
|
|
|
codex.__resetOffsets();
|
|
assert.equal(codex.ingestCodexOnce({}), 1);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, "tests");
|
|
|
|
// The real proof: the next append must still parse, which it only does if
|
|
// the stored offset is exactly on the previous line boundary.
|
|
fs.appendFileSync(file, record("git push origin feat/x") + "\n");
|
|
assert.equal(codex.ingestCodexOnce({}), 1);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, "ship");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("ingests a line appended exactly at the previous read's boundary", () => {
|
|
// The offset after a pass lands on a line boundary, so the next chunk's
|
|
// FIRST line is a whole record — dropping it as torn loses every append
|
|
// that arrives one line at a time, which is how a journal actually grows.
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-2026-08-27T10-00-04-fff.jsonl");
|
|
fs.writeFileSync(file, line({ cwd, session_id: "boundary" }, "session_meta") + "\n");
|
|
codex.__resetOffsets();
|
|
codex.ingestCodexOnce({}); // consumes the header, offset now at EOF
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
|
|
fs.appendFileSync(
|
|
file,
|
|
line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: cwd }),
|
|
}) + "\n"
|
|
);
|
|
assert.equal(codex.ingestCodexOnce({}), 1);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, "tests");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("re-reads a torn line whole on the next pass", () => {
|
|
// A pass that catches the writer mid-line must not consume that fragment:
|
|
// the record it belongs to would be lost forever.
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-2026-08-27T10-00-05-ggg.jsonl");
|
|
const record = line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: cwd }),
|
|
});
|
|
fs.writeFileSync(
|
|
file,
|
|
line({ cwd, session_id: "torn" }, "session_meta") + "\n" + record.slice(0, 40)
|
|
);
|
|
codex.__resetOffsets();
|
|
codex.ingestCodexOnce({});
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
|
|
fs.appendFileSync(file, record.slice(40) + "\n"); // the writer finishes the line
|
|
assert.equal(codex.ingestCodexOnce({}), 1);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, "tests");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("does not replay a journal it sees for the first time already idle", () => {
|
|
// A thread that finished days ago must not paint a stage now — the first
|
|
// sight of a stale file only records where it ends.
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = writeRollout("rollout-2026-08-20T10-00-00-eee.jsonl", cwd);
|
|
const old = Date.now() - 6 * 60 * 60 * 1000;
|
|
fs.utimesSync(file, old / 1000, old / 1000);
|
|
codex.__resetOffsets();
|
|
|
|
assert.equal(codex.ingestCodexOnce({}), 0);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("does not replay a journal whose mtime is in the FUTURE", () => {
|
|
// A clock stepped back (or a restore that preserved timestamps) leaves a
|
|
// finished thread's mtime ahead of now. Raw `now - mtime` is then negative,
|
|
// which reads as "written milliseconds ago" and would replay it as live work.
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = writeRollout("rollout-2026-08-27T10-00-00-fut.jsonl", cwd);
|
|
const future = Date.now() + 6 * 60 * 60 * 1000;
|
|
fs.utimesSync(file, future / 1000, future / 1000);
|
|
codex.__resetOffsets();
|
|
|
|
assert.equal(codex.ingestCodexOnce({}), 0);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("ignores a torn/partial JSONL line at the end of a file", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-torn-line-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-torn.jsonl");
|
|
fs.writeFileSync(
|
|
file,
|
|
[
|
|
line({ cwd, session_id: "torn" }, "session_meta"),
|
|
line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: cwd }),
|
|
}),
|
|
// A torn last line: not valid JSON, so it will be skipped on next pass
|
|
'{"timestamp":"2026-08-27T10:00:00.000Z","type":"response_item","paylo',
|
|
].join("\n")
|
|
);
|
|
codex.__resetOffsets();
|
|
|
|
const seen = [];
|
|
assert.equal(codex.ingestCodexOnce({ broadcast: (id) => seen.push(id) }), 1);
|
|
const after = lanes.getLane(lane.id);
|
|
assert.equal(after.detected_stage, "tests"); // the valid line was processed
|
|
assert.deepEqual(seen, [lane.id]);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("handles a journal whose session_meta is unparseable gracefully", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-bad-meta-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-bad-meta.jsonl");
|
|
// Write an invalid JSON line as the first line
|
|
fs.writeFileSync(file, "not valid json at all\n");
|
|
codex.__resetOffsets();
|
|
|
|
assert.equal(codex.ingestCodexOnce({}), 0);
|
|
assert.equal(lanes.getLane(lane.id).detected_stage, null);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("picks the deepest lane when a journal's cwd matches nested lanes", () => {
|
|
const outer = fs.mkdtempSync(path.join(os.tmpdir(), "codex-outer-"));
|
|
const inner = fs.mkdtempSync(path.join(outer, "inner"));
|
|
const outerLane = lanes.createLane({ title: "outer", cwd: outer, tool: "codex" });
|
|
const innerLane = lanes.createLane({ title: "inner", cwd: inner, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-nested.jsonl");
|
|
fs.writeFileSync(
|
|
file,
|
|
[
|
|
line({ cwd: inner, session_id: "nested" }, "session_meta"),
|
|
line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: inner }),
|
|
}),
|
|
].join("\n") + "\n"
|
|
);
|
|
codex.__resetOffsets();
|
|
|
|
const seen = [];
|
|
assert.equal(codex.ingestCodexOnce({ broadcast: (id) => seen.push(id) }), 1);
|
|
// Inner lane should be the one detected, not outer
|
|
assert.equal(lanes.getLane(innerLane.id).detected_stage, "tests");
|
|
assert.equal(lanes.getLane(outerLane.id).detected_stage, null);
|
|
assert.deepEqual(seen, [innerLane.id]);
|
|
lanes.deleteLane(innerLane.id);
|
|
lanes.deleteLane(outerLane.id);
|
|
});
|
|
|
|
it("does not touch status of a lane with an active run_id", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-run-id-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
lanes.updateLane(lane.id, { run_id: "active-run" });
|
|
writeRollout("rollout-2026-08-27T10-00-10-rrr.jsonl", cwd);
|
|
codex.__resetOffsets();
|
|
|
|
codex.ingestCodexOnce({});
|
|
const after = lanes.getLane(lane.id);
|
|
// Status should not change because run_id is present
|
|
assert.equal(after.status, "idle");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("does not change status from provisioning or failed", () => {
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-status-protected-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
lanes.updateLane(lane.id, { status: "provisioning" });
|
|
writeRollout("rollout-2026-08-27T10-00-11-ppp.jsonl", cwd);
|
|
codex.__resetOffsets();
|
|
|
|
codex.ingestCodexOnce({});
|
|
assert.equal(lanes.getLane(lane.id).status, "provisioning");
|
|
|
|
lanes.updateLane(lane.id, { status: "failed" });
|
|
codex.ingestCodexOnce({});
|
|
assert.equal(lanes.getLane(lane.id).status, "failed");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("respects DASHBOARD_CODEX_POLL_MS=0 to disable polling", () => {
|
|
const oldMs = process.env.DASHBOARD_CODEX_POLL_MS;
|
|
try {
|
|
process.env.DASHBOARD_CODEX_POLL_MS = "0";
|
|
// startCodexPoll should return early and not set up an interval
|
|
const timer = codex.startCodexPoll(() => {});
|
|
assert.equal(timer, undefined);
|
|
} finally {
|
|
if (oldMs === undefined) delete process.env.DASHBOARD_CODEX_POLL_MS;
|
|
else process.env.DASHBOARD_CODEX_POLL_MS = oldMs;
|
|
}
|
|
});
|
|
|
|
it("handles a shell call with command as an array in arguments", () => {
|
|
const events = codex.eventsFromPayload({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ command: ["npm", "run", "test:server"], workdir: "/repo" }),
|
|
});
|
|
// Should join array elements
|
|
assert.deepEqual(events, [
|
|
{ tool_name: "Bash", tool_input: { command: "npm run test:server" } },
|
|
]);
|
|
});
|
|
|
|
it("ignores a payload with a non-string name", () => {
|
|
// A payload with name as null or number should not match any tool
|
|
const events1 = codex.eventsFromPayload({
|
|
type: "function_call",
|
|
name: null,
|
|
arguments: "{}",
|
|
});
|
|
assert.deepEqual(events1, []);
|
|
|
|
const events2 = codex.eventsFromPayload({
|
|
type: "custom_tool_call",
|
|
name: 123,
|
|
input: "some script",
|
|
});
|
|
assert.deepEqual(events2, []);
|
|
});
|
|
|
|
it("uses the LAST detection in a batch to move the lane forward", () => {
|
|
// Within one ingest pass, if multiple tool calls map to different stages,
|
|
// recordDetection is called in order. Only the LAST one that actually writes
|
|
// should move the lane forward (forward-only). Simulate two tool calls:
|
|
// first impl call (implement), then tests call (tests).
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-batch-last-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
const file = path.join(DIR, "rollout-batch-last.jsonl");
|
|
fs.writeFileSync(
|
|
file,
|
|
[
|
|
line({ cwd, session_id: "batch" }, "session_meta"),
|
|
// Edit event (implement node)
|
|
line({
|
|
type: "custom_tool_call",
|
|
name: "exec",
|
|
input: 'await tools.edit("src/main.js", "add x");',
|
|
}),
|
|
// Bash event (tests node)
|
|
line({
|
|
type: "function_call",
|
|
name: "exec_command",
|
|
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: cwd }),
|
|
}),
|
|
].join("\n") + "\n"
|
|
);
|
|
codex.__resetOffsets();
|
|
|
|
const seen = [];
|
|
assert.equal(codex.ingestCodexOnce({ broadcast: (id) => seen.push(id) }), 1);
|
|
const after = lanes.getLane(lane.id);
|
|
// Should detect the tests node (later in batch), not implement
|
|
assert.equal(after.detected_stage, "tests");
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("limits file scan to MAX_FILES newest files", () => {
|
|
// Create more than MAX_FILES journal files and verify only newest are scanned
|
|
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-max-files-"));
|
|
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
|
|
|
// Create 50 old files (more than MAX_FILES=40)
|
|
const oldTime = Date.now() - 2 * 60 * 60 * 1000;
|
|
for (let i = 0; i < 50; i++) {
|
|
const file = path.join(DIR, `rollout-old-${String(i).padStart(2, "0")}.jsonl`);
|
|
fs.writeFileSync(file, line({ cwd, session_id: `old-${i}` }, "session_meta") + "\n");
|
|
fs.utimesSync(file, oldTime / 1000, oldTime / 1000);
|
|
}
|
|
codex.__resetOffsets();
|
|
|
|
// The scan should not stall and should return (no detection expected since all files are stale)
|
|
const result = codex.ingestCodexOnce({});
|
|
// All files are stale and not active, so no broadcast
|
|
assert.equal(result, 0);
|
|
lanes.deleteLane(lane.id);
|
|
});
|
|
|
|
it("detects correctly with Delete File in patch heredoc", () => {
|
|
const events = codex.eventsFromPayload({
|
|
type: "custom_tool_call",
|
|
name: "exec",
|
|
input: 'const patch = "*** Begin Patch\\n*** Delete File: old/removed.js\\n*** End Patch";',
|
|
});
|
|
// Delete creates an Edit event (not Write, not ignored)
|
|
assert.deepEqual(events, [{ tool_name: "Edit", tool_input: { file_path: "old/removed.js" } }]);
|
|
});
|
|
|
|
it("handles file paths with whitespace in patch patches", () => {
|
|
const events = codex.eventsFromPayload({
|
|
type: "custom_tool_call",
|
|
name: "exec",
|
|
input:
|
|
'const patch = "*** Begin Patch\\n*** Update File: src/my file.js \\n*** End Patch";',
|
|
});
|
|
// File path should be trimmed
|
|
assert.deepEqual(events, [{ tool_name: "Edit", tool_input: { file_path: "src/my file.js" } }]);
|
|
});
|
|
});
|