17d56d0e01
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).
126 lines
4.2 KiB
JavaScript
126 lines
4.2 KiB
JavaScript
/**
|
|
* @file Tests for server/lib/ci-status.js against a fake `gh` binary that
|
|
* returns canned `run list`/`run view` JSON for each scenario this module
|
|
* has to classify: no run yet, an in-progress run, a green run, and a red
|
|
* run — plus branch-vs-sha ref detection.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
const { describe, it, after } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
|
|
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-"));
|
|
const { ciStatus } = require("../lib/ci-status");
|
|
|
|
/**
|
|
* Fake `gh` that branches on the subcommand: `run list` returns `listJson`,
|
|
* `run view` returns `viewJson`. Also records every invocation's args (one
|
|
* line per call, space-joined) to `$CALLS_LOG` so a test can assert which
|
|
* flag (--branch vs --commit) was used.
|
|
*/
|
|
function stubGh(listJson, viewJson) {
|
|
const bin = path.join(ROOT, `bin-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
fs.mkdirSync(bin, { recursive: true });
|
|
const callsLog = path.join(bin, "calls.log");
|
|
fs.writeFileSync(callsLog, "");
|
|
const script = `#!/usr/bin/env bash
|
|
echo "$@" >> "${callsLog}"
|
|
if [ "$1" = "run" ] && [ "$2" = "list" ]; then
|
|
cat <<'EOF'
|
|
${JSON.stringify(listJson)}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
if [ "$1" = "run" ] && [ "$2" = "view" ]; then
|
|
cat <<'EOF'
|
|
${JSON.stringify(viewJson)}
|
|
EOF
|
|
exit 0
|
|
fi
|
|
echo "unexpected gh invocation: $@" >&2
|
|
exit 1
|
|
`;
|
|
fs.writeFileSync(path.join(bin, "gh"), script);
|
|
fs.chmodSync(path.join(bin, "gh"), 0o755);
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${bin}${path.delimiter}${originalPath}`;
|
|
return {
|
|
calls: () => fs.readFileSync(callsLog, "utf8").trim().split("\n").filter(Boolean),
|
|
restore: () => {
|
|
process.env.PATH = originalPath;
|
|
},
|
|
};
|
|
}
|
|
|
|
after(() => fs.rmSync(ROOT, { recursive: true, force: true }));
|
|
|
|
describe("ciStatus()", () => {
|
|
it("reports pending when no run exists yet for the ref", async () => {
|
|
const stub = stubGh([], {});
|
|
try {
|
|
const result = await ciStatus(ROOT, "development");
|
|
assert.deepEqual(result, { state: "pending", ref: "development", checks: [] });
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("reports pending when the latest run is still in progress", async () => {
|
|
const stub = stubGh([{ databaseId: 1, status: "in_progress", conclusion: null }], {
|
|
jobs: [{ name: "server-tests", status: "in_progress", conclusion: null, url: null }],
|
|
});
|
|
try {
|
|
const result = await ciStatus(ROOT, "development");
|
|
assert.equal(result.state, "pending");
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("reports success when the latest run completed clean", async () => {
|
|
const stub = stubGh([{ databaseId: 2, status: "completed", conclusion: "success" }], {
|
|
jobs: [
|
|
{ name: "server-tests", status: "completed", conclusion: "success", url: "https://x/1" },
|
|
{ name: "client-tests", status: "completed", conclusion: "skipped", url: "https://x/2" },
|
|
],
|
|
});
|
|
try {
|
|
const result = await ciStatus(ROOT, "development");
|
|
assert.equal(result.state, "success");
|
|
assert.equal(result.checks.length, 2);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("reports failure when any job failed", async () => {
|
|
const stub = stubGh([{ databaseId: 3, status: "completed", conclusion: "failure" }], {
|
|
jobs: [
|
|
{ name: "server-tests", status: "completed", conclusion: "failure", url: "https://x/3" },
|
|
],
|
|
});
|
|
try {
|
|
const result = await ciStatus(ROOT, "development");
|
|
assert.equal(result.state, "failure");
|
|
assert.equal(result.checks[0].conclusion, "failure");
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
|
|
it("uses --commit for a sha-shaped ref and --branch otherwise", async () => {
|
|
const stub = stubGh([], {});
|
|
try {
|
|
await ciStatus(ROOT, "a1b2c3d");
|
|
await ciStatus(ROOT, "feat/some-slug");
|
|
const calls = stub.calls().filter((line) => line.startsWith("run list"));
|
|
assert.match(calls[0], /--commit a1b2c3d/);
|
|
assert.match(calls[1], /--branch feat\/some-slug/);
|
|
} finally {
|
|
stub.restore();
|
|
}
|
|
});
|
|
});
|