import { describe, expect, test } from "bun:test"; import type { Tale } from "@taleseal/core"; import { buildExposureReport, buildRedactionReport, decideGate } from "./gate"; import { redactDeep } from "./redact"; /** a tale whose prose quotes two secrets — redactDeep scrubs them, the report counts them */ const leakyTale: Tale = redactDeep({ version: 1, title: "Fixing the flaky checkout test", sender: { name: "Dev" }, stationery: "letter", blocks: [ { kind: "lead", text: "Deployed with the key AKIAAAAABBBBCCCCDDDD and it worked." }, { kind: "prose", markdown: "The API key sk-syntheticAAAABBBBCCCCDDDD was rotated. Edited ~/example-app/src/checkout.test.ts, " + "checked https://taleseal.com/t/abc and mailed dev@example.com. `taleseal@0.3.0` shipped.", }, ], } satisfies Tale); describe("buildRedactionReport", () => { test("counts every marker by kind across the whole tale", () => { const report = buildRedactionReport(leakyTale); expect(report.total).toBeGreaterThanOrEqual(2); expect(report.counts["api-key"]).toBeGreaterThanOrEqual(1); expect(report.counts["aws-access-key"]).toBeGreaterThanOrEqual(1); expect(Object.values(report.counts).reduce((a, b) => a + b, 0)).toBe(report.total); }); test("keeps at most three samples, each showing the marker in redacted context", () => { const report = buildRedactionReport(leakyTale); expect(report.samples.length).toBeLessThanOrEqual(3); expect(report.samples.length).toBeGreaterThan(0); for (const sample of report.samples) { expect(sample.context).toContain(`[REDACTED:${sample.kind}]`); // context is the marker plus ~40 chars of surroundings (and ellipses) expect(sample.context.length).toBeLessThanOrEqual(`[REDACTED:${sample.kind}]`.length + 60); } const serialised = JSON.stringify(report); expect(serialised).not.toContain("sk-synthetic"); expect(serialised).not.toContain("AKIAAAAABBBBCCCCDDDD"); }); test("a clean tale reports zero", () => { const clean: Tale = { version: 1, title: "Hello", sender: { name: "Dev" }, stationery: "letter", blocks: [{ kind: "lead", text: "Nothing sensitive happened here at all today." }], }; expect(buildRedactionReport(clean)).toEqual({ total: 0, counts: {}, samples: [] }); }); }); describe("decideGate", () => { test("--yes publishes without a prompt, TTY or not", () => { expect(decideGate({ yes: true, tty: false })).toEqual({ action: "publish" }); expect(decideGate({ yes: true, tty: true })).toEqual({ action: "publish" }); }); test("a TTY without --yes gets the interactive prompt", () => { expect(decideGate({ yes: false, tty: true })).toEqual({ action: "prompt" }); }); test("no TTY and no --yes is refused, telling hooks/CI to pass --yes", () => { const decision = decideGate({ yes: false, tty: false }); expect(decision.action).toBe("refuse"); if (decision.action === "refuse") { expect(decision.message).toContain("--yes"); expect(decision.message).toContain("not a TTY"); } }); }); describe("buildExposureReport", () => { test("the strings no scrubber can judge — paths, hosts, addresses", () => { const exposure = buildExposureReport(leakyTale, { cwd: "/repo" }); // a file under another home directory: tilde-form is still disclosure expect(exposure.outsidePaths).toContain("~/example-app/src/checkout.test.ts"); expect(exposure.hosts).toEqual(["taleseal.com"]); expect(exposure.emails).toEqual(["dev@example.com"]); // taleseal@0.3.0 is a version, not a person }); test("ignores paths inside the project", () => { const tale: Tale = { version: 1, title: "Ship it", sender: { name: "Dev" }, stationery: "letter", blocks: [{ kind: "lead", text: "Touched ~/project/src/index.ts and nothing else." }], }; // the anchor arrives tilde-form, exactly as redaction supplies it const exposure = buildExposureReport(tale, { cwd: "~/project" }); expect(exposure.outsidePaths).toHaveLength(0); // the project itself is the point }); test("a truncated host prefix is folded into the full host it abbreviates", () => { const tale: Tale = { version: 1, title: "Check the listings", sender: { name: "Dev" }, stationery: "letter", blocks: [ { kind: "prose", // an excerpt-truncated string leaves a bare prefix of the same host markdown: "Compared https://alternativeto.net/software/x — see also https://alternativeto.ne", }, ], }; const exposure = buildExposureReport(tale, { cwd: "/home/dev/project" }); expect(exposure.hosts).toEqual(["alternativeto.net"]); }); });