pi-system-one 1.1.3 → 1.2.0
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/README.md +101 -1
- package/demo.gif +0 -0
- package/package.json +1 -1
- package/prompts/judge.md +29 -0
- package/skills/system-one/SKILL.md +91 -0
- package/skills/system-one/references/use-cases.md +171 -0
- package/src/commands.ts +1 -1
- package/src/extension.ts +30 -0
- package/src/tool.ts +452 -51
- package/tests/commands.test.ts +6 -1
- package/tests/extension.test.ts +123 -0
- package/tests/packaging.test.ts +49 -0
- package/tests/tool.test.ts +1176 -3
package/tests/tool.test.ts
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
import assert from "node:assert/strict";
|
|
4
4
|
import { describe, it } from "node:test";
|
|
5
5
|
import { MockSystemOneProvider } from "system-one-core";
|
|
6
|
-
import {
|
|
6
|
+
import { Value } from "typebox/value";
|
|
7
|
+
import {
|
|
8
|
+
buildSystemOneTool,
|
|
9
|
+
prepareSystemOneArgs,
|
|
10
|
+
systemOneParams,
|
|
11
|
+
} from "../src/tool.ts";
|
|
7
12
|
|
|
8
13
|
describe("system_one tool", () => {
|
|
9
14
|
it("delegates a mixed batch and preserves details", async () => {
|
|
@@ -94,7 +99,6 @@ describe("system_one tool", () => {
|
|
|
94
99
|
"id-3",
|
|
95
100
|
{
|
|
96
101
|
state: "hi",
|
|
97
|
-
model: "jev-latest",
|
|
98
102
|
questions: {
|
|
99
103
|
c: {
|
|
100
104
|
type: "choice",
|
|
@@ -118,7 +122,1176 @@ describe("system_one tool", () => {
|
|
|
118
122
|
assert.equal((res.details.answers.s as { score: number }).score, 1.5);
|
|
119
123
|
assert.deepEqual(res.details.usage, { inputTokens: 5, outputTokens: 2 });
|
|
120
124
|
assert.deepEqual(res.details.metadata, { provider: "stub" });
|
|
121
|
-
assert.equal(seenRequest.model, "jev-latest");
|
|
122
125
|
assert.equal(seenOptions.signal, controller.signal);
|
|
126
|
+
assert.equal(seenRequest.state, "hi");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("low-quality model tolerance", () => {
|
|
130
|
+
// Each raw input is a shape weak models emit that failed validation
|
|
131
|
+
// against the old union/literal schema. prepareSystemOneArgs must coerce
|
|
132
|
+
// it into something the flat schema accepts.
|
|
133
|
+
const sloppy: Array<[string, unknown]> = [
|
|
134
|
+
[
|
|
135
|
+
"capitalised type",
|
|
136
|
+
{
|
|
137
|
+
state: "hi",
|
|
138
|
+
questions: {
|
|
139
|
+
t: { type: "Choice", instructions: "W?", criteria: { a: null } },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"options alias",
|
|
145
|
+
{
|
|
146
|
+
state: "hi",
|
|
147
|
+
questions: {
|
|
148
|
+
t: { type: "choice", instructions: "W?", options: ["a", "b"] },
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
[
|
|
153
|
+
"choices record alias",
|
|
154
|
+
{
|
|
155
|
+
state: "hi",
|
|
156
|
+
questions: {
|
|
157
|
+
t: { type: "choice", instructions: "W?", choices: { a: null } },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
"array questions",
|
|
163
|
+
{
|
|
164
|
+
state: "hi",
|
|
165
|
+
questions: [
|
|
166
|
+
{
|
|
167
|
+
name: "t",
|
|
168
|
+
type: "choice",
|
|
169
|
+
instructions: "W?",
|
|
170
|
+
criteria: { a: null },
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
[
|
|
176
|
+
"singular question key",
|
|
177
|
+
{
|
|
178
|
+
state: "hi",
|
|
179
|
+
question: {
|
|
180
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
"unwrapped single question",
|
|
186
|
+
{
|
|
187
|
+
state: "hi",
|
|
188
|
+
questions: {
|
|
189
|
+
type: "choice",
|
|
190
|
+
instructions: "W?",
|
|
191
|
+
criteria: { a: null },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
[
|
|
196
|
+
"junk description field",
|
|
197
|
+
{
|
|
198
|
+
state: "hi",
|
|
199
|
+
questions: {
|
|
200
|
+
t: {
|
|
201
|
+
type: "choice",
|
|
202
|
+
instructions: "W?",
|
|
203
|
+
criteria: { a: null },
|
|
204
|
+
description: "extra",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
[
|
|
210
|
+
"boolean synonym",
|
|
211
|
+
{
|
|
212
|
+
state: "x",
|
|
213
|
+
questions: { q: { type: "boolean", instructions: "Is it?" } },
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
"rating synonym",
|
|
218
|
+
{
|
|
219
|
+
state: "x",
|
|
220
|
+
questions: {
|
|
221
|
+
q: {
|
|
222
|
+
type: "rating",
|
|
223
|
+
instructions: "How?",
|
|
224
|
+
criteria: ["low", "high"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
[
|
|
230
|
+
"choice array criteria",
|
|
231
|
+
{
|
|
232
|
+
state: "hi",
|
|
233
|
+
questions: {
|
|
234
|
+
t: { type: "choice", instructions: "W?", criteria: ["a", "b"] },
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
[
|
|
239
|
+
"context alias",
|
|
240
|
+
{
|
|
241
|
+
context: "hi",
|
|
242
|
+
questions: {
|
|
243
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"prompt alias",
|
|
249
|
+
{
|
|
250
|
+
state: "hi",
|
|
251
|
+
questions: {
|
|
252
|
+
t: { type: "choice", prompt: "W?", criteria: { a: null } },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
[
|
|
257
|
+
"yes/no synonym with slash",
|
|
258
|
+
{
|
|
259
|
+
state: "x",
|
|
260
|
+
questions: { q: { type: "Yes/No", instructions: "Is it?" } },
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
[
|
|
264
|
+
"YesNo camelCase synonym",
|
|
265
|
+
{
|
|
266
|
+
state: "x",
|
|
267
|
+
questions: { q: { type: "YesNo", instructions: "Is it?" } },
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
[
|
|
271
|
+
"noul null criteria reads as omitted",
|
|
272
|
+
{
|
|
273
|
+
state: "x",
|
|
274
|
+
questions: {
|
|
275
|
+
q: { type: "noul", instructions: "Is it?", criteria: null },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
[
|
|
280
|
+
"duplicate array names",
|
|
281
|
+
{
|
|
282
|
+
state: "hi",
|
|
283
|
+
questions: [
|
|
284
|
+
{
|
|
285
|
+
name: "t",
|
|
286
|
+
type: "choice",
|
|
287
|
+
instructions: "W?",
|
|
288
|
+
criteria: { a: null },
|
|
289
|
+
},
|
|
290
|
+
{ name: "t", type: "noul", instructions: "Is it?" },
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
];
|
|
295
|
+
for (const [name, raw] of sloppy) {
|
|
296
|
+
it(`accepts ${name}`, () => {
|
|
297
|
+
const prepared = prepareSystemOneArgs(raw);
|
|
298
|
+
assert.equal(
|
|
299
|
+
Value.Check(systemOneParams, prepared),
|
|
300
|
+
true,
|
|
301
|
+
`prepared args still fail validation: ${JSON.stringify(prepared)}`,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
it("folds options array to null-valued criteria record", () => {
|
|
307
|
+
assert.deepEqual(
|
|
308
|
+
prepareSystemOneArgs({
|
|
309
|
+
state: "hi",
|
|
310
|
+
questions: {
|
|
311
|
+
t: { type: "choice", instructions: "W?", options: ["a", "b"] },
|
|
312
|
+
},
|
|
313
|
+
}),
|
|
314
|
+
{
|
|
315
|
+
state: "hi",
|
|
316
|
+
questions: {
|
|
317
|
+
t: {
|
|
318
|
+
type: "choice",
|
|
319
|
+
instructions: "W?",
|
|
320
|
+
criteria: { a: null, b: null },
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("suffixes duplicate array question names instead of dropping", () => {
|
|
328
|
+
assert.deepEqual(
|
|
329
|
+
prepareSystemOneArgs({
|
|
330
|
+
state: "hi",
|
|
331
|
+
questions: [
|
|
332
|
+
{
|
|
333
|
+
name: "t",
|
|
334
|
+
type: "choice",
|
|
335
|
+
instructions: "W?",
|
|
336
|
+
criteria: { a: null },
|
|
337
|
+
},
|
|
338
|
+
{ name: "t", type: "noul", instructions: "Is it?" },
|
|
339
|
+
],
|
|
340
|
+
}),
|
|
341
|
+
{
|
|
342
|
+
state: "hi",
|
|
343
|
+
questions: {
|
|
344
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
345
|
+
t_2: { type: "noul", instructions: "Is it?" },
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("unwraps single questions with extra fields so execute reports the field", async () => {
|
|
352
|
+
const prepared = prepareSystemOneArgs({
|
|
353
|
+
state: "hi",
|
|
354
|
+
questions: {
|
|
355
|
+
type: "choice",
|
|
356
|
+
instructions: "W?",
|
|
357
|
+
criteria: { a: null },
|
|
358
|
+
foo: "bar",
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
assert.deepEqual(prepared, {
|
|
362
|
+
state: "hi",
|
|
363
|
+
questions: {
|
|
364
|
+
q: {
|
|
365
|
+
type: "choice",
|
|
366
|
+
instructions: "W?",
|
|
367
|
+
criteria: { a: null },
|
|
368
|
+
foo: "bar",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
// The permissive boundary schema lets this through (older Pi must be
|
|
373
|
+
// able to reach execute); the semantic gate rejects it precisely.
|
|
374
|
+
assert.equal(Value.Check(systemOneParams, prepared), true);
|
|
375
|
+
let calls = 0;
|
|
376
|
+
const tool = buildSystemOneTool({
|
|
377
|
+
provider: {
|
|
378
|
+
id: "stub",
|
|
379
|
+
async evaluate() {
|
|
380
|
+
calls += 1;
|
|
381
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
382
|
+
},
|
|
383
|
+
} as never,
|
|
384
|
+
});
|
|
385
|
+
await assert.rejects(
|
|
386
|
+
tool.execute(
|
|
387
|
+
"id-foo",
|
|
388
|
+
prepared as never,
|
|
389
|
+
undefined,
|
|
390
|
+
undefined,
|
|
391
|
+
{} as never,
|
|
392
|
+
),
|
|
393
|
+
/question "q" has unknown field "foo"/,
|
|
394
|
+
);
|
|
395
|
+
assert.equal(calls, 0);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("lets unknown types through the schema so execute rejects them clearly", async () => {
|
|
399
|
+
const prepared = prepareSystemOneArgs({
|
|
400
|
+
state: "hi",
|
|
401
|
+
questions: { t: { type: "essay", instructions: "W?" } },
|
|
402
|
+
}) as unknown as { questions: { t: { type: string } } };
|
|
403
|
+
assert.equal(prepared.questions.t.type, "essay");
|
|
404
|
+
assert.equal(Value.Check(systemOneParams, prepared), true);
|
|
405
|
+
let calls = 0;
|
|
406
|
+
const tool = buildSystemOneTool({
|
|
407
|
+
provider: {
|
|
408
|
+
id: "stub",
|
|
409
|
+
async evaluate() {
|
|
410
|
+
calls += 1;
|
|
411
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
412
|
+
},
|
|
413
|
+
} as never,
|
|
414
|
+
});
|
|
415
|
+
await assert.rejects(
|
|
416
|
+
tool.execute(
|
|
417
|
+
"id-essay",
|
|
418
|
+
prepared as never,
|
|
419
|
+
undefined,
|
|
420
|
+
undefined,
|
|
421
|
+
{} as never,
|
|
422
|
+
),
|
|
423
|
+
/unknown type "essay".*use "choice", "noul", or "score"/,
|
|
424
|
+
);
|
|
425
|
+
assert.equal(calls, 0);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("exposes prepareArguments on the tool definition", () => {
|
|
429
|
+
const tool = buildSystemOneTool({
|
|
430
|
+
provider: new MockSystemOneProvider({ answers: {} }),
|
|
431
|
+
});
|
|
432
|
+
const prepared = tool.prepareArguments?.({
|
|
433
|
+
state: "hi",
|
|
434
|
+
questions: {
|
|
435
|
+
t: { type: "Choice", instructions: "W?", options: ["a"] },
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
assert.deepEqual(prepared, {
|
|
439
|
+
state: "hi",
|
|
440
|
+
questions: {
|
|
441
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("execute tolerates sloppy args end to end", async () => {
|
|
447
|
+
let seenRequest: unknown;
|
|
448
|
+
const stub = {
|
|
449
|
+
id: "stub",
|
|
450
|
+
async evaluate(req: unknown) {
|
|
451
|
+
seenRequest = req;
|
|
452
|
+
return {
|
|
453
|
+
answers: {
|
|
454
|
+
t: {
|
|
455
|
+
type: "choice",
|
|
456
|
+
choice: "a",
|
|
457
|
+
probabilities: { a: 1 },
|
|
458
|
+
confidence: 1,
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
metadata: { provider: "stub" },
|
|
462
|
+
};
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
const tool = buildSystemOneTool({ provider: stub as never });
|
|
466
|
+
const res = await tool.execute(
|
|
467
|
+
"id-sloppy",
|
|
468
|
+
{
|
|
469
|
+
state: "hi",
|
|
470
|
+
questions: {
|
|
471
|
+
t: { type: "Choice", instructions: "W?", options: ["a", "b"] },
|
|
472
|
+
},
|
|
473
|
+
} as never,
|
|
474
|
+
undefined,
|
|
475
|
+
undefined,
|
|
476
|
+
{} as never,
|
|
477
|
+
);
|
|
478
|
+
assert.deepEqual(seenRequest, {
|
|
479
|
+
state: "hi",
|
|
480
|
+
questions: {
|
|
481
|
+
t: {
|
|
482
|
+
type: "choice",
|
|
483
|
+
instructions: "W?",
|
|
484
|
+
criteria: { a: null, b: null },
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
assert.equal((res.details.answers.t as { choice: string }).choice, "a");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("execute rejects non-object args with an actionable error, not a TypeError", async () => {
|
|
492
|
+
let calls = 0;
|
|
493
|
+
const tool = buildSystemOneTool({
|
|
494
|
+
provider: {
|
|
495
|
+
id: "stub",
|
|
496
|
+
async evaluate() {
|
|
497
|
+
calls += 1;
|
|
498
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
499
|
+
},
|
|
500
|
+
} as never,
|
|
501
|
+
});
|
|
502
|
+
for (const raw of [null, "just a string", [{ state: "hi" }]]) {
|
|
503
|
+
await assert.rejects(
|
|
504
|
+
tool.execute(
|
|
505
|
+
"id-nonobject",
|
|
506
|
+
raw as never,
|
|
507
|
+
undefined,
|
|
508
|
+
undefined,
|
|
509
|
+
{} as never,
|
|
510
|
+
),
|
|
511
|
+
/request must be an object with "state" and "questions"/,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
assert.equal(calls, 0);
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
describe("pre-validation compatibility (older Pi without prepareArguments)", () => {
|
|
519
|
+
// On older Pi the raw model output is validated against the public
|
|
520
|
+
// schema BEFORE execute() can normalize it. Every repairable shape
|
|
521
|
+
// must therefore pass Value.Check raw — testing prepareSystemOneArgs
|
|
522
|
+
// alone would not prove that.
|
|
523
|
+
const rawShapes: Array<[string, unknown]> = [
|
|
524
|
+
[
|
|
525
|
+
"singular question with capitalized type and options array",
|
|
526
|
+
{
|
|
527
|
+
state: "hi",
|
|
528
|
+
question: { type: "Choice", instructions: "W?", options: ["a", "b"] },
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
[
|
|
532
|
+
"array questions with bool type and prompt alias",
|
|
533
|
+
{
|
|
534
|
+
state: "hi",
|
|
535
|
+
questions: [{ type: "bool", prompt: "Is this safe?" }],
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
[
|
|
539
|
+
"context alias with singular rating question and choices",
|
|
540
|
+
{
|
|
541
|
+
context: "some evidence",
|
|
542
|
+
question: {
|
|
543
|
+
type: "rating",
|
|
544
|
+
prompt: "Rate it",
|
|
545
|
+
choices: ["bad", "ok", "good"],
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
[
|
|
550
|
+
"queries and evidence aliases",
|
|
551
|
+
{
|
|
552
|
+
evidence: { doc: "..." },
|
|
553
|
+
queries: {
|
|
554
|
+
q: { type: "select", instructions: "W?", criteria: { a: null } },
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
[
|
|
559
|
+
"unknown top-level keys are ignored, not rejected",
|
|
560
|
+
{
|
|
561
|
+
state: "hi",
|
|
562
|
+
questions: {
|
|
563
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
564
|
+
},
|
|
565
|
+
session_id: "abc",
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
[
|
|
569
|
+
"ambiguous type alias still reaches execute for a clear rejection",
|
|
570
|
+
{
|
|
571
|
+
state: "hi",
|
|
572
|
+
questions: { t: { type: "probability", instructions: "W?" } },
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
[
|
|
576
|
+
"missing state still reaches execute for a clear rejection",
|
|
577
|
+
{
|
|
578
|
+
questions: {
|
|
579
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
],
|
|
583
|
+
[
|
|
584
|
+
"scalar questions still reach execute for a clear rejection",
|
|
585
|
+
{ state: "hi", questions: "do something" },
|
|
586
|
+
],
|
|
587
|
+
];
|
|
588
|
+
for (const [name, raw] of rawShapes) {
|
|
589
|
+
it(`schema accepts raw: ${name}`, () => {
|
|
590
|
+
assert.equal(
|
|
591
|
+
Value.Check(systemOneParams, raw),
|
|
592
|
+
true,
|
|
593
|
+
`raw payload fails pre-execution validation: ${JSON.stringify(raw)}`,
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe("weak-model adversarial corpus", () => {
|
|
600
|
+
function stubTool() {
|
|
601
|
+
let calls = 0;
|
|
602
|
+
const stub = {
|
|
603
|
+
id: "stub",
|
|
604
|
+
async evaluate(_req: unknown) {
|
|
605
|
+
calls += 1;
|
|
606
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
607
|
+
},
|
|
608
|
+
};
|
|
609
|
+
return {
|
|
610
|
+
tool: buildSystemOneTool({ provider: stub as never }),
|
|
611
|
+
calls: () => calls,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Each entry: repaired shapes must BOTH survive pre-execution schema
|
|
616
|
+
// validation raw AND succeed end to end; rejected shapes must fail
|
|
617
|
+
// with an actionable error before any backend call.
|
|
618
|
+
const repaired: Array<[string, unknown]> = [
|
|
619
|
+
[
|
|
620
|
+
"wrong capitalization",
|
|
621
|
+
{
|
|
622
|
+
state: "hi",
|
|
623
|
+
questions: {
|
|
624
|
+
t: { type: "Choice", instructions: "W?", criteria: { a: null } },
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
[
|
|
629
|
+
"singular question key",
|
|
630
|
+
{
|
|
631
|
+
state: "hi",
|
|
632
|
+
question: {
|
|
633
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
[
|
|
638
|
+
"array where object was expected",
|
|
639
|
+
{
|
|
640
|
+
state: "hi",
|
|
641
|
+
questions: [
|
|
642
|
+
{
|
|
643
|
+
name: "t",
|
|
644
|
+
type: "choice",
|
|
645
|
+
instructions: "W?",
|
|
646
|
+
criteria: { a: null },
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
},
|
|
650
|
+
],
|
|
651
|
+
[
|
|
652
|
+
"criteria as string array",
|
|
653
|
+
{
|
|
654
|
+
state: "hi",
|
|
655
|
+
questions: {
|
|
656
|
+
t: { type: "choice", instructions: "W?", criteria: ["a", "b"] },
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
[
|
|
661
|
+
"criteria as keyed object",
|
|
662
|
+
{
|
|
663
|
+
state: "hi",
|
|
664
|
+
questions: {
|
|
665
|
+
t: {
|
|
666
|
+
type: "choice",
|
|
667
|
+
instructions: "W?",
|
|
668
|
+
choices: { a: "first", b: "second" },
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
[
|
|
674
|
+
"duplicate question names",
|
|
675
|
+
{
|
|
676
|
+
state: "hi",
|
|
677
|
+
questions: [
|
|
678
|
+
{
|
|
679
|
+
name: "t",
|
|
680
|
+
type: "choice",
|
|
681
|
+
instructions: "W?",
|
|
682
|
+
criteria: { a: null },
|
|
683
|
+
},
|
|
684
|
+
{ name: "t", type: "noul", instructions: "Is it?" },
|
|
685
|
+
],
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
[
|
|
689
|
+
"context alias",
|
|
690
|
+
{
|
|
691
|
+
context: "hi",
|
|
692
|
+
questions: { t: { type: "noul", instructions: "Is it?" } },
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
[
|
|
696
|
+
"prompt alias",
|
|
697
|
+
{
|
|
698
|
+
state: "hi",
|
|
699
|
+
questions: { t: { type: "noul", prompt: "Is it?" } },
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
[
|
|
703
|
+
"boolean naming variation",
|
|
704
|
+
{
|
|
705
|
+
state: "x",
|
|
706
|
+
questions: { q: { type: "YesNo", instructions: "Is it?" } },
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
[
|
|
710
|
+
"rating naming variation",
|
|
711
|
+
{
|
|
712
|
+
state: "x",
|
|
713
|
+
questions: {
|
|
714
|
+
q: {
|
|
715
|
+
type: "rating",
|
|
716
|
+
instructions: "How?",
|
|
717
|
+
options: ["low", "high"],
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
],
|
|
722
|
+
[
|
|
723
|
+
"noul null criteria",
|
|
724
|
+
{
|
|
725
|
+
state: "x",
|
|
726
|
+
questions: {
|
|
727
|
+
q: { type: "noul", instructions: "Is it?", criteria: null },
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
],
|
|
731
|
+
[
|
|
732
|
+
"noul object criteria",
|
|
733
|
+
{
|
|
734
|
+
state: "x",
|
|
735
|
+
questions: {
|
|
736
|
+
q: {
|
|
737
|
+
type: "noul",
|
|
738
|
+
instructions: "Is it?",
|
|
739
|
+
criteria: { true: "yes", false: "no" },
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
[
|
|
745
|
+
"junk naming fields",
|
|
746
|
+
{
|
|
747
|
+
state: "hi",
|
|
748
|
+
questions: {
|
|
749
|
+
t: {
|
|
750
|
+
type: "choice",
|
|
751
|
+
instructions: "W?",
|
|
752
|
+
criteria: { a: null },
|
|
753
|
+
name: "t",
|
|
754
|
+
title: "pick",
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
],
|
|
759
|
+
];
|
|
760
|
+
for (const [name, raw] of repaired) {
|
|
761
|
+
it(`repairs ${name}`, async () => {
|
|
762
|
+
assert.equal(
|
|
763
|
+
Value.Check(systemOneParams, raw),
|
|
764
|
+
true,
|
|
765
|
+
`repaired shape must survive pre-execution validation: ${JSON.stringify(raw)}`,
|
|
766
|
+
);
|
|
767
|
+
const { tool, calls } = stubTool();
|
|
768
|
+
await tool.execute(
|
|
769
|
+
`id-rep-${name}`,
|
|
770
|
+
raw as never,
|
|
771
|
+
undefined,
|
|
772
|
+
undefined,
|
|
773
|
+
{} as never,
|
|
774
|
+
);
|
|
775
|
+
assert.equal(calls(), 1);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const rejected: Array<[string, unknown, RegExp]> = [
|
|
780
|
+
[
|
|
781
|
+
"ambiguous probability alias",
|
|
782
|
+
{
|
|
783
|
+
state: "hi",
|
|
784
|
+
questions: { t: { type: "probability", instructions: "W?" } },
|
|
785
|
+
},
|
|
786
|
+
/unknown type "probability".*use "choice", "noul", or "score"/,
|
|
787
|
+
],
|
|
788
|
+
[
|
|
789
|
+
"ambiguous likelihood alias",
|
|
790
|
+
{
|
|
791
|
+
state: "hi",
|
|
792
|
+
questions: { t: { type: "likelihood", instructions: "W?" } },
|
|
793
|
+
},
|
|
794
|
+
/unknown type "likelihood"/,
|
|
795
|
+
],
|
|
796
|
+
[
|
|
797
|
+
"ambiguous ranking alias",
|
|
798
|
+
{
|
|
799
|
+
state: "hi",
|
|
800
|
+
questions: {
|
|
801
|
+
t: { type: "ranking", instructions: "W?", criteria: ["a", "b"] },
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
/unknown type "ranking"/,
|
|
805
|
+
],
|
|
806
|
+
[
|
|
807
|
+
"missing instructions",
|
|
808
|
+
{
|
|
809
|
+
state: "hi",
|
|
810
|
+
questions: { t: { type: "choice", criteria: { a: null } } },
|
|
811
|
+
},
|
|
812
|
+
/question "t" needs "instructions"/,
|
|
813
|
+
],
|
|
814
|
+
[
|
|
815
|
+
"null instructions",
|
|
816
|
+
{
|
|
817
|
+
state: "hi",
|
|
818
|
+
questions: {
|
|
819
|
+
t: { type: "choice", instructions: null, criteria: { a: null } },
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
/question "t" needs "instructions"/,
|
|
823
|
+
],
|
|
824
|
+
[
|
|
825
|
+
"missing state",
|
|
826
|
+
{
|
|
827
|
+
questions: {
|
|
828
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
/state is required/,
|
|
832
|
+
],
|
|
833
|
+
["missing questions", { state: "hi" }, /questions is required/],
|
|
834
|
+
[
|
|
835
|
+
"empty questions object",
|
|
836
|
+
{ state: "hi", questions: {} },
|
|
837
|
+
/at least one question is required/,
|
|
838
|
+
],
|
|
839
|
+
[
|
|
840
|
+
"empty questions array",
|
|
841
|
+
{ state: "hi", questions: [] },
|
|
842
|
+
/at least one question is required/,
|
|
843
|
+
],
|
|
844
|
+
[
|
|
845
|
+
"scalar string questions",
|
|
846
|
+
{ state: "hi", questions: "foo" },
|
|
847
|
+
/"questions" must resolve to a question map/,
|
|
848
|
+
],
|
|
849
|
+
[
|
|
850
|
+
"scalar number questions",
|
|
851
|
+
{ state: "hi", questions: 42 },
|
|
852
|
+
/"questions" must resolve to a question map/,
|
|
853
|
+
],
|
|
854
|
+
[
|
|
855
|
+
"scalar boolean questions",
|
|
856
|
+
{ state: "hi", questions: true },
|
|
857
|
+
/"questions" must resolve to a question map/,
|
|
858
|
+
],
|
|
859
|
+
[
|
|
860
|
+
"null questions reads as missing",
|
|
861
|
+
{ state: "hi", questions: null },
|
|
862
|
+
/questions is required/,
|
|
863
|
+
],
|
|
864
|
+
[
|
|
865
|
+
"unknown field",
|
|
866
|
+
{
|
|
867
|
+
state: "hi",
|
|
868
|
+
questions: {
|
|
869
|
+
t: {
|
|
870
|
+
type: "choice",
|
|
871
|
+
instructions: "W?",
|
|
872
|
+
criteria: { a: null },
|
|
873
|
+
confidence: 0.9,
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
/question "t" has unknown field "confidence"/,
|
|
878
|
+
],
|
|
879
|
+
[
|
|
880
|
+
"noul criteria as array",
|
|
881
|
+
{
|
|
882
|
+
state: "hi",
|
|
883
|
+
questions: {
|
|
884
|
+
t: { type: "noul", instructions: "Is it?", criteria: ["y", "n"] },
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
/noul question "t" needs criteria omitted/,
|
|
888
|
+
],
|
|
889
|
+
[
|
|
890
|
+
"mixed valid and invalid questions",
|
|
891
|
+
{
|
|
892
|
+
state: "hi",
|
|
893
|
+
questions: {
|
|
894
|
+
ok: { type: "noul", instructions: "Is it?" },
|
|
895
|
+
bad: { type: "ranking", instructions: "Order them" },
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
/question "bad" has unknown type "ranking"/,
|
|
899
|
+
],
|
|
900
|
+
];
|
|
901
|
+
for (const [name, raw, expected] of rejected) {
|
|
902
|
+
it(`rejects ${name}`, async () => {
|
|
903
|
+
const { tool, calls } = stubTool();
|
|
904
|
+
await assert.rejects(
|
|
905
|
+
tool.execute(
|
|
906
|
+
`id-rej-${name}`,
|
|
907
|
+
raw as never,
|
|
908
|
+
undefined,
|
|
909
|
+
undefined,
|
|
910
|
+
{} as never,
|
|
911
|
+
),
|
|
912
|
+
expected,
|
|
913
|
+
);
|
|
914
|
+
assert.equal(calls(), 0);
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
describe("prototype-key safety", () => {
|
|
919
|
+
function captureTool() {
|
|
920
|
+
let calls = 0;
|
|
921
|
+
let seen: any;
|
|
922
|
+
const stub = {
|
|
923
|
+
id: "stub",
|
|
924
|
+
async evaluate(req: any) {
|
|
925
|
+
calls += 1;
|
|
926
|
+
seen = req;
|
|
927
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
928
|
+
},
|
|
929
|
+
};
|
|
930
|
+
return {
|
|
931
|
+
tool: buildSystemOneTool({ provider: stub as never }),
|
|
932
|
+
calls: () => calls,
|
|
933
|
+
seen: () => seen,
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
for (const tricky of ["__proto__", "constructor", "toString"]) {
|
|
938
|
+
it(`handles question id "${tricky}" as an ordinary entry`, async () => {
|
|
939
|
+
const { tool, calls, seen } = captureTool();
|
|
940
|
+
// Build via JSON so "__proto__" is an own key, as from a model.
|
|
941
|
+
const raw = JSON.parse(
|
|
942
|
+
JSON.stringify({
|
|
943
|
+
state: "hi",
|
|
944
|
+
questions: {
|
|
945
|
+
[tricky]: { type: "noul", instructions: "Is it?" },
|
|
946
|
+
},
|
|
947
|
+
}),
|
|
948
|
+
);
|
|
949
|
+
await tool.execute(
|
|
950
|
+
"id-proto",
|
|
951
|
+
raw as never,
|
|
952
|
+
undefined,
|
|
953
|
+
undefined,
|
|
954
|
+
{} as never,
|
|
955
|
+
);
|
|
956
|
+
assert.equal(calls(), 1);
|
|
957
|
+
assert.ok(Object.hasOwn(seen().questions, tricky));
|
|
958
|
+
assert.equal(Object.getPrototypeOf(seen().questions), Object.prototype);
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
it("keeps array-form duplicate-prone names prototype-safe", async () => {
|
|
963
|
+
const { tool, calls, seen } = captureTool();
|
|
964
|
+
const raw = JSON.parse(
|
|
965
|
+
JSON.stringify({
|
|
966
|
+
state: "hi",
|
|
967
|
+
questions: [
|
|
968
|
+
{ name: "__proto__", type: "noul", instructions: "Is it?" },
|
|
969
|
+
],
|
|
970
|
+
}),
|
|
971
|
+
);
|
|
972
|
+
await tool.execute(
|
|
973
|
+
"id-proto-arr",
|
|
974
|
+
raw as never,
|
|
975
|
+
undefined,
|
|
976
|
+
undefined,
|
|
977
|
+
{} as never,
|
|
978
|
+
);
|
|
979
|
+
assert.equal(calls(), 1);
|
|
980
|
+
assert.ok(Object.hasOwn(seen().questions, "__proto__"));
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
for (const label of ["__proto__", "constructor"]) {
|
|
984
|
+
it(`folds choice label "${label}" without reparenting criteria`, async () => {
|
|
985
|
+
const { tool, calls, seen } = captureTool();
|
|
986
|
+
await tool.execute(
|
|
987
|
+
"id-proto-label",
|
|
988
|
+
{
|
|
989
|
+
state: "hi",
|
|
990
|
+
questions: {
|
|
991
|
+
t: {
|
|
992
|
+
type: "choice",
|
|
993
|
+
instructions: "W?",
|
|
994
|
+
criteria: ["a", label],
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
} as never,
|
|
998
|
+
undefined,
|
|
999
|
+
undefined,
|
|
1000
|
+
{} as never,
|
|
1001
|
+
);
|
|
1002
|
+
assert.equal(calls(), 1);
|
|
1003
|
+
const criteria = seen().questions.t.criteria;
|
|
1004
|
+
assert.ok(Object.hasOwn(criteria, label));
|
|
1005
|
+
assert.equal(Object.keys(criteria).length, 2);
|
|
1006
|
+
assert.equal(Object.getPrototypeOf(criteria), Object.prototype);
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
it('never resolves "__proto__" or "constructor" as a type alias', async () => {
|
|
1011
|
+
for (const tricky of ["__proto__", "constructor"]) {
|
|
1012
|
+
const prepared = prepareSystemOneArgs({
|
|
1013
|
+
state: "hi",
|
|
1014
|
+
questions: { t: { type: tricky, instructions: "W?" } },
|
|
1015
|
+
}) as unknown as { questions: { t: { type: unknown } } };
|
|
1016
|
+
// Passes through untouched (a string), so execute rejects it
|
|
1017
|
+
// precisely instead of aliasing Object.prototype.
|
|
1018
|
+
assert.equal(prepared.questions.t.type, tricky);
|
|
1019
|
+
}
|
|
1020
|
+
const { tool, calls } = captureTool();
|
|
1021
|
+
await assert.rejects(
|
|
1022
|
+
tool.execute(
|
|
1023
|
+
"id-proto-type",
|
|
1024
|
+
{
|
|
1025
|
+
state: "hi",
|
|
1026
|
+
questions: { t: { type: "__proto__", instructions: "W?" } },
|
|
1027
|
+
} as never,
|
|
1028
|
+
undefined,
|
|
1029
|
+
undefined,
|
|
1030
|
+
{} as never,
|
|
1031
|
+
),
|
|
1032
|
+
/unknown type "__proto__"/,
|
|
1033
|
+
);
|
|
1034
|
+
assert.equal(calls(), 0);
|
|
1035
|
+
});
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
describe("tool advertising copy", () => {
|
|
1039
|
+
// Pi appends promptGuidelines flat with no tool-name prefix, so every
|
|
1040
|
+
// bullet must name the tool; and the copy must bid against direct
|
|
1041
|
+
// answering or routers skip the tool when it isn't named explicitly.
|
|
1042
|
+
it("names the tool in every guideline and bids against direct answers", () => {
|
|
1043
|
+
const tool = buildSystemOneTool({
|
|
1044
|
+
provider: new MockSystemOneProvider({ answers: {} }),
|
|
1045
|
+
});
|
|
1046
|
+
assert.ok(tool.promptSnippet && tool.promptSnippet.length > 0);
|
|
1047
|
+
for (const guideline of tool.promptGuidelines ?? []) {
|
|
1048
|
+
assert.match(guideline, /system_one/);
|
|
1049
|
+
}
|
|
1050
|
+
assert.match(tool.description, /instead of answering directly/);
|
|
1051
|
+
assert.match(tool.description, /calibrated probabilities/);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("keeps discovery and type selection mechanical in one layer", () => {
|
|
1055
|
+
const tool = buildSystemOneTool({
|
|
1056
|
+
provider: new MockSystemOneProvider({ answers: {} }),
|
|
1057
|
+
});
|
|
1058
|
+
// Concept-level assertions only: the exact prose may evolve, but
|
|
1059
|
+
// promptGuidelines must remain the authoritative layer teaching the
|
|
1060
|
+
// router when to call and which type to pick.
|
|
1061
|
+
const guidelines = (tool.promptGuidelines ?? []).join("\n");
|
|
1062
|
+
assert.match(guidelines, /bounded judgment.*system_one/i);
|
|
1063
|
+
assert.match(guidelines, /unordered alternatives.*choice/i);
|
|
1064
|
+
assert.match(guidelines, /yes\/no.*noul/i);
|
|
1065
|
+
assert.match(guidelines, /ordered scale.*score/i);
|
|
1066
|
+
assert.match(guidelines, /never use choice for an ordered scale/i);
|
|
1067
|
+
assert.match(guidelines, /batch.*one call/i);
|
|
1068
|
+
assert.match(guidelines, /retrieve.*first/i);
|
|
1069
|
+
// The snippet advertises the capability; it must not duplicate the
|
|
1070
|
+
// full decision algorithm.
|
|
1071
|
+
assert.ok(
|
|
1072
|
+
(tool.promptSnippet ?? "").length < guidelines.length / 4,
|
|
1073
|
+
"promptSnippet should stay a short advertisement, not a second copy of the rules",
|
|
1074
|
+
);
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
describe("backend minima guardrails", () => {
|
|
1079
|
+
function stubTool() {
|
|
1080
|
+
let calls = 0;
|
|
1081
|
+
const stub = {
|
|
1082
|
+
id: "stub",
|
|
1083
|
+
async evaluate(_req: unknown) {
|
|
1084
|
+
calls += 1;
|
|
1085
|
+
return {
|
|
1086
|
+
answers: {
|
|
1087
|
+
t: {
|
|
1088
|
+
type: "choice",
|
|
1089
|
+
choice: "a",
|
|
1090
|
+
probabilities: { a: 1 },
|
|
1091
|
+
confidence: 1,
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
metadata: { provider: "stub" },
|
|
1095
|
+
};
|
|
1096
|
+
},
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
tool: buildSystemOneTool({ provider: stub as never }),
|
|
1100
|
+
calls: () => calls,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
it("rejects choice questions without criteria before hitting the backend", async () => {
|
|
1105
|
+
const { tool, calls } = stubTool();
|
|
1106
|
+
await assert.rejects(
|
|
1107
|
+
tool.execute(
|
|
1108
|
+
"id-min-1",
|
|
1109
|
+
{
|
|
1110
|
+
state: "hi",
|
|
1111
|
+
questions: { t: { type: "choice", instructions: "W?" } },
|
|
1112
|
+
} as never,
|
|
1113
|
+
undefined,
|
|
1114
|
+
undefined,
|
|
1115
|
+
{} as never,
|
|
1116
|
+
),
|
|
1117
|
+
/choice question "t" needs criteria.*at least one choice/,
|
|
1118
|
+
);
|
|
1119
|
+
assert.equal(calls(), 0);
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
it("rejects choice questions with empty criteria", async () => {
|
|
1123
|
+
const { tool, calls } = stubTool();
|
|
1124
|
+
await assert.rejects(
|
|
1125
|
+
tool.execute(
|
|
1126
|
+
"id-min-2",
|
|
1127
|
+
{
|
|
1128
|
+
state: "hi",
|
|
1129
|
+
questions: {
|
|
1130
|
+
t: { type: "choice", instructions: "W?", criteria: {} },
|
|
1131
|
+
},
|
|
1132
|
+
} as never,
|
|
1133
|
+
undefined,
|
|
1134
|
+
undefined,
|
|
1135
|
+
{} as never,
|
|
1136
|
+
),
|
|
1137
|
+
/at least one choice/,
|
|
1138
|
+
);
|
|
1139
|
+
assert.equal(calls(), 0);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it("rejects score questions with fewer than two levels", async () => {
|
|
1143
|
+
const { tool, calls } = stubTool();
|
|
1144
|
+
await assert.rejects(
|
|
1145
|
+
tool.execute(
|
|
1146
|
+
"id-min-3",
|
|
1147
|
+
{
|
|
1148
|
+
state: "hi",
|
|
1149
|
+
questions: {
|
|
1150
|
+
s: { type: "score", instructions: "How?", criteria: ["only"] },
|
|
1151
|
+
},
|
|
1152
|
+
} as never,
|
|
1153
|
+
undefined,
|
|
1154
|
+
undefined,
|
|
1155
|
+
{} as never,
|
|
1156
|
+
),
|
|
1157
|
+
/score question "s" needs criteria.*at least two levels/,
|
|
1158
|
+
);
|
|
1159
|
+
assert.equal(calls(), 0);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("rejects unknown question types with the valid set", async () => {
|
|
1163
|
+
const { tool, calls } = stubTool();
|
|
1164
|
+
await assert.rejects(
|
|
1165
|
+
tool.execute(
|
|
1166
|
+
"id-min-4",
|
|
1167
|
+
{
|
|
1168
|
+
state: "hi",
|
|
1169
|
+
questions: { t: { type: "essay", instructions: "W?" } },
|
|
1170
|
+
} as never,
|
|
1171
|
+
undefined,
|
|
1172
|
+
undefined,
|
|
1173
|
+
{} as never,
|
|
1174
|
+
),
|
|
1175
|
+
/unknown type "essay".*use "choice", "noul", or "score"/,
|
|
1176
|
+
);
|
|
1177
|
+
assert.equal(calls(), 0);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
it("rejects non-object questions", async () => {
|
|
1181
|
+
const { tool, calls } = stubTool();
|
|
1182
|
+
await assert.rejects(
|
|
1183
|
+
tool.execute(
|
|
1184
|
+
"id-min-5",
|
|
1185
|
+
{ state: "hi", questions: { t: "just a string" } } as never,
|
|
1186
|
+
undefined,
|
|
1187
|
+
undefined,
|
|
1188
|
+
{} as never,
|
|
1189
|
+
),
|
|
1190
|
+
/question "t" must be an object/,
|
|
1191
|
+
);
|
|
1192
|
+
assert.equal(calls(), 0);
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
it("rejects empty questions", async () => {
|
|
1196
|
+
const { tool, calls } = stubTool();
|
|
1197
|
+
await assert.rejects(
|
|
1198
|
+
tool.execute(
|
|
1199
|
+
"id-min-6",
|
|
1200
|
+
{ state: "hi", questions: {} } as never,
|
|
1201
|
+
undefined,
|
|
1202
|
+
undefined,
|
|
1203
|
+
{} as never,
|
|
1204
|
+
),
|
|
1205
|
+
/at least one question is required/,
|
|
1206
|
+
);
|
|
1207
|
+
assert.equal(calls(), 0);
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
it("rejects non-object noul criteria", async () => {
|
|
1211
|
+
const { tool, calls } = stubTool();
|
|
1212
|
+
await assert.rejects(
|
|
1213
|
+
tool.execute(
|
|
1214
|
+
"id-min-7",
|
|
1215
|
+
{
|
|
1216
|
+
state: "hi",
|
|
1217
|
+
questions: {
|
|
1218
|
+
n: { type: "noul", instructions: "Is it?", criteria: ["x"] },
|
|
1219
|
+
},
|
|
1220
|
+
} as never,
|
|
1221
|
+
undefined,
|
|
1222
|
+
undefined,
|
|
1223
|
+
{} as never,
|
|
1224
|
+
),
|
|
1225
|
+
/noul question "n" needs criteria omitted or as an object/,
|
|
1226
|
+
);
|
|
1227
|
+
assert.equal(calls(), 0);
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
it("rejects score questions with object criteria", async () => {
|
|
1231
|
+
const { tool, calls } = stubTool();
|
|
1232
|
+
await assert.rejects(
|
|
1233
|
+
tool.execute(
|
|
1234
|
+
"id-min-8",
|
|
1235
|
+
{
|
|
1236
|
+
state: "hi",
|
|
1237
|
+
questions: {
|
|
1238
|
+
s: {
|
|
1239
|
+
type: "score",
|
|
1240
|
+
instructions: "How?",
|
|
1241
|
+
criteria: { low: "bad", high: "good" },
|
|
1242
|
+
},
|
|
1243
|
+
},
|
|
1244
|
+
} as never,
|
|
1245
|
+
undefined,
|
|
1246
|
+
undefined,
|
|
1247
|
+
{} as never,
|
|
1248
|
+
),
|
|
1249
|
+
/score question "s" needs criteria as an ordered array/,
|
|
1250
|
+
);
|
|
1251
|
+
assert.equal(calls(), 0);
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
it("rejects choice questions with non-string array criteria", async () => {
|
|
1255
|
+
const { tool, calls } = stubTool();
|
|
1256
|
+
await assert.rejects(
|
|
1257
|
+
tool.execute(
|
|
1258
|
+
"id-min-9",
|
|
1259
|
+
{
|
|
1260
|
+
state: "hi",
|
|
1261
|
+
questions: {
|
|
1262
|
+
t: { type: "choice", instructions: "W?", criteria: [1, 2] },
|
|
1263
|
+
},
|
|
1264
|
+
} as never,
|
|
1265
|
+
undefined,
|
|
1266
|
+
undefined,
|
|
1267
|
+
{} as never,
|
|
1268
|
+
),
|
|
1269
|
+
/choice question "t" needs criteria as an object/,
|
|
1270
|
+
);
|
|
1271
|
+
assert.equal(calls(), 0);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
it("lets boundary-valid questions through (1 choice, 2 levels)", async () => {
|
|
1275
|
+
const { tool, calls } = stubTool();
|
|
1276
|
+
await tool.execute(
|
|
1277
|
+
"id-min-5",
|
|
1278
|
+
{
|
|
1279
|
+
state: "hi",
|
|
1280
|
+
questions: {
|
|
1281
|
+
c: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
1282
|
+
s: {
|
|
1283
|
+
type: "score",
|
|
1284
|
+
instructions: "How?",
|
|
1285
|
+
criteria: ["low", "high"],
|
|
1286
|
+
},
|
|
1287
|
+
n: { type: "noul", instructions: "Is it?" },
|
|
1288
|
+
},
|
|
1289
|
+
} as never,
|
|
1290
|
+
undefined,
|
|
1291
|
+
undefined,
|
|
1292
|
+
{} as never,
|
|
1293
|
+
);
|
|
1294
|
+
assert.equal(calls(), 1);
|
|
1295
|
+
});
|
|
123
1296
|
});
|
|
124
1297
|
});
|