Files
ci/src/core/audit.test.ts
T
Jon Chery 04c4489e70 fix(P01): migrate audit trail to git-native and replace audit_file with commit_hash
---ci---
project: ci
phase: 1
milestone: v0.8
status: in_progress
decisions:
  - id: D-024
    decision: Audit trail reads from git log instead of .ciagent/audit/*.json
    rationale: Git-native context means audit data should come from commit history, not files
    confidence: 0.88
  - id: D-025
    decision: Replace audit_file with commit_hash in Escalation type
    rationale: Escalations are committed to git; reference by hash instead of deprecated file path
    confidence: 0.90
requirements:
  covered: [FIX-04, FIX-05]
---/ci---

FIX-04: audit.ts logDecision/logEscalation now emit deprecation warnings
and are no-ops (decisions/escalations live in ---ci--- blocks). readAudit()
and getAuditSummary() parse git log for ---ci--- blocks instead of reading
.ciagent/audit/*.json files. ArtifactManager no longer creates audit dir.

FIX-05: Escalation type replaces audit_file: string with commit_hash: string.
All consumers updated (escalation.ts, ollama-base.ts, opencode.ts).
Audit tests rewritten for git-native approach.
2026-05-29 20:02:07 +00:00

193 lines
5.7 KiB
TypeScript

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);
});
});
});