Files
gstack/test/redact-audit-log.test.ts
T
Garry Tan 7bae40c40d feat(spec,cso): wire shared redaction — semantic pass + scan-at-sink + taxonomy
/spec Phase 4.5 rewrite:
- Phase 4.5a: in-conversation semantic content review (named-criticism,
  customer complaints, unannounced strategy, NDA, codename bleed). Injection-
  hardened (a body containing the SEMANTIC_REVIEW marker forces flagged).
  Content-free audit trail to ~/.gstack/security/semantic-reviews.jsonl.
- Phase 4.5b: replaces the inline 7-regex prose with the shared gstack-redact
  scan-at-sink (exact-byte temp file). Three enforcement points: pre-codex,
  pre-issue (files via --body-file from the scanned file), pre-archive (D2:
  sanitized body to the archive). --no-gate skips codex score only; redaction
  always runs, no flag disables it.

/cso: renders the full generated taxonomy table as its canonical pattern catalog
(shared source), keeps its git-history archaeology (different use case).

lib/redact-audit-log.ts: 0600 append-only semantic-review trail (no body text).
Resolver gains compact-table + brief-block variants so /spec references the
catalog instead of inlining it (stays under the v1.47 size budget).

Tests: extended spec invariants (semantic pass, scan-at-sink, no-promotion),
audit-log, cso/spec alignment. All green; spec 1.050× / cso 1.046× baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 07:20:18 -07:00

104 lines
3.4 KiB
TypeScript

/**
* Audit-log tests (D5/T14). The semantic-review trail records outcome +
* categories + a body sha256 — never the body text. File is 0600. The CLI
* stamps ts + hash from a body file.
*/
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { spawnSync } from "child_process";
import { appendSemanticReview, sha256 } from "../lib/redact-audit-log";
const LIB = path.resolve(import.meta.dir, "..", "lib", "redact-audit-log.ts");
let home: string;
function logPath(): string {
return path.join(home, "security", "semantic-reviews.jsonl");
}
beforeEach(() => {
home = fs.mkdtempSync(path.join(os.tmpdir(), "audit-"));
process.env.GSTACK_HOME = home;
});
afterEach(() => {
delete process.env.GSTACK_HOME;
fs.rmSync(home, { recursive: true, force: true });
});
describe("appendSemanticReview", () => {
test("writes a JSONL line with the expected shape", () => {
appendSemanticReview({
ts: "2026-05-28T00:00:00Z",
repo_visibility: "public",
outcome: "flagged",
categories_flagged: ["legal", "internal"],
body_sha256: sha256("hello"),
});
const line = JSON.parse(fs.readFileSync(logPath(), "utf8").trim());
expect(line.outcome).toBe("flagged");
expect(line.categories_flagged).toEqual(["legal", "internal"]);
expect(line.body_sha256).toBe(sha256("hello"));
expect(line.repo_visibility).toBe("public");
});
test("never contains body content — only the hash", () => {
const secret = "Bob Smith is incompetent and customer ACME is churning";
appendSemanticReview({
ts: "2026-05-28T00:00:00Z",
repo_visibility: "private",
outcome: "flagged",
categories_flagged: ["legal"],
body_sha256: sha256(secret),
});
const raw = fs.readFileSync(logPath(), "utf8");
expect(raw).not.toContain("Bob Smith");
expect(raw).not.toContain("ACME");
expect(raw).toContain(sha256(secret));
});
test("file is mode 0600", () => {
appendSemanticReview({
ts: "t",
repo_visibility: "private",
outcome: "clean",
categories_flagged: [],
body_sha256: sha256(""),
});
const mode = fs.statSync(logPath()).mode & 0o777;
expect(mode).toBe(0o600);
});
test("appends (does not overwrite)", () => {
for (const o of ["clean", "flagged"] as const) {
appendSemanticReview({
ts: "t",
repo_visibility: "private",
outcome: o,
categories_flagged: [],
body_sha256: sha256(o),
});
}
const lines = fs.readFileSync(logPath(), "utf8").trim().split("\n");
expect(lines).toHaveLength(2);
});
});
describe("CLI", () => {
test("stamps ts + body_sha256 from a body file", () => {
const bodyFile = path.join(home, "body.txt");
fs.writeFileSync(bodyFile, "some draft content");
const r = spawnSync(
"bun",
[LIB, JSON.stringify({ repo_visibility: "public", outcome: "flagged", categories_flagged: ["pii"] }), bodyFile],
{ env: { ...process.env, GSTACK_HOME: home }, encoding: "utf8" },
);
expect(r.status).toBe(0);
const line = JSON.parse(fs.readFileSync(logPath(), "utf8").trim());
expect(line.outcome).toBe("flagged");
expect(line.body_sha256).toBe(sha256("some draft content"));
expect(typeof line.ts).toBe("string");
expect(line.ts.length).toBeGreaterThan(10);
});
});