/** * @file Tests for server/lib/proof.js: proof-directory listing, containment- * checked file resolution, delete granularities, and the clone-root symlink * convergence (ensureProofLink). * @author Nguyễn Ngọc Trí Vĩ */ const os = require("node:os"); const path = require("node:path"); const fs = require("node:fs"); const SUITE_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-proof-")); process.env.DASHBOARD_DB_PATH = path.join(SUITE_ROOT, "dashboard.db"); process.env.LANES_ROOT = path.join(SUITE_ROOT, "lanes"); const { describe, it, after } = require("node:test"); const assert = require("node:assert/strict"); const lanesLib = require("../lib/lanes"); const features = require("../lib/lane-features"); const proof = require("../lib/proof"); after(() => fs.rmSync(SUITE_ROOT, { recursive: true, force: true })); let laneSeq = 0; function makeLane() { laneSeq += 1; const cwd = path.join(SUITE_ROOT, `lane-cwd-${laneSeq}`); fs.mkdirSync(cwd, { recursive: true }); return lanesLib.createLane({ title: `lane ${laneSeq}`, cwd, kind: "managed" }); } function writeShot(lane, slug, group, name) { const dir = path.join(lane.cwd, ".playwright-mcp", "proof", slug, group); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, name), "fake-png"); } describe("listProof", () => { it("groups screenshots by slug then phase group", () => { const lane = makeLane(); writeShot(lane, "one", "qc-local", "a.png"); writeShot(lane, "one", "qc-local", "b.png"); writeShot(lane, "one", "qc-dev", "c.png"); const list = proof.listProof(lane.id); const one = list.find((f) => f.slug === "one"); assert.deepEqual(one.groups["qc-local"].sort(), ["a.png", "b.png"]); assert.deepEqual(one.groups["qc-dev"], ["c.png"]); }); it("surfaces the ticket report path when present", () => { const lane = makeLane(); const ticketDir = path.join(lane.cwd, ".playwright-mcp", "proof", "one", "ticket"); fs.mkdirSync(ticketDir, { recursive: true }); fs.writeFileSync(path.join(ticketDir, "REPORT.html"), ""); const one = proof.listProof(lane.id).find((f) => f.slug === "one"); assert.equal(one.ticket_report, "one/ticket/REPORT.html"); }); it("never lists the ticket dir as a screenshot group", () => { const lane = makeLane(); const ticketDir = path.join(lane.cwd, ".playwright-mcp", "proof", "one", "ticket"); fs.mkdirSync(ticketDir, { recursive: true }); fs.writeFileSync(path.join(ticketDir, "REPORT.html"), ""); const one = proof.listProof(lane.id).find((f) => f.slug === "one"); assert.equal(one.groups.ticket, undefined); }); it("surfaces an activated feature with zero screenshots yet, groups empty", () => { const lane = makeLane(); features.activateFeature(lane.id, "brand-new"); const list = proof.listProof(lane.id); const f = list.find((x) => x.slug === "brand-new"); assert.ok(f); assert.deepEqual(f.groups, {}); }); }); describe("proofFile", () => { it("resolves a real screenshot inside the proof root", () => { const lane = makeLane(); writeShot(lane, "one", "qc-local", "a.png"); const p = proof.proofFile(lane.id, "one", "qc-local", "a.png"); assert.ok(p); assert.ok(fs.existsSync(p)); }); it("refuses a traversal attempt via ..", () => { const lane = makeLane(); writeShot(lane, "one", "qc-local", "a.png"); assert.equal(proof.proofFile(lane.id, "one", "..", "..%2fsecret"), null); assert.equal(proof.proofFile(lane.id, "../../etc", "x", "passwd"), null); }); it("refuses a symlink that escapes the proof root", () => { const lane = makeLane(); const dir = path.join(lane.cwd, ".playwright-mcp", "proof", "one", "qc-local"); fs.mkdirSync(dir, { recursive: true }); const outside = path.join(SUITE_ROOT, "outside-secret.png"); fs.writeFileSync(outside, "secret"); fs.symlinkSync(outside, path.join(dir, "escape.png")); assert.equal(proof.proofFile(lane.id, "one", "qc-local", "escape.png"), null); }); it("refuses a non-image, non-html extension", () => { const lane = makeLane(); const dir = path.join(lane.cwd, ".playwright-mcp", "proof", "one", "qc-local"); fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(path.join(dir, "a.txt"), "not an image"); assert.equal(proof.proofFile(lane.id, "one", "qc-local", "a.txt"), null); }); it("returns null for a missing lane", () => { assert.equal(proof.proofFile(999999, "one", "qc-local", "a.png"), null); }); }); describe("deleteProof", () => { it("deletes listed images and prunes the group dir if it becomes empty", () => { const lane = makeLane(); writeShot(lane, "one", "qc-local", "a.png"); const result = proof.deleteProof(lane.id, { slug: "one", group: "qc-local", images: ["a.png"], }); assert.equal(result.deleted, 1); assert.equal( fs.existsSync(path.join(lane.cwd, ".playwright-mcp", "proof", "one", "qc-local")), false ); }); it("deletes a whole group, leaves siblings intact", () => { const lane = makeLane(); writeShot(lane, "one", "qc-local", "a.png"); writeShot(lane, "one", "qc-dev", "b.png"); proof.deleteProof(lane.id, { slug: "one", group: "qc-local" }); assert.equal( fs.existsSync(path.join(lane.cwd, ".playwright-mcp", "proof", "one", "qc-local")), false ); assert.ok( fs.existsSync(path.join(lane.cwd, ".playwright-mcp", "proof", "one", "qc-dev", "b.png")) ); }); it("refuses to delete the ticket group by name", () => { const lane = makeLane(); const ticketDir = path.join(lane.cwd, ".playwright-mcp", "proof", "one", "ticket"); fs.mkdirSync(ticketDir, { recursive: true }); fs.writeFileSync(path.join(ticketDir, "REPORT.html"), ""); assert.throws( () => proof.deleteProof(lane.id, { slug: "one", group: "ticket" }), (err) => err.code === "EBADPATH" ); }); it("whole-feature delete removes every group but keeps ticket/", () => { const lane = makeLane(); writeShot(lane, "one", "qc-local", "a.png"); const ticketDir = path.join(lane.cwd, ".playwright-mcp", "proof", "one", "ticket"); fs.mkdirSync(ticketDir, { recursive: true }); fs.writeFileSync(path.join(ticketDir, "REPORT.html"), ""); proof.deleteProof(lane.id, { slug: "one" }); assert.equal( fs.existsSync(path.join(lane.cwd, ".playwright-mcp", "proof", "one", "qc-local")), false ); assert.ok(fs.existsSync(path.join(ticketDir, "REPORT.html"))); }); it("refuses a slug containing a path separator or ..", () => { const lane = makeLane(); assert.throws( () => proof.deleteProof(lane.id, { slug: "../escape" }), (err) => err.code === "EBADPATH" ); }); it("throws ENOFEATURE for a slug with no proof directory", () => { const lane = makeLane(); assert.throws( () => proof.deleteProof(lane.id, { slug: "never-had-proof" }), (err) => err.code === "ENOFEATURE" ); }); }); describe("ensureProofLink", () => { it("creates the symlink when neither clone-root proof/ nor the canonical dir exist yet", () => { const lane = makeLane(); const result = proof.ensureProofLink(lanesLib.getLane(lane.id)); assert.equal(result.linked, true); const strayPath = path.join(lane.cwd, "proof"); assert.equal(fs.lstatSync(strayPath).isSymbolicLink(), true); assert.equal(fs.readlinkSync(strayPath), path.join(".playwright-mcp", "proof")); }); it("merges a pre-existing real proof/ dir into the canonical root, then links it", () => { const lane = makeLane(); const strayDir = path.join(lane.cwd, "proof", "stranded", "qc-local"); fs.mkdirSync(strayDir, { recursive: true }); fs.writeFileSync(path.join(strayDir, "old.png"), "stranded shot"); proof.ensureProofLink(lanesLib.getLane(lane.id)); const merged = path.join( lane.cwd, ".playwright-mcp", "proof", "stranded", "qc-local", "old.png" ); assert.ok(fs.existsSync(merged)); assert.equal(fs.lstatSync(path.join(lane.cwd, "proof")).isSymbolicLink(), true); }); it("is a no-op (linked: false) when the symlink already points at the right place", () => { const lane = makeLane(); proof.ensureProofLink(lanesLib.getLane(lane.id)); const second = proof.ensureProofLink(lanesLib.getLane(lane.id)); assert.equal(second.linked, false); }); });