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.
This commit is contained in:
+128
-64
@@ -1,16 +1,23 @@
|
||||
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", () => {
|
||||
describe("Audit (git-native)", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-audit-test-"));
|
||||
fs.mkdirSync(path.join(tempDir, ".ciagent", "audit"), { recursive: true });
|
||||
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(() => {
|
||||
@@ -40,12 +47,48 @@ describe("Audit", () => {
|
||||
],
|
||||
default_option_id: "A",
|
||||
resolution: "pending",
|
||||
audit_file: ".ciagent/audit/test.json",
|
||||
commit_hash: "",
|
||||
};
|
||||
|
||||
describe("logDecision", () => {
|
||||
it("logs a decision to the audit trail", () => {
|
||||
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);
|
||||
@@ -53,47 +96,35 @@ describe("Audit", () => {
|
||||
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 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);
|
||||
@@ -101,29 +132,62 @@ describe("Audit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
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", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user