import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { logDecision, logEscalation, readAudit, getAuditSummary } from "../core/audit.js"; import { Decision } from "../types/decisions.js"; import { Escalation } from "../types/escalation.js"; describe("Audit", () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ci-audit-test-")); fs.mkdirSync(path.join(tempDir, ".ci", "audit"), { recursive: true }); }); afterEach(() => { fs.rmSync(tempDir, { recursive: true, force: true }); }); const sampleDecision: Decision = { id: "D-001", timestamp: new Date().toISOString(), decision: "Use PostgreSQL", rationale: "ACID compliance needed", confidence: 0.92, category: "technology_choice", alternatives_considered: [{ option: "MongoDB", rejected_reason: "No ACID" }], learnship_equivalent: "discuss-phase would ask: What database?", human_override: null, }; const sampleEscalation: Escalation = { id: "E-001", timestamp: new Date().toISOString(), type: "irreversible_action", phase: "1", description: "Deploy to staging", context: "All tests pass", options: [ { id: "A", label: "Deploy", description: "Deploy to staging", recommended: true }, ], default_option_id: "A", resolution: "pending", audit_file: ".ci/audit/test.json", }; describe("logDecision", () => { it("logs a decision to the audit trail", () => { logDecision(tempDir, 1, sampleDecision); const audit = readAudit(tempDir); expect(audit).toHaveLength(1); expect(audit[0].phase).toBe(1); expect(audit[0].decisions).toHaveLength(1); expect(audit[0].decisions[0].id).toBe("D-001"); }); it("appends multiple decisions to same phase file", () => { logDecision(tempDir, 1, { ...sampleDecision, id: "D-001" }); logDecision(tempDir, 1, { ...sampleDecision, id: "D-002" }); const audit = readAudit(tempDir); expect(audit[0].decisions).toHaveLength(2); }); it("separates decisions into different phase files", () => { logDecision(tempDir, 1, sampleDecision); logDecision(tempDir, 2, { ...sampleDecision, id: "D-002" }); const audit = readAudit(tempDir); expect(audit).toHaveLength(2); }); }); describe("logEscalation", () => { it("logs an escalation to the audit trail", () => { logEscalation(tempDir, 1, sampleEscalation); const audit = readAudit(tempDir); expect(audit).toHaveLength(1); expect(audit[0].escalations).toHaveLength(1); }); it("can mix decisions and escalations in same phase", () => { logDecision(tempDir, 1, sampleDecision); logEscalation(tempDir, 1, sampleEscalation); const audit = readAudit(tempDir); expect(audit[0].decisions).toHaveLength(1); expect(audit[0].escalations).toHaveLength(1); }); }); describe("readAudit", () => { it("returns empty array when no audit files exist", () => { const audit = readAudit(tempDir); expect(audit).toEqual([]); }); it("filters by phase number", () => { logDecision(tempDir, 1, sampleDecision); logDecision(tempDir, 2, { ...sampleDecision, id: "D-002" }); const phase1 = readAudit(tempDir, 1); expect(phase1).toHaveLength(1); expect(phase1[0].phase).toBe(1); }); }); describe("getAuditSummary", () => { it("returns summary with counts", () => { logDecision(tempDir, 1, { ...sampleDecision, confidence: 0.95 }); logDecision(tempDir, 1, { ...sampleDecision, id: "D-002", confidence: 0.7 }); logDecision(tempDir, 2, { ...sampleDecision, id: "D-003", confidence: 0.4 }); logEscalation(tempDir, 1, sampleEscalation); const summary = getAuditSummary(tempDir); expect(summary.total_decisions).toBe(3); expect(summary.total_escalations).toBe(1); expect(summary.phases).toContain(1); expect(summary.phases).toContain(2); expect(summary.decisions_by_confidence.high).toBe(1); expect(summary.decisions_by_confidence.medium).toBe(1); expect(summary.decisions_by_confidence.low).toBe(1); expect(summary.escalations_by_type.irreversible_action).toBe(1); }); it("returns zeros for empty audit", () => { const summary = getAuditSummary(tempDir); expect(summary.total_decisions).toBe(0); expect(summary.total_escalations).toBe(0); expect(summary.phases).toHaveLength(0); }); }); });