@evoclock/pi-agentic-driver 0.9.1 → 0.9.3
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.
- package/.agentic-driver/router.defaults.json +64 -0
- package/LICENSE +11 -8
- package/README.md +116 -32
- package/extensions/attended-authority-guard.ts +1 -1
- package/extensions/code-phage.js +1 -1
- package/extensions/herdr-communication.ts +1 -1
- package/extensions/herdr-dispatch.ts +1 -1
- package/extensions/herdr-lifecycle.ts +1 -1
- package/extensions/linux-microvm.ts +1 -1
- package/extensions/pulse.ts +33 -2
- package/extensions/task-board.ts +1 -1
- package/janus/README.draft.md +210 -0
- package/janus/README.md +196 -0
- package/janus/janus/audit.ts +213 -0
- package/janus/janus/budget.ts +101 -0
- package/janus/janus/budget_breaker.ts +105 -0
- package/janus/janus/config.ts +332 -0
- package/janus/janus/errors.ts +90 -0
- package/janus/janus/evaluate.ts +230 -0
- package/janus/janus/keychain.ts +119 -0
- package/janus/janus/providers.ts +544 -0
- package/janus/janus/schema.ts +344 -0
- package/janus/janus/server.ts +267 -0
- package/janus/janus/single_flight.ts +99 -0
- package/janus/janus/verdict.ts +192 -0
- package/janus/package-lock.json +616 -0
- package/janus/package.json +29 -0
- package/janus/tests/accounting.test.ts +229 -0
- package/janus/tests/allowlist.test.ts +97 -0
- package/janus/tests/config.test.ts +151 -0
- package/janus/tests/errors.test.ts +147 -0
- package/janus/tests/evaluate_pipeline.test.ts +358 -0
- package/janus/tests/helpers.ts +217 -0
- package/janus/tests/providers.test.ts +292 -0
- package/janus/tests/redaction_budget.test.ts +227 -0
- package/janus/tests/schema.test.ts +356 -0
- package/janus/tests/server.test.ts +288 -0
- package/janus/tests/single_flight.test.ts +84 -0
- package/janus/tsconfig.json +19 -0
- package/janus/user.janus.plist +51 -0
- package/lib/adapters/diff-scope.mjs +1 -1
- package/lib/adapters/evidence.mjs +1 -1
- package/lib/adapters/narrative.mjs +1 -1
- package/lib/adapters/review-feedback.mjs +1 -1
- package/lib/adapters/visualization.mjs +1 -1
- package/lib/code-phage-core.mjs +1 -1
- package/lib/python_ast_metrics.py +1 -1
- package/lib/typescript_ast_metrics.mjs +1 -1
- package/package.json +14 -3
- package/scripts/aidr_writing_review.js +1 -1
- package/scripts/enforcement/attended_authority_guard.js +1 -1
- package/scripts/enforcement/guest_containment_taxonomy.v1.json +1 -1
- package/scripts/enforcement/herdr_async_dispatch_pi.js +1 -1
- package/scripts/enforcement/herdr_communication_pi.js +13 -8
- package/scripts/enforcement/herdr_lifecycle_pi.js +3 -3
- package/scripts/enforcement/linux_microvm_cutover_pi.js +1 -1
- package/scripts/enforcement/native_tui_context.js +1 -1
- package/scripts/enforcement/pulse_core_pi.js +38 -14
- package/scripts/enforcement/pulse_scheduler_pi.js +188 -25
- package/scripts/enforcement/router_engine_pi.js +483 -0
- package/scripts/enforcement/router_ranking_pi.js +146 -0
- package/scripts/enforcement/router_replacement_pi.js +98 -0
- package/scripts/enforcement/router_runtime_pi.js +299 -0
- package/scripts/enforcement/router_schemas_pi.js +654 -0
- package/scripts/enforcement/router_store_pi.js +438 -0
- package/scripts/enforcement/task_board_core_pi.js +255 -44
- package/skills/pulse/SKILL.md +1 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
// Unit tests: question schema contract v1 enforcement (§6), atomic
|
|
5
|
+
// fail-closed semantics, and the boolean threshold contract.
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { enforceRequest } from "../janus/schema.js";
|
|
10
|
+
import { JanusError } from "../janus/errors.js";
|
|
11
|
+
import { enforceAnswers } from "../janus/verdict.js";
|
|
12
|
+
|
|
13
|
+
function expectValidationError(fn: () => unknown, fragment?: string): void {
|
|
14
|
+
try {
|
|
15
|
+
fn();
|
|
16
|
+
assert.fail("expected validation_error");
|
|
17
|
+
} catch (error) {
|
|
18
|
+
assert.ok(error instanceof JanusError);
|
|
19
|
+
assert.equal(error.code, "validation_error");
|
|
20
|
+
if (fragment) assert.match(error.message, new RegExp(fragment));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("boolean: default threshold is 0.9 with >= comparison", () => {
|
|
25
|
+
const request = enforceRequest({
|
|
26
|
+
state: "hello",
|
|
27
|
+
questions: [{ type: "boolean", name: "refund", question: "Was a refund issued?" }],
|
|
28
|
+
});
|
|
29
|
+
assert.equal(request.questions.length, 1);
|
|
30
|
+
const q = request.questions[0]!;
|
|
31
|
+
assert.equal(q.kind, "boolean");
|
|
32
|
+
assert.equal((q as { threshold: number }).threshold, 0.9);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("boolean: custom threshold accepted within [0,1]", () => {
|
|
36
|
+
const request = enforceRequest({
|
|
37
|
+
state: "hello",
|
|
38
|
+
questions: [{ type: "boolean", name: "q", question: "q", threshold: 0.5 }],
|
|
39
|
+
});
|
|
40
|
+
assert.equal((request.questions[0] as { threshold: number }).threshold, 0.5);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("boolean: threshold out of range is validation_error", () => {
|
|
44
|
+
expectValidationError(() =>
|
|
45
|
+
enforceRequest({
|
|
46
|
+
state: "s",
|
|
47
|
+
questions: [{ type: "boolean", name: "q", question: "q", threshold: 1.5 }],
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
expectValidationError(() =>
|
|
51
|
+
enforceRequest({
|
|
52
|
+
state: "s",
|
|
53
|
+
questions: [{ type: "boolean", name: "q", question: "q", threshold: "high" }],
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("choice: choices array is REQUIRED (§6)", () => {
|
|
59
|
+
expectValidationError(() =>
|
|
60
|
+
enforceRequest({ state: "s", questions: [{ type: "choice", name: "route", question: "route" }] }),
|
|
61
|
+
);
|
|
62
|
+
expectValidationError(() =>
|
|
63
|
+
enforceRequest({ state: "s", questions: [{ type: "choice", name: "route", question: "route", choices: [] }] }),
|
|
64
|
+
);
|
|
65
|
+
expectValidationError(() =>
|
|
66
|
+
enforceRequest({
|
|
67
|
+
state: "s",
|
|
68
|
+
questions: [{ type: "choice", name: "route", question: "route", choices: ["a", "a"] }],
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
expectValidationError(() =>
|
|
72
|
+
enforceRequest({
|
|
73
|
+
state: "s",
|
|
74
|
+
questions: [{ type: "choice", name: "route", question: "route", choices: ["a", 3] }],
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("choice: valid choices accepted", () => {
|
|
80
|
+
const request = enforceRequest({
|
|
81
|
+
state: "s",
|
|
82
|
+
questions: [{ type: "choice", name: "route", question: "route", choices: ["billing", "tech", "other"] }],
|
|
83
|
+
});
|
|
84
|
+
assert.deepEqual((request.questions[0] as { choices: string[] }).choices, [
|
|
85
|
+
"billing",
|
|
86
|
+
"tech",
|
|
87
|
+
"other",
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("score: fixed 0..5 vocabulary; ordinal vocabularies must be choice (§6)", () => {
|
|
92
|
+
const request = enforceRequest({
|
|
93
|
+
state: "s",
|
|
94
|
+
questions: [{ type: "score", name: "rate", question: "rate" }],
|
|
95
|
+
});
|
|
96
|
+
assert.equal((request.questions[0] as { levels: number }).levels, 6);
|
|
97
|
+
// Closed validation: per-question overrides like choices/levels are rejected.
|
|
98
|
+
expectValidationError(() =>
|
|
99
|
+
enforceRequest({
|
|
100
|
+
state: "s",
|
|
101
|
+
questions: [{ type: "score", name: "rate", question: "rate", choices: ["poor", "good"] }],
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
expectValidationError(() =>
|
|
105
|
+
enforceRequest({
|
|
106
|
+
state: "s",
|
|
107
|
+
questions: [{ type: "score", name: "rate", question: "rate", levels: 10 }],
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("unknown question type is validation_error", () => {
|
|
113
|
+
expectValidationError(() =>
|
|
114
|
+
enforceRequest({ state: "s", questions: [{ type: "ranking", name: "q", question: "q" }] }),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("atomic fail-closed: one malformed question fails the whole request", () => {
|
|
119
|
+
expectValidationError(() =>
|
|
120
|
+
enforceRequest({
|
|
121
|
+
state: "s",
|
|
122
|
+
questions: [
|
|
123
|
+
{ type: "boolean", name: "ok", question: "ok?" },
|
|
124
|
+
{ type: "choice", name: "route", question: "route" }, // missing choices
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("missing state / missing questions / bad body shape are validation_error", () => {
|
|
131
|
+
expectValidationError(() => enforceRequest({ questions: [{ type: "boolean", name: "q", question: "q" }] }));
|
|
132
|
+
expectValidationError(() => enforceRequest({ state: "s", questions: [] }));
|
|
133
|
+
expectValidationError(() => enforceRequest({ state: "s" }));
|
|
134
|
+
expectValidationError(() => enforceRequest(null));
|
|
135
|
+
expectValidationError(() => enforceRequest("state only"));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("duplicate question name is validation_error", () => {
|
|
139
|
+
expectValidationError(() =>
|
|
140
|
+
enforceRequest({
|
|
141
|
+
state: "s",
|
|
142
|
+
questions: [
|
|
143
|
+
{ type: "boolean", name: "same", question: "same" },
|
|
144
|
+
{ type: "boolean", name: "same", question: "same" },
|
|
145
|
+
],
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("state accepts string, object, and array", () => {
|
|
151
|
+
for (const state of ["text", { a: 1 }, [{ role: "user" }]]) {
|
|
152
|
+
const request = enforceRequest({
|
|
153
|
+
state,
|
|
154
|
+
questions: [{ type: "boolean", name: "q", question: "q" }],
|
|
155
|
+
});
|
|
156
|
+
assert.deepEqual(request.state, state);
|
|
157
|
+
}
|
|
158
|
+
expectValidationError(() =>
|
|
159
|
+
enforceRequest({ state: 42, questions: [{ type: "boolean", name: "q", question: "q" }] }),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("verdict enforcement: model returned false is a normal 200 verdict with passed=false", () => {
|
|
164
|
+
const request = enforceRequest({
|
|
165
|
+
state: "s",
|
|
166
|
+
questions: [{ type: "boolean", name: "ok", question: "ok?" }],
|
|
167
|
+
});
|
|
168
|
+
const verdict = enforceAnswers(
|
|
169
|
+
request.questions,
|
|
170
|
+
{ ok: { type: "boolean", probability: 0.2 } },
|
|
171
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
172
|
+
);
|
|
173
|
+
assert.equal(verdict.answers.ok!.type, "boolean");
|
|
174
|
+
assert.equal((verdict.answers.ok as { passed: boolean }).passed, false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("verdict enforcement: probability >= threshold passes", () => {
|
|
178
|
+
const request = enforceRequest({
|
|
179
|
+
state: "s",
|
|
180
|
+
questions: [{ type: "boolean", name: "ok", question: "ok?", threshold: 0.9 }],
|
|
181
|
+
});
|
|
182
|
+
const verdict = enforceAnswers(
|
|
183
|
+
request.questions,
|
|
184
|
+
{ ok: { type: "boolean", probability: 0.9 } },
|
|
185
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
186
|
+
);
|
|
187
|
+
assert.equal((verdict.answers.ok as { passed: boolean }).passed, true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("verdict enforcement: choice answer must be a member of the choices array", () => {
|
|
191
|
+
const request = enforceRequest({
|
|
192
|
+
state: "s",
|
|
193
|
+
questions: [{ type: "choice", name: "route", question: "route", choices: ["a", "b"] }],
|
|
194
|
+
});
|
|
195
|
+
assert.throws(
|
|
196
|
+
() =>
|
|
197
|
+
enforceAnswers(
|
|
198
|
+
request.questions,
|
|
199
|
+
{ route: { type: "choice", choice: "c" } },
|
|
200
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
201
|
+
),
|
|
202
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("verdict enforcement: missing answer fails the whole request with validation_error (atomic, review fix #8)", () => {
|
|
207
|
+
const request = enforceRequest({
|
|
208
|
+
state: "s",
|
|
209
|
+
questions: [
|
|
210
|
+
{ type: "boolean", name: "a", question: "a" },
|
|
211
|
+
{ type: "boolean", name: "b", question: "b" },
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
assert.throws(
|
|
215
|
+
() =>
|
|
216
|
+
enforceAnswers(
|
|
217
|
+
request.questions,
|
|
218
|
+
{ a: { type: "boolean", probability: 0.99 } },
|
|
219
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
220
|
+
),
|
|
221
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("verdict enforcement: score is rounded to one decimal and range-checked", () => {
|
|
226
|
+
const request = enforceRequest({
|
|
227
|
+
state: "s",
|
|
228
|
+
questions: [{ type: "score", name: "rate", question: "rate" }],
|
|
229
|
+
});
|
|
230
|
+
const verdict = enforceAnswers(
|
|
231
|
+
request.questions,
|
|
232
|
+
{ rate: { type: "score", score: 2.96 } },
|
|
233
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
234
|
+
);
|
|
235
|
+
assert.equal((verdict.answers.rate as { score: number }).score, 3.0);
|
|
236
|
+
assert.throws(
|
|
237
|
+
() =>
|
|
238
|
+
enforceAnswers(
|
|
239
|
+
request.questions,
|
|
240
|
+
{ rate: { type: "score", score: 5.5 } },
|
|
241
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
242
|
+
),
|
|
243
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("question name is REQUIRED; missing or blank is validation_error (review fix #1)", () => {
|
|
248
|
+
expectValidationError(() =>
|
|
249
|
+
enforceRequest({ state: "s", questions: [{ type: "boolean", question: "ok?" }] }),
|
|
250
|
+
);
|
|
251
|
+
expectValidationError(() =>
|
|
252
|
+
enforceRequest({ state: "s", questions: [{ type: "boolean", name: " ", question: "ok?" }] }),
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("question text is REQUIRED; missing or blank is validation_error, never synthetic question[i] (review fix #1)", () => {
|
|
257
|
+
expectValidationError(() =>
|
|
258
|
+
enforceRequest({ state: "s", questions: [{ type: "boolean", name: "q" }] }),
|
|
259
|
+
);
|
|
260
|
+
expectValidationError(() =>
|
|
261
|
+
enforceRequest({ state: "s", questions: [{ type: "boolean", name: "q", question: "" }] }),
|
|
262
|
+
);
|
|
263
|
+
expectValidationError(() =>
|
|
264
|
+
enforceRequest({ state: "s", questions: [{ type: "boolean", name: "q", question: " " }] }),
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("enforced question carries both name and question text verbatim", () => {
|
|
269
|
+
const request = enforceRequest({
|
|
270
|
+
state: "s",
|
|
271
|
+
questions: [{ type: "boolean", name: "refund-gate", question: "Was a refund issued?" }],
|
|
272
|
+
});
|
|
273
|
+
const q = request.questions[0]! as { name: string; question: string };
|
|
274
|
+
assert.equal(q.name, "refund-gate");
|
|
275
|
+
assert.equal(q.question, "Was a refund issued?");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("verdict enforcement: wrong answer type fails with validation_error (review fix #8)", () => {
|
|
279
|
+
const request = enforceRequest({
|
|
280
|
+
state: "s",
|
|
281
|
+
questions: [{ type: "boolean", name: "ok", question: "ok?" }],
|
|
282
|
+
});
|
|
283
|
+
assert.throws(
|
|
284
|
+
() =>
|
|
285
|
+
enforceAnswers(
|
|
286
|
+
request.questions,
|
|
287
|
+
{ ok: { type: "score", score: 3 } },
|
|
288
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
289
|
+
),
|
|
290
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("verdict enforcement: invalid probability and non-finite score fail with validation_error (review fix #8)", () => {
|
|
295
|
+
const booleanRequest = enforceRequest({
|
|
296
|
+
state: "s",
|
|
297
|
+
questions: [{ type: "boolean", name: "ok", question: "ok?" }],
|
|
298
|
+
});
|
|
299
|
+
assert.throws(
|
|
300
|
+
() =>
|
|
301
|
+
enforceAnswers(
|
|
302
|
+
booleanRequest.questions,
|
|
303
|
+
{ ok: { type: "boolean", probability: 1.5 } },
|
|
304
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
305
|
+
),
|
|
306
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
307
|
+
);
|
|
308
|
+
assert.throws(
|
|
309
|
+
() =>
|
|
310
|
+
enforceAnswers(
|
|
311
|
+
booleanRequest.questions,
|
|
312
|
+
{ ok: { type: "boolean", probability: "high" } },
|
|
313
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
314
|
+
),
|
|
315
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
316
|
+
);
|
|
317
|
+
const scoreRequest = enforceRequest({
|
|
318
|
+
state: "s",
|
|
319
|
+
questions: [{ type: "score", name: "rate", question: "rate" }],
|
|
320
|
+
});
|
|
321
|
+
assert.throws(
|
|
322
|
+
() =>
|
|
323
|
+
enforceAnswers(
|
|
324
|
+
scoreRequest.questions,
|
|
325
|
+
{ rate: { type: "score", score: Number.NaN } },
|
|
326
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
327
|
+
),
|
|
328
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("verdict enforcement: malformed probabilities map fails with validation_error (review fix #8)", () => {
|
|
333
|
+
const request = enforceRequest({
|
|
334
|
+
state: "s",
|
|
335
|
+
questions: [{ type: "choice", name: "route", question: "route", choices: ["a", "b"] }],
|
|
336
|
+
});
|
|
337
|
+
assert.throws(
|
|
338
|
+
() =>
|
|
339
|
+
enforceAnswers(
|
|
340
|
+
request.questions,
|
|
341
|
+
{ route: { type: "choice", choice: "a", probabilities: { a: "high" } } },
|
|
342
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
343
|
+
),
|
|
344
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
345
|
+
);
|
|
346
|
+
// Probabilities keys outside the vocabulary are rejected.
|
|
347
|
+
assert.throws(
|
|
348
|
+
() =>
|
|
349
|
+
enforceAnswers(
|
|
350
|
+
request.questions,
|
|
351
|
+
{ route: { type: "choice", choice: "a", probabilities: { zzz: 0.5 } } },
|
|
352
|
+
{ inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
353
|
+
),
|
|
354
|
+
(error: unknown) => error instanceof JanusError && error.code === "validation_error",
|
|
355
|
+
);
|
|
356
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
// HTTP surface tests: loopback bind, endpoint contract, JSON error shape,
|
|
5
|
+
// and the GET routes. The SDK is mocked; no live gateway calls.
|
|
6
|
+
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { createJanusService, type JanusService } from "../janus/server.js";
|
|
13
|
+
import { Evaluator } from "../janus/evaluate.js";
|
|
14
|
+
import { SingleFlight } from "../janus/single_flight.js";
|
|
15
|
+
import { DailyBudget } from "../janus/budget_breaker.js";
|
|
16
|
+
import { AuditLog } from "../janus/audit.js";
|
|
17
|
+
import { testConfig, mockChain } from "./helpers.js";
|
|
18
|
+
|
|
19
|
+
async function listenOnRandomPort(): Promise<{ service: JanusService; base: string; cleanup: () => Promise<void> }> {
|
|
20
|
+
const dir = mkdtempSync(join(tmpdir(), "janus-server-test-"));
|
|
21
|
+
const config = testConfig({
|
|
22
|
+
port: 0,
|
|
23
|
+
audit: { path: join(dir, "audit"), maxBytes: 1024 * 1024, retentionDays: 90 },
|
|
24
|
+
});
|
|
25
|
+
const audit = new AuditLog(config.audit.path, config.audit.maxBytes, config.audit.retentionDays);
|
|
26
|
+
audit.start();
|
|
27
|
+
const evaluator = new Evaluator({
|
|
28
|
+
config,
|
|
29
|
+
singleFlight: new SingleFlight(4),
|
|
30
|
+
budget: new DailyBudget(0, 0, 0.5),
|
|
31
|
+
audit,
|
|
32
|
+
chain: mockChain({ answers: { q: { type: "boolean", probability: 0.95 } } }) as never,
|
|
33
|
+
});
|
|
34
|
+
const service = createJanusService(config, evaluator, async () => ({ ready: true }));
|
|
35
|
+
await new Promise<void>((resolvePromise) => service.server.on("listening", resolvePromise));
|
|
36
|
+
const address = service.server.address();
|
|
37
|
+
assert.ok(address && typeof address === "object");
|
|
38
|
+
const base = `http://127.0.0.1:${address.port}`;
|
|
39
|
+
return {
|
|
40
|
+
service,
|
|
41
|
+
base,
|
|
42
|
+
cleanup: async () => {
|
|
43
|
+
await service.stop();
|
|
44
|
+
audit.stop();
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("binds loopback only", async () => {
|
|
51
|
+
const { service, cleanup } = await listenOnRandomPort();
|
|
52
|
+
try {
|
|
53
|
+
const address = service.server.address();
|
|
54
|
+
assert.ok(address && typeof address === "object");
|
|
55
|
+
assert.equal(address.address, "127.0.0.1");
|
|
56
|
+
} finally {
|
|
57
|
+
await cleanup();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("GET /healthz returns ok", async () => {
|
|
62
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(`${base}/healthz`);
|
|
65
|
+
assert.equal(res.status, 200);
|
|
66
|
+
const body = (await res.json()) as { status: string };
|
|
67
|
+
assert.equal(body.status, "ok");
|
|
68
|
+
} finally {
|
|
69
|
+
await cleanup();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("GET /version returns janus, AI SDK, schema versions, and allowlist", async () => {
|
|
74
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(`${base}/version`);
|
|
77
|
+
assert.equal(res.status, 200);
|
|
78
|
+
const body = (await res.json()) as Record<string, unknown>;
|
|
79
|
+
assert.equal(body.janus, "0.1.0");
|
|
80
|
+
assert.equal(body.typesafeSdk, "0.6.0");
|
|
81
|
+
assert.equal(body.questionSchema, "janus.question-schema.v2");
|
|
82
|
+
assert.deepEqual(body.providers, ["typesafe-direct"]);
|
|
83
|
+
} finally {
|
|
84
|
+
await cleanup();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("POST /evaluate happy path returns verdict with request_id", async () => {
|
|
89
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
90
|
+
try {
|
|
91
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
body: JSON.stringify({ state: "s", questions: [{ type: "boolean", name: "q", question: "q" }] }),
|
|
95
|
+
});
|
|
96
|
+
assert.equal(res.status, 200);
|
|
97
|
+
const body = (await res.json()) as { request_id: string; verdict: Record<string, unknown> };
|
|
98
|
+
assert.ok(body.request_id.length > 0);
|
|
99
|
+
assert.deepEqual(body.verdict, { q: { type: "boolean", probability: 0.95, passed: true } });
|
|
100
|
+
} finally {
|
|
101
|
+
await cleanup();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("POST /evaluate invalid JSON is validation_error", async () => {
|
|
106
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
body: "{not json",
|
|
112
|
+
});
|
|
113
|
+
assert.equal(res.status, 400);
|
|
114
|
+
const body = (await res.json()) as { error: string };
|
|
115
|
+
assert.equal(body.error, "validation_error");
|
|
116
|
+
} finally {
|
|
117
|
+
await cleanup();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("unknown route is 404 with the taxonomy shape", async () => {
|
|
122
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(`${base}/nope`);
|
|
125
|
+
assert.equal(res.status, 404);
|
|
126
|
+
const body = (await res.json()) as { error: string };
|
|
127
|
+
assert.equal(body.error, "validation_error");
|
|
128
|
+
} finally {
|
|
129
|
+
await cleanup();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("POST /evaluate without content-type is validation_error (review fix #10)", async () => {
|
|
134
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
body: JSON.stringify({ state: "s", questions: [] }),
|
|
139
|
+
});
|
|
140
|
+
assert.equal(res.status, 400);
|
|
141
|
+
const body = (await res.json()) as { error: string; message: string };
|
|
142
|
+
assert.equal(body.error, "validation_error");
|
|
143
|
+
assert.match(body.message, /content-type/);
|
|
144
|
+
} finally {
|
|
145
|
+
await cleanup();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("POST /evaluate with malformed content-type is validation_error (review fix #10)", async () => {
|
|
150
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
151
|
+
try {
|
|
152
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
153
|
+
method: "POST",
|
|
154
|
+
headers: { "content-type": "not a valid type;;;" },
|
|
155
|
+
body: "{}",
|
|
156
|
+
});
|
|
157
|
+
assert.equal(res.status, 400);
|
|
158
|
+
const body = (await res.json()) as { error: string };
|
|
159
|
+
assert.equal(body.error, "validation_error");
|
|
160
|
+
} finally {
|
|
161
|
+
await cleanup();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("POST /evaluate with non-JSON content-type is validation_error (review fix #10)", async () => {
|
|
166
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
167
|
+
try {
|
|
168
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "content-type": "text/plain" },
|
|
171
|
+
body: "{}",
|
|
172
|
+
});
|
|
173
|
+
assert.equal(res.status, 400);
|
|
174
|
+
const body = (await res.json()) as { error: string };
|
|
175
|
+
assert.equal(body.error, "validation_error");
|
|
176
|
+
} finally {
|
|
177
|
+
await cleanup();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("POST /evaluate accepts application/json with parameters (charset) (review fix #10)", async () => {
|
|
182
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
183
|
+
try {
|
|
184
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
185
|
+
method: "POST",
|
|
186
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
187
|
+
body: JSON.stringify({ state: "s", questions: [{ type: "boolean", name: "q", question: "q" }] }),
|
|
188
|
+
});
|
|
189
|
+
assert.equal(res.status, 200);
|
|
190
|
+
} finally {
|
|
191
|
+
await cleanup();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("oversized body returns structured JSON 413 (review fix #10)", async () => {
|
|
196
|
+
const { base, cleanup } = await listenOnRandomPort();
|
|
197
|
+
try {
|
|
198
|
+
// 9 MB body against the 8 MB cap, sent with a JSON content-type.
|
|
199
|
+
const big = "x".repeat(9 * 1024 * 1024);
|
|
200
|
+
const res = await fetch(`${base}/evaluate`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { "content-type": "application/json" },
|
|
203
|
+
body: JSON.stringify({ state: big, questions: [{ type: "boolean", name: "q", question: "q" }] }),
|
|
204
|
+
});
|
|
205
|
+
assert.ok([400, 413].includes(res.status));
|
|
206
|
+
const body = (await res.json()) as { error: string };
|
|
207
|
+
assert.equal(body.error, "validation_error");
|
|
208
|
+
} finally {
|
|
209
|
+
await cleanup();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("GET /readyz reports readiness from the provider chain", async () => {
|
|
214
|
+
const dir = mkdtempSync(join(tmpdir(), "janus-server-test-"));
|
|
215
|
+
try {
|
|
216
|
+
const config = testConfig({
|
|
217
|
+
port: 0,
|
|
218
|
+
audit: { path: join(dir, "audit"), maxBytes: 1024 * 1024, retentionDays: 90 },
|
|
219
|
+
});
|
|
220
|
+
const audit = new AuditLog(config.audit.path, config.audit.maxBytes, config.audit.retentionDays);
|
|
221
|
+
audit.start();
|
|
222
|
+
const evaluator = new Evaluator({
|
|
223
|
+
config,
|
|
224
|
+
singleFlight: new SingleFlight(4),
|
|
225
|
+
budget: new DailyBudget(0, 0, 0.5),
|
|
226
|
+
audit,
|
|
227
|
+
chain: mockChain({ answers: { q: { type: "boolean", probability: 0.95 } } }) as never,
|
|
228
|
+
});
|
|
229
|
+
const service = createJanusService(config, evaluator, async () => ({
|
|
230
|
+
ready: false,
|
|
231
|
+
reason: "no provider credential resolvable (checked: typesafe-direct)",
|
|
232
|
+
}));
|
|
233
|
+
await new Promise<void>((resolvePromise) => service.server.on("listening", resolvePromise));
|
|
234
|
+
const address = service.server.address();
|
|
235
|
+
assert.ok(address && typeof address === "object");
|
|
236
|
+
const res = await fetch(`http://127.0.0.1:${address.port}/readyz`);
|
|
237
|
+
assert.equal(res.status, 503);
|
|
238
|
+
const body = (await res.json()) as { reason: string };
|
|
239
|
+
assert.match(body.reason, /no provider credential resolvable/);
|
|
240
|
+
assert.ok(!/sk-|test-key/i.test(body.reason), "no key fragments in the public reason");
|
|
241
|
+
await service.stop();
|
|
242
|
+
audit.stop();
|
|
243
|
+
} finally {
|
|
244
|
+
rmSync(dir, { recursive: true, force: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("internal evaluator crash returns a fixed public degraded message (review fix #5)", async () => {
|
|
249
|
+
const dir = mkdtempSync(join(tmpdir(), "janus-server-test-"));
|
|
250
|
+
try {
|
|
251
|
+
const config = testConfig({
|
|
252
|
+
port: 0,
|
|
253
|
+
audit: { path: join(dir, "audit"), maxBytes: 1024 * 1024, retentionDays: 90 },
|
|
254
|
+
});
|
|
255
|
+
const audit = new AuditLog(config.audit.path, config.audit.maxBytes, config.audit.retentionDays);
|
|
256
|
+
audit.start();
|
|
257
|
+
const evaluator = new Evaluator({
|
|
258
|
+
config,
|
|
259
|
+
singleFlight: new SingleFlight(4),
|
|
260
|
+
budget: new DailyBudget(0, 0, 0.5),
|
|
261
|
+
audit,
|
|
262
|
+
chain: {
|
|
263
|
+
evaluate: async () => {
|
|
264
|
+
throw new Error("SECRET sk-live-abc123 internal stack trace detail");
|
|
265
|
+
},
|
|
266
|
+
readiness: async () => ({ ready: true, providers: [] }),
|
|
267
|
+
} as never,
|
|
268
|
+
});
|
|
269
|
+
const service = createJanusService(config, evaluator, async () => ({ ready: true }));
|
|
270
|
+
await new Promise<void>((resolvePromise) => service.server.on("listening", resolvePromise));
|
|
271
|
+
const address = service.server.address();
|
|
272
|
+
assert.ok(address && typeof address === "object");
|
|
273
|
+
const res = await fetch(`http://127.0.0.1:${address.port}/evaluate`, {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: { "content-type": "application/json" },
|
|
276
|
+
body: JSON.stringify({ state: "s", questions: [{ type: "boolean", name: "q", question: "q" }] }),
|
|
277
|
+
});
|
|
278
|
+
assert.equal(res.status, 503);
|
|
279
|
+
const body = (await res.json()) as { error: string; message: string };
|
|
280
|
+
assert.equal(body.error, "gateway_unavailable");
|
|
281
|
+
assert.equal(body.message, "provider evaluation failed");
|
|
282
|
+
assert.ok(!body.message.includes("SECRET"), "internal error text must never reach the caller");
|
|
283
|
+
await service.stop();
|
|
284
|
+
audit.stop();
|
|
285
|
+
} finally {
|
|
286
|
+
rmSync(dir, { recursive: true, force: true });
|
|
287
|
+
}
|
|
288
|
+
});
|