Files
AgentHub/server/__tests__/codex-ingest.test.js
T
nntrivi2001 952e8584f0 feat: per-change-block hunk actions (#50), codex lanes without a pipeline, macOS LaunchAgent
- git (#50): stage-hunk/discard-hunk take an optional `block` — one run of
  consecutive +/- lines inside an @@ hunk. `onlyBlock` (server/lib/git-diff.js)
  cuts git's fresh hunk down to it, folding the other blocks into the side
  being checked; LaneDiffView hangs one review bar per block (unified + split)
  and its arrows walk blocks.
- git exclude: entries AgentHub writes for a lane go to the COMMON git dir with
  the lane's path prefix (server/lib/git-exclude.js, used by router-apply and
  lane-mcp) — a linked worktree's own info/exclude is never read by git, and a
  subdir lane has no .git. MCP sync no longer fails with ENOTDIR on a worktree
  lane or creates a stray .git in a non-repo lane.
- codex lanes carry no pipeline: codex-ingest mirrors only idle/running,
  codex-nudge is removed, agents install writes nothing for codex, the
  Workspace hides stage/pipeline/feature pickers/PipelineMap for them, and
  every server start clears leftover codex stage data and the old AGENTS.md
  stage block.
- Changes panel: a whole file opens for reading and enters the editor on a
  click on its text (caret at the clicked line), not automatically.
- macOS: LaunchAgent start-at-login (scripts/launchd-service.js, picked by
  scripts/service-manager.js).
- Adds the AgentHub introduction slides and their image-embedding script.
2026-09-25 17:30:28 +07:00

211 lines
7.1 KiB
JavaScript

/**
* @file Unit tests for the Codex lane status mirror: one pass over fixture
* rollout journals flips a codex lane idle↔running, broadcasts it, and never
* touches a lane's stage.
* @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 status mirror", () => {
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), "codex-home-"));
const DIR = path.join(HOME, "sessions", "2026", "08", "27");
let prevHome;
let seq = 0;
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.__resetCache();
fs.rmSync(HOME, { recursive: true, force: true });
});
/** A rollout journal for `cwd` with one `npm run test:server` shell call. */
function writeRollout(cwd, { mtime } = {}) {
seq += 1;
const file = path.join(DIR, `rollout-2026-08-27T10-00-00-${seq}.jsonl`);
fs.writeFileSync(
file,
[
line({ cwd, session_id: `s${seq}` }, "session_meta"),
line({
type: "function_call",
name: "exec_command",
arguments: JSON.stringify({ cmd: "npm run test:server", workdir: cwd }),
}),
].join("\n") + "\n"
);
if (mtime !== undefined) fs.utimesSync(file, mtime / 1000, mtime / 1000);
codex.__resetCache();
return file;
}
const codexLane = (prefix = "codex-lane-") =>
lanes.createLane({
title: "cx",
cwd: fs.mkdtempSync(path.join(os.tmpdir(), prefix)),
tool: "codex",
});
it("marks a codex lane running while its journal is fresh, and broadcasts it", () => {
const lane = codexLane();
writeRollout(lane.cwd);
const seen = [];
assert.equal(codex.ingestCodexOnce({ broadcast: (id) => seen.push(id) }), 1);
assert.equal(lanes.getLane(lane.id).status, "running");
assert.deepEqual(seen, [lane.id]);
lanes.deleteLane(lane.id);
});
it("never writes a stage or a detection, whatever the journal ran", () => {
const lane = codexLane();
writeRollout(lane.cwd);
codex.ingestCodexOnce({});
const after = lanes.getLane(lane.id);
assert.equal(after.stage, "idle");
assert.equal(after.detected_stage, null);
assert.equal(after.detected_signal, null);
lanes.deleteLane(lane.id);
});
it("a second pass with nothing new changes nothing", () => {
const lane = codexLane();
writeRollout(lane.cwd);
codex.ingestCodexOnce({});
assert.equal(codex.ingestCodexOnce({}), 0);
lanes.deleteLane(lane.id);
});
it("puts a running lane back to idle once its journal goes quiet", () => {
const lane = codexLane();
lanes.updateLane(lane.id, { status: "running" });
writeRollout(lane.cwd, { mtime: Date.now() - 6 * 60 * 60 * 1000 });
assert.equal(codex.ingestCodexOnce({}), 1);
assert.equal(lanes.getLane(lane.id).status, "idle");
lanes.deleteLane(lane.id);
});
it("treats a journal whose mtime is in the FUTURE as idle", () => {
// A clock stepped back (or a restore that preserved timestamps) leaves a
// finished thread's mtime ahead of now; raw `now - mtime` would read as live.
const lane = codexLane();
writeRollout(lane.cwd, { mtime: Date.now() + 6 * 60 * 60 * 1000 });
assert.equal(codex.ingestCodexOnce({}), 0);
assert.equal(lanes.getLane(lane.id).status, "idle");
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(cwd);
assert.equal(codex.ingestCodexOnce({}), 0);
assert.equal(lanes.getLane(lane.id).status, "idle");
lanes.deleteLane(lane.id);
});
it("ignores a journal whose cwd belongs to no lane", () => {
const lane = codexLane();
writeRollout(fs.mkdtempSync(path.join(os.tmpdir(), "codex-orphan-")));
assert.equal(codex.ingestCodexOnce({}), 0);
assert.equal(lanes.getLane(lane.id).status, "idle");
lanes.deleteLane(lane.id);
});
it("handles a journal whose session_meta is unparseable gracefully", () => {
const lane = codexLane();
fs.writeFileSync(path.join(DIR, "rollout-bad-meta.jsonl"), "not valid json at all\n");
codex.__resetCache();
assert.equal(codex.ingestCodexOnce({}), 0);
assert.equal(lanes.getLane(lane.id).status, "idle");
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" });
writeRollout(inner);
const seen = [];
assert.equal(codex.ingestCodexOnce({ broadcast: (id) => seen.push(id) }), 1);
assert.equal(lanes.getLane(innerLane.id).status, "running");
assert.equal(lanes.getLane(outerLane.id).status, "idle");
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 lane = codexLane();
lanes.updateLane(lane.id, { run_id: "active-run" });
writeRollout(lane.cwd);
codex.ingestCodexOnce({});
assert.equal(lanes.getLane(lane.id).status, "idle");
lanes.deleteLane(lane.id);
});
it("does not change status from provisioning or failed", () => {
const lane = codexLane();
lanes.updateLane(lane.id, { status: "provisioning" });
writeRollout(lane.cwd);
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";
assert.equal(
codex.startCodexPoll(() => {}),
undefined
);
} finally {
if (oldMs === undefined) delete process.env.DASHBOARD_CODEX_POLL_MS;
else process.env.DASHBOARD_CODEX_POLL_MS = oldMs;
}
});
});