/** * @file Tests for server/lib/gh.js's execFile wrapper against real fake `gh` * scripts on PATH — the same fixture style server/__tests__/lane-lifecycle.test.js * uses for a fake `claude` binary, since this codebase's server tests use real * subprocesses instead of mocking child_process. * @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-gh-")); const { gh } = require("../lib/gh"); /** Puts a fake `gh` script on PATH for the duration of one test. */ function stubGh(scriptBody) { const bin = path.join(ROOT, `bin-${Date.now()}-${Math.random().toString(36).slice(2)}`); fs.mkdirSync(bin, { recursive: true }); const bin_gh = path.join(bin, "gh"); fs.writeFileSync(bin_gh, `#!/usr/bin/env bash\n${scriptBody}\n`); fs.chmodSync(bin_gh, 0o755); const originalPath = process.env.PATH; process.env.PATH = `${bin}${path.delimiter}${originalPath}`; return () => { process.env.PATH = originalPath; }; } after(() => fs.rmSync(ROOT, { recursive: true, force: true })); describe("gh()", () => { it("returns stdout on success", async () => { const restore = stubGh('echo "$@" >&2\necho \'{"ok":true}\''); try { const result = await gh(ROOT, ["run", "list"]); assert.match(result.stdout, /"ok":true/); } finally { restore(); } }); it("throws with .gh details on a non-zero exit", async () => { const restore = stubGh('echo "boom" >&2\nexit 7'); try { await assert.rejects( () => gh(ROOT, ["run", "list"]), (err) => err.gh.code === 7 && /boom/.test(err.gh.stderr) ); } finally { restore(); } }); it("throws ENOGH when gh is not on PATH", async () => { const originalPath = process.env.PATH; process.env.PATH = ROOT; // a directory with no gh binary in it try { await assert.rejects(() => gh(ROOT, ["run", "list"]), { code: "ENOGH" }); } finally { process.env.PATH = originalPath; } }); });