/** * @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ĩ */ 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(); } }); });