classify.test.ts
2550 bytes
1import { describe, expect, test } from "bun:test";
2import { createClassifierReviewer, type PiClassifierCaller } from "../../src/core/classify";
3import { createPolicyReviewer } from "../../src/core/review";
4import type { ClassifierReviewerConfig } from "../../src/core/types";
5
6const config: ClassifierReviewerConfig = {
7 kind: "classifier",
8 provider: "openrouter",
9 model: "typesafe/jev-1.13",
10 askThreshold: 0.2,
11};
12
13function caller(probabilities: Record<string, number> = {}): PiClassifierCaller {
14 return async (_, input) => ({
15 provider: config.provider,
16 model: config.model,
17 api: "typesafe-system-one",
18 timestamp: Date.now(),
19 stopReason: "stop",
20 answers: Object.fromEntries(
21 Object.keys(input.questions).map((id) => [id, { type: "bool", probability: probabilities[id] ?? 0.01 }]),
22 ),
23 });
24}
25
26const request = { toolName: "bash", input: { command: "example" } };
27
28describe("classifier decisions", () => {
29 test("allows when all risks are below the threshold", async () => {
30 const result = await createClassifierReviewer(config, caller()).evaluate(request, {});
31 expect(result.decision.decision).toBe("allow");
32 });
33
34 test.each(["privilege", "destruction", "secrets", "files", "remote", "git", "home_search"])(
35 "asks for %s at the threshold",
36 async (id) => {
37 const result = await createClassifierReviewer(config, caller({ [id]: config.askThreshold })).evaluate(
38 request,
39 {},
40 );
41 expect(result.decision.decision).toBe("ask");
42 },
43 );
44
45 test.each([NaN, Infinity, -0.1, 1.1])("asks for invalid probability %s", async (probability) => {
46 const result = await createClassifierReviewer(config, caller({ privilege: probability })).evaluate(request, {});
47 expect(result.decision.decision).toBe("ask");
48 });
49
50 test("asks when answers are missing", async () => {
51 const result = await createClassifierReviewer(config, async (...args) => ({
52 ...(await caller()(...args)),
53 answers: {},
54 })).evaluate(request, {});
55 expect(result.decision.decision).toBe("ask");
56 });
57
58 test("asks on classifier failure", async () => {
59 const result = await createClassifierReviewer(config, async () => {
60 throw new Error("Unavailable");
61 }).evaluate(request, {});
62 expect(result.decision.decision).toBe("ask");
63 });
64
65 test("asks when the classifier adapter is absent", async () => {
66 const result = await createPolicyReviewer(config, async () => "").evaluate(request, {});
67 expect(result.decision.decision).toBe("ask");
68 });
69});