04c4489e70
---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.
193 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
}); |