@evoclock/pi-agentic-driver 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/PROVENANCE.md +9 -0
  2. package/README.md +178 -73
  3. package/extensions/tasks.ts +570 -0
  4. package/janus/README.md +196 -0
  5. package/janus/janus/audit.ts +213 -0
  6. package/janus/janus/budget.ts +101 -0
  7. package/janus/janus/budget_breaker.ts +105 -0
  8. package/janus/janus/config.ts +332 -0
  9. package/janus/janus/errors.ts +90 -0
  10. package/janus/janus/evaluate.ts +230 -0
  11. package/janus/janus/keychain.ts +119 -0
  12. package/janus/janus/providers.ts +544 -0
  13. package/janus/janus/schema.ts +344 -0
  14. package/janus/janus/server.ts +267 -0
  15. package/janus/janus/single_flight.ts +99 -0
  16. package/janus/janus/verdict.ts +192 -0
  17. package/janus/package-lock.json +616 -0
  18. package/janus/package.json +29 -0
  19. package/janus/tests/accounting.test.ts +229 -0
  20. package/janus/tests/allowlist.test.ts +97 -0
  21. package/janus/tests/config.test.ts +151 -0
  22. package/janus/tests/errors.test.ts +147 -0
  23. package/janus/tests/evaluate_pipeline.test.ts +358 -0
  24. package/janus/tests/helpers.ts +217 -0
  25. package/janus/tests/providers.test.ts +292 -0
  26. package/janus/tests/redaction_budget.test.ts +227 -0
  27. package/janus/tests/schema.test.ts +356 -0
  28. package/janus/tests/server.test.ts +288 -0
  29. package/janus/tests/single_flight.test.ts +84 -0
  30. package/janus/tsconfig.json +19 -0
  31. package/janus/user.janus.plist +51 -0
  32. package/package.json +7 -2
  33. package/scripts/enforcement/router_schemas_pi.js +63 -13
  34. package/scripts/enforcement/session_tasks_core_pi.js +605 -0
  35. package/scripts/enforcement/session_tasks_validate_pi.js +235 -0
  36. package/skills/tasks/SKILL.md +110 -0
  37. package/templates/AGENTS.md +11 -0
@@ -0,0 +1,292 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ // Adapter mapping tests (review fix #2): the transport contract maps onto the
5
+ // sole provider adapter (typesafe-direct) with mocked clients and NO network,
6
+ // NO live credentials. Question composition (review fix #1), the single-retry
7
+ // policy, and failed-attempt usage accumulation are verified here. The Vercel
8
+ // AI Gateway and the merge gateway are DROPPED — no test references them.
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import {
13
+ defaultAdapters,
14
+ ProviderChain,
15
+ ProviderChainError,
16
+ ProviderTransportError,
17
+ TypesafeDirectAdapter,
18
+ composeQuestionPayload,
19
+ providerQuestionText,
20
+ type TransportRequest,
21
+ type TypeSafeClientLike,
22
+ } from "../janus/providers.js";
23
+ import { gatewayUnavailable, timeoutError, modelUnauthorized } from "../janus/errors.js";
24
+ import { testProviders, testRetryProviders, fakeKeyReader, mockAdapter } from "./helpers.js";
25
+
26
+ const SAMPLE_REQUEST: TransportRequest = {
27
+ state: { ticket: "I was charged twice" },
28
+ questions: [
29
+ { name: "billing", kind: "boolean", question: "Is this about billing?", instructions: "" },
30
+ {
31
+ name: "route",
32
+ kind: "choice",
33
+ question: "Route this ticket",
34
+ instructions: "Prefer the most specific class.",
35
+ choices: ["billing", "tech", "other"],
36
+ },
37
+ { name: "quality", kind: "score", question: "Rate this PR", instructions: "" },
38
+ ],
39
+ };
40
+
41
+ test("composeQuestionPayload: question text is always present and first (review fix #1)", () => {
42
+ const withoutInstructions = composeQuestionPayload("Is this about billing?", "");
43
+ assert.equal(withoutInstructions.question, "Is this about billing?");
44
+ assert.equal(withoutInstructions.framing, null);
45
+
46
+ const withInstructions = composeQuestionPayload("Is this about billing?", "Be strict.");
47
+ assert.equal(withInstructions.question, "Is this about billing?");
48
+ assert.ok(withInstructions.framing!.includes("Be strict."));
49
+ assert.ok(withInstructions.framing!.includes("Additional framing instructions"));
50
+ // Unambiguous: framing block is clearly delimited, question comes first.
51
+ const composed = providerQuestionText({
52
+ name: "q",
53
+ kind: "boolean",
54
+ question: "Is this about billing?",
55
+ instructions: "Be strict.",
56
+ });
57
+ assert.ok(composed.startsWith("Is this about billing?"));
58
+ assert.ok(composed.indexOf("Is this about billing?") < composed.indexOf("Be strict."));
59
+ });
60
+
61
+ test("typesafe-direct adapter maps the transport contract to noul/choice/score shapes", async () => {
62
+ let captured: { state: unknown; questions: Record<string, unknown> } | undefined;
63
+ const fakeClient: TypeSafeClientLike = {
64
+ async systemOne(request) {
65
+ captured = { state: request.state, questions: request.questions as Record<string, unknown> };
66
+ return {
67
+ answers: {
68
+ billing: { type: "noul", noul: 0.93 },
69
+ route: { type: "choice", choice: "billing", probabilities: { billing: 0.8, tech: 0.1, other: 0.1 } },
70
+ quality: { type: "score", score: 3.4, probabilities: { "3": 0.5, "4": 0.5 } },
71
+ },
72
+ usage: { input_tokens: 11, output_tokens: 7 },
73
+ model: "jev-latest",
74
+ } as never;
75
+ },
76
+ };
77
+ const adapter = new TypesafeDirectAdapter(() => fakeClient);
78
+ const result = await adapter.evaluate(SAMPLE_REQUEST, "fake-key", new AbortController().signal);
79
+
80
+ assert.ok(captured);
81
+ // State passes through untouched.
82
+ assert.deepEqual(captured.state, SAMPLE_REQUEST.state);
83
+ // Question text is the composed payload (question first, framing after).
84
+ const billing = captured.questions.billing as { type: string; instructions: string };
85
+ assert.equal(billing.type, "noul");
86
+ assert.equal(billing.instructions, "Is this about billing?");
87
+ const route = captured.questions.route as { type: string; instructions: string; criteria: Record<string, unknown> };
88
+ assert.equal(route.type, "choice");
89
+ assert.ok(route.instructions.startsWith("Route this ticket"));
90
+ assert.ok(route.instructions.includes("Prefer the most specific class."));
91
+ assert.deepEqual(Object.keys(route.criteria), ["billing", "tech", "other"]);
92
+ const quality = captured.questions.quality as { type: string; criteria: string[] };
93
+ assert.equal(quality.type, "score");
94
+ assert.deepEqual(quality.criteria, ["0", "1", "2", "3", "4", "5"]);
95
+
96
+ // Answers normalize onto the janus verdict vocabulary.
97
+ assert.deepEqual(result.answers.billing, { type: "boolean", probability: 0.93 });
98
+ assert.deepEqual(result.answers.route, {
99
+ type: "choice",
100
+ choice: "billing",
101
+ probabilities: { billing: 0.8, tech: 0.1, other: 0.1 },
102
+ });
103
+ assert.deepEqual(result.answers.quality, {
104
+ type: "score",
105
+ score: 3.4,
106
+ probabilities: { "3": 0.5, "4": 0.5 },
107
+ });
108
+ assert.deepEqual(result.usage, { inputTokens: 11, outputTokens: 7, totalTokens: 18 });
109
+ assert.equal(result.modelId, "jev-latest");
110
+ });
111
+
112
+ test("typesafe-direct adapter: SDK errors map to fixed-public transport failures", async () => {
113
+ const failingClient: TypeSafeClientLike = {
114
+ async systemOne() {
115
+ const error = new Error("401 Unauthorized — key sk-live-secret invalid");
116
+ throw error;
117
+ },
118
+ };
119
+ const adapter = new TypesafeDirectAdapter(() => failingClient);
120
+ await assert.rejects(
121
+ adapter.evaluate(SAMPLE_REQUEST, "fake-key", new AbortController().signal),
122
+ (error: unknown) =>
123
+ error instanceof ProviderTransportError &&
124
+ error.failure.error.code === "gateway_unavailable" &&
125
+ !error.failure.error.message.includes("sk-live-secret"),
126
+ );
127
+ });
128
+
129
+ test("provider surface is closed: typesafe-direct is the only adapter (fallbacks dropped)", () => {
130
+ assert.deepEqual(
131
+ defaultAdapters().map((adapter) => adapter.id),
132
+ ["typesafe-direct"],
133
+ );
134
+ });
135
+
136
+ test("chain: single provider succeeds on the first attempt — no retry", async () => {
137
+ const calls: unknown[] = [];
138
+ const chain = new ProviderChain(
139
+ testProviders(),
140
+ [mockAdapter({ id: "typesafe-direct", calls: calls as never })],
141
+ fakeKeyReader(),
142
+ );
143
+ const outcome = await chain.evaluate(SAMPLE_REQUEST, new AbortController().signal);
144
+ assert.equal(outcome.attempts, 1);
145
+ assert.equal(outcome.provider, "typesafe-direct");
146
+ assert.equal(calls.length, 1);
147
+ assert.deepEqual(outcome.failedAttemptUsage, []);
148
+ });
149
+
150
+ test("chain: exactly one retry after an eligible transport failure, identical request", async () => {
151
+ const calls: unknown[] = [];
152
+ let first = true;
153
+ const chain = new ProviderChain(
154
+ testRetryProviders(),
155
+ [
156
+ mockAdapter({
157
+ id: "typesafe-direct",
158
+ calls: calls as never,
159
+ fail: () => {
160
+ if (first) {
161
+ first = false;
162
+ return new ProviderTransportError("timeout", timeoutError("slow"));
163
+ }
164
+ return undefined;
165
+ },
166
+ }),
167
+ ],
168
+ fakeKeyReader(),
169
+ );
170
+ const outcome = await chain.evaluate(SAMPLE_REQUEST, new AbortController().signal);
171
+ assert.equal(outcome.attempts, 2);
172
+ assert.equal(outcome.provider, "typesafe-direct");
173
+ assert.equal(calls.length, 2, "exactly one retry attempt");
174
+ // Identical request on retry (question semantics preserved).
175
+ assert.deepEqual(
176
+ (calls[1] as { questions: unknown }).questions,
177
+ (calls[0] as { questions: unknown }).questions,
178
+ );
179
+ assert.deepEqual((calls[1] as { state: unknown }).state, (calls[0] as { state: unknown }).state);
180
+ });
181
+
182
+ test("chain: NO retry on validation errors", async () => {
183
+ const calls: unknown[] = [];
184
+ const validationShapeError = new ProviderTransportError(
185
+ "validation_error",
186
+ // Fixed taxonomy error with a non-eligible code: never retried.
187
+ { code: "validation_error", status: 400, message: "bad request shape", body: () => ({}) } as never,
188
+ );
189
+ const chain = new ProviderChain(
190
+ testRetryProviders(),
191
+ [
192
+ mockAdapter({
193
+ id: "typesafe-direct",
194
+ calls: calls as never,
195
+ fail: () => validationShapeError,
196
+ }),
197
+ ],
198
+ fakeKeyReader(),
199
+ );
200
+ await assert.rejects(
201
+ chain.evaluate(SAMPLE_REQUEST, new AbortController().signal),
202
+ (error: unknown) => error instanceof ProviderChainError && error.failure.code === "validation_error",
203
+ );
204
+ assert.equal(calls.length, 1, "validation errors never trigger a retry");
205
+ });
206
+
207
+ test("chain: terminal failure carries ALL accumulated attempt usage (review fix #12)", async () => {
208
+ const chain = new ProviderChain(
209
+ testRetryProviders(),
210
+ [
211
+ mockAdapter({
212
+ id: "typesafe-direct",
213
+ fail: () =>
214
+ new ProviderTransportError("gateway_unavailable", gatewayUnavailable("down"), {
215
+ inputTokens: 500,
216
+ outputTokens: 0,
217
+ totalTokens: 500,
218
+ }),
219
+ }),
220
+ ],
221
+ fakeKeyReader(),
222
+ );
223
+ await assert.rejects(
224
+ chain.evaluate(SAMPLE_REQUEST, new AbortController().signal),
225
+ (error: unknown) => {
226
+ assert.ok(error instanceof ProviderChainError);
227
+ assert.equal(error.failure.code, "gateway_unavailable");
228
+ assert.equal(error.failedAttemptUsage.length, 2, "both terminal attempts report usage");
229
+ assert.equal(
230
+ error.failedAttemptUsage.reduce((sum, usage) => sum + (usage.totalTokens ?? 0), 0),
231
+ 1000,
232
+ );
233
+ return true;
234
+ },
235
+ );
236
+ });
237
+
238
+ test("chain: only two attempts even with more provider entries (exactly one retry)", async () => {
239
+ const chain = new ProviderChain(
240
+ [...testRetryProviders(), testProviders()[0]!],
241
+ [
242
+ mockAdapter({
243
+ id: "typesafe-direct",
244
+ fail: () => new ProviderTransportError("timeout", timeoutError("slow")),
245
+ }),
246
+ ],
247
+ fakeKeyReader(),
248
+ );
249
+ await assert.rejects(
250
+ chain.evaluate(SAMPLE_REQUEST, new AbortController().signal),
251
+ (error: unknown) => error instanceof ProviderChainError && error.failure.code === "timeout",
252
+ );
253
+ });
254
+
255
+ test("readiness: ready when the sole provider credential resolves; not ready otherwise", async () => {
256
+ const readyChain = new ProviderChain(
257
+ testProviders(),
258
+ [mockAdapter({ id: "typesafe-direct" })],
259
+ fakeKeyReader(),
260
+ );
261
+ const ready = await readyChain.readiness();
262
+ assert.equal(ready.ready, true);
263
+
264
+ const unreadyChain = new ProviderChain(
265
+ testProviders(),
266
+ [mockAdapter({ id: "typesafe-direct" })],
267
+ fakeKeyReader(new Set(["typesafe-ai", "Typesafe AI"])),
268
+ );
269
+ const unready = await unreadyChain.readiness();
270
+ assert.equal(unready.ready, false);
271
+ assert.match(unready.reason ?? "", /no provider credential resolvable/);
272
+ assert.ok(!/test-key|secret value/i.test(unready.reason ?? ""), "no key fragments in the reason");
273
+ });
274
+
275
+ test("missing credential fails closed as model_unauthorized with no provider call", async () => {
276
+ const calls: unknown[] = [];
277
+ const chain = new ProviderChain(
278
+ testProviders(),
279
+ [mockAdapter({ id: "typesafe-direct", calls: calls as never })],
280
+ fakeKeyReader(new Set(["typesafe-ai"])),
281
+ );
282
+ await assert.rejects(
283
+ chain.evaluate(SAMPLE_REQUEST, new AbortController().signal),
284
+ (error: unknown) =>
285
+ error instanceof ProviderChainError && error.failure.code === "model_unauthorized",
286
+ );
287
+ assert.equal(calls.length, 0, "no provider attempt without a credential");
288
+ });
289
+
290
+ test("modelUnauthorized stays in the fixed taxonomy (sanity)", () => {
291
+ assert.equal(modelUnauthorized("provider rejected the credential").code, "model_unauthorized");
292
+ });
@@ -0,0 +1,227 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ // Unit tests: the secrets/PII redaction pass (§7, M10) and the token budget
5
+ // with state_too_large rejection (§2).
6
+ //
7
+ // Redaction semantics come from the shared library (typesafe-secure
8
+ // lib/redaction, MIT — SPEC.md DRAFT v1.1), which replaces janus's legacy
9
+ // janus/redact.ts. SPEC-normative divergences from the legacy module are
10
+ // asserted here: typed markers `[REDACTED:<type>]` / `[REDACTED:sensitive_key]`
11
+ // (SPEC A2.2), and the github_pat_classic shape floor of ≥36 chars (SPEC A4
12
+ // row 2), under which a short `ghp_…` lookalike survives verbatim.
13
+
14
+ import test from "node:test";
15
+ import assert from "node:assert/strict";
16
+ import { redactString, redactValue } from "@evoclock/redaction";
17
+ import { estimateRequestTokens, estimateStringTokens, evalInputDigest, canonicalJson } from "../janus/budget.js";
18
+ import { JanusError } from "../janus/errors.js";
19
+ import { enforceRequest } from "../janus/schema.js";
20
+ import { testConfig } from "./helpers.js";
21
+ import { Evaluator } from "../janus/evaluate.js";
22
+ import { SingleFlight } from "../janus/single_flight.js";
23
+ import { DailyBudget } from "../janus/budget_breaker.js";
24
+ import { AuditLog } from "../janus/audit.js";
25
+
26
+ test("redaction: API keys and tokens", () => {
27
+ assert.equal(redactString("key AKIAIOSFODNN7EXAMPLE in config"), "key [REDACTED:aws_access_key] in config");
28
+ // SPEC A4 row 2: github_pat_classic requires ≥36 chars after `ghp_`; the
29
+ // legacy janus rule matched ≥20 and redacted a 28-char lookalike. SPEC wins:
30
+ // the short lookalike survives, a real-length PAT is redacted.
31
+ assert.equal(redactString("ghp_abcdefghijklmnopqrstuvwxyzyx"), "ghp_abcdefghijklmnopqrstuvwxyzyx");
32
+ assert.equal(redactString("ghp_abcdefghijklmnopqrstuvwxyz0123456789ab"), "[REDACTED:github_pat_classic]");
33
+ assert.equal(redactString("sk-proj-abcdefghijklmnopqrst"), "[REDACTED:openai_key]");
34
+ assert.equal(redactString("xoxb-123456789-abcdef"), "[REDACTED:slack_token]");
35
+ assert.equal(redactString("AIzaSyA1234567890abcdefghijklmnopqrstuv"), "[REDACTED:google_api_key]");
36
+ });
37
+
38
+ test("redaction: JWTs and bearer headers", () => {
39
+ assert.equal(
40
+ redactString("auth eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c done"),
41
+ "auth [REDACTED:jwt] done",
42
+ );
43
+ assert.equal(
44
+ redactString("Authorization: Bearer abc123.def456"),
45
+ "Authorization: [REDACTED:bearer_authorization]",
46
+ );
47
+ });
48
+
49
+ test("redaction: private key blocks", () => {
50
+ const text = "-----BEGIN RSA PRIVATE KEY-----\nMIIEow\nabc\n-----END RSA PRIVATE KEY-----";
51
+ assert.equal(redactString(text), "[REDACTED:private_key_block]");
52
+ });
53
+
54
+ test("redaction: assigned secrets keep the key name, drop the value", () => {
55
+ assert.equal(redactString('password: "hunter2secret"'), "password: [REDACTED:assigned_secret]");
56
+ assert.equal(redactString("api_key=abcd1234efgh"), "api_key: [REDACTED:assigned_secret]");
57
+ });
58
+
59
+ test("redaction: emails and SSNs", () => {
60
+ assert.equal(redactString("contact jane.doe@example.com now"), "contact [REDACTED:email] now");
61
+ assert.equal(redactString("ssn 123-45-6789"), "ssn [REDACTED:ssn]");
62
+ });
63
+
64
+ test("redaction: credit-card-shaped digits masked, benign numbers kept", () => {
65
+ assert.equal(redactString("card 4111 1111 1111 1111"), "card •••• •••• •••• ••••");
66
+ assert.equal(redactString("count 42"), "count 42");
67
+ assert.equal(redactString("version 1.2.3"), "version 1.2.3");
68
+ });
69
+
70
+ test("redaction: recursive over objects and arrays; sensitive keys dropped", () => {
71
+ const input = {
72
+ user: "jane.doe@example.com",
73
+ password: "hunter2secret",
74
+ nested: { apiKey: "sk-proj-abcdefghijklmnopqrst", note: "clean" },
75
+ list: ["token=abcd1234efgh", 5, null],
76
+ };
77
+ const output = redactValue(input) as Record<string, unknown>;
78
+ assert.equal(output.user, "[REDACTED:email]");
79
+ assert.equal(output.password, "[REDACTED:sensitive_key]");
80
+ const nested = output.nested as Record<string, unknown>;
81
+ // `apiKey` as an object KEY is itself in the closed sensitive-key registry
82
+ // (SPEC A2.6: apikey) — the all-type key drop fires (A3.2) before any
83
+ // string rule, exactly as the legacy key regex did.
84
+ assert.equal(nested.apiKey, "[REDACTED:sensitive_key]");
85
+ assert.equal(nested.note, "clean");
86
+ assert.equal((output.list as unknown[])[0], "token: [REDACTED:assigned_secret]");
87
+ assert.equal((output.list as unknown[])[1], 5);
88
+ });
89
+
90
+ test("redaction: sensitive-key values dropped regardless of shape (review fix #4)", () => {
91
+ const input = {
92
+ credentials: { user: "u", password: "p" },
93
+ token: ["a", "b"],
94
+ apiKey: 12345,
95
+ private_key: true,
96
+ authorization: null,
97
+ secret: { nested: { deeper: ["x"] } },
98
+ };
99
+ const output = redactValue(input) as Record<string, unknown>;
100
+ assert.equal(output.credentials, "[REDACTED:sensitive_key]");
101
+ assert.equal(output.token, "[REDACTED:sensitive_key]");
102
+ assert.equal(output.apiKey, "[REDACTED:sensitive_key]");
103
+ assert.equal(output.private_key, "[REDACTED:sensitive_key]");
104
+ assert.equal(output.authorization, "[REDACTED:sensitive_key]");
105
+ assert.equal(output.secret, "[REDACTED:sensitive_key]");
106
+ // Nested objects under non-sensitive keys still get traversed.
107
+ const wrapper = redactValue({ meta: { password: "leak" } }) as Record<string, unknown>;
108
+ assert.equal((wrapper.meta as Record<string, unknown>).password, "[REDACTED:sensitive_key]");
109
+ });
110
+
111
+ test("budget: conservative bound dominates chars/4 for CJK, emoji, and dense JSON (review fix #3)", () => {
112
+ const cjk = "国境の長いトンネルを抜けると雪国であった".repeat(10);
113
+ const emoji = "🔒🔐🔑🗝️🧿🪬🧸🪙".repeat(10);
114
+ const denseJson = JSON.stringify({ payload: Buffer.from("dense").toString("base64").repeat(50) });
115
+ for (const text of [cjk, emoji, denseJson]) {
116
+ const bytes = Buffer.byteLength(text, "utf8");
117
+ const estimated = estimateStringTokens(text);
118
+ const charsOver4 = Math.ceil(text.length / 4);
119
+ assert.equal(estimated, Math.ceil(bytes / 2), "bound is bytes/2");
120
+ assert.ok(
121
+ estimated >= charsOver4,
122
+ `bytes/2 (${estimated}) must be >= chars/4 (${charsOver4}) for dense/multibyte content`,
123
+ );
124
+ }
125
+ });
126
+
127
+ test("budget: CJK state rejected at the 30k budget where chars/4 would undercount (review fix #3)", () => {
128
+ // 21k CJK chars = 63k UTF-8 bytes → 31.5k tokens under bytes/2 (rejected),
129
+ // but only ~5.25k under chars/4 (wrongly accepted). The conservative bound
130
+ // must over-reject, never materially undercount.
131
+ const request = enforceRequest({
132
+ state: "雪".repeat(21_000),
133
+ questions: [{ type: "boolean", name: "q", question: "q" }],
134
+ });
135
+ assert.throws(
136
+ () => estimateRequestTokens(request.state, request.questions, 30_000, 4_000_000),
137
+ (error: unknown) => error instanceof JanusError && error.code === "state_too_large",
138
+ );
139
+ });
140
+
141
+ test("budget: requests within the 30k budget pass", () => {
142
+ const request = enforceRequest({
143
+ state: "x".repeat(1000),
144
+ questions: [{ type: "boolean", name: "q", question: "q", instructions: "i".repeat(100) }],
145
+ });
146
+ const size = estimateRequestTokens(request.state, request.questions, 30_000, 4_000_000);
147
+ assert.ok(size.estimatedTokens > 0 && size.estimatedTokens < 30_000);
148
+ });
149
+
150
+ test("budget: oversized state rejected with state_too_large, never truncated", () => {
151
+ const request = enforceRequest({
152
+ state: "x".repeat(200_000),
153
+ questions: [{ type: "boolean", name: "q", question: "q" }],
154
+ });
155
+ try {
156
+ estimateRequestTokens(request.state, request.questions, 30_000, 4_000_000);
157
+ assert.fail("expected state_too_large");
158
+ } catch (error) {
159
+ assert.ok(error instanceof JanusError);
160
+ assert.equal(error.code, "state_too_large");
161
+ assert.match(error.message, /never truncated silently/);
162
+ }
163
+ });
164
+
165
+ test("budget: byte ceiling rejects pathological state before token math", () => {
166
+ const request = enforceRequest({
167
+ state: "x".repeat(5_000_000),
168
+ questions: [{ type: "boolean", name: "q", question: "q" }],
169
+ });
170
+ assert.throws(
171
+ () => estimateRequestTokens(request.state, request.questions, 30_000, 4_000_000),
172
+ (error: unknown) => error instanceof JanusError && error.code === "state_too_large",
173
+ );
174
+ });
175
+
176
+ test("evalInputDigest is stable and input-sensitive", () => {
177
+ const request = enforceRequest({
178
+ state: { a: 1, b: 2 },
179
+ questions: [{ type: "boolean", name: "q", question: "q" }],
180
+ });
181
+ const d1 = evalInputDigest(request.state, request.questions);
182
+ const d2 = evalInputDigest(request.state, request.questions);
183
+ assert.equal(d1, d2);
184
+ const d3 = evalInputDigest({ b: 2, a: 1 }, request.questions);
185
+ assert.equal(d1, d3, "key order must not change the digest");
186
+ const d4 = evalInputDigest({ a: 1, b: 3 }, request.questions);
187
+ assert.notEqual(d1, d4);
188
+ });
189
+
190
+ test("canonicalJson sorts keys deterministically", () => {
191
+ assert.equal(canonicalJson({ b: 1, a: { d: 4, c: 3 } }), '{"a":{"c":3,"d":4},"b":1}');
192
+ });
193
+
194
+ test("end-to-end: redaction runs before the SDK call", async () => {
195
+ const seen: { state: unknown }[] = [];
196
+ const audit = { append: () => {} } as unknown as AuditLog;
197
+ const evaluator = new Evaluator({
198
+ config: testConfig(),
199
+ singleFlight: new SingleFlight(4),
200
+ budget: new DailyBudget(0, 0, 0.5),
201
+ audit,
202
+ chain: {
203
+ async evaluate(request: { state: unknown }) {
204
+ seen.push({ state: request.state });
205
+ return {
206
+ result: {
207
+ answers: { q: { type: "boolean", probability: 0.99 } },
208
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
209
+ modelId: "jev-latest",
210
+ },
211
+ provider: "typesafe-direct",
212
+ attempts: 1,
213
+ failedAttemptUsage: [],
214
+ };
215
+ },
216
+ readiness: async () => ({ ready: true, providers: [] }),
217
+ } as never,
218
+ });
219
+ const response = await evaluator.handle({
220
+ state: { note: "email jane.doe@example.com", password: "hunter2secret" },
221
+ questions: [{ type: "boolean", name: "q", question: "q" }],
222
+ });
223
+ assert.equal(response.status, 200);
224
+ const state = seen[0]?.state as Record<string, unknown>;
225
+ assert.equal(state.note, "email [REDACTED:email]");
226
+ assert.equal(state.password, "[REDACTED:sensitive_key]");
227
+ });