matrix-builder / web /src /lib /__tests__ /gitpilot-client.test.ts
ruslanmv
Deploy: metrics + docs (Batch 12)
22b729d
Raw
History Blame Contribute Delete
10.4 kB
// Unit tests for the local GitPilot bridge (Batch 3).
//
// Run with: npm run test (node --test with type-stripping and the @/ alias resolver).
// Covers the pure URL/payload helpers and the probe/send behaviour against a
// stubbed fetch — including graceful failure when local GitPilot is down.
import assert from "node:assert/strict";
import { afterEach, describe, it } from "node:test";
import {
DEFAULT_LOCAL_GITPILOT_URL,
buildMatrixRunPayload,
cloudArtifactUrl,
createCloudRun,
fetchDiffText,
findingsFromReport,
getCloudRun,
isCloudRunTerminal,
localGitPilotBaseUrl,
localHealthUrl,
localRunsUrl,
openPr,
parseDiffLines,
probeLocalGitPilot,
repairCloudRun,
sendToLocalGitPilot,
stripTrailingSlash,
validateCloudRun,
type CloudValidationResult,
} from "@/lib/gitpilot-client";
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
});
describe("gitpilot-client · url helpers", () => {
it("strips trailing slashes", () => {
assert.equal(stripTrailingSlash("http://localhost:8000/"), "http://localhost:8000");
assert.equal(stripTrailingSlash("http://localhost:8000///"), "http://localhost:8000");
});
it("builds the matrix bridge health + runs urls", () => {
assert.equal(localHealthUrl("http://localhost:8000"), "http://localhost:8000/api/matrix/health");
assert.equal(localRunsUrl("http://localhost:8000/"), "http://localhost:8000/api/matrix/runs");
});
it("defaults the base url to localhost:8000", () => {
assert.equal(DEFAULT_LOCAL_GITPILOT_URL, "http://localhost:8000");
assert.equal(localGitPilotBaseUrl(), "http://localhost:8000");
});
});
describe("gitpilot-client · buildMatrixRunPayload", () => {
it("maps the contract and tags coder/source", () => {
const payload = buildMatrixRunPayload({
bundleUrl: "https://api.ruslanmv.com/v1/matrix-bundles/b1",
projectName: "Starter",
taskId: "TASK-001",
prompt: "do the thing",
allowedFiles: ["src/**"],
forbiddenFiles: ["MATRIX_STANDARDS.lock"],
validationCommands: ["pytest -q"],
});
assert.equal(payload.bundle_url, "https://api.ruslanmv.com/v1/matrix-bundles/b1");
assert.equal(payload.task_id, "TASK-001");
assert.equal(payload.coder, "gitpilot");
assert.equal(payload.source, "matrix-builder");
assert.equal(payload.mode, "ask"); // default
assert.deepEqual(payload.allowed_files, ["src/**"]);
// Copies, not references to the readonly inputs.
assert.deepEqual(payload.forbidden_files, ["MATRIX_STANDARDS.lock"]);
});
});
describe("gitpilot-client · probeLocalGitPilot", () => {
it("returns true on a healthy 200", async () => {
globalThis.fetch = (async () =>
new Response(JSON.stringify({ status: "ok" }), { status: 200 })) as typeof fetch;
assert.equal(await probeLocalGitPilot("http://localhost:8000", 200), true);
});
it("returns false on a non-200", async () => {
globalThis.fetch = (async () => new Response("nope", { status: 404 })) as typeof fetch;
assert.equal(await probeLocalGitPilot("http://localhost:8000", 200), false);
});
it("returns false (never throws) when the server is unreachable", async () => {
globalThis.fetch = (async () => {
throw new Error("ECONNREFUSED");
}) as typeof fetch;
assert.equal(await probeLocalGitPilot("http://localhost:8000", 200), false);
});
});
describe("gitpilot-client · sendToLocalGitPilot", () => {
const payload = buildMatrixRunPayload({
bundleUrl: "https://api.ruslanmv.com/v1/matrix-bundles/b1",
projectName: "Starter",
taskId: "TASK-001",
prompt: "do the thing",
allowedFiles: ["src/**"],
forbiddenFiles: ["MATRIX_STANDARDS.lock"],
validationCommands: ["pytest -q"],
});
it("posts the run and returns the queued result", async () => {
let seenUrl = "";
let seenBody: unknown = null;
globalThis.fetch = (async (url: string, init: RequestInit) => {
seenUrl = url;
seenBody = JSON.parse(String(init.body));
return new Response(
JSON.stringify({ run_id: "gp-run-abc", status: "queued", url: "http://localhost:8000/api/v1/gitpilot/runs/gp-run-abc" }),
{ status: 200 },
);
}) as typeof fetch;
const result = await sendToLocalGitPilot(payload, "http://localhost:8000");
assert.equal(seenUrl, "http://localhost:8000/api/matrix/runs");
assert.deepEqual((seenBody as { coder: string }).coder, "gitpilot");
assert.equal(result.run_id, "gp-run-abc");
assert.equal(result.status, "queued");
});
it("throws on a non-2xx so the caller can show a graceful message", async () => {
globalThis.fetch = (async () => new Response("boom", { status: 500 })) as typeof fetch;
await assert.rejects(() => sendToLocalGitPilot(payload, "http://localhost:8000"));
});
});
describe("gitpilot-client · cloud run (Matrix Builder backend)", () => {
it("isCloudRunTerminal recognises terminal states only", () => {
for (const s of ["completed", "blocked", "error", "needs_approval"]) {
assert.equal(isCloudRunTerminal(s), true);
}
for (const s of ["queued", "running"]) {
assert.equal(isCloudRunTerminal(s), false);
}
});
it("createCloudRun POSTs to the bundle's MB endpoint and returns the run", async () => {
let seenUrl = "";
globalThis.fetch = (async (url: string) => {
seenUrl = url;
return new Response(JSON.stringify({ run_id: "gp-run-1", status: "queued", url: "x" }), { status: 200 });
}) as typeof fetch;
const r = await createCloudRun("bundle_demo", { task_id: "T1" });
assert.match(seenUrl, /\/api\/v1\/bundles\/bundle_demo\/gitpilot\/runs$/);
assert.equal(r.run_id, "gp-run-1");
assert.equal(r.status, "queued");
});
it("getCloudRun reads status from the MB endpoint", async () => {
globalThis.fetch = (async () =>
new Response(
JSON.stringify({
run_id: "gp-run-1",
status: "completed",
summary: "done",
diff_url: "/api/v1/gitpilot/runs/gp-run-1/diff",
logs_url: "/api/v1/gitpilot/runs/gp-run-1/logs",
test_status: "passed",
changed_files: ["tests/test_health.py"],
}),
{ status: 200 },
)) as typeof fetch;
const s = await getCloudRun("gp-run-1");
assert.equal(s.status, "completed");
assert.equal(s.test_status, "passed");
assert.deepEqual(s.changed_files, ["tests/test_health.py"]);
});
it("cloudArtifactUrl prefixes the MB base and passes null through", () => {
assert.equal(cloudArtifactUrl(null), null);
// apiBaseUrl defaults to "/api/builder" in the test env (no NEXT_PUBLIC_API_BASE_URL).
assert.match(cloudArtifactUrl("/api/v1/gitpilot/runs/x/diff") ?? "", /\/api\/v1\/gitpilot\/runs\/x\/diff$/);
});
});
describe("gitpilot-client · validation gate + repair (Batches 7 & 8)", () => {
const approved: CloudValidationResult = {
run_id: "gp-run-1",
gate: { status: "approved", can_commit: true, can_repair: false, blocked: false },
report: { status: "approved", score: 100, violations: [], repair_prompt: null },
};
it("validateCloudRun POSTs to the run's validate endpoint and returns the gate", async () => {
let seenUrl = "";
globalThis.fetch = (async (url: string) => {
seenUrl = url;
return new Response(JSON.stringify(approved), { status: 200 });
}) as typeof fetch;
const result = await validateCloudRun("bundle_demo", "gp-run-1");
assert.match(seenUrl, /\/api\/v1\/bundles\/bundle_demo\/gitpilot\/runs\/gp-run-1\/validate$/);
assert.equal(result.gate.can_commit, true);
});
it("repairCloudRun POSTs findings and returns a new child run", async () => {
let seenUrl = "";
let seenBody: { repair_prompt?: string } = {};
globalThis.fetch = (async (url: string, init: RequestInit) => {
seenUrl = url;
seenBody = JSON.parse(String(init.body));
return new Response(JSON.stringify({ run_id: "gp-run-2", status: "queued", url: "x" }), { status: 200 });
}) as typeof fetch;
const child = await repairCloudRun("bundle_demo", "gp-run-1", { repair_prompt: "fix it" });
assert.match(seenUrl, /\/gitpilot\/runs\/gp-run-1\/repair$/);
assert.equal(seenBody.repair_prompt, "fix it");
assert.equal(child.run_id, "gp-run-2");
});
it("findingsFromReport flattens violations into rule: message strings", () => {
const findings = findingsFromReport({
status: "needs-repair",
score: 60,
repair_prompt: "x",
violations: [
{ rule_id: "R1", severity: "high", message: "outside allowed_paths" },
{ rule_id: "R2", severity: "medium", message: "missing test" },
],
});
assert.deepEqual(findings, ["R1: outside allowed_paths", "R2: missing test"]);
});
});
describe("gitpilot-client · PR flow + diff viewer (Batch 11)", () => {
it("parseDiffLines classifies add/del/hunk/meta/context", () => {
const lines = parseDiffLines(
["--- a/x", "+++ b/x", "@@ -1 +1 @@", "+added", "-removed", " context"].join("\n"),
);
assert.deepEqual(
lines.map((l) => l.kind),
["meta", "meta", "hunk", "add", "del", "context"],
);
});
it("openPr POSTs to the run's PR endpoint and returns the pr_url", async () => {
let seenUrl = "";
globalThis.fetch = (async (url: string) => {
seenUrl = url;
return new Response(
JSON.stringify({ run_id: "gp-run-1", pr_url: "https://github.com/o/r/pull/1", status: "draft", message: "" }),
{ status: 200 },
);
}) as typeof fetch;
const r = await openPr("bundle_demo", "gp-run-1", { title: "x" });
assert.match(seenUrl, /\/api\/v1\/bundles\/bundle_demo\/gitpilot\/runs\/gp-run-1\/pr$/);
assert.equal(r.pr_url, "https://github.com/o/r/pull/1");
});
it("openPr surfaces the 409 'Matrix approval required' as a clear error", async () => {
globalThis.fetch = (async () => new Response("{}", { status: 409 })) as typeof fetch;
await assert.rejects(() => openPr("b", "r"), /Matrix approval required/);
});
it("fetchDiffText fetches the proxied diff text", async () => {
globalThis.fetch = (async () => new Response("--- a\n+++ b\n", { status: 200 })) as typeof fetch;
const text = await fetchDiffText("/api/v1/gitpilot/runs/r/diff");
assert.match(text, /\+\+\+ b/);
});
});