mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
- stage-ify judgeAuthorize into auditEnrollment, runPreflightGates, prepareModelCall, and enforceAndEmit - extract tallyJoinedRows and tallyAttributable from computeMetrics, removing three dead locals - extract validateVerdictResponse from requestStructuredVerdict - extract collectUserTexts and charBudgetStart from buildConversationEvidence - table-drive corpus-replay parseArgs and split main into resolveReplayModel, selectCorpusCases, and replayCorpus - split analyzer cli main into loadReviewEvents, withinWindow, loadAuditEnrolled, and printReport with a run-as-script guard - add 81 tests covering parseEntry, extractBashCommandEvidence, validateVerdictResponse, forcedToolChoice, classifyGit dry-run paths, CLI arg/window/report rendering, and the infra-failure result path - add @vitest/coverage-istanbul for exact per-function CRAP scoring via fallow health --coverage
215 lines
7.8 KiB
TypeScript
215 lines
7.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { ReviewEvent } from "../../src/analyzer/analyze";
|
|
import { analyzeShadowReviewLog } from "../../src/analyzer/analyze";
|
|
import {
|
|
parseArgs,
|
|
parseTimestampOption,
|
|
printReport,
|
|
withinWindow,
|
|
} from "../../src/analyzer/cli";
|
|
|
|
/**
|
|
* In-process unit tests for the analyze-shadow CLI's pure helpers:
|
|
* argument parsing, the time-window filter, and report rendering. The
|
|
* end-to-end contract (file IO, dual-log mode, exit codes) lives in
|
|
* cli-args.test.ts / cli-audit.test.ts; these tests cover branch-level
|
|
* behavior without spawning tsx subprocesses.
|
|
*/
|
|
|
|
describe("parseArgs", () => {
|
|
it("parses the positional path with no options", () => {
|
|
expect(parseArgs(["node", "cli.ts", "review.jsonl"])).toEqual({
|
|
path: "review.jsonl",
|
|
after: null,
|
|
before: null,
|
|
audit: null,
|
|
});
|
|
});
|
|
|
|
it("parses --after, --before, and --audit together", () => {
|
|
const r = parseArgs([
|
|
"node",
|
|
"cli.ts",
|
|
"review.jsonl",
|
|
"--after", "2026-08-18T00:00:00Z",
|
|
"--before", "2026-08-19T00:00:00Z",
|
|
"--audit", "audit.jsonl",
|
|
]);
|
|
expect(r).toEqual({
|
|
path: "review.jsonl",
|
|
after: new Date("2026-08-18T00:00:00Z"),
|
|
before: new Date("2026-08-19T00:00:00Z"),
|
|
audit: "audit.jsonl",
|
|
});
|
|
});
|
|
|
|
it("returns the usage error for --help and -h", () => {
|
|
expect(parseArgs(["node", "cli.ts", "--help"])).toEqual({
|
|
error: expect.stringContaining("usage: analyze-shadow") as unknown,
|
|
});
|
|
expect(parseArgs(["node", "cli.ts", "-h"])).toHaveProperty("error");
|
|
});
|
|
|
|
it.each([
|
|
["missing input path", [] as const],
|
|
["multiple input paths", ["a.jsonl", "b.jsonl"] as const],
|
|
["unknown option", ["a.jsonl", "--bogus"] as const],
|
|
["--audit without value", ["a.jsonl", "--audit"] as const],
|
|
["--after without value", ["a.jsonl", "--after"] as const],
|
|
["invalid --after timestamp", ["a.jsonl", "--after", "nope"] as const],
|
|
["invalid --before timestamp", ["a.jsonl", "--before", "nope"] as const],
|
|
])("errors on %s", (_name, argv) => {
|
|
const r = parseArgs(["node", "cli.ts", ...argv]);
|
|
expect("error" in r && typeof r.error === "string").toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("parseTimestampOption", () => {
|
|
it("requires a value", () => {
|
|
expect(parseTimestampOption("--after", undefined)).toEqual({
|
|
error: "--after requires a value",
|
|
});
|
|
});
|
|
|
|
it("rejects unparseable timestamps", () => {
|
|
expect(parseTimestampOption("--before", "yesterday")).toEqual({
|
|
error: "invalid --before timestamp: yesterday",
|
|
});
|
|
});
|
|
|
|
it("accepts an ISO instant", () => {
|
|
expect(parseTimestampOption("--after", "2026-08-18T00:00:00Z")).toEqual({
|
|
date: new Date("2026-08-18T00:00:00Z"),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("withinWindow", () => {
|
|
const evt = (timestamp?: string): ReviewEvent =>
|
|
({ event: "e", ...(timestamp === undefined ? {} : { timestamp }) }) as ReviewEvent;
|
|
|
|
it("keeps events without a timestamp in any window", () => {
|
|
const window = {
|
|
after: new Date("2026-08-18T12:00:00Z"),
|
|
before: new Date("2026-08-18T13:00:00Z"),
|
|
};
|
|
expect(withinWindow(evt(), window)).toBe(true);
|
|
});
|
|
|
|
it("keeps events inside the window and drops those outside", () => {
|
|
const window = {
|
|
after: new Date("2026-08-18T12:00:00Z"),
|
|
before: new Date("2026-08-18T13:00:00Z"),
|
|
};
|
|
expect(withinWindow(evt("2026-08-18T12:30:00Z"), window)).toBe(true);
|
|
expect(withinWindow(evt("2026-08-18T11:59:00Z"), window)).toBe(false);
|
|
expect(withinWindow(evt("2026-08-18T13:01:00Z"), window)).toBe(false);
|
|
});
|
|
|
|
it("applies each bound independently", () => {
|
|
expect(
|
|
withinWindow(evt("2026-08-18T10:00:00Z"), { after: new Date("2026-08-18T09:00:00Z"), before: null }),
|
|
).toBe(true);
|
|
expect(
|
|
withinWindow(evt("2026-08-18T10:00:00Z"), { after: null, before: new Date("2026-08-18T09:00:00Z") }),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("keeps events with an unparseable timestamp regardless of bounds", () => {
|
|
const window = {
|
|
after: new Date("2026-08-18T12:00:00Z"),
|
|
before: null,
|
|
};
|
|
// Unparseable -> NaN time -> neither bound applies.
|
|
expect(withinWindow(evt("not-a-date"), window)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("printReport", () => {
|
|
function capture(fn: () => void): string {
|
|
const chunks: string[] = [];
|
|
const spy = vi
|
|
.spyOn(process.stdout, "write")
|
|
.mockImplementation(((s: unknown) => {
|
|
chunks.push(String(s));
|
|
return true;
|
|
}) as never);
|
|
try {
|
|
fn();
|
|
} finally {
|
|
spy.mockRestore();
|
|
}
|
|
return chunks.join("");
|
|
}
|
|
|
|
const parsed = {
|
|
path: "review.jsonl",
|
|
after: null,
|
|
before: null,
|
|
audit: null,
|
|
};
|
|
|
|
function analyze(rows: readonly ReviewEvent[]): ReturnType<
|
|
typeof import("../../src/analyzer/analyze").analyzeShadowReviewLog
|
|
> {
|
|
return analyzeShadowReviewLog(rows);
|
|
}
|
|
|
|
const joinedEvents: ReviewEvent[] = [
|
|
{ event: "authorizer_chain_resolved", requestId: "r1", links: ["ai-bash-judge"] },
|
|
{
|
|
event: "ai_bash_judge.result",
|
|
requestId: "r1",
|
|
resultKind: "judgment",
|
|
verdict: "allow",
|
|
judgeLatencyMs: 5,
|
|
modelLatencyMs: 3,
|
|
},
|
|
{ event: "permission_request.approved", requestId: "r1", resolution: "approved" },
|
|
];
|
|
|
|
it("renders header, counts, matrix, and latency lines", () => {
|
|
const { enrollments, metrics } = analyze(joinedEvents);
|
|
const out = capture(() => printReport(parsed, enrollments, metrics));
|
|
expect(out).toContain("AI Bash Judge — Shadow diagnostic report");
|
|
expect(out).toContain("grade: DIAGNOSTIC");
|
|
expect(out).toContain("enrollments (N): 1");
|
|
expect(out).toContain("allow|allow: 1");
|
|
expect(out).toMatch(/judge latency: p50=5ms/);
|
|
expect(out).not.toContain("audit:");
|
|
expect(out).not.toContain("quarantined rows:");
|
|
});
|
|
|
|
it("renders the audit source line in dual-log mode", () => {
|
|
const { enrollments, metrics } = analyze([]);
|
|
const out = capture(() =>
|
|
printReport(
|
|
{ ...parsed, audit: "audit.jsonl" },
|
|
enrollments,
|
|
metrics,
|
|
),
|
|
);
|
|
expect(out).toContain("audit: audit.jsonl (enrollment source, ADR 0006)");
|
|
});
|
|
|
|
it("renders the empty-matrix placeholder for an empty log", () => {
|
|
const { enrollments, metrics } = analyze([]);
|
|
const out = capture(() => printReport(parsed, enrollments, metrics));
|
|
expect(out).toContain("(empty)");
|
|
expect(out).toContain("N/A");
|
|
});
|
|
|
|
it("renders quarantine categories when integrity faults exist", () => {
|
|
const duplicate: ReviewEvent[] = [
|
|
{ event: "authorizer_chain_resolved", requestId: "r1", links: ["ai-bash-judge"] },
|
|
{ event: "ai_bash_judge.result", requestId: "r1", resultKind: "judgment", verdict: "allow" },
|
|
{ event: "ai_bash_judge.result", requestId: "r1", resultKind: "judgment", verdict: "deny" },
|
|
{ event: "permission_request.approved", requestId: "r1", resolution: "approved" },
|
|
];
|
|
const { enrollments, metrics } = analyze(duplicate);
|
|
const out = capture(() => printReport(parsed, enrollments, metrics));
|
|
expect(out).toContain("quarantined rows:");
|
|
expect(out).toContain("duplicate_result: 1");
|
|
});
|
|
});
|