Files
my-pi/pi-permission-system/test/auto-review-ledger.test.ts
T

142 lines
4.8 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG,
DEFAULT_AUTO_REVIEW_CONFIG,
} from "#src/auto-review/config";
import { AutoReviewLedger } from "#src/auto-review/ledger";
import type { PromptPermissionDetails } from "#src/service";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
function details(requestId: string): PromptPermissionDetails {
return {
requestId,
source: "tool",
surface: "bash",
toolName: "bash",
command: "npm install example",
agentName: "main",
} as unknown as PromptPermissionDetails;
}
describe("AutoReviewLedger", () => {
it("marks analysis pending only after the configured number of completed verdicts", () => {
const root = mkdtempSync(join(tmpdir(), "auto-review-ledger-"));
roots.push(root);
const ledger = new AutoReviewLedger(root);
const analysis = { ...DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG, threshold: 2 };
const assessment = {
outcome: "allow" as const,
riskLevel: "low" as const,
userAuthorization: "high" as const,
rationale: "directly authorized",
};
ledger.recordDecision({
details: details("request-1"),
assessment,
policyRevision: "policy-a",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
expect(ledger.hasPendingAnalysis()).toBe(false);
expect(ledger.prepareBatch(analysis)).toBeUndefined();
ledger.recordDecision({
details: details("request-2"),
assessment,
policyRevision: "policy-a",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
expect(ledger.hasPendingAnalysis()).toBe(true);
const batch = ledger.prepareBatch(analysis);
expect(batch?.fromSequence).toBe(1);
expect(batch?.toSequence).toBe(2);
expect(batch?.records).toHaveLength(2);
expect(batch?.records[0]).not.toHaveProperty("rationale");
expect(batch?.records[0].normalizedPattern).toBe("npm install *");
});
it("persists owner-only state and resumes a pending batch", () => {
const root = mkdtempSync(join(tmpdir(), "auto-review-ledger-"));
roots.push(root);
const analysis = { ...DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG, threshold: 1 };
const ledger = new AutoReviewLedger(root);
ledger.recordDecision({
details: details("request-1"),
assessment: {
outcome: "deny",
riskLevel: "high",
userAuthorization: "unknown",
rationale: "unsafe",
},
policyRevision: "policy-b",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
const resumed = new AutoReviewLedger(root);
expect(resumed.hasPendingAnalysis()).toBe(true);
expect(resumed.prepareBatch(analysis)?.records[0].outcome).toBe("deny");
expect(statSync(join(root, "state.json")).mode & 0o777).toBe(0o600);
expect(readFileSync(join(root, "ledger", "decisions.jsonl"), "utf8")).not.toContain("unsafe");
});
it("does not retain command arguments, secret path segments, or rationale", () => {
const root = mkdtempSync(join(tmpdir(), "auto-review-ledger-"));
roots.push(root);
const ledger = new AutoReviewLedger(root);
const analysis = { ...DEFAULT_AUTO_REVIEW_ANALYSIS_CONFIG, threshold: 10 };
const assessment = {
outcome: "allow" as const,
riskLevel: "medium" as const,
userAuthorization: "high" as const,
rationale: "secret-rationale",
};
const commandRecord = ledger.recordDecision({
details: {
requestId: "request-command",
source: "tool",
agentName: "main",
surface: "bash",
toolName: "bash",
command: "curl https://token.example/?api_key=secret-value",
} as unknown as PromptPermissionDetails,
assessment,
policyRevision: "policy-c",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
const pathRecord = ledger.recordDecision({
details: {
requestId: "request-path",
source: "tool",
agentName: "main",
surface: "path",
toolName: "read",
path: "clients/acme-secret/token.txt",
} as unknown as PromptPermissionDetails,
assessment,
policyRevision: "policy-c",
config: DEFAULT_AUTO_REVIEW_CONFIG,
analysis,
});
expect(commandRecord.normalizedPattern).toBe("curl *");
expect(pathRecord.normalizedPattern).toBe("<relative>/*");
const persisted = readFileSync(join(root, "ledger", "decisions.jsonl"), "utf8");
expect(persisted).not.toContain("secret-value");
expect(persisted).not.toContain("acme-secret");
expect(persisted).not.toContain("secret-rationale");
});
});