import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { execSync } from "node:child_process"; import { logDecision, logEscalation, readAudit, getAuditSummary } from "../core/audit.js"; import { Decision } from "../types/decisions.js"; import { Escalation } from "../types/escalation.js"; describe("Audit (git-native)", () => { let tempDir: string; beforeEach(() => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-audit-test-")); fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true }); execSync("git init", { cwd: tempDir, stdio: "pipe" }); execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: "pipe" }); execSync('git config user.name "Test"', { cwd: tempDir, stdio: "pipe" }); const placeholder = path.join(tempDir, "README.md"); fs.writeFileSync(placeholder, "# test\n"); execSync("git add -A && git commit -m 'initial'", { cwd: tempDir, stdio: "pipe" }); }); 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" }], 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", commit_hash: "", }; describe("deprecated log functions", () => { it("logDecision is a no-op that warns", () => { logDecision(tempDir, 1, sampleDecision); const audit = readAudit(tempDir); expect(audit).toHaveLength(0); }); it("logEscalation is a no-op that warns", () => { logEscalation(tempDir, 1, sampleEscalation); const audit = readAudit(tempDir); expect(audit).toHaveLength(0); }); }); describe("readAudit from git log", () => { it("returns empty array when no ci blocks exist", () => { const audit = readAudit(tempDir); expect(audit).toEqual([]); }); it("reads decisions from ---ci--- blocks in git log", () => { const ciBlock = `docs(P01): test commit ---ci--- project: ci phase: 1 milestone: v0.8 status: in_progress decisions: - id: D-001 decision: Use PostgreSQL rationale: ACID compliance needed confidence: 0.92 ---/ci---`; execSync(`git add -A && git commit -m "${ciBlock.replace(/"/g, '\\"')}" --allow-empty`, { cwd: tempDir, stdio: "pipe", }); 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("filters by phase number", () => { const ciBlock1 = `docs(P01): phase 1 commit ---ci--- project: ci phase: 1 milestone: v0.8 status: complete decisions: - id: D-001 decision: Phase 1 decision rationale: reason confidence: 0.90 ---/ci---`; const ciBlock2 = `docs(P02): phase 2 commit ---ci--- project: ci phase: 2 milestone: v0.8 status: in_progress decisions: - id: D-002 decision: Phase 2 decision rationale: reason confidence: 0.80 ---/ci---`; execSync(`git commit --allow-empty -m "${ciBlock1.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" }); execSync(`git commit --allow-empty -m "${ciBlock2.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" }); const phase1 = readAudit(tempDir, 1); expect(phase1).toHaveLength(1); expect(phase1[0].phase).toBe(1); }); }); describe("getAuditSummary from git log", () => { it("returns zeros for empty git log with no ci blocks", () => { const summary = getAuditSummary(tempDir); expect(summary.total_decisions).toBe(0); expect(summary.total_escalations).toBe(0); expect(summary.phases).toHaveLength(0); }); it("returns summary with decision counts and confidence breakdown", () => { const ciBlock = `docs(P01): multi-decision commit ---ci--- project: ci phase: 1 milestone: v0.8 status: complete decisions: - id: D-001 decision: High confidence decision rationale: reason confidence: 0.95 - id: D-002 decision: Medium confidence decision rationale: reason confidence: 0.70 - id: D-003 decision: Low confidence decision rationale: reason confidence: 0.40 ---/ci---`; execSync(`git commit --allow-empty -m "${ciBlock.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" }); const summary = getAuditSummary(tempDir); expect(summary.total_decisions).toBe(3); 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.phases).toContain(1); }); it("reads escalations from ci blocks", () => { const ciBlock = `escalation(P01): test escalation ---ci--- project: ci phase: 1 milestone: v0.8 escalations: - type: irreversible_action description: Deploy to production ---/ci---`; execSync(`git commit --allow-empty -m "${ciBlock.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" }); const summary = getAuditSummary(tempDir); expect(summary.total_escalations).toBe(1); expect(summary.escalations_by_type.irreversible_action).toBe(1); }); }); });