/** * @file Unit tests for the codex stage-declaration safety net: nudges a live * codex pane to run `ccam stage` when detection has clearly moved past the * declared stage and stayed there, without ever writing stage itself. * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const path = require("node:path"); const SUITE_DB_PATH = path.join( os.tmpdir(), `dashboard-codex-nudge-${Date.now()}-${process.pid}.db` ); process.env.DASHBOARD_DB_PATH = SUITE_DB_PATH; const { describe, it, beforeEach, afterEach } = require("node:test"); const assert = require("node:assert/strict"); const { db } = require("../db"); const lanes = require("../lib/lanes"); const tmux = require("../lib/tmux"); const nudge = require("../lib/codex-nudge"); /** Backdate a lane's detected_at so it looks `minutes` old to the sweep. */ function backdateDetection(laneId, minutes) { const iso = new Date(Date.now() - minutes * 60_000).toISOString(); db.prepare("UPDATE lanes SET detected_at = ? WHERE id = ?").run(iso, laneId); } describe("codex-nudge sweep", () => { let calls; beforeEach(() => { calls = []; tmux.__setExecImpl((args) => { calls.push(args); if (args[0] === "display-message") return "codex"; // pane running the codex CLI return ""; }); nudge.__resetNudgeState(); }); afterEach(() => { tmux.__reset(); }); function makeCodexLane({ cwd }) { const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" }); lanes.updateLane(lane.id, { run_id: `ccam-lane-${lane.id}` }); lanes.recordDetection(lane.id, { nodeId: "tests", signal: "npm run test:server" }); return lane; } it("nudges once a fresh detection has sat ahead of the declared stage past the delay", () => { const cwd = os.tmpdir(); const lane = makeCodexLane({ cwd: `${cwd}/nudge-a` }); backdateDetection(lane.id, 3); // older than the 2 min delay, well inside the 5 min TTL assert.equal(nudge.sweepCodexNudges(), 1); const sendKeys = calls.filter((a) => a[0] === "send-keys" && a[3] === "-l"); assert.equal(sendKeys.length, 1); assert.match(sendKeys[0][4], /ccam stage tests --tool codex/); // A second pass for the same still-undeclared detection must not repeat it. assert.equal(nudge.sweepCodexNudges(), 0); lanes.deleteLane(lane.id); }); it("never nudges before the delay has elapsed", () => { const cwd = os.tmpdir(); const lane = makeCodexLane({ cwd: `${cwd}/nudge-b` }); // detected_at is "now" — recordDetection just set it. assert.equal(nudge.sweepCodexNudges(), 0); lanes.deleteLane(lane.id); }); it("never nudges a stale detection (past the detection TTL)", () => { const cwd = os.tmpdir(); const lane = makeCodexLane({ cwd: `${cwd}/nudge-c` }); backdateDetection(lane.id, 10); // past the default 5 min TTL assert.equal(nudge.sweepCodexNudges(), 0); lanes.deleteLane(lane.id); }); it("never nudges a claude lane", () => { const cwd = os.tmpdir(); const lane = lanes.createLane({ title: "cc", cwd: `${cwd}/nudge-d`, tool: "claude" }); lanes.updateLane(lane.id, { run_id: `ccam-lane-${lane.id}` }); assert.equal(nudge.sweepCodexNudges(), 0); lanes.deleteLane(lane.id); }); it("never nudges a lane with no live run", () => { const cwd = os.tmpdir(); const lane = lanes.createLane({ title: "cx", cwd: `${cwd}/nudge-e`, tool: "codex" }); lanes.recordDetection(lane.id, { nodeId: "tests", signal: "npm run test:server" }); backdateDetection(lane.id, 3); assert.equal(nudge.sweepCodexNudges(), 0); lanes.deleteLane(lane.id); }); it("never types into a pane sitting at a bare shell prompt", () => { const cwd = os.tmpdir(); const lane = makeCodexLane({ cwd: `${cwd}/nudge-f` }); backdateDetection(lane.id, 3); tmux.__setExecImpl((args) => { calls.push(args); if (args[0] === "display-message") return "bash"; return ""; }); assert.equal(nudge.sweepCodexNudges(), 0); lanes.deleteLane(lane.id); }); it("nudges again once the target detected stage changes, after a prior nudge", () => { const cwd = os.tmpdir(); const lane = makeCodexLane({ cwd: `${cwd}/nudge-g` }); backdateDetection(lane.id, 3); assert.equal(nudge.sweepCodexNudges(), 1); // Detection advances to a later node; forward-only recordDetection accepts it. lanes.recordDetection(lane.id, { nodeId: "review", signal: "gh pr create" }); backdateDetection(lane.id, 3); assert.equal(nudge.sweepCodexNudges(), 1); lanes.deleteLane(lane.id); }); });