pi-system-one 1.2.1 → 1.2.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/README.md +4 -1
- package/demo.gif +0 -0
- package/package.json +1 -1
- package/skills/system-one/SKILL.md +147 -9
- package/src/render.ts +70 -5
- package/src/tool.ts +620 -60
- package/tests/properties.test.ts +565 -0
- package/tests/render.test.ts +47 -0
- package/tests/tool.test.ts +1015 -53
package/tests/tool.test.ts
CHANGED
|
@@ -11,6 +11,25 @@ import {
|
|
|
11
11
|
} from "../src/tool.ts";
|
|
12
12
|
|
|
13
13
|
describe("system_one tool", () => {
|
|
14
|
+
/**
|
|
15
|
+
* A tool whose provider records the request it was handed, so a test can
|
|
16
|
+
* assert the exact shape that reached the wire after normalization.
|
|
17
|
+
*/
|
|
18
|
+
function recordingTool() {
|
|
19
|
+
let captured: any = null;
|
|
20
|
+
const stub = {
|
|
21
|
+
id: "stub",
|
|
22
|
+
async evaluate(request: any) {
|
|
23
|
+
captured = request;
|
|
24
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
tool: buildSystemOneTool({ provider: stub as never }),
|
|
29
|
+
request: () => captured,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
14
33
|
it("delegates a mixed batch and preserves details", async () => {
|
|
15
34
|
const tool = buildSystemOneTool({
|
|
16
35
|
provider: new MockSystemOneProvider({
|
|
@@ -303,6 +322,66 @@ describe("system_one tool", () => {
|
|
|
303
322
|
});
|
|
304
323
|
}
|
|
305
324
|
|
|
325
|
+
it("decodes questions serialized as a JSON string (provider quirk)", () => {
|
|
326
|
+
const raw = {
|
|
327
|
+
state: "hi",
|
|
328
|
+
questions: JSON.stringify({
|
|
329
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
330
|
+
}),
|
|
331
|
+
};
|
|
332
|
+
const prepared = prepareSystemOneArgs(raw);
|
|
333
|
+
assert.deepEqual(prepared, {
|
|
334
|
+
state: "hi",
|
|
335
|
+
questions: {
|
|
336
|
+
t: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("decodes a question serialized as a JSON string inside an array", () => {
|
|
342
|
+
const q = { type: "choice", instructions: "W?", criteria: { a: null } };
|
|
343
|
+
// Every entry stringified, and the whole argument stringified too:
|
|
344
|
+
// both layers decode before normalization.
|
|
345
|
+
for (const questions of [[JSON.stringify(q)], JSON.stringify([q])]) {
|
|
346
|
+
assert.deepEqual(prepareSystemOneArgs({ state: "hi", questions }), {
|
|
347
|
+
state: "hi",
|
|
348
|
+
questions: { q1: q },
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("still rejects strings that are not JSON objects or arrays", async () => {
|
|
354
|
+
const tool = buildSystemOneTool({
|
|
355
|
+
provider: {
|
|
356
|
+
id: "stub",
|
|
357
|
+
async evaluate() {
|
|
358
|
+
return { answers: {}, metadata: { provider: "stub" } };
|
|
359
|
+
},
|
|
360
|
+
} as never,
|
|
361
|
+
});
|
|
362
|
+
for (const questions of [
|
|
363
|
+
"foo",
|
|
364
|
+
"{not json",
|
|
365
|
+
"{{{{",
|
|
366
|
+
"[[[",
|
|
367
|
+
"123",
|
|
368
|
+
"true",
|
|
369
|
+
"null",
|
|
370
|
+
'"hello"',
|
|
371
|
+
]) {
|
|
372
|
+
await assert.rejects(
|
|
373
|
+
tool.execute(
|
|
374
|
+
"id-rej-string",
|
|
375
|
+
{ state: "hi", questions } as never,
|
|
376
|
+
undefined,
|
|
377
|
+
undefined,
|
|
378
|
+
{} as never,
|
|
379
|
+
),
|
|
380
|
+
/"questions" must resolve to a question map/,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
306
385
|
it("folds options array to null-valued criteria record", () => {
|
|
307
386
|
assert.deepEqual(
|
|
308
387
|
prepareSystemOneArgs({
|
|
@@ -348,7 +427,7 @@ describe("system_one tool", () => {
|
|
|
348
427
|
);
|
|
349
428
|
});
|
|
350
429
|
|
|
351
|
-
it("unwraps single
|
|
430
|
+
it("unwraps a single question and drops its extra fields", async () => {
|
|
352
431
|
const prepared = prepareSystemOneArgs({
|
|
353
432
|
state: "hi",
|
|
354
433
|
questions: {
|
|
@@ -365,12 +444,12 @@ describe("system_one tool", () => {
|
|
|
365
444
|
type: "choice",
|
|
366
445
|
instructions: "W?",
|
|
367
446
|
criteria: { a: null },
|
|
368
|
-
foo: "bar",
|
|
369
447
|
},
|
|
370
448
|
},
|
|
371
449
|
});
|
|
372
450
|
// The permissive boundary schema lets this through (older Pi must be
|
|
373
|
-
// able to reach execute)
|
|
451
|
+
// able to reach execute), and the extra field is dropped rather than
|
|
452
|
+
// turned into a retry the model has to learn from.
|
|
374
453
|
assert.equal(Value.Check(systemOneParams, prepared), true);
|
|
375
454
|
let calls = 0;
|
|
376
455
|
const tool = buildSystemOneTool({
|
|
@@ -382,17 +461,14 @@ describe("system_one tool", () => {
|
|
|
382
461
|
},
|
|
383
462
|
} as never,
|
|
384
463
|
});
|
|
385
|
-
await
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
{} as never,
|
|
392
|
-
),
|
|
393
|
-
/question "q" has unknown field "foo"/,
|
|
464
|
+
await tool.execute(
|
|
465
|
+
"id-foo",
|
|
466
|
+
prepared as never,
|
|
467
|
+
undefined,
|
|
468
|
+
undefined,
|
|
469
|
+
{} as never,
|
|
394
470
|
);
|
|
395
|
-
assert.equal(calls,
|
|
471
|
+
assert.equal(calls, 1);
|
|
396
472
|
});
|
|
397
473
|
|
|
398
474
|
it("lets unknown types through the schema so execute rejects them clearly", async () => {
|
|
@@ -861,21 +937,6 @@ describe("system_one tool", () => {
|
|
|
861
937
|
{ state: "hi", questions: null },
|
|
862
938
|
/questions is required/,
|
|
863
939
|
],
|
|
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
940
|
[
|
|
880
941
|
"noul criteria as array",
|
|
881
942
|
{
|
|
@@ -1044,11 +1105,6 @@ describe("system_one tool", () => {
|
|
|
1044
1105
|
provider: new MockSystemOneProvider({ answers: {} }),
|
|
1045
1106
|
});
|
|
1046
1107
|
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
1108
|
});
|
|
1053
1109
|
|
|
1054
1110
|
it("keeps discovery and type selection mechanical in one layer", () => {
|
|
@@ -1167,7 +1223,7 @@ describe("system_one tool", () => {
|
|
|
1167
1223
|
instructions: "Rate?",
|
|
1168
1224
|
criteria: ["1", "5"],
|
|
1169
1225
|
},
|
|
1170
|
-
b: { type: "noul", instructions: "OK?"
|
|
1226
|
+
b: { type: "noul", instructions: "OK?" },
|
|
1171
1227
|
},
|
|
1172
1228
|
});
|
|
1173
1229
|
});
|
|
@@ -1466,18 +1522,92 @@ describe("system_one tool", () => {
|
|
|
1466
1522
|
assert.equal(calls(), 0);
|
|
1467
1523
|
});
|
|
1468
1524
|
|
|
1469
|
-
it("
|
|
1525
|
+
it("folds object score criteria into an ordered rubric", async () => {
|
|
1526
|
+
const { tool, request } = recordingTool();
|
|
1527
|
+
await tool.execute(
|
|
1528
|
+
"id-fold-score",
|
|
1529
|
+
{
|
|
1530
|
+
state: "hi",
|
|
1531
|
+
questions: {
|
|
1532
|
+
s: {
|
|
1533
|
+
type: "score",
|
|
1534
|
+
instructions: "How?",
|
|
1535
|
+
criteria: { low: "bad", high: "good" },
|
|
1536
|
+
},
|
|
1537
|
+
},
|
|
1538
|
+
} as never,
|
|
1539
|
+
undefined,
|
|
1540
|
+
undefined,
|
|
1541
|
+
{} as never,
|
|
1542
|
+
);
|
|
1543
|
+
assert.deepEqual(request().questions.s.criteria, [
|
|
1544
|
+
"low: bad",
|
|
1545
|
+
"high: good",
|
|
1546
|
+
]);
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
it("keeps a score level's label and description without duplicating", async () => {
|
|
1550
|
+
const { tool, request } = recordingTool();
|
|
1551
|
+
await tool.execute(
|
|
1552
|
+
"id-level-self-describing",
|
|
1553
|
+
{
|
|
1554
|
+
state: "hi",
|
|
1555
|
+
questions: {
|
|
1556
|
+
s: {
|
|
1557
|
+
type: "score",
|
|
1558
|
+
instructions: "How bad?",
|
|
1559
|
+
criteria: {
|
|
1560
|
+
low: "Bad: minor and reversible",
|
|
1561
|
+
high: "",
|
|
1562
|
+
moderate: "moderate",
|
|
1563
|
+
},
|
|
1564
|
+
},
|
|
1565
|
+
},
|
|
1566
|
+
} as never,
|
|
1567
|
+
undefined,
|
|
1568
|
+
undefined,
|
|
1569
|
+
{} as never,
|
|
1570
|
+
);
|
|
1571
|
+
assert.deepEqual(request().questions.s.criteria, [
|
|
1572
|
+
"low: Bad: minor and reversible",
|
|
1573
|
+
"high",
|
|
1574
|
+
"moderate",
|
|
1575
|
+
]);
|
|
1576
|
+
});
|
|
1577
|
+
|
|
1578
|
+
it("unwraps a single wrapper key holding the rubric", async () => {
|
|
1579
|
+
const { tool, request } = recordingTool();
|
|
1580
|
+
await tool.execute(
|
|
1581
|
+
"id-fold-score-wrapper",
|
|
1582
|
+
{
|
|
1583
|
+
state: "hi",
|
|
1584
|
+
questions: {
|
|
1585
|
+
s: {
|
|
1586
|
+
type: "score",
|
|
1587
|
+
instructions: "How?",
|
|
1588
|
+
criteria: { item: ["low", "high"] },
|
|
1589
|
+
},
|
|
1590
|
+
},
|
|
1591
|
+
} as never,
|
|
1592
|
+
undefined,
|
|
1593
|
+
undefined,
|
|
1594
|
+
{} as never,
|
|
1595
|
+
);
|
|
1596
|
+
assert.deepEqual(request().questions.s.criteria, ["low", "high"]);
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
it("rejects a one-level object rubric rather than unwrapping it", async () => {
|
|
1470
1600
|
const { tool, calls } = stubTool();
|
|
1471
1601
|
await assert.rejects(
|
|
1472
1602
|
tool.execute(
|
|
1473
|
-
"id-
|
|
1603
|
+
"id-fold-score-single",
|
|
1474
1604
|
{
|
|
1475
1605
|
state: "hi",
|
|
1476
1606
|
questions: {
|
|
1477
1607
|
s: {
|
|
1478
1608
|
type: "score",
|
|
1479
1609
|
instructions: "How?",
|
|
1480
|
-
criteria: {
|
|
1610
|
+
criteria: { only: "meh" },
|
|
1481
1611
|
},
|
|
1482
1612
|
},
|
|
1483
1613
|
} as never,
|
|
@@ -1490,40 +1620,803 @@ describe("system_one tool", () => {
|
|
|
1490
1620
|
assert.equal(calls(), 0);
|
|
1491
1621
|
});
|
|
1492
1622
|
|
|
1493
|
-
it("
|
|
1623
|
+
it("lifts bare noul outcomes into criteria", async () => {
|
|
1624
|
+
const { tool, request } = recordingTool();
|
|
1625
|
+
await tool.execute(
|
|
1626
|
+
"id-lift-noul",
|
|
1627
|
+
{
|
|
1628
|
+
state: "hi",
|
|
1629
|
+
questions: {
|
|
1630
|
+
n: {
|
|
1631
|
+
type: "noul",
|
|
1632
|
+
instructions: "Is it?",
|
|
1633
|
+
true: "it is",
|
|
1634
|
+
false: "it is not",
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
} as never,
|
|
1638
|
+
undefined,
|
|
1639
|
+
undefined,
|
|
1640
|
+
{} as never,
|
|
1641
|
+
);
|
|
1642
|
+
assert.deepEqual(request().questions.n, {
|
|
1643
|
+
type: "noul",
|
|
1644
|
+
instructions: "Is it?",
|
|
1645
|
+
criteria: { true: "it is", false: "it is not" },
|
|
1646
|
+
});
|
|
1647
|
+
});
|
|
1648
|
+
|
|
1649
|
+
it("merges choice labels spilled out of criteria", async () => {
|
|
1650
|
+
const { tool, request } = recordingTool();
|
|
1651
|
+
await tool.execute(
|
|
1652
|
+
"id-spilled-choice",
|
|
1653
|
+
{
|
|
1654
|
+
state: "hi",
|
|
1655
|
+
questions: {
|
|
1656
|
+
ownership: {
|
|
1657
|
+
type: "choice",
|
|
1658
|
+
instructions: "Who?",
|
|
1659
|
+
criteria: { sre_oncall: "" },
|
|
1660
|
+
account_team: "",
|
|
1661
|
+
ingest_service_owner: "",
|
|
1662
|
+
},
|
|
1663
|
+
},
|
|
1664
|
+
} as never,
|
|
1665
|
+
undefined,
|
|
1666
|
+
undefined,
|
|
1667
|
+
{} as never,
|
|
1668
|
+
);
|
|
1669
|
+
assert.deepEqual(Object.keys(request().questions.ownership.criteria), [
|
|
1670
|
+
"sre_oncall",
|
|
1671
|
+
"account_team",
|
|
1672
|
+
"ingest_service_owner",
|
|
1673
|
+
]);
|
|
1674
|
+
});
|
|
1675
|
+
|
|
1676
|
+
it("uses spilled labels as criteria when criteria is absent", async () => {
|
|
1677
|
+
const { tool, request } = recordingTool();
|
|
1678
|
+
await tool.execute(
|
|
1679
|
+
"id-spilled-only",
|
|
1680
|
+
{
|
|
1681
|
+
state: "hi",
|
|
1682
|
+
questions: {
|
|
1683
|
+
pick: {
|
|
1684
|
+
type: "choice",
|
|
1685
|
+
instructions: "Which?",
|
|
1686
|
+
billing: null,
|
|
1687
|
+
onboarding: null,
|
|
1688
|
+
},
|
|
1689
|
+
},
|
|
1690
|
+
} as never,
|
|
1691
|
+
undefined,
|
|
1692
|
+
undefined,
|
|
1693
|
+
{} as never,
|
|
1694
|
+
);
|
|
1695
|
+
assert.deepEqual(request().questions.pick.criteria, {
|
|
1696
|
+
billing: null,
|
|
1697
|
+
onboarding: null,
|
|
1698
|
+
});
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
it("drops an empty sibling that is a noul question's own name", async () => {
|
|
1702
|
+
const { tool, request } = recordingTool();
|
|
1703
|
+
await tool.execute(
|
|
1704
|
+
"id-noul-selfname",
|
|
1705
|
+
{
|
|
1706
|
+
state: "hi",
|
|
1707
|
+
questions: {
|
|
1708
|
+
q: {
|
|
1709
|
+
type: "noul",
|
|
1710
|
+
instructions: "Is it?",
|
|
1711
|
+
is_coordinated_credential_stuffing_run: "",
|
|
1712
|
+
},
|
|
1713
|
+
},
|
|
1714
|
+
} as never,
|
|
1715
|
+
undefined,
|
|
1716
|
+
undefined,
|
|
1717
|
+
{} as never,
|
|
1718
|
+
);
|
|
1719
|
+
assert.deepEqual(request().questions.q, {
|
|
1720
|
+
type: "noul",
|
|
1721
|
+
instructions: "Is it?",
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1725
|
+
it("drops a NON-empty extra field rather than failing the call", async () => {
|
|
1726
|
+
const { tool, request } = recordingTool();
|
|
1727
|
+
await tool.execute(
|
|
1728
|
+
"id-nonempty-extra",
|
|
1729
|
+
{
|
|
1730
|
+
state: "hi",
|
|
1731
|
+
questions: {
|
|
1732
|
+
q: {
|
|
1733
|
+
type: "noul",
|
|
1734
|
+
instructions: "Is it?",
|
|
1735
|
+
confidence: 0.9,
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
} as never,
|
|
1739
|
+
undefined,
|
|
1740
|
+
undefined,
|
|
1741
|
+
{} as never,
|
|
1742
|
+
);
|
|
1743
|
+
assert.deepEqual(request().questions.q, {
|
|
1744
|
+
type: "noul",
|
|
1745
|
+
instructions: "Is it?",
|
|
1746
|
+
});
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1749
|
+
it("salvages the type from an extra key that names the kind", async () => {
|
|
1750
|
+
const { tool, request } = recordingTool();
|
|
1751
|
+
await tool.execute(
|
|
1752
|
+
"id-salvage-type",
|
|
1753
|
+
{
|
|
1754
|
+
state: "hi",
|
|
1755
|
+
questions: {
|
|
1756
|
+
q: {
|
|
1757
|
+
approach: "yes_no",
|
|
1758
|
+
name: "finishes_in_window",
|
|
1759
|
+
instructions: "Will it fit?",
|
|
1760
|
+
},
|
|
1761
|
+
},
|
|
1762
|
+
} as never,
|
|
1763
|
+
undefined,
|
|
1764
|
+
undefined,
|
|
1765
|
+
{} as never,
|
|
1766
|
+
);
|
|
1767
|
+
assert.deepEqual(request().questions.q, {
|
|
1768
|
+
type: "noul",
|
|
1769
|
+
instructions: "Will it fit?",
|
|
1770
|
+
});
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
it("unwraps a single-key wrapper around an array of questions", async () => {
|
|
1774
|
+
const { tool, request } = recordingTool();
|
|
1775
|
+
await tool.execute(
|
|
1776
|
+
"id-wrapped-array",
|
|
1777
|
+
{
|
|
1778
|
+
state: "hi",
|
|
1779
|
+
questions: {
|
|
1780
|
+
item: [
|
|
1781
|
+
{ name: "plan_it", type: "noul", instructions: "Should we?" },
|
|
1782
|
+
{
|
|
1783
|
+
name: "how_soon",
|
|
1784
|
+
type: "score",
|
|
1785
|
+
instructions: "How soon?",
|
|
1786
|
+
criteria: ["now", "later"],
|
|
1787
|
+
},
|
|
1788
|
+
],
|
|
1789
|
+
},
|
|
1790
|
+
} as never,
|
|
1791
|
+
undefined,
|
|
1792
|
+
undefined,
|
|
1793
|
+
{} as never,
|
|
1794
|
+
);
|
|
1795
|
+
assert.deepEqual(Object.keys(request().questions), [
|
|
1796
|
+
"plan_it",
|
|
1797
|
+
"how_soon",
|
|
1798
|
+
]);
|
|
1799
|
+
});
|
|
1800
|
+
|
|
1801
|
+
it("drops string entries beside a real question", async () => {
|
|
1802
|
+
const { tool, request } = recordingTool();
|
|
1803
|
+
await tool.execute(
|
|
1804
|
+
"id-string-entry",
|
|
1805
|
+
{
|
|
1806
|
+
state: "hi",
|
|
1807
|
+
questions: {
|
|
1808
|
+
refund_decision: {
|
|
1809
|
+
type: "choice",
|
|
1810
|
+
instructions: "Which?",
|
|
1811
|
+
criteria: { deny: null },
|
|
1812
|
+
},
|
|
1813
|
+
deny: "refund it",
|
|
1814
|
+
"PR #419 (feat: add metrics endpoint)": "merge it",
|
|
1815
|
+
},
|
|
1816
|
+
} as never,
|
|
1817
|
+
undefined,
|
|
1818
|
+
undefined,
|
|
1819
|
+
{} as never,
|
|
1820
|
+
);
|
|
1821
|
+
assert.deepEqual(Object.keys(request().questions), ["refund_decision"]);
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
it("still rejects a question map of nothing but strings", async () => {
|
|
1494
1825
|
const { tool, calls } = stubTool();
|
|
1495
1826
|
await assert.rejects(
|
|
1496
1827
|
tool.execute(
|
|
1497
|
-
"id-
|
|
1828
|
+
"id-all-strings",
|
|
1498
1829
|
{
|
|
1499
1830
|
state: "hi",
|
|
1500
|
-
questions: {
|
|
1501
|
-
t: { type: "choice", instructions: "W?", criteria: [1, 2] },
|
|
1502
|
-
},
|
|
1831
|
+
questions: { deny: "refund it", allow: "let it go" },
|
|
1503
1832
|
} as never,
|
|
1504
1833
|
undefined,
|
|
1505
1834
|
undefined,
|
|
1506
1835
|
{} as never,
|
|
1507
1836
|
),
|
|
1508
|
-
/
|
|
1837
|
+
/must be an object with "type"/,
|
|
1509
1838
|
);
|
|
1510
1839
|
assert.equal(calls(), 0);
|
|
1511
1840
|
});
|
|
1512
1841
|
|
|
1513
|
-
it("
|
|
1514
|
-
const { tool,
|
|
1842
|
+
it("merges a name-to-array question map", async () => {
|
|
1843
|
+
const { tool, request } = recordingTool();
|
|
1515
1844
|
await tool.execute(
|
|
1516
|
-
"id-
|
|
1845
|
+
"id-array-map",
|
|
1517
1846
|
{
|
|
1518
1847
|
state: "hi",
|
|
1519
1848
|
questions: {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
type: "
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1849
|
+
fixes: [
|
|
1850
|
+
{ type: "noul", instructions: "Is it safe?" },
|
|
1851
|
+
{ type: "noul", instructions: "Is it cheap?" },
|
|
1852
|
+
],
|
|
1853
|
+
risks: [
|
|
1854
|
+
{
|
|
1855
|
+
type: "score",
|
|
1856
|
+
instructions: "How bad?",
|
|
1857
|
+
criteria: ["low", "high"],
|
|
1858
|
+
},
|
|
1859
|
+
],
|
|
1860
|
+
},
|
|
1861
|
+
} as never,
|
|
1862
|
+
undefined,
|
|
1863
|
+
undefined,
|
|
1864
|
+
{} as never,
|
|
1865
|
+
);
|
|
1866
|
+
assert.deepEqual(Object.keys(request().questions), [
|
|
1867
|
+
"fixes",
|
|
1868
|
+
"fixes_2",
|
|
1869
|
+
"risks",
|
|
1870
|
+
]);
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
it("unwraps one question filed under the wrapper's own name", async () => {
|
|
1874
|
+
const { tool, request } = recordingTool();
|
|
1875
|
+
await tool.execute(
|
|
1876
|
+
"id-named-questions",
|
|
1877
|
+
{
|
|
1878
|
+
state: "hi",
|
|
1879
|
+
questions: {
|
|
1880
|
+
questions: {
|
|
1881
|
+
type: "score",
|
|
1882
|
+
instructions: "How clear?",
|
|
1883
|
+
criteria: ["low", "high"],
|
|
1884
|
+
},
|
|
1885
|
+
},
|
|
1886
|
+
} as never,
|
|
1887
|
+
undefined,
|
|
1888
|
+
undefined,
|
|
1889
|
+
{} as never,
|
|
1890
|
+
);
|
|
1891
|
+
assert.deepEqual(Object.keys(request().questions), ["q"]);
|
|
1892
|
+
assert.equal(request().questions.q.type, "score");
|
|
1893
|
+
});
|
|
1894
|
+
|
|
1895
|
+
it("peels a wrapper nested more than one level deep", async () => {
|
|
1896
|
+
const { tool, request } = recordingTool();
|
|
1897
|
+
await tool.execute(
|
|
1898
|
+
"id-nested-wrapper",
|
|
1899
|
+
{
|
|
1900
|
+
state: "hi",
|
|
1901
|
+
questions: {
|
|
1902
|
+
impact: {
|
|
1903
|
+
type: "score",
|
|
1904
|
+
instructions: "How bad?",
|
|
1905
|
+
criteria: {
|
|
1906
|
+
item: { item: ["negligible", "moderate", "severe"] },
|
|
1907
|
+
},
|
|
1908
|
+
},
|
|
1909
|
+
},
|
|
1910
|
+
} as never,
|
|
1911
|
+
undefined,
|
|
1912
|
+
undefined,
|
|
1913
|
+
{} as never,
|
|
1914
|
+
);
|
|
1915
|
+
assert.deepEqual(request().questions.impact.criteria, [
|
|
1916
|
+
"negligible",
|
|
1917
|
+
"moderate",
|
|
1918
|
+
"severe",
|
|
1919
|
+
]);
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
it("still rejects a one-level rubric nested in a wrapper", async () => {
|
|
1923
|
+
const { tool, calls } = stubTool();
|
|
1924
|
+
await assert.rejects(
|
|
1925
|
+
tool.execute(
|
|
1926
|
+
"id-nested-one-level",
|
|
1927
|
+
{
|
|
1928
|
+
state: "hi",
|
|
1929
|
+
questions: {
|
|
1930
|
+
impact: {
|
|
1931
|
+
type: "score",
|
|
1932
|
+
instructions: "How bad?",
|
|
1933
|
+
criteria: { item: { only: "meh" } },
|
|
1934
|
+
},
|
|
1935
|
+
},
|
|
1936
|
+
} as never,
|
|
1937
|
+
undefined,
|
|
1938
|
+
undefined,
|
|
1939
|
+
{} as never,
|
|
1940
|
+
),
|
|
1941
|
+
/score question "impact" needs criteria as an ordered array/,
|
|
1942
|
+
);
|
|
1943
|
+
assert.equal(calls(), 0);
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
it("transposes parallel arrays into named questions", async () => {
|
|
1947
|
+
const { tool, request } = recordingTool();
|
|
1948
|
+
await tool.execute(
|
|
1949
|
+
"id-parallel-arrays",
|
|
1950
|
+
{
|
|
1951
|
+
state: "hi",
|
|
1952
|
+
questions: {
|
|
1953
|
+
item: ["refund", "risk", "followup"],
|
|
1954
|
+
type: ["choice", "score", "noul"],
|
|
1955
|
+
instructions: [
|
|
1956
|
+
"Which action?",
|
|
1957
|
+
"How risky?",
|
|
1958
|
+
"Will they follow up?",
|
|
1959
|
+
],
|
|
1960
|
+
criteria: [{ refund: null, deny: null }, ["low", "high"], null],
|
|
1961
|
+
},
|
|
1962
|
+
} as never,
|
|
1963
|
+
undefined,
|
|
1964
|
+
undefined,
|
|
1965
|
+
{} as never,
|
|
1966
|
+
);
|
|
1967
|
+
const asked = request().questions;
|
|
1968
|
+
assert.deepEqual(Object.keys(asked), ["refund", "risk", "followup"]);
|
|
1969
|
+
assert.equal(asked.refund.type, "choice");
|
|
1970
|
+
assert.deepEqual(asked.risk.criteria, ["low", "high"]);
|
|
1971
|
+
assert.equal(asked.followup.type, "noul");
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
it("unwraps a question under the wrapper name beside other junk keys", async () => {
|
|
1975
|
+
const { tool, request } = recordingTool();
|
|
1976
|
+
await tool.execute(
|
|
1977
|
+
"id-alias-siblings",
|
|
1978
|
+
{
|
|
1979
|
+
state: "hi",
|
|
1980
|
+
questions: {
|
|
1981
|
+
policy: "",
|
|
1982
|
+
questions: {
|
|
1983
|
+
type: "choice",
|
|
1984
|
+
instructions: "Which policy?",
|
|
1985
|
+
criteria: { a: null, b: null },
|
|
1986
|
+
},
|
|
1987
|
+
},
|
|
1988
|
+
} as never,
|
|
1989
|
+
undefined,
|
|
1990
|
+
undefined,
|
|
1991
|
+
{} as never,
|
|
1992
|
+
);
|
|
1993
|
+
assert.deepEqual(Object.keys(request().questions), ["q"]);
|
|
1994
|
+
assert.equal(request().questions.q.type, "choice");
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
it("drops a question FIELD that landed in the name slot", async () => {
|
|
1998
|
+
const { tool, request } = recordingTool();
|
|
1999
|
+
await tool.execute(
|
|
2000
|
+
"id-field-in-name-slot",
|
|
2001
|
+
{
|
|
2002
|
+
state: "hi",
|
|
2003
|
+
questions: {
|
|
2004
|
+
pick: {
|
|
2005
|
+
type: "choice",
|
|
2006
|
+
instructions: "Which library?",
|
|
2007
|
+
criteria: { date_fns: null, dayjs: null },
|
|
2008
|
+
},
|
|
2009
|
+
criteria: { stray: true },
|
|
2010
|
+
instructions: "stray",
|
|
2011
|
+
},
|
|
2012
|
+
} as never,
|
|
2013
|
+
undefined,
|
|
2014
|
+
undefined,
|
|
2015
|
+
{} as never,
|
|
2016
|
+
);
|
|
2017
|
+
assert.deepEqual(Object.keys(request().questions), ["pick"]);
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
it("splices a question map that landed in a name slot", async () => {
|
|
2021
|
+
const { tool, request } = recordingTool();
|
|
2022
|
+
await tool.execute(
|
|
2023
|
+
"id-nested-map",
|
|
2024
|
+
{
|
|
2025
|
+
state: "hi",
|
|
2026
|
+
questions: {
|
|
2027
|
+
"Which single library should this CLI adopt?": {
|
|
2028
|
+
which_date_library: {
|
|
2029
|
+
type: "choice",
|
|
2030
|
+
instructions: "Which one?",
|
|
2031
|
+
criteria: { date_fns: null, dayjs: null },
|
|
2032
|
+
},
|
|
2033
|
+
},
|
|
2034
|
+
},
|
|
2035
|
+
} as never,
|
|
2036
|
+
undefined,
|
|
2037
|
+
undefined,
|
|
2038
|
+
{} as never,
|
|
2039
|
+
);
|
|
2040
|
+
assert.deepEqual(Object.keys(request().questions), [
|
|
2041
|
+
"which_date_library",
|
|
2042
|
+
]);
|
|
2043
|
+
assert.equal(request().questions.which_date_library.type, "choice");
|
|
2044
|
+
});
|
|
2045
|
+
|
|
2046
|
+
it("does not mistake a question for a nested map", async () => {
|
|
2047
|
+
const { tool, request } = recordingTool();
|
|
2048
|
+
await tool.execute(
|
|
2049
|
+
"id-not-nested",
|
|
2050
|
+
{
|
|
2051
|
+
state: "hi",
|
|
2052
|
+
questions: {
|
|
2053
|
+
pick: {
|
|
2054
|
+
type: "choice",
|
|
2055
|
+
instructions: "Which one?",
|
|
2056
|
+
criteria: { a: null, b: null },
|
|
2057
|
+
},
|
|
2058
|
+
},
|
|
2059
|
+
} as never,
|
|
2060
|
+
undefined,
|
|
2061
|
+
undefined,
|
|
2062
|
+
{} as never,
|
|
2063
|
+
);
|
|
2064
|
+
assert.deepEqual(Object.keys(request().questions), ["pick"]);
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
it("takes description as the instructions when there are none", async () => {
|
|
2068
|
+
const { tool, request } = recordingTool();
|
|
2069
|
+
await tool.execute(
|
|
2070
|
+
"id-description-instructions",
|
|
2071
|
+
{
|
|
2072
|
+
state: "hi",
|
|
2073
|
+
questions: {
|
|
2074
|
+
q: {
|
|
2075
|
+
type: "score",
|
|
2076
|
+
description: "Rate the documentation quality.",
|
|
2077
|
+
criteria: ["low", "high"],
|
|
2078
|
+
},
|
|
2079
|
+
},
|
|
2080
|
+
} as never,
|
|
2081
|
+
undefined,
|
|
2082
|
+
undefined,
|
|
2083
|
+
{} as never,
|
|
2084
|
+
);
|
|
2085
|
+
assert.equal(
|
|
2086
|
+
request().questions.q.instructions,
|
|
2087
|
+
"Rate the documentation quality.",
|
|
2088
|
+
);
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
it("keeps real instructions ahead of a description", async () => {
|
|
2092
|
+
const { tool, request } = recordingTool();
|
|
2093
|
+
await tool.execute(
|
|
2094
|
+
"id-description-loser",
|
|
2095
|
+
{
|
|
2096
|
+
state: "hi",
|
|
2097
|
+
questions: {
|
|
2098
|
+
q: {
|
|
2099
|
+
type: "noul",
|
|
2100
|
+
instructions: "Is it?",
|
|
2101
|
+
description: "some metadata",
|
|
2102
|
+
},
|
|
2103
|
+
},
|
|
2104
|
+
} as never,
|
|
2105
|
+
undefined,
|
|
2106
|
+
undefined,
|
|
2107
|
+
{} as never,
|
|
2108
|
+
);
|
|
2109
|
+
assert.deepEqual(request().questions.q, {
|
|
2110
|
+
type: "noul",
|
|
2111
|
+
instructions: "Is it?",
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
it("splices a question map nested more than one level deep", async () => {
|
|
2116
|
+
const { tool, request } = recordingTool();
|
|
2117
|
+
await tool.execute(
|
|
2118
|
+
"id-nested-two-deep",
|
|
2119
|
+
{
|
|
2120
|
+
state: "hi",
|
|
2121
|
+
questions: {
|
|
2122
|
+
no_audit: {
|
|
2123
|
+
no_audit: {
|
|
2124
|
+
q1: {
|
|
2125
|
+
type: "choice",
|
|
2126
|
+
instructions: "Which action?",
|
|
2127
|
+
criteria: { keep: null, replace: null },
|
|
2128
|
+
},
|
|
2129
|
+
},
|
|
2130
|
+
},
|
|
2131
|
+
},
|
|
2132
|
+
} as never,
|
|
2133
|
+
undefined,
|
|
2134
|
+
undefined,
|
|
2135
|
+
{} as never,
|
|
2136
|
+
);
|
|
2137
|
+
assert.deepEqual(Object.keys(request().questions), ["q1"]);
|
|
2138
|
+
assert.equal(request().questions.q1.type, "choice");
|
|
2139
|
+
});
|
|
2140
|
+
|
|
2141
|
+
it("lifts a state nested inside a question to the top level", async () => {
|
|
2142
|
+
const { tool, request } = recordingTool();
|
|
2143
|
+
await tool.execute(
|
|
2144
|
+
"id-lift-state",
|
|
2145
|
+
{
|
|
2146
|
+
questions: [
|
|
2147
|
+
{
|
|
2148
|
+
type: "score",
|
|
2149
|
+
instructions: "How clear?",
|
|
2150
|
+
criteria: ["low", "high"],
|
|
2151
|
+
state: { readme: "## Usage\nRun it." },
|
|
2152
|
+
},
|
|
2153
|
+
],
|
|
2154
|
+
} as never,
|
|
2155
|
+
undefined,
|
|
2156
|
+
undefined,
|
|
2157
|
+
{} as never,
|
|
2158
|
+
);
|
|
2159
|
+
assert.deepEqual(request().state, { readme: "## Usage\nRun it." });
|
|
2160
|
+
assert.equal(
|
|
2161
|
+
Object.hasOwn(request().questions.q1, "state"),
|
|
2162
|
+
false,
|
|
2163
|
+
"the state must be removed from the question it was found in",
|
|
2164
|
+
);
|
|
2165
|
+
});
|
|
2166
|
+
|
|
2167
|
+
it("lifts context and evidence aliases too", async () => {
|
|
2168
|
+
const { tool, request } = recordingTool();
|
|
2169
|
+
await tool.execute(
|
|
2170
|
+
"id-lift-evidence",
|
|
2171
|
+
{
|
|
2172
|
+
questions: {
|
|
2173
|
+
q: {
|
|
2174
|
+
type: "noul",
|
|
2175
|
+
instructions: "Is it?",
|
|
2176
|
+
evidence: { signal: "weak" },
|
|
2177
|
+
},
|
|
2178
|
+
},
|
|
2179
|
+
} as never,
|
|
2180
|
+
undefined,
|
|
2181
|
+
undefined,
|
|
2182
|
+
{} as never,
|
|
2183
|
+
);
|
|
2184
|
+
assert.deepEqual(request().state, { signal: "weak" });
|
|
2185
|
+
});
|
|
2186
|
+
|
|
2187
|
+
it("keeps a top-level state ahead of a nested one", async () => {
|
|
2188
|
+
const { tool, request } = recordingTool();
|
|
2189
|
+
await tool.execute(
|
|
2190
|
+
"id-lift-state-priority",
|
|
2191
|
+
{
|
|
2192
|
+
state: "the real state",
|
|
2193
|
+
questions: {
|
|
2194
|
+
q: { type: "noul", instructions: "Is it?", state: "stray" },
|
|
2195
|
+
},
|
|
2196
|
+
} as never,
|
|
2197
|
+
undefined,
|
|
2198
|
+
undefined,
|
|
2199
|
+
{} as never,
|
|
2200
|
+
);
|
|
2201
|
+
assert.equal(request().state, "the real state");
|
|
2202
|
+
});
|
|
2203
|
+
|
|
2204
|
+
it("leaves a criteria key named state alone — that is an option label", async () => {
|
|
2205
|
+
const { tool, request } = recordingTool();
|
|
2206
|
+
await tool.execute(
|
|
2207
|
+
"id-criteria-state-label",
|
|
2208
|
+
{
|
|
2209
|
+
state: "s",
|
|
2210
|
+
questions: {
|
|
2211
|
+
q: {
|
|
2212
|
+
type: "choice",
|
|
2213
|
+
instructions: "Which level?",
|
|
2214
|
+
criteria: { state: null, federal: null },
|
|
2215
|
+
},
|
|
2216
|
+
},
|
|
2217
|
+
} as never,
|
|
2218
|
+
undefined,
|
|
2219
|
+
undefined,
|
|
2220
|
+
{} as never,
|
|
2221
|
+
);
|
|
2222
|
+
assert.deepEqual(Object.keys(request().questions.q.criteria), [
|
|
2223
|
+
"state",
|
|
2224
|
+
"federal",
|
|
2225
|
+
]);
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
it("does not mutate its input when lifting", () => {
|
|
2229
|
+
const deepFreeze = (v: unknown): void => {
|
|
2230
|
+
if (typeof v === "object" && v !== null && !Object.isFrozen(v)) {
|
|
2231
|
+
Object.freeze(v);
|
|
2232
|
+
for (const k of Object.keys(v)) {
|
|
2233
|
+
deepFreeze((v as Record<string, unknown>)[k]);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
};
|
|
2237
|
+
const input = {
|
|
2238
|
+
questions: {
|
|
2239
|
+
q: { type: "noul", instructions: "Is it?", state: "s" },
|
|
2240
|
+
},
|
|
2241
|
+
};
|
|
2242
|
+
deepFreeze(input);
|
|
2243
|
+
const out = prepareSystemOneArgs(input);
|
|
2244
|
+
assert.deepEqual(out, {
|
|
2245
|
+
state: "s",
|
|
2246
|
+
questions: { q: { type: "noul", instructions: "Is it?" } },
|
|
2247
|
+
});
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
it("strips a nested state even when a top-level one is supplied", async () => {
|
|
2251
|
+
// Regression: the lift used to early-return when a real top-level state
|
|
2252
|
+
// existed, leaving `state` inside the question where the provider could
|
|
2253
|
+
// see it. A question must never carry what it is judged against.
|
|
2254
|
+
const { tool, request } = recordingTool();
|
|
2255
|
+
await tool.execute(
|
|
2256
|
+
"id-lift-strip-always",
|
|
2257
|
+
{
|
|
2258
|
+
state: "the real state",
|
|
2259
|
+
questions: {
|
|
2260
|
+
q: { type: "noul", instructions: "Is it?", state: { stray: 1 } },
|
|
2261
|
+
},
|
|
2262
|
+
} as never,
|
|
2263
|
+
undefined,
|
|
2264
|
+
undefined,
|
|
2265
|
+
{} as never,
|
|
2266
|
+
);
|
|
2267
|
+
assert.equal(request().state, "the real state");
|
|
2268
|
+
assert.deepEqual(request().questions.q, {
|
|
2269
|
+
type: "noul",
|
|
2270
|
+
instructions: "Is it?",
|
|
2271
|
+
});
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
it("strips every state alias in one pass (idempotent)", async () => {
|
|
2275
|
+
const freeze = (v: unknown): void => {
|
|
2276
|
+
if (typeof v === "object" && v !== null && !Object.isFrozen(v)) {
|
|
2277
|
+
Object.freeze(v);
|
|
2278
|
+
for (const k of Object.keys(v)) {
|
|
2279
|
+
freeze((v as Record<string, unknown>)[k]);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
const input = {
|
|
2284
|
+
questions: {
|
|
2285
|
+
q: {
|
|
2286
|
+
type: "noul",
|
|
2287
|
+
instructions: "Is it?",
|
|
2288
|
+
state: { a: 1 },
|
|
2289
|
+
context: { b: 2 },
|
|
2290
|
+
evidence: { c: 3 },
|
|
2291
|
+
},
|
|
2292
|
+
},
|
|
2293
|
+
};
|
|
2294
|
+
freeze(input);
|
|
2295
|
+
const once = prepareSystemOneArgs(input);
|
|
2296
|
+
const twice = prepareSystemOneArgs(once);
|
|
2297
|
+
assert.deepEqual(twice, once);
|
|
2298
|
+
assert.deepEqual(once, {
|
|
2299
|
+
state: { a: 1 },
|
|
2300
|
+
questions: { q: { type: "noul", instructions: "Is it?" } },
|
|
2301
|
+
});
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
it("drops true/false from a question that is not a noul", async () => {
|
|
2305
|
+
// Regression: they are lifted into `criteria` on a noul question and
|
|
2306
|
+
// mean nothing elsewhere, but the copy-on-write fast path used to ship
|
|
2307
|
+
// them to the provider on a choice.
|
|
2308
|
+
const { tool, request } = recordingTool();
|
|
2309
|
+
await tool.execute(
|
|
2310
|
+
"id-true-false-choice",
|
|
2311
|
+
{
|
|
2312
|
+
state: "hi",
|
|
2313
|
+
questions: {
|
|
2314
|
+
q: {
|
|
2315
|
+
type: "choice",
|
|
2316
|
+
instructions: "Which?",
|
|
2317
|
+
criteria: { a: null },
|
|
2318
|
+
true: "yes",
|
|
2319
|
+
false: "no",
|
|
2320
|
+
},
|
|
2321
|
+
},
|
|
2322
|
+
} as never,
|
|
2323
|
+
undefined,
|
|
2324
|
+
undefined,
|
|
2325
|
+
{} as never,
|
|
2326
|
+
);
|
|
2327
|
+
assert.deepEqual(request().questions.q, {
|
|
2328
|
+
type: "choice",
|
|
2329
|
+
instructions: "Which?",
|
|
2330
|
+
criteria: { a: null },
|
|
2331
|
+
});
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2334
|
+
it("stays idempotent when an array mixes junk with questions", () => {
|
|
2335
|
+
// Regression: the array branch kept non-question entries while the
|
|
2336
|
+
// record branch dropped them, so `["junk", {a question}]` normalized to
|
|
2337
|
+
// different maps on the first and second pass.
|
|
2338
|
+
const input = {
|
|
2339
|
+
state: "S",
|
|
2340
|
+
questions: ["junk", { type: "noul", instructions: "Is it?" }, "junk"],
|
|
2341
|
+
};
|
|
2342
|
+
assert.deepEqual(prepareSystemOneArgs(input), {
|
|
2343
|
+
state: "S",
|
|
2344
|
+
questions: { q2: { type: "noul", instructions: "Is it?" } },
|
|
2345
|
+
});
|
|
2346
|
+
const once = prepareSystemOneArgs(input);
|
|
2347
|
+
assert.deepEqual(prepareSystemOneArgs(once), once);
|
|
2348
|
+
});
|
|
2349
|
+
|
|
2350
|
+
it("does not unwrap an array of non-questions", () => {
|
|
2351
|
+
// Regression: `{q1: [...]}` is a question map whose one entry is not a
|
|
2352
|
+
// question. Unwrapping it renamed the junk on every pass.
|
|
2353
|
+
const input = { state: "S", questions: { q1: ["text", "text"] } };
|
|
2354
|
+
const once = prepareSystemOneArgs(input);
|
|
2355
|
+
assert.deepEqual(prepareSystemOneArgs(once), once);
|
|
2356
|
+
assert.deepEqual(once, {
|
|
2357
|
+
state: "S",
|
|
2358
|
+
questions: { q1: ["text", "text"] },
|
|
2359
|
+
});
|
|
2360
|
+
});
|
|
2361
|
+
|
|
2362
|
+
it("rejects score questions with a one-element array rubric", async () => {
|
|
2363
|
+
const { tool, calls } = stubTool();
|
|
2364
|
+
await assert.rejects(
|
|
2365
|
+
tool.execute(
|
|
2366
|
+
"id-min-8",
|
|
2367
|
+
{
|
|
2368
|
+
state: "hi",
|
|
2369
|
+
questions: {
|
|
2370
|
+
s: {
|
|
2371
|
+
type: "score",
|
|
2372
|
+
instructions: "How?",
|
|
2373
|
+
criteria: ["lonely"],
|
|
2374
|
+
},
|
|
2375
|
+
},
|
|
2376
|
+
} as never,
|
|
2377
|
+
undefined,
|
|
2378
|
+
undefined,
|
|
2379
|
+
{} as never,
|
|
2380
|
+
),
|
|
2381
|
+
/score question "s" needs criteria as an ordered array/,
|
|
2382
|
+
);
|
|
2383
|
+
assert.equal(calls(), 0);
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2386
|
+
it("rejects choice questions with non-string array criteria", async () => {
|
|
2387
|
+
const { tool, calls } = stubTool();
|
|
2388
|
+
await assert.rejects(
|
|
2389
|
+
tool.execute(
|
|
2390
|
+
"id-min-9",
|
|
2391
|
+
{
|
|
2392
|
+
state: "hi",
|
|
2393
|
+
questions: {
|
|
2394
|
+
t: { type: "choice", instructions: "W?", criteria: [1, 2] },
|
|
2395
|
+
},
|
|
2396
|
+
} as never,
|
|
2397
|
+
undefined,
|
|
2398
|
+
undefined,
|
|
2399
|
+
{} as never,
|
|
2400
|
+
),
|
|
2401
|
+
/choice question "t" needs criteria as an object/,
|
|
2402
|
+
);
|
|
2403
|
+
assert.equal(calls(), 0);
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
it("lets boundary-valid questions through (1 choice, 2 levels)", async () => {
|
|
2407
|
+
const { tool, calls } = stubTool();
|
|
2408
|
+
await tool.execute(
|
|
2409
|
+
"id-min-5",
|
|
2410
|
+
{
|
|
2411
|
+
state: "hi",
|
|
2412
|
+
questions: {
|
|
2413
|
+
c: { type: "choice", instructions: "W?", criteria: { a: null } },
|
|
2414
|
+
s: {
|
|
2415
|
+
type: "score",
|
|
2416
|
+
instructions: "How?",
|
|
2417
|
+
criteria: ["low", "high"],
|
|
2418
|
+
},
|
|
2419
|
+
n: { type: "noul", instructions: "Is it?" },
|
|
1527
2420
|
},
|
|
1528
2421
|
} as never,
|
|
1529
2422
|
undefined,
|
|
@@ -1534,3 +2427,72 @@ describe("system_one tool", () => {
|
|
|
1534
2427
|
});
|
|
1535
2428
|
});
|
|
1536
2429
|
});
|
|
2430
|
+
|
|
2431
|
+
describe("discarded question reporting", () => {
|
|
2432
|
+
function toolWithAnswers(answers: Record<string, unknown>) {
|
|
2433
|
+
const stub = {
|
|
2434
|
+
id: "stub",
|
|
2435
|
+
async evaluate() {
|
|
2436
|
+
return { answers, metadata: { provider: "stub" } };
|
|
2437
|
+
},
|
|
2438
|
+
};
|
|
2439
|
+
return buildSystemOneTool({ provider: stub as never });
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
async function renderOf(args: unknown): Promise<string> {
|
|
2443
|
+
const tool = toolWithAnswers({
|
|
2444
|
+
kept: { type: "noul", noul: 0.7 },
|
|
2445
|
+
});
|
|
2446
|
+
const result = await tool.execute(
|
|
2447
|
+
"id",
|
|
2448
|
+
args as never,
|
|
2449
|
+
undefined,
|
|
2450
|
+
undefined,
|
|
2451
|
+
{} as never,
|
|
2452
|
+
);
|
|
2453
|
+
return (result.content as Array<{ text: string }>)[0].text;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// A real defect the scenario suite could not see: the model sends a
|
|
2457
|
+
// two-question batch, one of which is filed under the wrapper's own name.
|
|
2458
|
+
// The unwrap returned that question and dropped its sibling silently, so
|
|
2459
|
+
// the model received a confident answer to half of what it asked.
|
|
2460
|
+
it("reports a question abandoned by the wrapper-alias unwrap", async () => {
|
|
2461
|
+
const text = await renderOf({
|
|
2462
|
+
state: "an outage and a refund",
|
|
2463
|
+
questions: {
|
|
2464
|
+
questions: {
|
|
2465
|
+
type: "choice",
|
|
2466
|
+
instructions: "Which?",
|
|
2467
|
+
criteria: { a: null },
|
|
2468
|
+
},
|
|
2469
|
+
risk: { type: "noul", instructions: "Is there a risk?" },
|
|
2470
|
+
},
|
|
2471
|
+
});
|
|
2472
|
+
assert.match(text, /NOTE:/);
|
|
2473
|
+
assert.match(text, /discarded/);
|
|
2474
|
+
assert.match(text, /risk/);
|
|
2475
|
+
// The kept answer is still complete and still first.
|
|
2476
|
+
assert.match(text, /kept:[\s\S]*noul: 0\.7/);
|
|
2477
|
+
});
|
|
2478
|
+
|
|
2479
|
+
it("reports a name-slot entry dropped beside a real question", async () => {
|
|
2480
|
+
const text = await renderOf({
|
|
2481
|
+
state: "S",
|
|
2482
|
+
questions: {
|
|
2483
|
+
q1: { type: "noul", instructions: "Is it?" },
|
|
2484
|
+
stray_label: "refund it",
|
|
2485
|
+
},
|
|
2486
|
+
});
|
|
2487
|
+
assert.match(text, /NOTE:/);
|
|
2488
|
+
assert.match(text, /stray_label/);
|
|
2489
|
+
});
|
|
2490
|
+
|
|
2491
|
+
it("adds no note when nothing was discarded", async () => {
|
|
2492
|
+
const text = await renderOf({
|
|
2493
|
+
state: "S",
|
|
2494
|
+
questions: { q1: { type: "noul", instructions: "Is it?" } },
|
|
2495
|
+
});
|
|
2496
|
+
assert.doesNotMatch(text, /NOTE:/);
|
|
2497
|
+
});
|
|
2498
|
+
});
|