/** * @file Tests for `server/lib/clone-queue.js`: no more clones run at once * than the limit, the rest start in arrival order as slots free, a failing * clone frees its slot too, the per-clone thread share, and a tracked * clone's progress: its queue position, git's --progress lines parsed, and * shown only to its owner. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, afterEach } = require("node:test"); const assert = require("node:assert/strict"); const os = require("node:os"); const queue = require("../lib/clone-queue"); const saved = process.env.AGENTHUB_CLONE_CONCURRENCY; function gate() { let open; const p = new Promise((r) => (open = r)); return { p, open }; } describe("clone-queue", () => { afterEach(() => { if (saved === undefined) delete process.env.AGENTHUB_CLONE_CONCURRENCY; else process.env.AGENTHUB_CLONE_CONCURRENCY = saved; }); it("runs at most the limit at once, the rest in arrival order", async () => { process.env.AGENTHUB_CLONE_CONCURRENCY = "2"; const started = []; const gates = [gate(), gate(), gate(), gate()]; const runs = gates.map((g, i) => queue.withCloneSlot(async () => { started.push(i); await g.p; return i; }) ); await new Promise((r) => setImmediate(r)); assert.deepEqual(started, [0, 1]); assert.deepEqual(queue.cloneQueueState(), { running: 2, waiting: 2, limit: 2 }); gates[1].open(); await runs[1]; await new Promise((r) => setImmediate(r)); assert.deepEqual(started, [0, 1, 2]); gates[0].open(); gates[2].open(); gates[3].open(); assert.deepEqual(await Promise.all(runs), [0, 1, 2, 3]); assert.deepEqual(queue.cloneQueueState(), { running: 0, waiting: 0, limit: 2 }); }); it("frees the slot when a clone fails", async () => { process.env.AGENTHUB_CLONE_CONCURRENCY = "1"; await assert.rejects( queue.withCloneSlot(async () => { throw new Error("boom"); }), /boom/ ); assert.equal(await queue.withCloneSlot(async () => "ok"), "ok"); assert.equal(queue.cloneQueueState().running, 0); }); it("splits the cores between the running clones", () => { const cores = os.availableParallelism?.() ?? os.cpus().length; process.env.AGENTHUB_CLONE_CONCURRENCY = "1"; assert.equal(queue.threads(), Math.max(1, cores)); process.env.AGENTHUB_CLONE_CONCURRENCY = String(cores * 4); assert.equal(queue.threads(), 1); delete process.env.AGENTHUB_CLONE_CONCURRENCY; assert.equal(queue.concurrency(), Math.max(1, Math.floor(cores / 2))); }); it("reports a tracked clone's queue position, then git's progress, to its owner only", async () => { process.env.AGENTHUB_CLONE_CONCURRENCY = "1"; const first = gate(); const a = queue.track("tok-aaaaaaaaaaaaaaaa", 7); const b = queue.track("tok-bbbbbbbbbbbbbbbb", 7); const ra = queue.withCloneSlot(() => first.p, a); let release; const rb = queue.withCloneSlot(() => new Promise((r) => (release = r)), b); await new Promise((r) => setImmediate(r)); assert.equal(queue.progress("tok-aaaaaaaaaaaaaaaa", 7).state, "running"); const waitingView = queue.progress("tok-bbbbbbbbbbbbbbbb", 7); assert.equal(waitingView.state, "queued"); assert.equal(waitingView.position, 1); assert.equal(waitingView.running, 1); // Another user — or nobody — never sees it. assert.equal(queue.progress("tok-bbbbbbbbbbbbbbbb", 8), null); assert.equal(queue.progress("tok-cccccccccccccccc", 7), null); queue.report( a, "Cloning into 'r'...\nremote: Counting objects: 100% (5/5), done.\n" + "Receiving objects: 12% (120/1000), 3.00 MiB | 1.50 MiB/s\rReceiving objects: 45% (450/1000), 120.50 MiB | 5.20 MiB/s" ); const v = queue.progress("tok-aaaaaaaaaaaaaaaa", 7); assert.equal(v.phase, "receiving"); assert.equal(v.percent, 45); assert.equal(v.detail, "450/1000, 120.50 MiB | 5.20 MiB/s"); queue.report(a, "Resolving deltas: 100% (300/300), done.\n"); assert.equal(queue.progress("tok-aaaaaaaaaaaaaaaa", 7).phase, "resolving"); assert.equal(queue.progress("tok-aaaaaaaaaaaaaaaa", 7).detail, "300/300"); first.open(); await ra; queue.finish(a, true); assert.equal(queue.progress("tok-aaaaaaaaaaaaaaaa", 7).state, "done"); await new Promise((r) => setImmediate(r)); assert.equal(queue.progress("tok-bbbbbbbbbbbbbbbb", 7).state, "running"); release(); await rb; queue.finish(b, false); assert.equal(queue.progress("tok-bbbbbbbbbbbbbbbb", 7).state, "failed"); }); it("refuses a malformed or reused token, and keeps progress out of an error", () => { assert.throws(() => queue.track("short", null), { code: "EBADFIELD" }); assert.throws(() => queue.track("../../etc/passwd-xxxxxxxx", null), { code: "EBADFIELD" }); queue.track("tok-dddddddddddddddd", null); assert.throws(() => queue.track("tok-dddddddddddddddd", null), { code: "EBADFIELD" }); assert.equal( queue.stripProgress( "Cloning into 'r'...\nReceiving objects: 45% (450/1000)\rfatal: early EOF\n" ), "Cloning into 'r'...\nfatal: early EOF" ); }); });