refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` -> `AGENTHUB_`), the Prometheus metric namespace (`ccam_*` -> `agenthub_*`), the tmux session prefix (`ccam-lane-<id>` -> `agenthub-lane-<id>`), the per-repo runtime profile directory (`.ccam/profile` -> `.agenthub/profile`), the lanes state root (`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` -> `/agenthub:*`). Because there is no alias, an existing install has to be updated by hand: rename its `.ccam/profile` directory and any `CCAM_*` variables, and reinstall the plugin under its new name. Two other name families are deliberately left alone here, since they are not just strings: the `Claude-Code-Monitor` repository/marketplace name, and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard` runtime directory, which holds the live database and needs a migration rather than a rename. Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file Tests for the `ccam` umbrella CLI (bin/ccam.js). Spawns the real CLI
|
||||
* @file Tests for the `agenthub` umbrella CLI (bin/agenthub.js). Spawns the real CLI
|
||||
* as a subprocess against a live in-test dashboard server and asserts each
|
||||
* command's output shape across the whole surface: monitoring (health, stats,
|
||||
* kanban), data browsing (sessions, session detail, agents, events), insights
|
||||
@@ -23,23 +23,23 @@ const os = require("os");
|
||||
const http = require("http");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const STAMP = `ccam-cli-${Date.now()}-${process.pid}`;
|
||||
const STAMP = `agenthub-cli-${Date.now()}-${process.pid}`;
|
||||
const TMP = path.join(os.tmpdir(), STAMP);
|
||||
process.env.DASHBOARD_DB_PATH = path.join(TMP, "dashboard.db");
|
||||
process.env.CLAUDE_HOME = path.join(TMP, "home");
|
||||
process.env.DASHBOARD_DATA_DIR = path.join(TMP, "data");
|
||||
process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
||||
// Every spawn below inherits `process.env` — a shell that forces color
|
||||
// (FORCE_COLOR/CCAM_COLOR set in the environment running this test) would
|
||||
// (FORCE_COLOR/AGENTHUB_COLOR set in the environment running this test) would
|
||||
// leak into the spawned CLI child and defeat its own "colors off when piped"
|
||||
// behavior, which every plain-text assertion in this file assumes.
|
||||
delete process.env.FORCE_COLOR;
|
||||
delete process.env.CCAM_COLOR;
|
||||
delete process.env.AGENTHUB_COLOR;
|
||||
|
||||
const { createApp, startServer } = require("../index");
|
||||
const { db } = require("../db");
|
||||
|
||||
const CLI = path.resolve(__dirname, "..", "..", "bin", "ccam.js");
|
||||
const CLI = path.resolve(__dirname, "..", "..", "bin", "agenthub.js");
|
||||
|
||||
let server;
|
||||
let PORT;
|
||||
@@ -49,7 +49,7 @@ let PORT;
|
||||
* the server runs in this same process, so a blocking spawnSync would starve
|
||||
* its event loop and every CLI request would hang until timeout.
|
||||
*/
|
||||
function ccam(...args) {
|
||||
function agenthub(...args) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(process.execPath, [CLI, ...args], {
|
||||
env: { ...process.env, DASHBOARD_PORT: String(PORT) },
|
||||
@@ -98,7 +98,7 @@ before(async () => {
|
||||
// Seed one session via the real hook path so list commands have a row.
|
||||
const code = await post("/api/hooks/event", {
|
||||
hook_type: "Stop",
|
||||
data: { session_id: "cli-test-session-0001", cwd: "/tmp/ccam-cli" },
|
||||
data: { session_id: "cli-test-session-0001", cwd: "/tmp/agenthub-cli" },
|
||||
});
|
||||
assert.equal(code, 200);
|
||||
});
|
||||
@@ -113,9 +113,9 @@ after(() => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("ccam CLI — monitoring", () => {
|
||||
describe("agenthub CLI — monitoring", () => {
|
||||
it("health reports the dashboard as up", async () => {
|
||||
const { code, out } = await ccam("health");
|
||||
const { code, out } = await agenthub("health");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Dashboard/);
|
||||
assert.match(out, /up/);
|
||||
@@ -124,7 +124,7 @@ describe("ccam CLI — monitoring", () => {
|
||||
});
|
||||
|
||||
it("stats prints totals including the seeded session", async () => {
|
||||
const { code, out } = await ccam("stats");
|
||||
const { code, out } = await agenthub("stats");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Total sessions/);
|
||||
assert.match(out, /Events today/);
|
||||
@@ -132,7 +132,7 @@ describe("ccam CLI — monitoring", () => {
|
||||
});
|
||||
|
||||
it("kanban groups sessions and agents into status columns", async () => {
|
||||
const { code, out } = await ccam("kanban");
|
||||
const { code, out } = await agenthub("kanban");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Sessions/);
|
||||
assert.match(out, /Agents/);
|
||||
@@ -141,22 +141,22 @@ describe("ccam CLI — monitoring", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — data browsing", () => {
|
||||
describe("agenthub CLI — data browsing", () => {
|
||||
it("sessions lists the seeded session", async () => {
|
||||
const { code, out } = await ccam("sessions", "--limit", "5");
|
||||
const { code, out } = await agenthub("sessions", "--limit", "5");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /cli-test/);
|
||||
assert.match(out, /of 1 session/);
|
||||
});
|
||||
|
||||
it("sessions --status filters correctly", async () => {
|
||||
const { code, out } = await ccam("sessions", "--status", "completed");
|
||||
const { code, out } = await agenthub("sessions", "--status", "completed");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /of 0 session/);
|
||||
});
|
||||
|
||||
it("session <id> shows detail with agents and recent events", async () => {
|
||||
const { code, out } = await ccam("session", "cli-test-session-0001");
|
||||
const { code, out } = await agenthub("session", "cli-test-session-0001");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /cli-test-session-0001/);
|
||||
assert.match(out, /Agents/);
|
||||
@@ -164,54 +164,54 @@ describe("ccam CLI — data browsing", () => {
|
||||
});
|
||||
|
||||
it("session without an id exits 1 with usage", async () => {
|
||||
const { code, err } = await ccam("session");
|
||||
const { code, err } = await agenthub("session");
|
||||
assert.equal(code, 1);
|
||||
assert.match(err, /Usage: ccam session/);
|
||||
assert.match(err, /Usage: agenthub session/);
|
||||
});
|
||||
|
||||
it("agents lists the auto-created main agent", async () => {
|
||||
const { code, out } = await ccam("agents", "--session", "cli-test-session-0001");
|
||||
const { code, out } = await agenthub("agents", "--session", "cli-test-session-0001");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /main/);
|
||||
});
|
||||
|
||||
it("events lists the seeded Stop event", async () => {
|
||||
const { code, out } = await ccam("events", "--session", "cli-test-session-0001");
|
||||
const { code, out } = await agenthub("events", "--session", "cli-test-session-0001");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Stop/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — insights", () => {
|
||||
describe("agenthub CLI — insights", () => {
|
||||
it("analytics prints token totals and averages", async () => {
|
||||
const { code, out } = await ccam("analytics");
|
||||
const { code, out } = await agenthub("analytics");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Tokens/);
|
||||
assert.match(out, /Input/);
|
||||
});
|
||||
|
||||
it("workflows prints intelligence stats", async () => {
|
||||
const { code, out } = await ccam("workflows");
|
||||
const { code, out } = await agenthub("workflows");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Workflow intelligence/);
|
||||
assert.match(out, /Sessions analyzed/);
|
||||
});
|
||||
|
||||
it("runs lists dynamic workflow runs (empty is fine)", async () => {
|
||||
const { code, out } = await ccam("runs");
|
||||
const { code, out } = await agenthub("runs");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Run/);
|
||||
assert.match(out, /Status/);
|
||||
});
|
||||
|
||||
it("cost prints a total", async () => {
|
||||
const { code, out } = await ccam("cost");
|
||||
const { code, out } = await agenthub("cost");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Total estimated cost: \$/);
|
||||
});
|
||||
|
||||
it("cost --session scopes the total to one session", async () => {
|
||||
const { code, out } = await ccam("cost", "--session", "cli-test-session-0001");
|
||||
const { code, out } = await agenthub("cost", "--session", "cli-test-session-0001");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Session cost/);
|
||||
assert.match(out, /cli-test-session-0001/);
|
||||
@@ -225,7 +225,7 @@ describe("ccam CLI — insights", () => {
|
||||
"INSERT INTO token_usage (session_id, model, web_search_requests) VALUES (?, ?, ?)"
|
||||
).run("cli-test-session-0001", "claude-sonnet-5", 200);
|
||||
try {
|
||||
const { code, out } = await ccam("cost", "--session", "cli-test-session-0001");
|
||||
const { code, out } = await agenthub("cost", "--session", "cli-test-session-0001");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Server-tool surcharges/);
|
||||
assert.match(out, /web search \$/);
|
||||
@@ -241,58 +241,58 @@ describe("ccam CLI — insights", () => {
|
||||
// and reports it via unpriced_models; the CLI must surface that.
|
||||
db.prepare(
|
||||
"INSERT INTO token_usage (session_id, model, input_tokens, output_tokens) VALUES (?, ?, ?, ?)"
|
||||
).run("cli-test-session-0001", "ccam-mystery-model-9", 1200, 300);
|
||||
).run("cli-test-session-0001", "agenthub-mystery-model-9", 1200, 300);
|
||||
try {
|
||||
const { code, out } = await ccam("cost");
|
||||
const { code, out } = await agenthub("cost");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /no pricing rule/);
|
||||
assert.match(out, /ccam-mystery-model-9/);
|
||||
assert.match(out, /ccam pricing set/);
|
||||
assert.match(out, /agenthub-mystery-model-9/);
|
||||
assert.match(out, /agenthub pricing set/);
|
||||
} finally {
|
||||
db.prepare("DELETE FROM token_usage WHERE model = 'ccam-mystery-model-9'").run();
|
||||
db.prepare("DELETE FROM token_usage WHERE model = 'agenthub-mystery-model-9'").run();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — alerts, rules, webhooks", () => {
|
||||
describe("agenthub CLI — alerts, rules, webhooks", () => {
|
||||
it("alerts lists the (empty) fired-alert feed", async () => {
|
||||
const { code, out } = await ccam("alerts");
|
||||
const { code, out } = await agenthub("alerts");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /unacknowledged of/);
|
||||
});
|
||||
|
||||
it("alerts ack-all succeeds on an empty feed", async () => {
|
||||
const { code, out } = await ccam("alerts", "ack-all");
|
||||
const { code, out } = await agenthub("alerts", "ack-all");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Acknowledged/);
|
||||
});
|
||||
|
||||
it("rules lists alert rules", async () => {
|
||||
const { code, out } = await ccam("rules");
|
||||
const { code, out } = await agenthub("rules");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Enabled/);
|
||||
});
|
||||
|
||||
it("webhooks lists targets", async () => {
|
||||
const { code, out } = await ccam("webhooks");
|
||||
const { code, out } = await agenthub("webhooks");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Provider/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — pricing", () => {
|
||||
describe("agenthub CLI — pricing", () => {
|
||||
it("pricing lists the default rules", async () => {
|
||||
const { code, out } = await ccam("pricing");
|
||||
const { code, out } = await agenthub("pricing");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Pattern/);
|
||||
assert.match(out, /claude/);
|
||||
});
|
||||
|
||||
it("pricing set / delete round-trips a custom rule", async () => {
|
||||
const set = await ccam(
|
||||
const set = await agenthub(
|
||||
"pricing",
|
||||
"set",
|
||||
"ccam-test-model%",
|
||||
"agenthub-test-model%",
|
||||
"--input",
|
||||
"1",
|
||||
"--output",
|
||||
@@ -304,18 +304,18 @@ describe("ccam CLI — pricing", () => {
|
||||
);
|
||||
assert.equal(set.code, 0, `stderr: ${set.err} stdout: ${set.out}`);
|
||||
assert.match(set.out, /saved/);
|
||||
const list = await ccam("pricing");
|
||||
assert.match(list.out, /ccam-test-model%/);
|
||||
const del = await ccam("pricing", "delete", "ccam-test-model%");
|
||||
const list = await agenthub("pricing");
|
||||
assert.match(list.out, /agenthub-test-model%/);
|
||||
const del = await agenthub("pricing", "delete", "agenthub-test-model%");
|
||||
assert.equal(del.code, 0);
|
||||
assert.match(del.out, /deleted/);
|
||||
});
|
||||
|
||||
it("pricing set persists fast-mode and intro rates via flags", async () => {
|
||||
const set = await ccam(
|
||||
const set = await agenthub(
|
||||
"pricing",
|
||||
"set",
|
||||
"ccam-fast-model%",
|
||||
"agenthub-fast-model%",
|
||||
"--input",
|
||||
"5",
|
||||
"--output",
|
||||
@@ -332,37 +332,45 @@ describe("ccam CLI — pricing", () => {
|
||||
"2099-01-01"
|
||||
);
|
||||
assert.equal(set.code, 0, `stderr: ${set.err} stdout: ${set.out}`);
|
||||
const list = await ccam("pricing");
|
||||
assert.match(list.out, /ccam-fast-model%/);
|
||||
const list = await agenthub("pricing");
|
||||
assert.match(list.out, /agenthub-fast-model%/);
|
||||
assert.match(list.out, /\$10\/\$50/); // fast in/out column
|
||||
assert.match(list.out, /\$2\/\$10/); // intro in/out column
|
||||
assert.match(list.out, /2099-01-01/);
|
||||
// A plain rate edit without intro flags must preserve the promo (the
|
||||
// intro block is only sent when an --intro-* flag is present).
|
||||
const edit = await ccam("pricing", "set", "ccam-fast-model%", "--input", "6", "--output", "30");
|
||||
const edit = await agenthub(
|
||||
"pricing",
|
||||
"set",
|
||||
"agenthub-fast-model%",
|
||||
"--input",
|
||||
"6",
|
||||
"--output",
|
||||
"30"
|
||||
);
|
||||
assert.equal(edit.code, 0);
|
||||
const after = await ccam("pricing");
|
||||
const after = await agenthub("pricing");
|
||||
assert.match(after.out, /\$6/);
|
||||
assert.match(after.out, /2099-01-01/);
|
||||
await ccam("pricing", "delete", "ccam-fast-model%");
|
||||
await agenthub("pricing", "delete", "agenthub-fast-model%");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — import & administration", () => {
|
||||
describe("agenthub CLI — import & administration", () => {
|
||||
it("import rescan runs against the (empty) default projects dir", async () => {
|
||||
const { code, out } = await ccam("import", "rescan");
|
||||
const { code, out } = await agenthub("import", "rescan");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /imported \d+/);
|
||||
});
|
||||
|
||||
it("import without a subcommand exits 1 with usage", async () => {
|
||||
const { code, err } = await ccam("import");
|
||||
const { code, err } = await agenthub("import");
|
||||
assert.equal(code, 1);
|
||||
assert.match(err, /Usage: ccam import/);
|
||||
assert.match(err, /Usage: agenthub import/);
|
||||
});
|
||||
|
||||
it("doctor reports API, hooks, database, remotes, and uptime lines", async () => {
|
||||
const { code, out } = await ccam("doctor");
|
||||
const { code, out } = await agenthub("doctor");
|
||||
// Exit 1 when hooks are missing (common in the test harness) — still prints a full report.
|
||||
assert.ok(code === 0 || code === 1, `unexpected exit ${code}`);
|
||||
assert.match(out, /API reachable/);
|
||||
@@ -373,7 +381,7 @@ describe("ccam CLI — import & administration", () => {
|
||||
});
|
||||
|
||||
it("info dumps system info JSON", async () => {
|
||||
const { code, out } = await ccam("info");
|
||||
const { code, out } = await agenthub("info");
|
||||
assert.equal(code, 0);
|
||||
const parsed = JSON.parse(out);
|
||||
assert.ok(parsed.db);
|
||||
@@ -382,7 +390,7 @@ describe("ccam CLI — import & administration", () => {
|
||||
|
||||
it("export writes a JSON file containing the seeded session", async () => {
|
||||
const file = path.join(TMP, "export.json");
|
||||
const { code, out } = await ccam("export", file);
|
||||
const { code, out } = await agenthub("export", file);
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Exported to/);
|
||||
const data = JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
@@ -393,49 +401,49 @@ describe("ccam CLI — import & administration", () => {
|
||||
// The test process runs inside the real repo clone, so the route always
|
||||
// reports git_repo: true; the CLI exits 0 whether the checkout is behind,
|
||||
// current, or the remote is unreachable (fetch errors are informational).
|
||||
const { code, out } = await ccam("update-check");
|
||||
const { code, out } = await agenthub("update-check");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Dashboard updates/);
|
||||
assert.match(out, /checkout|commit|remote|update/i);
|
||||
});
|
||||
|
||||
it("cleanup without flags exits 1 with usage", async () => {
|
||||
const { code, err } = await ccam("cleanup");
|
||||
const { code, err } = await agenthub("cleanup");
|
||||
assert.equal(code, 1);
|
||||
assert.match(err, /Usage: ccam cleanup/);
|
||||
assert.match(err, /Usage: agenthub cleanup/);
|
||||
});
|
||||
|
||||
it("cleanup with flags runs", async () => {
|
||||
const { code, out } = await ccam("cleanup", "--days", "3650");
|
||||
const { code, out } = await agenthub("cleanup", "--days", "3650");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Cleanup done/);
|
||||
});
|
||||
|
||||
it("clear-data REFUSES without --yes", async () => {
|
||||
const { code, err } = await ccam("clear-data");
|
||||
const { code, err } = await agenthub("clear-data");
|
||||
assert.equal(code, 1);
|
||||
assert.match(err, /--yes/);
|
||||
// Data must be intact.
|
||||
const { out } = await ccam("sessions");
|
||||
const { out } = await agenthub("sessions");
|
||||
assert.match(out, /of 1 session/);
|
||||
});
|
||||
|
||||
it("clear-data --yes wipes rows (runs last)", async () => {
|
||||
const { code, out } = await ccam("clear-data", "--yes");
|
||||
const { code, out } = await agenthub("clear-data", "--yes");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /cleared/);
|
||||
const { out: after } = await ccam("sessions");
|
||||
const { out: after } = await agenthub("sessions");
|
||||
assert.match(after, /of 0 session/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — offline mode (server down, DB read directly)", () => {
|
||||
describe("agenthub CLI — offline mode (server down, DB read directly)", () => {
|
||||
// The online admin suite ends with clear-data, so re-seed one session here
|
||||
// (through the live server) for the offline reads to find.
|
||||
before(async () => {
|
||||
const code = await post("/api/hooks/event", {
|
||||
hook_type: "Stop",
|
||||
data: { session_id: "cli-test-offline-0002", cwd: "/tmp/ccam-cli-off" },
|
||||
data: { session_id: "cli-test-offline-0002", cwd: "/tmp/agenthub-cli-off" },
|
||||
});
|
||||
assert.equal(code, 200);
|
||||
});
|
||||
@@ -463,7 +471,7 @@ describe("ccam CLI — offline mode (server down, DB read directly)", () => {
|
||||
const { code, out } = await offline("sessions", "--limit", "5");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Offline mode/);
|
||||
assert.match(out, /ccam start/);
|
||||
assert.match(out, /agenthub start/);
|
||||
assert.match(out, /cli-test/);
|
||||
assert.match(out, /of 1 session/);
|
||||
});
|
||||
@@ -564,7 +572,7 @@ describe("ccam CLI — offline mode (server down, DB read directly)", () => {
|
||||
return;
|
||||
}
|
||||
// Run offline WITHOUT the probe kill-switch: the seeded session's cwd
|
||||
// (/tmp/ccam-cli-off) has no running claude process, so its stored
|
||||
// (/tmp/agenthub-cli-off) has no running claude process, so its stored
|
||||
// "active" status must be DISPLAYED as completed, with the footnote —
|
||||
// and the database itself must keep the stored status.
|
||||
const r = await new Promise((resolve) => {
|
||||
@@ -593,9 +601,9 @@ describe("ccam CLI — offline mode (server down, DB read directly)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — help & errors", () => {
|
||||
describe("agenthub CLI — help & errors", () => {
|
||||
it("help lists every command group", async () => {
|
||||
const { code, out } = await ccam("help");
|
||||
const { code, out } = await agenthub("help");
|
||||
assert.equal(code, 0);
|
||||
for (const word of [
|
||||
"status",
|
||||
@@ -631,45 +639,45 @@ describe("ccam CLI — help & errors", () => {
|
||||
});
|
||||
|
||||
it("no arguments prints help and exits 0", async () => {
|
||||
const { code, out } = await ccam();
|
||||
const { code, out } = await agenthub();
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /Usage: ccam <command>/);
|
||||
assert.match(out, /Usage: agenthub <command>/);
|
||||
});
|
||||
|
||||
it("version prints the package version", async () => {
|
||||
const pkg = JSON.parse(
|
||||
fs.readFileSync(path.resolve(__dirname, "..", "..", "package.json"), "utf8")
|
||||
);
|
||||
const { code, out } = await ccam("version");
|
||||
const { code, out } = await agenthub("version");
|
||||
assert.equal(code, 0);
|
||||
assert.equal(out.trim(), `ccam ${pkg.version}`);
|
||||
const flag = await ccam("--version");
|
||||
assert.equal(flag.out.trim(), `ccam ${pkg.version}`);
|
||||
assert.equal(out.trim(), `agenthub ${pkg.version}`);
|
||||
const flag = await agenthub("--version");
|
||||
assert.equal(flag.out.trim(), `agenthub ${pkg.version}`);
|
||||
});
|
||||
|
||||
it("--no-color is accepted anywhere on the command line", async () => {
|
||||
const before = await ccam("--no-color", "health");
|
||||
const before = await agenthub("--no-color", "health");
|
||||
assert.equal(before.code, 0);
|
||||
assert.match(before.out, /Dashboard/);
|
||||
const after = await ccam("sessions", "--no-color");
|
||||
const after = await agenthub("sessions", "--no-color");
|
||||
assert.equal(after.code, 0);
|
||||
assert.match(after.out, /of 1 session/);
|
||||
});
|
||||
|
||||
it("piped output contains no ANSI escape codes (colors off when not a TTY)", async () => {
|
||||
const { out } = await ccam("stats");
|
||||
const { out } = await agenthub("stats");
|
||||
assert.ok(!out.includes("\x1b["), "piped output must be plain text");
|
||||
});
|
||||
|
||||
it("tables render with box-drawing borders and an Updated column", async () => {
|
||||
const { out } = await ccam("sessions");
|
||||
const { out } = await agenthub("sessions");
|
||||
assert.ok(out.includes("╭"), "table should have a top border");
|
||||
assert.ok(out.includes("│"), "table should have column separators");
|
||||
assert.match(out, /Updated/);
|
||||
});
|
||||
|
||||
it("unknown command exits 1 with an error", async () => {
|
||||
const { code, err } = await ccam("frobnicate");
|
||||
const { code, err } = await agenthub("frobnicate");
|
||||
assert.equal(code, 1);
|
||||
assert.match(err, /Unknown command/);
|
||||
});
|
||||
@@ -685,12 +693,12 @@ describe("ccam CLI — help & errors", () => {
|
||||
});
|
||||
assert.equal(r.code, 1);
|
||||
assert.match(r.err, /Dashboard server is NOT running/);
|
||||
assert.match(r.err, /ccam start/);
|
||||
assert.match(r.err, /agenthub start/);
|
||||
assert.match(r.err, /npm run dev/);
|
||||
});
|
||||
|
||||
it("status reports running when the server is up", async () => {
|
||||
const { code, out } = await ccam("status");
|
||||
const { code, out } = await agenthub("status");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /running/);
|
||||
assert.match(out, new RegExp(String(PORT)));
|
||||
@@ -707,20 +715,20 @@ describe("ccam CLI — help & errors", () => {
|
||||
});
|
||||
assert.equal(r.code, 1);
|
||||
assert.match(r.out, /NOT running/);
|
||||
assert.match(r.out, /ccam start/);
|
||||
assert.match(r.out, /agenthub start/);
|
||||
});
|
||||
|
||||
it("start no-ops with a pointer when a server is already running", async () => {
|
||||
const { code, out } = await ccam("start");
|
||||
const { code, out } = await agenthub("start");
|
||||
assert.equal(code, 0);
|
||||
assert.match(out, /already running/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI — interactive REPL", () => {
|
||||
describe("agenthub CLI — interactive REPL", () => {
|
||||
/**
|
||||
* Drive `ccam repl` with piped stdin (non-TTY). Each typed line is executed
|
||||
* as a child `ccam` process, so grandchildren reach the in-test server via
|
||||
* Drive `agenthub repl` with piped stdin (non-TTY). Each typed line is executed
|
||||
* as a child `agenthub` process, so grandchildren reach the in-test server via
|
||||
* the DASHBOARD_PORT override; async spawn keeps the event loop free.
|
||||
*/
|
||||
function repl(input, port = PORT) {
|
||||
@@ -746,7 +754,7 @@ describe("ccam CLI — interactive REPL", () => {
|
||||
const { code, out } = await repl("version\nstats\nexit\n");
|
||||
assert.equal(code, 0);
|
||||
// version output precedes the stats table → sequential execution.
|
||||
const vIdx = out.indexOf("ccam ");
|
||||
const vIdx = out.indexOf("agenthub ");
|
||||
const sIdx = out.indexOf("Total sessions");
|
||||
assert.ok(vIdx >= 0 && sIdx >= 0 && vIdx < sIdx, `order: v=${vIdx} s=${sIdx}`);
|
||||
});
|
||||
@@ -339,7 +339,7 @@ describe("readPluginContributions", () => {
|
||||
assert.equal(c.items.commands[0].description, null);
|
||||
});
|
||||
|
||||
// The real motivation: this repo's own `ccam` plugin keeps nothing in
|
||||
// The real motivation: this repo's own `agenthub` plugin keeps nothing in
|
||||
// `commands/` and declares each file in plugin.json instead, so a
|
||||
// directory-only reader reported "0 commands" for it.
|
||||
it("includes commands the manifest declares outside the conventional directory", () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ccplugin-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ccplugin-"));
|
||||
const cli = require("../lib/cc-plugin-cli");
|
||||
|
||||
/**
|
||||
@@ -72,9 +72,15 @@ describe("cc-plugin-cli argument validation", () => {
|
||||
});
|
||||
|
||||
it("accepts <name> and <name>@<marketplace> plugin specs only", () => {
|
||||
assert.equal(cli.assertPluginSpec("ccam"), "ccam");
|
||||
assert.equal(cli.assertPluginSpec("ccam@sg"), "ccam@sg");
|
||||
for (const bad of ["ccam@bad/repo", "../ccam", "ccam ccam", "@sg", "-ccam"]) {
|
||||
assert.equal(cli.assertPluginSpec("agenthub"), "agenthub");
|
||||
assert.equal(cli.assertPluginSpec("agenthub@sg"), "agenthub@sg");
|
||||
for (const bad of [
|
||||
"agenthub@bad/repo",
|
||||
"../agenthub",
|
||||
"agenthub agenthub",
|
||||
"@sg",
|
||||
"-agenthub",
|
||||
]) {
|
||||
assert.throws(() => cli.assertPluginSpec(bad), undefined, `accepted ${bad}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const path = require("path");
|
||||
|
||||
const { sourceHash, mcpBuildStatus, writeHash } = require("../../scripts/check-mcp-build");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-mcpbuild-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-mcpbuild-"));
|
||||
const SRC = path.join(ROOT, "mcp", "src");
|
||||
const BUILD = path.join(ROOT, "mcp", "build");
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-"));
|
||||
const { ciStatus } = require("../lib/ci-status");
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* client bundle is served from the writable runtime dir instead of the
|
||||
* checkout's client/dist. Also asserts the API still answers when the
|
||||
* configured bundle directory does not exist yet (the normal state before
|
||||
* /ccam-open builds it).
|
||||
* /agenthub-open builds it).
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ const os = require("os");
|
||||
const path = require("path");
|
||||
const http = require("http");
|
||||
|
||||
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-clientdist-"));
|
||||
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-clientdist-"));
|
||||
const DIST = path.join(TMP, "client-dist");
|
||||
fs.mkdirSync(DIST);
|
||||
fs.writeFileSync(path.join(DIST, "index.html"), "<!doctype html><title>from-runtime</title>");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file Unit tests for the codex evidence safety net: nudges a live codex pane
|
||||
* to run `ccam stage --evidence` when the lane has sat at an inferred (
|
||||
* to run `agenthub stage --evidence` when the lane has sat at an inferred (
|
||||
* evidence-less) stage past the delay, without ever writing stage itself.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -52,7 +52,7 @@ describe("codex-nudge sweep", () => {
|
||||
|
||||
function makeCodexLane({ cwd, stage = "tests", evidence }) {
|
||||
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
||||
lanes.updateLane(lane.id, { run_id: `ccam-lane-${lane.id}` });
|
||||
lanes.updateLane(lane.id, { run_id: `agenthub-lane-${lane.id}` });
|
||||
lanes.setStage(lane.id, { stage, evidence });
|
||||
return lane;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ describe("codex-nudge sweep", () => {
|
||||
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 --evidence/);
|
||||
assert.match(sendKeys[0][4], /agenthub stage tests --tool codex --evidence/);
|
||||
|
||||
// A second pass for the same still-unevidenced stage must not repeat it.
|
||||
assert.equal(nudge.sweepCodexNudges(), 0);
|
||||
@@ -91,7 +91,7 @@ describe("codex-nudge sweep", () => {
|
||||
it("never nudges a lane that has not entered a pipeline node", () => {
|
||||
const cwd = os.tmpdir();
|
||||
const lane = lanes.createLane({ title: "cx", cwd: `${cwd}/nudge-h`, tool: "codex" });
|
||||
lanes.updateLane(lane.id, { run_id: `ccam-lane-${lane.id}` });
|
||||
lanes.updateLane(lane.id, { run_id: `agenthub-lane-${lane.id}` });
|
||||
assert.equal(nudge.sweepCodexNudges(), 0);
|
||||
lanes.deleteLane(lane.id);
|
||||
});
|
||||
@@ -99,7 +99,7 @@ describe("codex-nudge sweep", () => {
|
||||
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}` });
|
||||
lanes.updateLane(lane.id, { run_id: `agenthub-lane-${lane.id}` });
|
||||
lanes.setStage(lane.id, { stage: "tests" });
|
||||
backdateStage(lane.id, 3);
|
||||
assert.equal(nudge.sweepCodexNudges(), 0);
|
||||
|
||||
@@ -11,7 +11,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gh-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-gh-"));
|
||||
const { gh } = require("../lib/gh");
|
||||
|
||||
/** Puts a fake `gh` script on PATH for the duration of one test. */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* A Claude Code Workflow-tool run (dynamic workflow / fleet of sub-agents)
|
||||
* emits NO hooks, so in a headless `claude -p` run, a CI job, or an HPC/cluster
|
||||
* compute node its per-run journal is never ingested live. Before this fix the
|
||||
* CLI import path (`ccam import rescan` → importAllSessions, `ccam import path`
|
||||
* CLI import path (`agenthub import rescan` → importAllSessions, `agenthub import path`
|
||||
* → importFromDirectory) never called the workflow-journal ingest, so the
|
||||
* nested inner-agent transcripts under
|
||||
* `<sid>/subagents/workflows/<runId>/agent-*.jsonl` were imported with
|
||||
@@ -28,7 +28,7 @@ const path = require("path");
|
||||
|
||||
// Scope the importer at a throwaway CLAUDE_HOME and DB before any server module
|
||||
// loads — import-history captures PROJECTS_DIR from CLAUDE_HOME at require time.
|
||||
const TMP_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), `ccam-wf-link-${process.pid}-`));
|
||||
const TMP_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), `agenthub-wf-link-${process.pid}-`));
|
||||
process.env.CLAUDE_HOME = TMP_ROOT;
|
||||
process.env.DASHBOARD_DATA_DIR = path.join(TMP_ROOT, "data");
|
||||
process.env.DASHBOARD_DB_PATH = path.join(TMP_ROOT, "dashboard.db");
|
||||
@@ -148,7 +148,7 @@ after(() => {
|
||||
});
|
||||
|
||||
describe("offline CLI import links Workflow-tool inner agents", () => {
|
||||
it("importFromDirectory (ccam import path) links every inner agent to its run", async () => {
|
||||
it("importFromDirectory (agenthub import path) links every inner agent to its run", async () => {
|
||||
const SID = "aaaa1111-0000-4000-8000-000000000001";
|
||||
const RUN = "wf_dirimport01";
|
||||
const AGENTS = ["d1", "d2", "d3"];
|
||||
@@ -171,7 +171,7 @@ describe("offline CLI import links Workflow-tool inner agents", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("importAllSessions (ccam import rescan) links every inner agent to its run", async () => {
|
||||
it("importAllSessions (agenthub import rescan) links every inner agent to its run", async () => {
|
||||
const SID = "bbbb2222-0000-4000-8000-000000000002";
|
||||
const RUN = "wf_rescan02";
|
||||
const AGENTS = ["r1", "r2", "r3", "r4"];
|
||||
|
||||
@@ -108,7 +108,7 @@ function fixtureLines(sessionId, cwd, model, inputTok, outputTok) {
|
||||
}
|
||||
|
||||
function writeFixtureDir() {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-fixture-"));
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-fixture-"));
|
||||
const projDir = path.join(root, "-Users-demo-project");
|
||||
fs.mkdirSync(projDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -175,14 +175,14 @@ describe("POST /api/import/scan-path validation", () => {
|
||||
|
||||
it("rejects non-existent paths", async () => {
|
||||
const res = await post("/api/import/scan-path", {
|
||||
path: "/definitely/does/not/exist/ccam-" + Date.now(),
|
||||
path: "/definitely/does/not/exist/agenthub-" + Date.now(),
|
||||
});
|
||||
assert.equal(res.status, 400);
|
||||
assert.equal(res.body.error.code, "PATH_NOT_FOUND");
|
||||
});
|
||||
|
||||
it("rejects files (not directories)", async () => {
|
||||
const tmp = path.join(os.tmpdir(), `ccam-not-dir-${Date.now()}.txt`);
|
||||
const tmp = path.join(os.tmpdir(), `agenthub-not-dir-${Date.now()}.txt`);
|
||||
fs.writeFileSync(tmp, "hello");
|
||||
try {
|
||||
const res = await post("/api/import/scan-path", { path: tmp });
|
||||
@@ -260,14 +260,14 @@ describe("POST /api/import/scan-path happy path", () => {
|
||||
|
||||
describe("archive helpers", () => {
|
||||
it("isPathInside rejects traversal", () => {
|
||||
const root = path.resolve("/tmp/ccam-root");
|
||||
assert.equal(archive.isPathInside(root, "/tmp/ccam-root/ok.jsonl"), true);
|
||||
const root = path.resolve("/tmp/agenthub-root");
|
||||
assert.equal(archive.isPathInside(root, "/tmp/agenthub-root/ok.jsonl"), true);
|
||||
assert.equal(archive.isPathInside(root, "/tmp/other/bad.jsonl"), false);
|
||||
assert.equal(archive.isPathInside(root, "/tmp/ccam-root/../escape"), false);
|
||||
assert.equal(archive.isPathInside(root, "/tmp/agenthub-root/../escape"), false);
|
||||
});
|
||||
|
||||
it("safeJoin rejects absolute and traversal entries", () => {
|
||||
const root = path.resolve("/tmp/ccam-root");
|
||||
const root = path.resolve("/tmp/agenthub-root");
|
||||
assert.equal(archive.safeJoin(root, "/etc/passwd"), path.join(root, "etc/passwd"));
|
||||
assert.equal(archive.safeJoin(root, "../escape.txt"), null);
|
||||
assert.equal(archive.safeJoin(root, "deep/../../escape"), null);
|
||||
@@ -286,7 +286,7 @@ describe("archive helpers", () => {
|
||||
});
|
||||
|
||||
it("extractGzSingle decompresses plain gz", async () => {
|
||||
const dest = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gz-"));
|
||||
const dest = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-gz-"));
|
||||
const src = path.join(dest, "sample.jsonl.gz");
|
||||
fs.writeFileSync(src, zlib.gzipSync(Buffer.from('{"ok":true}\n')));
|
||||
try {
|
||||
@@ -301,7 +301,7 @@ describe("archive helpers", () => {
|
||||
|
||||
describe("importFromDirectory directly", () => {
|
||||
it("reports progress and never throws on empty dirs", async () => {
|
||||
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-empty-"));
|
||||
const empty = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-empty-"));
|
||||
try {
|
||||
const events = [];
|
||||
const counters = await importHistory.importFromDirectory({ db, stmts }, empty, {
|
||||
@@ -317,7 +317,7 @@ describe("importFromDirectory directly", () => {
|
||||
it("matches the legacy importer's token totals on the same fixtures", async () => {
|
||||
// Clean any tokens from prior tests for a fresh comparison.
|
||||
const freshSession = "cccccccc-3333-4333-8333-cccccccccccc";
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-fresh-"));
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-fresh-"));
|
||||
const projDir = path.join(root, "-Users-demo-fresh");
|
||||
fs.mkdirSync(projDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -345,9 +345,9 @@ describe("importFromDirectory directly", () => {
|
||||
// (default 30 days). Import must snapshot the transcript into the
|
||||
// dashboard's own data dir so the conversation survives that deletion.
|
||||
const prevDataDir = process.env.DASHBOARD_DATA_DIR;
|
||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-data-"));
|
||||
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-data-"));
|
||||
process.env.DASHBOARD_DATA_DIR = dataDir;
|
||||
const src = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-src-"));
|
||||
const src = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-src-"));
|
||||
const sessionId = "dddddddd-4444-4444-8444-dddddddddddd";
|
||||
fs.writeFileSync(
|
||||
path.join(src, `${sessionId}.jsonl`),
|
||||
@@ -396,7 +396,7 @@ describe("POST /api/import/rescan", () => {
|
||||
describe("tar path-traversal hardening", () => {
|
||||
it("extractTar rejects entries with ../ segments", async () => {
|
||||
const tar = require("tar");
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-tar-bad-"));
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-tar-bad-"));
|
||||
const stageDir = path.join(tmp, "stage");
|
||||
const targetDir = path.join(tmp, "target");
|
||||
fs.mkdirSync(stageDir, { recursive: true });
|
||||
@@ -435,12 +435,12 @@ describe("tar path-traversal hardening", () => {
|
||||
|
||||
describe("extraction size cap", () => {
|
||||
it("extractGzSingle aborts past MAX_EXTRACT_BYTES", async () => {
|
||||
const prev = process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES;
|
||||
process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES = "128";
|
||||
const prev = process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES;
|
||||
process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES = "128";
|
||||
// Re-require to pick up the lowered limit for this one check.
|
||||
delete require.cache[require.resolve("../lib/archive")];
|
||||
const localArchive = require("../lib/archive");
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-bomb-"));
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-bomb-"));
|
||||
const gzPath = path.join(tmp, "bomb.jsonl.gz");
|
||||
// 2 KB of zeros compresses to a few bytes — decompressing blows past 128 B.
|
||||
fs.writeFileSync(gzPath, zlib.gzipSync(Buffer.alloc(2048, 0)));
|
||||
@@ -451,8 +451,8 @@ describe("extraction size cap", () => {
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
if (prev === undefined) delete process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES;
|
||||
else process.env.CCAM_IMPORT_MAX_EXTRACT_BYTES = prev;
|
||||
if (prev === undefined) delete process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES;
|
||||
else process.env.AGENTHUB_IMPORT_MAX_EXTRACT_BYTES = prev;
|
||||
// Restore the module with production limits for subsequent tests.
|
||||
delete require.cache[require.resolve("../lib/archive")];
|
||||
require("../lib/archive");
|
||||
@@ -464,7 +464,7 @@ describe("orphan subagent inference", () => {
|
||||
it("attaches subagent via Layout 2: <proj>/subagents/<sessionId>/agent.jsonl", async () => {
|
||||
const orphanSession = "dddddddd-4444-4444-8444-dddddddddddd";
|
||||
// Seed a parent session first.
|
||||
const seedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-orphan-seed-"));
|
||||
const seedRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-orphan-seed-"));
|
||||
const seedProj = path.join(seedRoot, "project");
|
||||
fs.mkdirSync(seedProj, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
@@ -477,7 +477,7 @@ describe("orphan subagent inference", () => {
|
||||
assert.ok(stmts.getSession.get(orphanSession), "parent session must exist before orphan pass");
|
||||
|
||||
// Now create an "orphan" subagent tree in the non-standard layout.
|
||||
const orphanRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-orphan-"));
|
||||
const orphanRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-orphan-"));
|
||||
const orphanLayoutDir = path.join(orphanRoot, "project", "subagents", orphanSession);
|
||||
fs.mkdirSync(orphanLayoutDir, { recursive: true });
|
||||
const subAgentId = "agent-xyz";
|
||||
@@ -519,8 +519,8 @@ describe("concurrent scan-path requests", () => {
|
||||
it("two concurrent imports of different folders both succeed without clobbering", async () => {
|
||||
const sessA = "eeeeeeee-5555-4555-8555-eeeeeeeeeeee";
|
||||
const sessB = "ffffffff-6666-4666-8666-ffffffffffff";
|
||||
const rootA = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-concurrent-a-"));
|
||||
const rootB = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-concurrent-b-"));
|
||||
const rootA = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-concurrent-a-"));
|
||||
const rootB = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-concurrent-b-"));
|
||||
fs.mkdirSync(path.join(rootA, "-Users-demo-a"), { recursive: true });
|
||||
fs.mkdirSync(path.join(rootB, "-Users-demo-b"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* @file Tests the host-only guard in scripts/install-hooks.js (issue #193):
|
||||
* the installer must refuse to write a container-internal handler path into a
|
||||
* (possibly bind-mounted) host ~/.claude/settings.json. Container detection is
|
||||
* driven deterministically via CCAM_FORCE_CONTAINER / CCAM_FORCE_HOST so these
|
||||
* driven deterministically via AGENTHUB_FORCE_CONTAINER / AGENTHUB_FORCE_HOST so these
|
||||
* tests pass whether or not the CI runner itself is containerized.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -16,7 +16,7 @@ const path = require("path");
|
||||
// Point the installer at a throwaway CLAUDE_HOME BEFORE requiring it — the
|
||||
// settings path is resolved at module load. (`node --test` isolates each test
|
||||
// file in its own process, so this does not leak into other suites.)
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-hooks-"));
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-hooks-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
const SETTINGS = path.join(TMP_HOME, "settings.json");
|
||||
|
||||
@@ -34,9 +34,9 @@ const HOOK_TYPES = [
|
||||
];
|
||||
|
||||
function clearEnv() {
|
||||
delete process.env.CCAM_FORCE_CONTAINER;
|
||||
delete process.env.CCAM_FORCE_HOST;
|
||||
delete process.env.CCAM_ALLOW_CONTAINER_HOOKS;
|
||||
delete process.env.AGENTHUB_FORCE_CONTAINER;
|
||||
delete process.env.AGENTHUB_FORCE_HOST;
|
||||
delete process.env.AGENTHUB_ALLOW_CONTAINER_HOOKS;
|
||||
}
|
||||
|
||||
function rmSettings() {
|
||||
@@ -63,7 +63,7 @@ describe("install-hooks host-only guard (#193)", () => {
|
||||
});
|
||||
|
||||
it("refuses inside a container and writes no settings file", () => {
|
||||
process.env.CCAM_FORCE_CONTAINER = "1";
|
||||
process.env.AGENTHUB_FORCE_CONTAINER = "1";
|
||||
const ok = installHooks(true);
|
||||
assert.equal(ok, false);
|
||||
assert.equal(
|
||||
@@ -74,8 +74,8 @@ describe("install-hooks host-only guard (#193)", () => {
|
||||
});
|
||||
|
||||
it("writes when the explicit container override is set", () => {
|
||||
process.env.CCAM_FORCE_CONTAINER = "1";
|
||||
process.env.CCAM_ALLOW_CONTAINER_HOOKS = "1";
|
||||
process.env.AGENTHUB_FORCE_CONTAINER = "1";
|
||||
process.env.AGENTHUB_ALLOW_CONTAINER_HOOKS = "1";
|
||||
const ok = installHooks(true);
|
||||
assert.equal(ok, true);
|
||||
assert.ok(fs.existsSync(SETTINGS));
|
||||
@@ -87,14 +87,14 @@ describe("install-hooks host-only guard (#193)", () => {
|
||||
});
|
||||
|
||||
it("writes on a host (not a container)", () => {
|
||||
process.env.CCAM_FORCE_HOST = "1";
|
||||
process.env.AGENTHUB_FORCE_HOST = "1";
|
||||
const ok = installHooks(true);
|
||||
assert.equal(ok, true);
|
||||
assert.ok(fs.existsSync(SETTINGS));
|
||||
});
|
||||
|
||||
it("is idempotent — re-running updates in place with no duplicate entries", () => {
|
||||
process.env.CCAM_FORCE_HOST = "1";
|
||||
process.env.AGENTHUB_FORCE_HOST = "1";
|
||||
installHooks(true);
|
||||
installHooks(true);
|
||||
const settings = JSON.parse(fs.readFileSync(SETTINGS, "utf8"));
|
||||
@@ -105,10 +105,10 @@ describe("install-hooks host-only guard (#193)", () => {
|
||||
});
|
||||
|
||||
it("isInsideContainer honors the force flags", () => {
|
||||
process.env.CCAM_FORCE_CONTAINER = "1";
|
||||
process.env.AGENTHUB_FORCE_CONTAINER = "1";
|
||||
assert.equal(isInsideContainer(), true);
|
||||
delete process.env.CCAM_FORCE_CONTAINER;
|
||||
process.env.CCAM_FORCE_HOST = "1";
|
||||
delete process.env.AGENTHUB_FORCE_CONTAINER;
|
||||
process.env.AGENTHUB_FORCE_HOST = "1";
|
||||
assert.equal(isInsideContainer(), false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-agents-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-agents-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -124,7 +124,7 @@ describe("installAgents", () => {
|
||||
assert.deepEqual(result.installed, ["AGENTS.md"]);
|
||||
assert.equal(fs.existsSync(path.join(cwd, ".claude", "agents")), false);
|
||||
const doc = fs.readFileSync(path.join(cwd, "AGENTS.md"), "utf8");
|
||||
assert.match(doc, /ccam stage <stage> --tool codex/);
|
||||
assert.match(doc, /agenthub stage <stage> --tool codex/);
|
||||
});
|
||||
|
||||
it("preserves the user's own AGENTS.md prose and replaces only the block", async () => {
|
||||
@@ -138,7 +138,7 @@ describe("installAgents", () => {
|
||||
|
||||
const doc = fs.readFileSync(file, "utf8");
|
||||
assert.match(doc, /Never force-push\./);
|
||||
assert.equal(doc.split("<!-- ccam:stage:start -->").length - 1, 1);
|
||||
assert.equal(doc.split("<!-- ccam:stage:end -->").length - 1, 1);
|
||||
assert.equal(doc.split("<!-- agenthub:stage:start -->").length - 1, 1);
|
||||
assert.equal(doc.split("<!-- agenthub:stage:end -->").length - 1, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* The database lifecycle tests use a SQLite-shaped fixture hook (write a file,
|
||||
* remove a file) rather than a real Postgres — the property worth proving is
|
||||
* CCAM's own orchestration (when a hook runs, what name it is trusted with,
|
||||
* AgentHub's own orchestration (when a hook runs, what name it is trusted with,
|
||||
* that a repeat call is a no-op), not any particular database engine.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -13,10 +13,10 @@ const os = require("node:os");
|
||||
const pathMod = require("node:path");
|
||||
const fsMod = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "ccam-data-"));
|
||||
const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "agenthub-data-"));
|
||||
process.env.DASHBOARD_DB_PATH = pathMod.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = pathMod.join(SUITE_ROOT, "lanes");
|
||||
process.env.CCAM_SECRETS_PATH = pathMod.join(SUITE_ROOT, "secrets.env");
|
||||
process.env.AGENTHUB_SECRETS_PATH = pathMod.join(SUITE_ROOT, "secrets.env");
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
@@ -48,7 +48,7 @@ function makeLane(over = {}) {
|
||||
|
||||
/** Write a profile into a repo directory. `hooks` maps name -> script body. */
|
||||
function writeProfile(root, envText, hooks = {}) {
|
||||
const dir = pathMod.join(root, ".ccam", "profile");
|
||||
const dir = pathMod.join(root, ".agenthub", "profile");
|
||||
fsMod.mkdirSync(pathMod.join(dir, "hooks"), { recursive: true });
|
||||
fsMod.writeFileSync(pathMod.join(dir, "profile.env"), envText);
|
||||
for (const [name, body] of Object.entries(hooks)) {
|
||||
@@ -70,7 +70,7 @@ const SQLITE_DB_HOOKS = {
|
||||
};
|
||||
|
||||
describe("secrets", () => {
|
||||
const secretsPath = process.env.CCAM_SECRETS_PATH;
|
||||
const secretsPath = process.env.AGENTHUB_SECRETS_PATH;
|
||||
|
||||
it("falls back to defaults when the file is absent", () => {
|
||||
assert.equal(fsMod.existsSync(secretsPath), false);
|
||||
@@ -300,7 +300,7 @@ describe("lane data lifecycle: provision / reset / remove", () => {
|
||||
const adoptedLane = lanesLib.getLane(lane.id);
|
||||
const profile = profileLib.resolveProfile(adoptedLane);
|
||||
await runtime.removeLaneData(adoptedLane, profile, {});
|
||||
assert.ok(fsMod.existsSync(dbFile), "an adopted lane's data is never CCAM's to destroy");
|
||||
assert.ok(fsMod.existsSync(dbFile), "an adopted lane's data is never AgentHub's to destroy");
|
||||
|
||||
slots.releaseSlot(lane.id);
|
||||
});
|
||||
@@ -329,8 +329,8 @@ describe("lane data lifecycle: provision / reset / remove", () => {
|
||||
|
||||
describe("runtimeFacts never leaks a secret value", () => {
|
||||
it("reports only names and an index, never a connection string", async () => {
|
||||
fsMod.writeFileSync(process.env.CCAM_SECRETS_PATH, "PG_PASS=super-secret-password\n");
|
||||
fsMod.chmodSync(process.env.CCAM_SECRETS_PATH, 0o600);
|
||||
fsMod.writeFileSync(process.env.AGENTHUB_SECRETS_PATH, "PG_PASS=super-secret-password\n");
|
||||
fsMod.chmodSync(process.env.AGENTHUB_SECRETS_PATH, 0o600);
|
||||
|
||||
const lane = makeLane();
|
||||
writeProfile(lane.cwd, "DB_PREFIX=leak_l\nREDIS=1\n", SQLITE_DB_HOOKS);
|
||||
@@ -347,8 +347,8 @@ describe("runtimeFacts never leaks a secret value", () => {
|
||||
it("redacts a hook echoing $DATABASE_URL, including its URL-encoded form", async () => {
|
||||
// A password with a character that changes shape once URL-encoded, so
|
||||
// both the raw and encoded substrings actually get exercised.
|
||||
fsMod.writeFileSync(process.env.CCAM_SECRETS_PATH, "PG_PASS=p@ss/word\n");
|
||||
fsMod.chmodSync(process.env.CCAM_SECRETS_PATH, 0o600);
|
||||
fsMod.writeFileSync(process.env.AGENTHUB_SECRETS_PATH, "PG_PASS=p@ss/word\n");
|
||||
fsMod.chmodSync(process.env.AGENTHUB_SECRETS_PATH, 0o600);
|
||||
|
||||
const lane = makeLane();
|
||||
writeProfile(lane.cwd, "DB_PREFIX=echo_l\n", {
|
||||
|
||||
@@ -11,13 +11,13 @@ const fs = require("node:fs");
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-detect-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-detect-"));
|
||||
// Must be set before the first require of anything that transitively loads
|
||||
// server/lib/secrets.js (lane-detect.js does, for checkProfile) — SECRETS_PATH
|
||||
// is bound once at that module's first load, and without this a test run on a
|
||||
// machine with a real ~/.ccam/secrets.env would read that file instead of a
|
||||
// machine with a real ~/.agenthub/secrets.env would read that file instead of a
|
||||
// deterministic, isolated one.
|
||||
process.env.CCAM_SECRETS_PATH = path.join(SUITE_ROOT, "secrets.env");
|
||||
process.env.AGENTHUB_SECRETS_PATH = path.join(SUITE_ROOT, "secrets.env");
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
@@ -180,7 +180,7 @@ describe("scaffoldProfile", () => {
|
||||
|
||||
const result = detect.scaffoldProfile(repo, facts);
|
||||
|
||||
const profileDir = path.join(repo, ".ccam", "profile");
|
||||
const profileDir = path.join(repo, ".agenthub", "profile");
|
||||
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
|
||||
assert.match(env, /PORTS="app"/);
|
||||
assert.match(env, /PORT_BASE_app=3000/);
|
||||
@@ -209,7 +209,7 @@ describe("scaffoldProfile", () => {
|
||||
|
||||
const result = detect.scaffoldProfile(repo, facts);
|
||||
|
||||
const profileDir = path.join(repo, ".ccam", "profile");
|
||||
const profileDir = path.join(repo, ".agenthub", "profile");
|
||||
const env = fs.readFileSync(path.join(profileDir, "profile.env"), "utf8");
|
||||
assert.match(env, /DB_PREFIX=/);
|
||||
assert.match(env, /REDIS=1/);
|
||||
@@ -238,7 +238,10 @@ describe("scaffoldProfile", () => {
|
||||
env: null,
|
||||
};
|
||||
const result = detect.scaffoldProfile(repo, facts);
|
||||
const boot = fs.readFileSync(path.join(repo, ".ccam", "profile", "hooks", "boot.sh"), "utf8");
|
||||
const boot = fs.readFileSync(
|
||||
path.join(repo, ".agenthub", "profile", "hooks", "boot.sh"),
|
||||
"utf8"
|
||||
);
|
||||
assert.match(boot, /# TODO: no start\/dev script found/);
|
||||
assert.equal(result.todos.length, 1);
|
||||
});
|
||||
@@ -307,7 +310,7 @@ describe("checkProfile", () => {
|
||||
const repo = makeRepo();
|
||||
writePkg(repo, { start: "node index.js" });
|
||||
detect.scaffoldProfile(repo, detect.detectNode(repo));
|
||||
fs.rmSync(path.join(repo, ".ccam", "profile", "hooks", "health.sh"));
|
||||
fs.rmSync(path.join(repo, ".agenthub", "profile", "hooks", "health.sh"));
|
||||
const result = await detect.checkProfile(repo);
|
||||
assert.equal(result.ok, false);
|
||||
assert.ok(result.errors.some((e) => /health/.test(e)));
|
||||
@@ -319,7 +322,7 @@ describe("checkProfile", () => {
|
||||
writeCompose(repo, " postgres:\n image: postgres:16\n");
|
||||
detect.scaffoldProfile(repo, detect.detectNode(repo));
|
||||
// Remove the migrate/seed TODO lines so only the secrets warning is left to check.
|
||||
const hooksDir = path.join(repo, ".ccam", "profile", "hooks");
|
||||
const hooksDir = path.join(repo, ".agenthub", "profile", "hooks");
|
||||
fs.writeFileSync(path.join(hooksDir, "migrate.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
||||
fs.writeFileSync(path.join(hooksDir, "seed.sh"), "#!/usr/bin/env bash\nexit 0\n");
|
||||
const result = await detect.checkProfile(repo);
|
||||
@@ -330,7 +333,7 @@ describe("checkProfile", () => {
|
||||
const repo = makeRepo();
|
||||
writePkg(repo, { start: "node index.js" });
|
||||
detect.scaffoldProfile(repo, detect.detectNode(repo));
|
||||
const hookPath = path.join(repo, ".ccam", "profile", "hooks", "health.sh");
|
||||
const hookPath = path.join(repo, ".agenthub", "profile", "hooks", "health.sh");
|
||||
fs.chmodSync(hookPath, 0o000);
|
||||
try {
|
||||
const result = await detect.checkProfile(repo);
|
||||
|
||||
@@ -9,7 +9,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-features-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-features-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-features-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-features-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const path = require("node:path");
|
||||
|
||||
// LANES_ROOT is read once at require time (from worktree.js) — set it before
|
||||
// the first require, then use it as a fixed root every test writes under.
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-gc-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-gc-"));
|
||||
process.env.LANES_ROOT = SUITE_ROOT;
|
||||
|
||||
const laneGc = require("../lib/lane-gc");
|
||||
|
||||
@@ -20,7 +20,7 @@ process.env.DASHBOARD_DB_PATH = TEST_DB;
|
||||
process.env.DASHBOARD_REMOTE_SYNC_MS = "0";
|
||||
process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lifecycle-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lifecycle-"));
|
||||
process.env.LANES_ROOT = path.join(ROOT, "lanes");
|
||||
|
||||
const { createApp, startServer } = require("../index");
|
||||
|
||||
@@ -13,7 +13,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-mcp-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-mcp-"));
|
||||
const FAKE_HOME = path.join(SUITE_ROOT, "home");
|
||||
fs.mkdirSync(FAKE_HOME, { recursive: true });
|
||||
process.env.HOME = FAKE_HOME;
|
||||
|
||||
@@ -7,10 +7,10 @@ const os = require("node:os");
|
||||
const pathMod = require("node:path");
|
||||
const fsMod = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "ccam-profile-"));
|
||||
const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "agenthub-profile-"));
|
||||
process.env.DASHBOARD_DB_PATH = pathMod.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = pathMod.join(SUITE_ROOT, "lanes");
|
||||
process.env.CCAM_SECRETS_PATH = pathMod.join(SUITE_ROOT, "secrets.env");
|
||||
process.env.AGENTHUB_SECRETS_PATH = pathMod.join(SUITE_ROOT, "secrets.env");
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
@@ -38,7 +38,7 @@ function makeLane(over = {}) {
|
||||
|
||||
/** Write a profile into a repo directory. `hooks` maps name -> script body. */
|
||||
function writeProfile(root, envText, hooks = {}) {
|
||||
const dir = pathMod.join(root, ".ccam", "profile");
|
||||
const dir = pathMod.join(root, ".agenthub", "profile");
|
||||
fsMod.mkdirSync(pathMod.join(dir, "hooks"), { recursive: true });
|
||||
fsMod.writeFileSync(pathMod.join(dir, "profile.env"), envText);
|
||||
for (const [name, body] of Object.entries(hooks)) {
|
||||
@@ -123,7 +123,7 @@ describe("isIntegrationEnabled", () => {
|
||||
const lane = makeLane();
|
||||
writeProfile(lane.cwd, "PORTS=api\n");
|
||||
fsMod.writeFileSync(
|
||||
pathMod.join(lane.cwd, ".ccam", "profile", "integrations.env"),
|
||||
pathMod.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
|
||||
"TRACKER_ENABLED=1\nTRACKER_PROJECT=demo\n"
|
||||
);
|
||||
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "tracker"), true);
|
||||
@@ -133,7 +133,7 @@ describe("isIntegrationEnabled", () => {
|
||||
const lane = makeLane();
|
||||
writeProfile(lane.cwd, "PORTS=api\n");
|
||||
fsMod.writeFileSync(
|
||||
pathMod.join(lane.cwd, ".ccam", "profile", "integrations.env"),
|
||||
pathMod.join(lane.cwd, ".agenthub", "profile", "integrations.env"),
|
||||
"DEV_QC_ENABLED=0\n"
|
||||
);
|
||||
assert.equal(profileLib.isIntegrationEnabled(lanesLib.getLane(lane.id), "dev_qc"), false);
|
||||
|
||||
@@ -15,7 +15,7 @@ const os = require("node:os");
|
||||
const pathMod = require("node:path");
|
||||
const fsMod = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "ccam-runtime-"));
|
||||
const SUITE_ROOT = fsMod.mkdtempSync(pathMod.join(os.tmpdir(), "agenthub-runtime-"));
|
||||
process.env.DASHBOARD_DB_PATH = pathMod.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = pathMod.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -65,7 +65,7 @@ function makeLane(over = {}) {
|
||||
|
||||
/** Write a profile into a repo directory. `hooks` maps name -> script body. */
|
||||
function writeProfile(root, envText, hooks = {}) {
|
||||
const dir = pathMod.join(root, ".ccam", "profile");
|
||||
const dir = pathMod.join(root, ".agenthub", "profile");
|
||||
fsMod.mkdirSync(pathMod.join(dir, "hooks"), { recursive: true });
|
||||
fsMod.writeFileSync(pathMod.join(dir, "profile.env"), envText);
|
||||
for (const [name, body] of Object.entries(hooks)) {
|
||||
|
||||
@@ -15,7 +15,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-sync-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-sync-"));
|
||||
|
||||
const laneSync = require("../lib/lane-sync");
|
||||
|
||||
@@ -172,7 +172,7 @@ describe("lane-sync merge: generated-file merge driver + regen fold-in", () => {
|
||||
gc(GEN_PUSHER, "push", "-q", "origin", "development");
|
||||
|
||||
// A regen hook the fold-in step will run.
|
||||
const profileDir = path.join(GEN_LANE, ".ccam", "profile");
|
||||
const profileDir = path.join(GEN_LANE, ".agenthub", "profile");
|
||||
fs.mkdirSync(path.join(profileDir, "hooks"), { recursive: true });
|
||||
fs.writeFileSync(path.join(profileDir, "profile.env"), "PORTS=api\n");
|
||||
fs.writeFileSync(
|
||||
@@ -187,7 +187,7 @@ describe("lane-sync merge: generated-file merge driver + regen fold-in", () => {
|
||||
env: { MIGRATIONS_DIR: "", GENERATED_MERGE_PATHS: "api/openapi.json" },
|
||||
generatedMergePaths: ["api/openapi.json"],
|
||||
hooks: new Set(["regen"]),
|
||||
dir: path.join(GEN_LANE, ".ccam", "profile"),
|
||||
dir: path.join(GEN_LANE, ".agenthub", "profile"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@ describe("lane actions", () => {
|
||||
describe("GET /api/lanes/:id/git", () => {
|
||||
const fs = require("node:fs");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-api-git-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-api-git-"));
|
||||
const g = (cwd, ...args) => {
|
||||
const env = { ...process.env };
|
||||
for (const k of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"]) delete env[k];
|
||||
@@ -741,7 +741,7 @@ describe("GET /api/lanes/:id/git", () => {
|
||||
describe("GET /api/lanes/branches", () => {
|
||||
const fs = require("node:fs");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-api-branches-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-api-branches-"));
|
||||
const g = (cwd, ...args) => {
|
||||
const env = { ...process.env };
|
||||
for (const k of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR"]) delete env[k];
|
||||
@@ -980,11 +980,11 @@ describe("GET /:id/ci/status", () => {
|
||||
let bin;
|
||||
|
||||
before(async () => {
|
||||
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-route-"));
|
||||
laneCwd = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-route-"));
|
||||
const r = await request("POST", "/api/lanes", { title: "CI status lane", cwd: laneCwd });
|
||||
laneId = r.body.lane.id;
|
||||
|
||||
bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-bin-"));
|
||||
bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-bin-"));
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "gh"),
|
||||
`#!/usr/bin/env bash
|
||||
@@ -1193,7 +1193,7 @@ describe("/api/lanes tool tests", () => {
|
||||
it("POST /api/lanes/worktree with tool parameter creates a lane with the specified tool", async () => {
|
||||
const sourceRepo = require("path").join(
|
||||
require("os").tmpdir(),
|
||||
`ccam-worktree-src-${Date.now()}`
|
||||
`agenthub-worktree-src-${Date.now()}`
|
||||
);
|
||||
const fs = require("fs");
|
||||
fs.mkdirSync(sourceRepo, { recursive: true });
|
||||
@@ -1221,7 +1221,7 @@ describe("/api/lanes tool tests", () => {
|
||||
it("POST /api/lanes/worktree defaults to 'claude' tool when tool is not specified", async () => {
|
||||
const sourceRepo = require("path").join(
|
||||
require("os").tmpdir(),
|
||||
`ccam-worktree-default-${Date.now()}`
|
||||
`agenthub-worktree-default-${Date.now()}`
|
||||
);
|
||||
const fs = require("fs");
|
||||
fs.mkdirSync(sourceRepo, { recursive: true });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file Tests for the `ccam stage` / `ccam lanes` CLI subcommands: lane
|
||||
* @file Tests for the `agenthub stage` / `agenthub lanes` CLI subcommands: lane
|
||||
* resolution from the current directory, the stage round-trip through the HTTP
|
||||
* API, and the non-zero exit when the cwd belongs to no lane.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -14,10 +14,10 @@ const http = require("http");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const TEST_DB = path.join(os.tmpdir(), `dashboard-lanes-cli-${Date.now()}-${process.pid}.db`);
|
||||
const PIPELINE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-pipeline-fixture-"));
|
||||
const LANES_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-managed-lanes-cli-"));
|
||||
const SOURCE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-worktree-source-"));
|
||||
const REMOTE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-worktree-remote-"));
|
||||
const PIPELINE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-pipeline-fixture-"));
|
||||
const LANES_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-managed-lanes-cli-"));
|
||||
const SOURCE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-worktree-source-"));
|
||||
const REMOTE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-worktree-remote-"));
|
||||
process.env.DASHBOARD_DB_PATH = TEST_DB;
|
||||
process.env.DASHBOARD_PIPELINES_DIR = PIPELINE_DIR;
|
||||
process.env.DASHBOARD_REMOTE_SYNC_MS = "0";
|
||||
@@ -39,8 +39,8 @@ fs.writeFileSync(
|
||||
|
||||
const { createApp, startServer } = require("../index");
|
||||
|
||||
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-cli-"));
|
||||
const CLI = path.join(__dirname, "..", "..", "bin", "ccam.js");
|
||||
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-cli-"));
|
||||
const CLI = path.join(__dirname, "..", "..", "bin", "agenthub.js");
|
||||
|
||||
let server;
|
||||
let BASE;
|
||||
@@ -188,7 +188,7 @@ after(() => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("ccam stage", () => {
|
||||
describe("agenthub stage", () => {
|
||||
it("reports a stage for the lane owning the current directory", async () => {
|
||||
const r = await cli(["stage", "review", "--evidence", "3 findings"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -231,9 +231,9 @@ describe("ccam stage", () => {
|
||||
|
||||
// A cwd can back one `claude` lane AND one `codex` lane (idx_lanes_cwd_tool),
|
||||
// so cwd alone no longer identifies a lane. Picking the first of the tie means
|
||||
// a codex session's `ccam stage` silently drives the claude lane's card.
|
||||
describe("ccam stage in a directory shared by two lanes", () => {
|
||||
const DUAL_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-dual-lane-"));
|
||||
// a codex session's `agenthub stage` silently drives the claude lane's card.
|
||||
describe("agenthub stage in a directory shared by two lanes", () => {
|
||||
const DUAL_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-dual-lane-"));
|
||||
let claudeLane;
|
||||
let codexLane;
|
||||
// The suite itself runs under Claude Code, whose CLAUDECODE marker would
|
||||
@@ -251,10 +251,10 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
return (await get(`/api/lanes/${id}`)).body.lane.stage;
|
||||
}
|
||||
|
||||
it("CCAM_LANE_ID wins — a dashboard-started pane always knows its own lane", async () => {
|
||||
it("AGENTHUB_LANE_ID wins — a dashboard-started pane always knows its own lane", async () => {
|
||||
const r = await cli(["stage", "gate"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: String(codexLane.id),
|
||||
AGENTHUB_LANE_ID: String(codexLane.id),
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(await stageOf(codexLane.id), "gate");
|
||||
@@ -287,7 +287,7 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
});
|
||||
|
||||
it("refuses when the requested tool owns no lane here", async () => {
|
||||
const solo = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-solo-lane-"));
|
||||
const solo = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-solo-lane-"));
|
||||
await post("/api/lanes", { cwd: solo, title: "solo claude" });
|
||||
const r = await cli(["stage", "plan", "--tool", "codex"], solo, NO_HARNESS);
|
||||
assert.notEqual(r.status, 0);
|
||||
@@ -302,20 +302,20 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
assert.notEqual(await stageOf(claudeLane.id), "review");
|
||||
});
|
||||
|
||||
it("positional numeric ID acts like --lane for commands that support it (e.g. ccam lanes pipeline)", async () => {
|
||||
// Commands like `ccam lanes pipeline` treat a leading numeric arg as the lane ID.
|
||||
// Other commands like `ccam stage` have their own positional argument and don't support it.
|
||||
it("positional numeric ID acts like --lane for commands that support it (e.g. agenthub lanes pipeline)", async () => {
|
||||
// Commands like `agenthub lanes pipeline` treat a leading numeric arg as the lane ID.
|
||||
// Other commands like `agenthub stage` have their own positional argument and don't support it.
|
||||
const r = await cli(["lanes", "pipeline", String(codexLane.id)], DUAL_DIR, NO_HARNESS);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, new RegExp(`lane #${codexLane.id}`));
|
||||
});
|
||||
|
||||
it("non-numeric CCAM_LANE_ID is ignored", async () => {
|
||||
// If CCAM_LANE_ID is garbage (not a number), it should be skipped and
|
||||
it("non-numeric AGENTHUB_LANE_ID is ignored", async () => {
|
||||
// If AGENTHUB_LANE_ID is garbage (not a number), it should be skipped and
|
||||
// fall through to --tool or env markers
|
||||
const r = await cli(["stage", "ship"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: "not-a-number",
|
||||
AGENTHUB_LANE_ID: "not-a-number",
|
||||
CLAUDECODE: "1", // This env marker should pick claude lane
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -323,27 +323,27 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
assert.notEqual(await stageOf(codexLane.id), "ship");
|
||||
});
|
||||
|
||||
it("a CCAM_LANE_ID naming no lane falls through instead of addressing it", async () => {
|
||||
it("a AGENTHUB_LANE_ID naming no lane falls through instead of addressing it", async () => {
|
||||
// A pane outlives the lane it was started for, and lane ids are reused
|
||||
// after a delete. Trusting the number blind would either 404 or, worse,
|
||||
// write this session's stage onto whatever lane inherited that id.
|
||||
const r = await cli(["stage", "gate"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: "999999",
|
||||
AGENTHUB_LANE_ID: "999999",
|
||||
CLAUDECODE: "1",
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(await stageOf(claudeLane.id), "gate");
|
||||
});
|
||||
|
||||
it("a CCAM_LANE_ID for a lane outside this directory is not used", async () => {
|
||||
it("a AGENTHUB_LANE_ID for a lane outside this directory is not used", async () => {
|
||||
// The pane's lane is the truth only while the command runs inside it; an
|
||||
// agent that cd'd into a different repo must resolve by that directory.
|
||||
const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-elsewhere-"));
|
||||
const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-elsewhere-"));
|
||||
const other = (await post("/api/lanes", { cwd: elsewhere, title: "elsewhere" })).body.lane;
|
||||
const r = await cli(["stage", "review"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: String(other.id),
|
||||
AGENTHUB_LANE_ID: String(other.id),
|
||||
CLAUDECODE: "1",
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -352,9 +352,9 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes add", () => {
|
||||
describe("agenthub lanes add", () => {
|
||||
it("creates a lane and it appears in lanes list", async () => {
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-add-"));
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-add-"));
|
||||
try {
|
||||
const r = await cli(["lanes", "add", "--cwd", addDir, "--title", "CLI added"], addDir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -369,7 +369,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
|
||||
it("defaults to 'default' pipeline when --pipeline is omitted", async () => {
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-no-pipeline-"));
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-no-pipeline-"));
|
||||
try {
|
||||
const r = await cli(["lanes", "add", "--cwd", addDir, "--title", "Default pipeline"], addDir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -385,7 +385,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
|
||||
it("accepts --pipeline flag and creates lane with custom pipeline", async () => {
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-custom-pipeline-"));
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-custom-pipeline-"));
|
||||
try {
|
||||
const r = await cli(
|
||||
[
|
||||
@@ -458,7 +458,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
|
||||
it("reports the failure notes, not success, when provisioning fails", async () => {
|
||||
const emptyRepo = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-cli-unborn-repo-"));
|
||||
const emptyRepo = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-cli-unborn-repo-"));
|
||||
try {
|
||||
await git(["init", "-b", "main"], emptyRepo);
|
||||
const r = await cli(
|
||||
@@ -475,7 +475,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes destructive lifecycle", () => {
|
||||
describe("agenthub lanes destructive lifecycle", () => {
|
||||
// Every test provisions its OWN lane. These tests used to share one mutable
|
||||
// `managedLane` across describe blocks, which meant they only ever exercised
|
||||
// whatever state the previous test happened to leave behind — the same
|
||||
@@ -557,7 +557,7 @@ describe("ccam lanes destructive lifecycle", () => {
|
||||
});
|
||||
|
||||
it("refuses to RESET an adopted lane but FORGETS it on remove, leaving the directory intact", async () => {
|
||||
const adoptedDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-adopted-remove-"));
|
||||
const adoptedDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-adopted-remove-"));
|
||||
const preserved = path.join(adoptedDir, "preserve.txt");
|
||||
fs.writeFileSync(preserved, "do not delete\n");
|
||||
try {
|
||||
@@ -602,9 +602,9 @@ describe("ccam lanes destructive lifecycle", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes — inferred stage detection", () => {
|
||||
describe("agenthub lanes — inferred stage detection", () => {
|
||||
it("prints the inferred stage when detection leads the declared stage", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-detect-lead-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-detect-lead-"));
|
||||
try {
|
||||
const created = await post("/api/lanes", { cwd: dir, title: "Detect Lead" });
|
||||
const id = created.body.lane.id;
|
||||
@@ -626,7 +626,7 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
const r = await cli(["lanes"], dir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const line = r.stdout.split("\n").find((l) => l.startsWith(`#${id}`));
|
||||
assert.ok(line, "lane row not found in ccam lanes output");
|
||||
assert.ok(line, "lane row not found in agenthub lanes output");
|
||||
assert.match(line, /⇢ detected:tests/);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
@@ -634,7 +634,7 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
});
|
||||
|
||||
it("does not print an inferred stage when the declaration already leads", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-detect-behind-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-detect-behind-"));
|
||||
try {
|
||||
const created = await post("/api/lanes", { cwd: dir, title: "Detect Behind" });
|
||||
const id = created.body.lane.id;
|
||||
@@ -658,7 +658,7 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
const r = await cli(["lanes"], dir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const line = r.stdout.split("\n").find((l) => l.startsWith(`#${id}`));
|
||||
assert.ok(line, "lane row not found in ccam lanes output");
|
||||
assert.ok(line, "lane row not found in agenthub lanes output");
|
||||
assert.doesNotMatch(line, /detected:/);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
@@ -666,15 +666,15 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam ship-full / ccam ship-quick", () => {
|
||||
describe("agenthub ship-full / agenthub ship-quick", () => {
|
||||
it("ship-quick prints the 8-stage pipeline's skill, ship-full the 16-stage one", async () => {
|
||||
const quick = await cli(["ship-quick", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(quick.status, 0, quick.stderr);
|
||||
assert.match(quick.stdout, /claude "\/ccam:ship-feature add an export button"/);
|
||||
assert.match(quick.stdout, /claude "\/agenthub:ship-feature add an export button"/);
|
||||
|
||||
const full = await cli(["ship-full", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(full.status, 0, full.stderr);
|
||||
assert.match(full.stdout, /claude "\/ccam:ship-feature-lane add an export button"/);
|
||||
assert.match(full.stdout, /claude "\/agenthub:ship-feature-lane add an export button"/);
|
||||
});
|
||||
|
||||
it("--print switches nothing: the lane keeps whatever template it was on", async () => {
|
||||
@@ -689,21 +689,21 @@ describe("ccam ship-full / ccam ship-quick", () => {
|
||||
it("exits non-zero with the command's own usage when no requirement is given", async () => {
|
||||
const r = await cli(["ship-full"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(r.stderr, /usage: ccam ship-full/);
|
||||
assert.match(r.stderr, /usage: agenthub ship-full/);
|
||||
|
||||
const q = await cli(["ship-quick"], LANE_DIR);
|
||||
assert.notEqual(q.status, 0);
|
||||
assert.match(q.stderr, /usage: ccam ship-quick/);
|
||||
assert.match(q.stderr, /usage: agenthub ship-quick/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes node", () => {
|
||||
describe("agenthub lanes node", () => {
|
||||
it("with no node id, lists every node and its command, and exits non-zero with usage", async () => {
|
||||
const r = await cli(["lanes", "node"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(r.stdout, /\bintake\b/);
|
||||
assert.match(r.stdout, /\bdone\b/);
|
||||
assert.match(r.stderr, /usage: ccam lanes node/);
|
||||
assert.match(r.stderr, /usage: agenthub lanes node/);
|
||||
});
|
||||
|
||||
it("--print shows a slash-command node as a claude prompt, extra args appended", async () => {
|
||||
@@ -717,7 +717,7 @@ describe("ccam lanes node", () => {
|
||||
await cli(["lanes", "pipeline", "ship-feature"], LANE_DIR);
|
||||
const r = await cli(["lanes", "node", "gates", "--print"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /"ccam" "lanes" "hook" "ci-gate"/);
|
||||
assert.match(r.stdout, /"agenthub" "lanes" "hook" "ci-gate"/);
|
||||
});
|
||||
|
||||
it("every node of both shipped templates declares a runnable command", async () => {
|
||||
@@ -731,7 +731,7 @@ describe("ccam lanes node", () => {
|
||||
|
||||
it("matches a node by alias, so one slash command works on either template", async () => {
|
||||
// `default` calls the step `implement`, `ship-feature` calls it
|
||||
// `implementing` — /ccam:implement must not depend on which one is active.
|
||||
// `implementing` — /agenthub:implement must not depend on which one is active.
|
||||
await cli(["lanes", "pipeline", "ship-feature"], LANE_DIR);
|
||||
const ship = await cli(["lanes", "node", "implement", "--print"], LANE_DIR);
|
||||
assert.equal(ship.status, 0, ship.stderr);
|
||||
@@ -751,12 +751,12 @@ describe("ccam lanes node", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes ci status", () => {
|
||||
describe("agenthub lanes ci status", () => {
|
||||
let restorePath;
|
||||
let bin;
|
||||
|
||||
before(() => {
|
||||
bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-cli-bin-"));
|
||||
bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-cli-bin-"));
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "gh"),
|
||||
`#!/usr/bin/env bash
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("pipelines", () => {
|
||||
});
|
||||
|
||||
it("keeps the evidence of a stage declared under an alias", () => {
|
||||
// `lane.stages` is keyed by the raw declared string, so `ccam stage
|
||||
// `lane.stages` is keyed by the raw declared string, so `agenthub stage
|
||||
// planning --evidence x` files the record under "planning" while the node
|
||||
// is "plan". Looking it up by node id alone loses the evidence and paints
|
||||
// a `done` node amber — silently, and only for agents that use an alias.
|
||||
@@ -151,12 +151,12 @@ describe("per-node driver command", () => {
|
||||
|
||||
assert.equal(byId.intake, "/superpowers:brainstorming");
|
||||
assert.equal(byId.implementing, "/superpowers:test-driven-development");
|
||||
assert.equal(byId.gates, "ccam lanes hook ci-gate");
|
||||
assert.equal(byId.gates, "agenthub lanes hook ci-gate");
|
||||
// A terminal or gate node has no work of its own to run: reaching it IS
|
||||
// the declaration, so its command is the declaration - which keeps
|
||||
// `ccam lanes node <id>` runnable for every node in the template.
|
||||
assert.equal(byId.done, "ccam stage done");
|
||||
assert.equal(byId["e2e-feature-passed"], "ccam stage e2e-feature-passed");
|
||||
// `agenthub lanes node <id>` runnable for every node in the template.
|
||||
assert.equal(byId.done, "agenthub stage done");
|
||||
assert.equal(byId["e2e-feature-passed"], "agenthub stage e2e-feature-passed");
|
||||
});
|
||||
|
||||
it("defaults a template node that declares no command to the empty string", () => {
|
||||
@@ -334,7 +334,7 @@ describe("lanes lib", () => {
|
||||
assert.equal(p.liveness, "idle");
|
||||
// The template's driving slash command rides along so the run console can
|
||||
// pre-fill its prompt with it.
|
||||
assert.equal(p.pipeline_skill, "/ccam:ship-feature");
|
||||
assert.equal(p.pipeline_skill, "/agenthub:ship-feature");
|
||||
lanes.deleteLane(l.id);
|
||||
});
|
||||
|
||||
@@ -1080,7 +1080,7 @@ describe("stage detection", () => {
|
||||
db.prepare("UPDATE lanes SET detected_at = ? WHERE id = ?").run(oldTime, l.id);
|
||||
|
||||
// With nothing declared, the last detection IS the lane's last known
|
||||
// progress — blanking it left every non-CCAM repo's card 100% pending
|
||||
// progress — blanking it left every non-AgentHub repo's card 100% pending
|
||||
// forever. Still amber (`detected`), still never `done`.
|
||||
const payload = lanes.lanePayload(lanes.getLane(l.id));
|
||||
const tests = payload.pipeline_nodes.find((n) => n.id === "tests");
|
||||
|
||||
@@ -10,7 +10,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-locks-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-locks-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
|
||||
@@ -89,22 +89,22 @@ describe("GET /api/metrics", () => {
|
||||
|
||||
// Core families present with HELP + TYPE headers.
|
||||
for (const name of [
|
||||
"ccam_up",
|
||||
"ccam_build_info",
|
||||
"ccam_process_uptime_seconds",
|
||||
"ccam_process_resident_memory_bytes",
|
||||
"ccam_sessions",
|
||||
"ccam_agents",
|
||||
"ccam_events_total",
|
||||
"ccam_websocket_clients",
|
||||
"ccam_remote_sources",
|
||||
"ccam_tokens_total",
|
||||
"agenthub_up",
|
||||
"agenthub_build_info",
|
||||
"agenthub_process_uptime_seconds",
|
||||
"agenthub_process_resident_memory_bytes",
|
||||
"agenthub_sessions",
|
||||
"agenthub_agents",
|
||||
"agenthub_events_total",
|
||||
"agenthub_websocket_clients",
|
||||
"agenthub_remote_sources",
|
||||
"agenthub_tokens_total",
|
||||
]) {
|
||||
assert.ok(res.body.includes(`# HELP ${name} `), `HELP for ${name}`);
|
||||
assert.ok(res.body.includes(`# TYPE ${name} `), `TYPE for ${name}`);
|
||||
}
|
||||
|
||||
assert.equal(sampleValue(res.body, "ccam_up"), 1);
|
||||
assert.equal(sampleValue(res.body, "agenthub_up"), 1);
|
||||
// No sample line may carry a NaN/undefined value.
|
||||
for (const line of res.body.split("\n")) {
|
||||
if (!line || line.startsWith("#")) continue;
|
||||
@@ -117,29 +117,29 @@ describe("GET /api/metrics", () => {
|
||||
const res = await request("GET", "/api/metrics");
|
||||
for (const status of ["active", "completed", "error", "abandoned"]) {
|
||||
assert.notEqual(
|
||||
sampleValue(res.body, "ccam_sessions", `status="${status}"`),
|
||||
sampleValue(res.body, "agenthub_sessions", `status="${status}"`),
|
||||
null,
|
||||
`ccam_sessions status=${status} present`
|
||||
`agenthub_sessions status=${status} present`
|
||||
);
|
||||
}
|
||||
for (const status of ["working", "waiting", "completed", "error"]) {
|
||||
assert.notEqual(
|
||||
sampleValue(res.body, "ccam_agents", `status="${status}"`),
|
||||
sampleValue(res.body, "agenthub_agents", `status="${status}"`),
|
||||
null,
|
||||
`ccam_agents status=${status} present`
|
||||
`agenthub_agents status=${status} present`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("reflects real data seeded through the hook API", async () => {
|
||||
const before = sampleValue(await scrapeText(), "ccam_sessions", 'status="active"') || 0;
|
||||
const before = sampleValue(await scrapeText(), "agenthub_sessions", 'status="active"') || 0;
|
||||
|
||||
await request("POST", "/api/hooks/event", {
|
||||
hook_type: "SessionStart",
|
||||
data: { session_id: "metrics-sess-1", cwd: "/tmp/metrics" },
|
||||
});
|
||||
|
||||
const after = sampleValue(await scrapeText(), "ccam_sessions", 'status="active"');
|
||||
const after = sampleValue(await scrapeText(), "agenthub_sessions", 'status="active"');
|
||||
assert.equal(after, before + 1, "a new active session bumps the gauge by 1");
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const fs = require("node:fs");
|
||||
const { describe, it, after, beforeEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-named-lock-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-named-lock-"));
|
||||
process.env.LANES_ROOT = SUITE_ROOT;
|
||||
after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true }));
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* makes a plugin install self-sufficient. Covers the fast path, the Node
|
||||
* version gate, stale-lock reclaim, legacy hook removal (the duplicate-hook
|
||||
* double-counting bug) and the CLI launcher's refusal to clobber a foreign
|
||||
* `ccam`. Every path is exercised against injected temp directories — nothing
|
||||
* `agenthub`. Every path is exercised against injected temp directories — nothing
|
||||
* here touches the real $HOME.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -14,7 +14,7 @@ const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-bootstrap-"));
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-bootstrap-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
delete process.env.DASHBOARD_DATA_DIR;
|
||||
|
||||
@@ -135,17 +135,17 @@ describe("skill-plugin auto-install gate", () => {
|
||||
// Both cases here must return WITHOUT reaching server/lib/skill-plugins.js —
|
||||
// that module shells out to `claude plugin install`, and no test may.
|
||||
beforeEach(resetRuntime);
|
||||
after(() => delete process.env.CCAM_AUTO_INSTALL_PLUGINS);
|
||||
after(() => delete process.env.AGENTHUB_AUTO_INSTALL_PLUGINS);
|
||||
|
||||
for (const flag of ["0", "false", "off"]) {
|
||||
it(`is off when CCAM_AUTO_INSTALL_PLUGINS=${flag}`, () => {
|
||||
process.env.CCAM_AUTO_INSTALL_PLUGINS = flag;
|
||||
it(`is off when AGENTHUB_AUTO_INSTALL_PLUGINS=${flag}`, () => {
|
||||
process.env.AGENTHUB_AUTO_INSTALL_PLUGINS = flag;
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT), "off");
|
||||
});
|
||||
}
|
||||
|
||||
it("runs once: a settled state is returned verbatim, not re-attempted", () => {
|
||||
delete process.env.CCAM_AUTO_INSTALL_PLUGINS;
|
||||
delete process.env.AGENTHUB_AUTO_INSTALL_PLUGINS;
|
||||
boot.writeState({ skillPluginsSettled: "installed" }, RT);
|
||||
|
||||
// Verbatim matters: writeState records whatever comes back, so a marker
|
||||
@@ -153,8 +153,8 @@ describe("skill-plugin auto-install gate", () => {
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT), "installed");
|
||||
});
|
||||
|
||||
it("is off under the flag even when forced, so /ccam-update honours the opt-out", () => {
|
||||
process.env.CCAM_AUTO_INSTALL_PLUGINS = "0";
|
||||
it("is off under the flag even when forced, so /agenthub-update honours the opt-out", () => {
|
||||
process.env.AGENTHUB_AUTO_INSTALL_PLUGINS = "0";
|
||||
boot.writeState({ skillPluginsSettled: "failed" }, RT);
|
||||
assert.equal(boot.autoInstallSkillPlugins(RT, { force: true }), "off");
|
||||
});
|
||||
@@ -200,7 +200,7 @@ describe("legacy hook cleanup", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmSync(settings, { force: true });
|
||||
fs.rmSync(`${settings}.ccam-bak`, { force: true });
|
||||
fs.rmSync(`${settings}.agenthub-bak`, { force: true });
|
||||
});
|
||||
|
||||
it("removes checkout-installed hook entries and backs the file up first", () => {
|
||||
@@ -234,7 +234,7 @@ describe("legacy hook cleanup", () => {
|
||||
assert.equal(after.hooks.PreToolUse.length, 1, "unrelated hooks must survive");
|
||||
assert.match(JSON.stringify(after.hooks.PreToolUse), /other\/tool\.js/);
|
||||
assert.equal(after.hooks.SessionStart, undefined, "an emptied list is dropped");
|
||||
assert.ok(fs.existsSync(`${settings}.ccam-bak`), "must back up before writing");
|
||||
assert.ok(fs.existsSync(`${settings}.agenthub-bak`), "must back up before writing");
|
||||
});
|
||||
|
||||
it("never removes the plugin's own cache-resolved hook entries", () => {
|
||||
@@ -249,7 +249,7 @@ describe("legacy hook cleanup", () => {
|
||||
"plugins",
|
||||
"cache",
|
||||
"claude-code-agent-monitor-plugins",
|
||||
"ccam",
|
||||
"agenthub",
|
||||
"abc123",
|
||||
"scripts",
|
||||
"hook-handler.js"
|
||||
@@ -280,10 +280,10 @@ describe("legacy hook cleanup", () => {
|
||||
assert.match(JSON.stringify(after.hooks.PreToolUse), /plugins.*cache/);
|
||||
});
|
||||
|
||||
it("leaves a file with no CCAM hooks untouched and writes no backup", () => {
|
||||
it("leaves a file with no AgentHub hooks untouched and writes no backup", () => {
|
||||
fs.writeFileSync(settings, JSON.stringify({ hooks: { Stop: [{ hooks: [] }] } }));
|
||||
assert.equal(boot.stripLegacyHooks(settings), 0);
|
||||
assert.equal(fs.existsSync(`${settings}.ccam-bak`), false);
|
||||
assert.equal(fs.existsSync(`${settings}.agenthub-bak`), false);
|
||||
});
|
||||
|
||||
it("is a no-op when no settings file exists", () => {
|
||||
@@ -291,17 +291,17 @@ describe("legacy hook cleanup", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam CLI launcher", () => {
|
||||
describe("agenthub CLI launcher", () => {
|
||||
const bin = path.join(TMP_HOME, "bin");
|
||||
const target = path.join(bin, process.platform === "win32" ? "ccam.cmd" : "ccam");
|
||||
const target = path.join(bin, process.platform === "win32" ? "agenthub.cmd" : "agenthub");
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmSync(bin, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("writes a launcher that points at the plugin's bin/ccam.js", () => {
|
||||
it("writes a launcher that points at the plugin's bin/agenthub.js", () => {
|
||||
assert.equal(boot.linkCli(bin), "written");
|
||||
assert.match(fs.readFileSync(target, "utf8"), /bin[/\\]ccam\.js/);
|
||||
assert.match(fs.readFileSync(target, "utf8"), /bin[/\\]agenthub\.js/);
|
||||
});
|
||||
|
||||
it("is idempotent", () => {
|
||||
@@ -309,9 +309,9 @@ describe("ccam CLI launcher", () => {
|
||||
assert.equal(boot.linkCli(bin), "exists");
|
||||
});
|
||||
|
||||
it("never clobbers a ccam it did not write (a linked checkout keeps winning)", () => {
|
||||
it("never clobbers a agenthub it did not write (a linked checkout keeps winning)", () => {
|
||||
fs.mkdirSync(bin, { recursive: true });
|
||||
fs.writeFileSync(target, '#!/bin/sh\nexec node /my/checkout/bin/ccam.js "$@"\n');
|
||||
fs.writeFileSync(target, '#!/bin/sh\nexec node /my/checkout/bin/agenthub.js "$@"\n');
|
||||
assert.equal(boot.linkCli(bin), "foreign");
|
||||
assert.match(fs.readFileSync(target, "utf8"), /my\/checkout/);
|
||||
});
|
||||
@@ -319,10 +319,10 @@ describe("ccam CLI launcher", () => {
|
||||
|
||||
describe("plugin-open / plugin-bootstrap require cycle", () => {
|
||||
// bootstrap() lazily requires plugin-open for buildClient, and plugin-open
|
||||
// requires bootstrap at load. When plugin-open is the entry point (/ccam-open)
|
||||
// requires bootstrap at load. When plugin-open is the entry point (/agenthub-open)
|
||||
// that lazy require resolves to whatever sits on its module.exports at that
|
||||
// instant — so the assignment must come before the entry block that calls
|
||||
// bootstrap(), or /ccam-open dies with "buildClient is not a function".
|
||||
// bootstrap(), or /agenthub-open dies with "buildClient is not a function".
|
||||
const src = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "scripts", "plugin-open.js"),
|
||||
"utf8"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @file Tests the `Skill plugins` row of scripts/plugin-doctor.js — the check
|
||||
* that tells a fresh install its pipelines name slash commands no installed
|
||||
* plugin provides (today: `superpowers`, which `/ccam:ship-feature-lane` drives
|
||||
* plugin provides (today: `superpowers`, which `/agenthub:ship-feature-lane` drives
|
||||
* step by step). The row must WARN and never FAIL: without that plugin the
|
||||
* dashboard, hooks, CLI and MCP all still work, so failing a doctor run over it
|
||||
* would cry wolf. Every case runs against an injected temp CLAUDE_HOME.
|
||||
@@ -14,7 +14,7 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-doctor-skills-"));
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-doctor-skills-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
|
||||
const { diagnose } = require("../../scripts/plugin-doctor");
|
||||
|
||||
@@ -40,10 +40,10 @@ function listMd(dir) {
|
||||
}
|
||||
}
|
||||
|
||||
// The marketplace mixes two shapes: the root-source `ccam` plugin (the whole
|
||||
// The marketplace mixes two shapes: the root-source `agenthub` plugin (the whole
|
||||
// repo — hooks, server, CLI, MCP) and ten subdirectory plugins. The root
|
||||
// plugin's commands live in plugins/ccam/, which is therefore NOT a plugin dir.
|
||||
const ROOT_PLUGIN = "ccam";
|
||||
// plugin's commands live in plugins/agenthub/, which is therefore NOT a plugin dir.
|
||||
const ROOT_PLUGIN = "agenthub";
|
||||
|
||||
describe("plugin marketplace", () => {
|
||||
const marketplace = readJson(MARKETPLACE);
|
||||
@@ -181,7 +181,7 @@ describe("plugin marketplace", () => {
|
||||
const manifest = readJson(path.join(REPO_ROOT, ".claude-plugin", "plugin.json"));
|
||||
|
||||
it("is declared with the repo root as its source", () => {
|
||||
assert.ok(entry, "the root ccam entry is missing from marketplace.json");
|
||||
assert.ok(entry, "the root agenthub entry is missing from marketplace.json");
|
||||
assert.equal(entry.source, "./");
|
||||
assert.equal(entry.path, undefined, "a root-source entry must not also carry a path");
|
||||
assert.ok(Array.isArray(entry.tags) && entry.tags.length > 0);
|
||||
@@ -231,8 +231,8 @@ describe("plugin marketplace", () => {
|
||||
|
||||
it("lists every command file that exists — an unlisted one never loads", () => {
|
||||
// `commands` is an explicit array, not a directory scan: a file added to
|
||||
// plugins/ccam/commands/ and forgotten here is invisible to every user.
|
||||
const dir = path.join(REPO_ROOT, "plugins/ccam/commands");
|
||||
// plugins/agenthub/commands/ and forgotten here is invisible to every user.
|
||||
const dir = path.join(REPO_ROOT, "plugins/agenthub/commands");
|
||||
const onDisk = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
||||
const listed = new Set(manifest.commands.map((rel) => path.basename(rel)));
|
||||
for (const f of onDisk) assert.ok(listed.has(f), `${f} is not in plugin.json commands`);
|
||||
|
||||
@@ -9,7 +9,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-proof-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-proof-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
|
||||
@@ -48,13 +48,13 @@ describe("pty-attach", () => {
|
||||
tmux.__reset();
|
||||
});
|
||||
|
||||
it("rejects a runId that doesn't match ccam-lane-<digits>", () => {
|
||||
it("rejects a runId that doesn't match agenthub-lane-<digits>", () => {
|
||||
assert.throws(() => ptyAttach.validateRunId("../../etc/passwd"), /EBADRUNID/);
|
||||
assert.throws(() => ptyAttach.validateRunId("some-other-session"), /EBADRUNID/);
|
||||
});
|
||||
|
||||
it("accepts a well-formed runId", () => {
|
||||
assert.doesNotThrow(() => ptyAttach.validateRunId("ccam-lane-42"));
|
||||
assert.doesNotThrow(() => ptyAttach.validateRunId("agenthub-lane-42"));
|
||||
});
|
||||
|
||||
it("turns tmux mouse mode on for the session it attaches to", () => {
|
||||
@@ -63,22 +63,22 @@ describe("pty-attach", () => {
|
||||
calls.push(args);
|
||||
return "";
|
||||
});
|
||||
ptyAttach.attach(makeFakeWs(), "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
assert.deepEqual(calls[0], ["set-option", "-t", "ccam-lane-1", "mouse", "on"]);
|
||||
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
assert.deepEqual(calls[0], ["set-option", "-t", "agenthub-lane-1", "mouse", "on"]);
|
||||
});
|
||||
|
||||
it("still attaches even if the mouse-mode set-option call fails", () => {
|
||||
tmux.__setExecImpl(() => {
|
||||
throw new Error("no such session: ccam-lane-1");
|
||||
throw new Error("no such session: agenthub-lane-1");
|
||||
});
|
||||
assert.doesNotThrow(() =>
|
||||
ptyAttach.attach(makeFakeWs(), "ccam-lane-1", { cols: 80, rows: 24 })
|
||||
ptyAttach.attach(makeFakeWs(), "agenthub-lane-1", { cols: 80, rows: 24 })
|
||||
);
|
||||
});
|
||||
|
||||
it("wires PTY data to binary WS frames and WS binary frames to PTY writes", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
|
||||
fakePty.__emitter.emit("data", "hello from claude");
|
||||
assert.equal(ws.__sent.length, 1);
|
||||
@@ -91,7 +91,7 @@ describe("pty-attach", () => {
|
||||
|
||||
it("routes a JSON text frame with type resize to pty.resize", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
ws.__emitter.emit(
|
||||
"message",
|
||||
Buffer.from(JSON.stringify({ type: "resize", cols: 100, rows: 40 })),
|
||||
@@ -108,7 +108,7 @@ describe("pty-attach", () => {
|
||||
// arrives here as non-binary. This must reach the pty, not be dropped as
|
||||
// an unparseable control message.
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
ws.__emitter.emit("message", Buffer.from("ls -la\r"), { binary: false });
|
||||
assert.deepEqual(fakePty.__writes[0], "ls -la\r");
|
||||
});
|
||||
@@ -119,7 +119,7 @@ describe("pty-attach", () => {
|
||||
ws.close = () => {
|
||||
closed = true;
|
||||
};
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
fakePty.__emitter.emit("exit", { exitCode: 0 });
|
||||
const last = ws.__sent[ws.__sent.length - 1];
|
||||
assert.equal(last.binary, false);
|
||||
@@ -129,7 +129,7 @@ describe("pty-attach", () => {
|
||||
|
||||
it("kills the PTY attach process when the WS connection closes", () => {
|
||||
const ws = makeFakeWs();
|
||||
ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 });
|
||||
ptyAttach.attach(ws, "agenthub-lane-1", { cols: 80, rows: 24 });
|
||||
let killed = false;
|
||||
fakePty.kill = () => {
|
||||
killed = true;
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("pty-run", () => {
|
||||
tmux.__reset();
|
||||
});
|
||||
|
||||
it("spawnRun creates a new tmux session named ccam-lane-<id> when none exists", () => {
|
||||
it("spawnRun creates a new tmux session named agenthub-lane-<id> when none exists", () => {
|
||||
const calls = [];
|
||||
tmux.__setExecImpl((args) => {
|
||||
calls.push(args);
|
||||
@@ -42,14 +42,14 @@ describe("pty-run", () => {
|
||||
return "";
|
||||
});
|
||||
const handle = pty.spawnRun({ laneId: 42, cwd: "/tmp/repo", model: "opus" });
|
||||
assert.equal(handle.id, "ccam-lane-42");
|
||||
assert.equal(handle.id, "agenthub-lane-42");
|
||||
const newSessionCall = calls.find((c) => c[0] === "new-session");
|
||||
assert.ok(newSessionCall, "expected a new-session call");
|
||||
assert.deepEqual(newSessionCall.slice(0, 6), [
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
"ccam-lane-42",
|
||||
"agenthub-lane-42",
|
||||
"-c",
|
||||
"/tmp/repo",
|
||||
]);
|
||||
@@ -66,7 +66,7 @@ describe("pty-run", () => {
|
||||
return ""; // has-session succeeds → already running
|
||||
});
|
||||
const handle = pty.spawnRun({ laneId: 7, cwd: "/tmp/repo" });
|
||||
assert.equal(handle.id, "ccam-lane-7");
|
||||
assert.equal(handle.id, "agenthub-lane-7");
|
||||
assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
|
||||
assert.ok(!calls.some((c) => c[0] === "send-keys"), "must not type over a live agent");
|
||||
});
|
||||
@@ -82,7 +82,7 @@ describe("pty-run", () => {
|
||||
assert.ok(!calls.some((c) => c[0] === "new-session"), "must not create a duplicate session");
|
||||
const literal = calls.find((c) => c[0] === "send-keys" && c[3] === "-l");
|
||||
assert.ok(literal, "expected a literal send-keys with the command line");
|
||||
assert.match(literal[4], /^CCAM_LANE_ID='8' 'claude' .*'--resume' 'abc12345'$/);
|
||||
assert.match(literal[4], /^AGENTHUB_LANE_ID='8' 'claude' .*'--resume' 'abc12345'$/);
|
||||
assert.ok(
|
||||
calls.some((c) => c[0] === "send-keys" && c[3] === "Enter"),
|
||||
"expected the command to be submitted"
|
||||
@@ -100,7 +100,7 @@ describe("pty-run", () => {
|
||||
assert.ok(literal.endsWith(`'don'\\''t; rm -rf /'`), literal);
|
||||
});
|
||||
|
||||
it("spawnRun puts CCAM_LANE_ID in the new session's environment", () => {
|
||||
it("spawnRun puts AGENTHUB_LANE_ID in the new session's environment", () => {
|
||||
let newSessionArgv = null;
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "has-session") {
|
||||
@@ -114,12 +114,12 @@ describe("pty-run", () => {
|
||||
pty.spawnRun({ laneId: 33, cwd: "/tmp/repo", tool: "codex" });
|
||||
const at = newSessionArgv.indexOf("-e");
|
||||
assert.ok(at > -1, "expected a tmux -e environment flag");
|
||||
assert.equal(newSessionArgv[at + 1], "CCAM_LANE_ID=33");
|
||||
assert.equal(newSessionArgv[at + 1], "AGENTHUB_LANE_ID=33");
|
||||
// Must stay ahead of the `--` that starts the pane's own argv.
|
||||
assert.ok(at < newSessionArgv.indexOf("--"));
|
||||
});
|
||||
|
||||
it("spawnRun prefixes CCAM_LANE_ID when typing into an existing shell pane", () => {
|
||||
it("spawnRun prefixes AGENTHUB_LANE_ID when typing into an existing shell pane", () => {
|
||||
let literal = null;
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "display-message") return "bash\n";
|
||||
@@ -129,7 +129,7 @@ describe("pty-run", () => {
|
||||
pty.spawnRun({ laneId: 12, cwd: "/tmp/repo", tool: "codex" });
|
||||
// An assignment prefix on the same line: tmux's -e only applies at session
|
||||
// creation, and a live shell takes its env from the command line.
|
||||
assert.match(literal, /^CCAM_LANE_ID='12' 'codex'/);
|
||||
assert.match(literal, /^AGENTHUB_LANE_ID='12' 'codex'/);
|
||||
});
|
||||
|
||||
it("spawnRun with resumeSessionId passes --resume in argv", () => {
|
||||
@@ -189,33 +189,33 @@ describe("pty-run", () => {
|
||||
calls.push(args);
|
||||
return "";
|
||||
});
|
||||
assert.equal(pty.killRun("ccam-lane-5"), true);
|
||||
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "ccam-lane-5"));
|
||||
assert.equal(pty.killRun("agenthub-lane-5"), true);
|
||||
assert.ok(calls.some((c) => c[0] === "kill-session" && c[2] === "agenthub-lane-5"));
|
||||
});
|
||||
|
||||
it("listRuns reflects live tmux-session state, not cached memory", () => {
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "list-sessions") return "ccam-lane-1\nccam-lane-2\n";
|
||||
if (args[0] === "list-sessions") return "agenthub-lane-1\nagenthub-lane-2\n";
|
||||
return "";
|
||||
});
|
||||
const first = pty.listRuns();
|
||||
assert.deepEqual(first.map((r) => r.id).sort(), ["ccam-lane-1", "ccam-lane-2"]);
|
||||
assert.deepEqual(first.map((r) => r.id).sort(), ["agenthub-lane-1", "agenthub-lane-2"]);
|
||||
|
||||
// Session killed out-of-band (not through killRun) — next list() call
|
||||
// must self-correct, proving state is computed, not stored.
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "list-sessions") return "ccam-lane-1\n";
|
||||
if (args[0] === "list-sessions") return "agenthub-lane-1\n";
|
||||
return "";
|
||||
});
|
||||
const second = pty.listRuns();
|
||||
assert.deepEqual(
|
||||
second.map((r) => r.id),
|
||||
["ccam-lane-1"]
|
||||
["agenthub-lane-1"]
|
||||
);
|
||||
});
|
||||
|
||||
it("laneIdFromRunId parses the numeric lane id back out", () => {
|
||||
assert.equal(pty.laneIdFromRunId("ccam-lane-42"), 42);
|
||||
assert.equal(pty.laneIdFromRunId("agenthub-lane-42"), 42);
|
||||
assert.equal(pty.laneIdFromRunId("not-a-run-id"), null);
|
||||
});
|
||||
|
||||
@@ -242,7 +242,7 @@ describe("pty-run", () => {
|
||||
initialPrompt: "the live prompt",
|
||||
});
|
||||
const retrieved = pty.getRun(handle.id);
|
||||
assert.equal(retrieved.id, "ccam-lane-99");
|
||||
assert.equal(retrieved.id, "agenthub-lane-99");
|
||||
assert.equal(retrieved.promptPreview, "the live prompt");
|
||||
assert.equal(retrieved.status, "running");
|
||||
});
|
||||
|
||||
@@ -206,7 +206,7 @@ describe("remote-sync command builders", () => {
|
||||
});
|
||||
|
||||
it("connectionSuccessMessage reflects explicit vs auto-detected WSL", () => {
|
||||
const wslProbe = "wsl.exe -e sh -c 'test -d ~/.claude/projects && echo CCAM_OK'";
|
||||
const wslProbe = "wsl.exe -e sh -c 'test -d ~/.claude/projects && echo AGENTHUB_OK'";
|
||||
assert.match(
|
||||
remoteSync.connectionSuccessMessage({ remote_home: "wsl:~/.claude" }, wslProbe),
|
||||
/wsl:~\/\.claude/
|
||||
@@ -220,7 +220,7 @@ describe("remote-sync command builders", () => {
|
||||
/auto-detected/i
|
||||
);
|
||||
assert.match(
|
||||
remoteSync.connectionSuccessMessage({ remote_home: null }, "sh -c 'echo CCAM_OK'"),
|
||||
remoteSync.connectionSuccessMessage({ remote_home: null }, "sh -c 'echo AGENTHUB_OK'"),
|
||||
/Remote Claude Code history found/
|
||||
);
|
||||
});
|
||||
@@ -263,7 +263,7 @@ describe("remote-sync command builders", () => {
|
||||
it("uses sh probe for POSIX absolute remote homes", () => {
|
||||
const probes = remoteSync.connectionProbeCommands({ remote_home: "/opt/cc" });
|
||||
assert.deepEqual(probes, [
|
||||
"sh -c 'test -d /opt/cc/projects && echo CCAM_OK || echo CCAM_NO_DIR'",
|
||||
"sh -c 'test -d /opt/cc/projects && echo AGENTHUB_OK || echo AGENTHUB_NO_DIR'",
|
||||
]);
|
||||
});
|
||||
it("accepts Windows-style remote_home with forward slashes", () => {
|
||||
@@ -337,7 +337,7 @@ describe("remote-sync command builders", () => {
|
||||
assert.equal(remoteSync.remoteProjectsPath({ remote_home: "/opt/cc" }), "/opt/cc/projects");
|
||||
});
|
||||
it("identifies top-level session ids in a mirrored tree (skips subagents)", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-staged-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-staged-"));
|
||||
const proj = path.join(dir, "-Users-x-proj");
|
||||
fs.mkdirSync(path.join(proj, "sess-1", "subagents"), { recursive: true });
|
||||
fs.writeFileSync(path.join(proj, "sess-1.jsonl"), "{}\n");
|
||||
@@ -564,7 +564,7 @@ describe("reconcileRemoteSessionStatus", () => {
|
||||
let stageRoot;
|
||||
|
||||
before(() => {
|
||||
stageRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-recon-"));
|
||||
stageRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-recon-"));
|
||||
});
|
||||
after(() => {
|
||||
try {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("/api/run", () => {
|
||||
});
|
||||
const spawned = await fetchJson("/api/run", { method: "POST", body: { laneId: 9, cwd: TMP } });
|
||||
assert.equal(spawned.status, 201);
|
||||
assert.equal(spawned.body.id, "ccam-lane-9");
|
||||
assert.equal(spawned.body.id, "agenthub-lane-9");
|
||||
|
||||
const fetched = await fetchJson(`/api/run/${spawned.body.id}`);
|
||||
assert.equal(fetched.status, 200);
|
||||
@@ -145,7 +145,7 @@ describe("/api/run", () => {
|
||||
|
||||
it("DELETE /:id kills a live tmux session", async () => {
|
||||
tmux.__setExecImpl(() => ""); // has-session succeeds; kill-session succeeds
|
||||
const { status, body } = await fetchJson("/api/run/ccam-lane-9", { method: "DELETE" });
|
||||
const { status, body } = await fetchJson("/api/run/agenthub-lane-9", { method: "DELETE" });
|
||||
assert.equal(status, 200);
|
||||
assert.deepEqual(body, { ok: true });
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe("/api/run", () => {
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const { status } = await fetchJson("/api/run/ccam-lane-999", { method: "DELETE" });
|
||||
const { status } = await fetchJson("/api/run/agenthub-lane-999", { method: "DELETE" });
|
||||
assert.equal(status, 404);
|
||||
});
|
||||
|
||||
|
||||
@@ -173,13 +173,13 @@ describe("probeLiveCwds — probe availability", () => {
|
||||
assert.equal(r.cwds.size, 0);
|
||||
});
|
||||
|
||||
it("is disabled inside a container (CCAM_FORCE_CONTAINER)", () => {
|
||||
it("is disabled inside a container (AGENTHUB_FORCE_CONTAINER)", () => {
|
||||
delete process.env.DASHBOARD_LIVENESS_PROBE;
|
||||
process.env.CCAM_FORCE_CONTAINER = "1";
|
||||
process.env.AGENTHUB_FORCE_CONTAINER = "1";
|
||||
try {
|
||||
assert.equal(realProbe().available, false);
|
||||
} finally {
|
||||
delete process.env.CCAM_FORCE_CONTAINER;
|
||||
delete process.env.AGENTHUB_FORCE_CONTAINER;
|
||||
process.env.DASHBOARD_LIVENESS_PROBE = "0";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-sync-yield-"));
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-sync-yield-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
process.env.DASHBOARD_DB_PATH = path.join(TMP_HOME, "dashboard.db");
|
||||
process.env.DASHBOARD_DATA_DIR = path.join(TMP_HOME, "data");
|
||||
|
||||
@@ -17,7 +17,7 @@ const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-sync-home-"));
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-sync-home-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
process.env.DASHBOARD_DB_PATH = path.join(TMP_HOME, "dashboard.db");
|
||||
process.env.DASHBOARD_DATA_DIR = path.join(TMP_HOME, "data");
|
||||
|
||||
@@ -17,8 +17,8 @@ const fs = require("node:fs");
|
||||
const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-skill-plugins-home-"));
|
||||
const TMP_PIPELINES = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-skill-plugins-tpl-"));
|
||||
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skill-plugins-home-"));
|
||||
const TMP_PIPELINES = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skill-plugins-tpl-"));
|
||||
process.env.CLAUDE_HOME = TMP_HOME;
|
||||
|
||||
const skillPlugins = require("../lib/skill-plugins");
|
||||
@@ -89,10 +89,10 @@ describe("requiredSkillPlugins", () => {
|
||||
assert.ok(required.get("superpowers").has("writing-plans"));
|
||||
});
|
||||
|
||||
it("ignores ccam's own commands and anything that is not a /plugin:skill", () => {
|
||||
it("ignores agenthub's own commands and anything that is not a /plugin:skill", () => {
|
||||
useTemplate({
|
||||
id: "tpl-shell",
|
||||
skill: "/ccam:ship-feature",
|
||||
skill: "/agenthub:ship-feature",
|
||||
nodes: [
|
||||
{ id: "a", command: "npm test" },
|
||||
{ id: "b", command: "/reload-plugins" },
|
||||
@@ -102,7 +102,7 @@ describe("requiredSkillPlugins", () => {
|
||||
|
||||
const required = skillPlugins.requiredSkillPlugins();
|
||||
|
||||
assert.equal(required.has("ccam"), false, "ccam is the plugin asking");
|
||||
assert.equal(required.has("agenthub"), false, "agenthub is the plugin asking");
|
||||
// Only the built-ins' superpowers requirement survives.
|
||||
assert.deepEqual([...required.keys()], ["superpowers"]);
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-skills-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-skills-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
|
||||
const { describe, it, after } = require("node:test");
|
||||
|
||||
@@ -464,7 +464,7 @@ test.describe("tests rule covers the runners this repo actually uses", () => {
|
||||
for (const command of [
|
||||
"node scripts/generate-openapi-yaml.js",
|
||||
"node -e \"console.log('hello')\"",
|
||||
"node bin/ccam.js start",
|
||||
"node bin/agenthub.js start",
|
||||
]) {
|
||||
const r = detect(pipeline, { tool_name: "Bash", tool_input: { command } });
|
||||
assert.notEqual(r && r.nodeId, "tests", command);
|
||||
@@ -551,25 +551,25 @@ test.describe("ship-feature pipeline template", () => {
|
||||
"Agent",
|
||||
{ description: "Design the fix", prompt: "Propose an approach.", subagent_type: "Plan" },
|
||||
],
|
||||
["intake", "Bash", { command: "ccam feature activate lane-stage-detect" }],
|
||||
["intake", "Bash", { command: "agenthub feature activate lane-stage-detect" }],
|
||||
["plan", "Skill", { skill: "superpowers:writing-plans" }],
|
||||
["plan", "Write", { file_path: "docs/superpowers/specs/lane-foo.md" }],
|
||||
["implementing", "Skill", { skill: "superpowers:test-driven-development" }],
|
||||
["implementing", "Skill", { skill: "superpowers:systematic-debugging" }],
|
||||
["implementing", "Edit", { file_path: "server/lib/app.js" }],
|
||||
["implementing", "Write", { file_path: "server/lib/app.js" }],
|
||||
["gates", "Bash", { command: "ccam lanes hook ci-gate" }],
|
||||
["gates", "Bash", { command: "ccam lanes sync-base --check feat/foo" }],
|
||||
["gates", "Bash", { command: "agenthub lanes hook ci-gate" }],
|
||||
["gates", "Bash", { command: "agenthub lanes sync-base --check feat/foo" }],
|
||||
// Stage 1b's independent-test subagent runs the repo's raw test command
|
||||
// directly, not through `ccam` — without this rule that pass has no
|
||||
// detection fallback if the agent skips `ccam stage tests`.
|
||||
// directly, not through `agenthub` — without this rule that pass has no
|
||||
// detection fallback if the agent skips `agenthub stage tests`.
|
||||
["gates", "Bash", { command: "npm run test:server" }],
|
||||
["e2e-feature", "Bash", { command: "ccam lanes up --qc" }],
|
||||
["e2e-feature", "Bash", { command: "ccam lanes hook e2e" }],
|
||||
["e2e-feature", "Bash", { command: "agenthub lanes up --qc" }],
|
||||
["e2e-feature", "Bash", { command: "agenthub lanes hook e2e" }],
|
||||
["review", "Skill", { skill: "code-review" }],
|
||||
["review", "Skill", { skill: "superpowers:requesting-code-review" }],
|
||||
["qc", "Agent", { subagent_type: "qc-local" }],
|
||||
["qc", "Bash", { command: "ccam lanes proof-link" }],
|
||||
["qc", "Bash", { command: "agenthub lanes proof-link" }],
|
||||
["gate", "Agent", { subagent_type: "senior-gate-reviewer" }],
|
||||
["gate", "Skill", { skill: "superpowers:verification-before-completion" }],
|
||||
["publishing", "Skill", { skill: "superpowers:finishing-a-development-branch" }],
|
||||
@@ -641,14 +641,14 @@ test.describe("ship-feature pipeline template", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test.it("does not read the word push, or an unrelated ccam call, as a later stage", () => {
|
||||
test.it("does not read the word push, or an unrelated agenthub call, as a later stage", () => {
|
||||
const { reload } = require("../lib/pipelines");
|
||||
reload();
|
||||
const pipeline = getPipeline("ship-feature");
|
||||
for (const command of [
|
||||
'git commit -m "do not push this yet"',
|
||||
"ccam lanes logs 3 e2e",
|
||||
"ccam stage gates --status running",
|
||||
"agenthub lanes logs 3 e2e",
|
||||
"agenthub stage gates --status running",
|
||||
"gh pr diff 42",
|
||||
]) {
|
||||
const r = detect(pipeline, { tool_name: "Bash", tool_input: { command } });
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("tmux wrapper", () => {
|
||||
|
||||
it("hasSession returns true when execFileSync exits 0", () => {
|
||||
tmux.__setExecImpl(() => "");
|
||||
assert.equal(tmux.hasSession("ccam-lane-1"), true);
|
||||
assert.equal(tmux.hasSession("agenthub-lane-1"), true);
|
||||
});
|
||||
|
||||
it("hasSession returns false when execFileSync throws", () => {
|
||||
@@ -25,7 +25,7 @@ describe("tmux wrapper", () => {
|
||||
e.status = 1;
|
||||
throw e;
|
||||
});
|
||||
assert.equal(tmux.hasSession("ccam-lane-1"), false);
|
||||
assert.equal(tmux.hasSession("agenthub-lane-1"), false);
|
||||
});
|
||||
|
||||
it("newSession builds the correct argv", () => {
|
||||
@@ -34,12 +34,16 @@ describe("tmux wrapper", () => {
|
||||
calls.push(args);
|
||||
return "";
|
||||
});
|
||||
tmux.newSession({ name: "ccam-lane-1", cwd: "/tmp/repo", argv: ["claude", "--model", "opus"] });
|
||||
tmux.newSession({
|
||||
name: "agenthub-lane-1",
|
||||
cwd: "/tmp/repo",
|
||||
argv: ["claude", "--model", "opus"],
|
||||
});
|
||||
assert.deepEqual(calls[0], [
|
||||
"new-session",
|
||||
"-d",
|
||||
"-s",
|
||||
"ccam-lane-1",
|
||||
"agenthub-lane-1",
|
||||
"-c",
|
||||
"/tmp/repo",
|
||||
"--",
|
||||
@@ -47,7 +51,7 @@ describe("tmux wrapper", () => {
|
||||
"--model",
|
||||
"opus",
|
||||
]);
|
||||
assert.deepEqual(calls[1], ["set-option", "-t", "ccam-lane-1", "mouse", "on"]);
|
||||
assert.deepEqual(calls[1], ["set-option", "-t", "agenthub-lane-1", "mouse", "on"]);
|
||||
});
|
||||
|
||||
it("sendText types the literal string and presses Enter, with no quoting", () => {
|
||||
@@ -56,15 +60,15 @@ describe("tmux wrapper", () => {
|
||||
calls.push(args);
|
||||
return "";
|
||||
});
|
||||
tmux.sendText("ccam-lane-1", 'Reminder: run "ccam stage implement"');
|
||||
tmux.sendText("agenthub-lane-1", 'Reminder: run "agenthub stage implement"');
|
||||
assert.deepEqual(calls[0], [
|
||||
"send-keys",
|
||||
"-t",
|
||||
"ccam-lane-1",
|
||||
"agenthub-lane-1",
|
||||
"-l",
|
||||
'Reminder: run "ccam stage implement"',
|
||||
'Reminder: run "agenthub stage implement"',
|
||||
]);
|
||||
assert.deepEqual(calls[1], ["send-keys", "-t", "ccam-lane-1", "Enter"]);
|
||||
assert.deepEqual(calls[1], ["send-keys", "-t", "agenthub-lane-1", "Enter"]);
|
||||
});
|
||||
|
||||
it("killSession never throws when the session is already gone", () => {
|
||||
@@ -73,12 +77,12 @@ describe("tmux wrapper", () => {
|
||||
e.status = 1;
|
||||
throw e;
|
||||
});
|
||||
assert.doesNotThrow(() => tmux.killSession("ccam-lane-1"));
|
||||
assert.doesNotThrow(() => tmux.killSession("agenthub-lane-1"));
|
||||
});
|
||||
|
||||
it("listSessions filters by prefix and ignores unrelated sessions", () => {
|
||||
tmux.__setExecImpl(() => "ccam-lane-1\nccam-lane-2\nsome-other-session\n");
|
||||
assert.deepEqual(tmux.listSessions("ccam-lane-"), ["ccam-lane-1", "ccam-lane-2"]);
|
||||
tmux.__setExecImpl(() => "agenthub-lane-1\nagenthub-lane-2\nsome-other-session\n");
|
||||
assert.deepEqual(tmux.listSessions("agenthub-lane-"), ["agenthub-lane-1", "agenthub-lane-2"]);
|
||||
});
|
||||
|
||||
it("listSessions returns [] when tmux has no sessions at all (exit 1)", () => {
|
||||
@@ -87,7 +91,7 @@ describe("tmux wrapper", () => {
|
||||
e.status = 1;
|
||||
throw e;
|
||||
});
|
||||
assert.deepEqual(tmux.listSessions("ccam-lane-"), []);
|
||||
assert.deepEqual(tmux.listSessions("agenthub-lane-"), []);
|
||||
});
|
||||
|
||||
it("isTmuxAvailable reflects whether the binary resolves on PATH", () => {
|
||||
|
||||
@@ -48,7 +48,7 @@ describe("websocket shutdown race", () => {
|
||||
// reference. An unguarded handleUpgrade here takes the process down, so
|
||||
// simply reaching the assertions below is the test passing.
|
||||
await rawUpgrade(port, "/ws");
|
||||
await rawUpgrade(port, "/ws-pty/ccam-lane-1");
|
||||
await rawUpgrade(port, "/ws-pty/agenthub-lane-1");
|
||||
|
||||
assert.equal(ws.getConnectionCount(), 0);
|
||||
await new Promise((r) => server.close(r));
|
||||
|
||||
@@ -13,7 +13,7 @@ const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-usage-api-"));
|
||||
const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-usage-api-"));
|
||||
process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db");
|
||||
process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes");
|
||||
|
||||
@@ -175,33 +175,36 @@ describe("GET /api/workflows/skill-usage", () => {
|
||||
// Two full calls of the same skill: each emits Pre + Post, so a naive
|
||||
// COUNT(*) would report four.
|
||||
for (let i = 0; i < 2; i += 1) {
|
||||
ev("PreToolUse", "Skill", { tool_input: { skill: "ccam:ship-feature" } });
|
||||
ev("PostToolUse", "Skill", { tool_input: { skill: "ccam:ship-feature" } });
|
||||
ev("PreToolUse", "Skill", { tool_input: { skill: "agenthub:ship-feature" } });
|
||||
ev("PostToolUse", "Skill", { tool_input: { skill: "agenthub:ship-feature" } });
|
||||
}
|
||||
ev("PreToolUse", "Skill", { tool_input: { skill: "daily-report" } });
|
||||
ev("PreToolUse", "mcp__plugin_ccam_ccam__dashboard_health_check", {});
|
||||
ev("PostToolUse", "mcp__plugin_ccam_ccam__dashboard_health_check", {});
|
||||
ev("PreToolUse", "mcp__plugin_agenthub_agenthub__dashboard_health_check", {});
|
||||
ev("PostToolUse", "mcp__plugin_agenthub_agenthub__dashboard_health_check", {});
|
||||
});
|
||||
|
||||
it("counts one call per invocation, not one per hook event", async () => {
|
||||
const res = await get("/api/workflows/skill-usage");
|
||||
assert.equal(res.status, 200);
|
||||
const ship = res.body.skills.find((s) => s.name === "ccam:ship-feature");
|
||||
const ship = res.body.skills.find((s) => s.name === "agenthub:ship-feature");
|
||||
assert.equal(ship.count, 2);
|
||||
assert.equal(ship.sessions, 1);
|
||||
});
|
||||
|
||||
it("attributes a prefixed skill to its plugin and leaves a bare one unattributed", async () => {
|
||||
const res = await get("/api/workflows/skill-usage");
|
||||
assert.equal(res.body.skills.find((s) => s.name === "ccam:ship-feature").plugin, "ccam");
|
||||
assert.equal(
|
||||
res.body.skills.find((s) => s.name === "agenthub:ship-feature").plugin,
|
||||
"agenthub"
|
||||
);
|
||||
assert.equal(res.body.skills.find((s) => s.name === "daily-report").plugin, null);
|
||||
});
|
||||
|
||||
it("adds a plugin's own MCP tool calls to its totals", async () => {
|
||||
const res = await get("/api/workflows/skill-usage");
|
||||
const ccam = res.body.plugins.find((p) => p.name === "ccam");
|
||||
const agenthub = res.body.plugins.find((p) => p.name === "agenthub");
|
||||
assert.deepEqual(
|
||||
{ skills: ccam.skills, skillCalls: ccam.skillCalls, mcpCalls: ccam.mcpCalls },
|
||||
{ skills: agenthub.skills, skillCalls: agenthub.skillCalls, mcpCalls: agenthub.mcpCalls },
|
||||
{ skills: 1, skillCalls: 2, mcpCalls: 1 }
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const os = require("node:os");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-wt-"));
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-wt-"));
|
||||
process.env.LANES_ROOT = path.join(ROOT, "lanes");
|
||||
|
||||
const wt = require("../lib/worktree");
|
||||
|
||||
Reference in New Issue
Block a user