gentle-pi 3.2.1 → 3.4.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 +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
|
@@ -142,7 +142,6 @@ test("package verification names the native review runtime boundary and packaged
|
|
|
142
142
|
|
|
143
143
|
test("npm publication is bound to the exact package tag and triggering commit", () => {
|
|
144
144
|
const workflow = readFileSync(join(PACKAGE_ROOT, ".github", "workflows", "publish.yml"), "utf8");
|
|
145
|
-
const releaseSkill = readFileSync(join(PACKAGE_ROOT, "skills", "release", "SKILL.md"), "utf8");
|
|
146
145
|
const packageJson = readPackageJson();
|
|
147
146
|
const dispatchBlock = workflow.match(
|
|
148
147
|
/^ {2}workflow_dispatch:\n([\s\S]*?)^\npermissions:/m,
|
|
@@ -189,16 +188,6 @@ test("npm publication is bound to the exact package tag and triggering commit",
|
|
|
189
188
|
);
|
|
190
189
|
assert.match(workflow, /npm publish --provenance --access public/);
|
|
191
190
|
assert.doesNotMatch(workflow, /pnpm publish|--no-git-checks|NODE_AUTH_TOKEN/);
|
|
192
|
-
|
|
193
|
-
assert.match(releaseSkill, /tag="v\$\{version\}"/);
|
|
194
|
-
assert.match(releaseSkill, /release_sha="\$\(git rev-parse 'origin\/main\^\{commit\}'\)"/);
|
|
195
|
-
assert.match(releaseSkill, /git rev-parse "\$\{tag\}\^\{commit\}"/);
|
|
196
|
-
assert.match(releaseSkill, /git fetch --no-tags origin "refs\/tags\/\$\{tag\}"/);
|
|
197
|
-
assert.match(releaseSkill, /gh release create "\$\{tag\}"[\s\S]*--verify-tag/);
|
|
198
|
-
assert.match(releaseSkill, /--ref main/);
|
|
199
|
-
assert.match(releaseSkill, /-f tag="\$\{tag\}"/);
|
|
200
|
-
assert.match(releaseSkill, /trusted OIDC with provenance/);
|
|
201
|
-
assert.doesNotMatch(releaseSkill, /--ref "\$\{tag\}"|-f dist-tag=/);
|
|
202
191
|
});
|
|
203
192
|
|
|
204
193
|
test("Pi delivery relay is absent from the packaged extension", () => {
|
|
@@ -309,20 +298,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
|
|
|
309
298
|
);
|
|
310
299
|
});
|
|
311
300
|
|
|
312
|
-
test("package verification binds the published Gentle AI v3.
|
|
301
|
+
test("package verification binds the published Gentle AI v3.5.0 runtime pin", () => {
|
|
313
302
|
const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
|
|
314
303
|
const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
|
|
315
304
|
const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
|
|
316
305
|
|
|
317
|
-
assert.match(installer, /INSTALLER_VERSION = "3\.
|
|
306
|
+
assert.match(installer, /INSTALLER_VERSION = "3\.5\.0"/);
|
|
318
307
|
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
|
|
319
|
-
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
308
|
+
assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:3y\+Nb7CtgGB3ne3bSNxkzV6j19etkOnGDXIiuv7tMp8="/);
|
|
320
309
|
assert.match(installer, /GOTOOLCHAIN: "local"/);
|
|
321
310
|
assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
|
|
322
311
|
assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
|
|
323
312
|
assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
|
|
324
313
|
assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
|
|
325
|
-
assert.match(verifier, /v3\.
|
|
314
|
+
assert.match(verifier, /v3\.5\.0/);
|
|
326
315
|
});
|
|
327
316
|
|
|
328
317
|
|
|
@@ -1574,9 +1563,9 @@ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink in
|
|
|
1574
1563
|
assert.match(wrapper, /quietToolsEnabled/);
|
|
1575
1564
|
});
|
|
1576
1565
|
|
|
1577
|
-
test("Gentle Shell v3.
|
|
1566
|
+
test("Gentle Shell v3.4.0 package and runtime stop before publication", () => {
|
|
1578
1567
|
const packageJson = readPackageJson();
|
|
1579
|
-
assert.equal(packageJson.version, "3.
|
|
1568
|
+
assert.equal(packageJson.version, "3.4.0", "the release manifest must remain explicitly pinned to v3.4.0");
|
|
1580
1569
|
assert.equal(
|
|
1581
1570
|
packageJson.scripts?.test,
|
|
1582
1571
|
"node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import {
|
|
5
|
+
CUSTOM_ROW_LABEL,
|
|
6
|
+
MAX_HEADER_LENGTH,
|
|
7
|
+
MAX_LABEL_LENGTH,
|
|
8
|
+
MAX_OPTIONS,
|
|
9
|
+
MAX_QUESTIONS,
|
|
10
|
+
MIN_OPTIONS,
|
|
11
|
+
QuestionParamsSchema,
|
|
12
|
+
type OptionData,
|
|
13
|
+
type QuestionParams,
|
|
14
|
+
} from "../lib/questionnaire/schema.ts";
|
|
15
|
+
import { validateQuestionnaire, type QuestionnaireError } from "../lib/questionnaire/validate.ts";
|
|
16
|
+
|
|
17
|
+
const H = "Header";
|
|
18
|
+
const option = (label: string, preview?: string): OptionData =>
|
|
19
|
+
preview === undefined ? { label, description: `${label} description` } : { label, description: `${label} description`, preview };
|
|
20
|
+
|
|
21
|
+
const question = (
|
|
22
|
+
text: string,
|
|
23
|
+
options: OptionData[],
|
|
24
|
+
overrides: { header?: string; multiSelect?: boolean } = {},
|
|
25
|
+
) => ({
|
|
26
|
+
question: text,
|
|
27
|
+
header: overrides.header ?? H,
|
|
28
|
+
options,
|
|
29
|
+
...(overrides.multiSelect === undefined ? {} : { multiSelect: overrides.multiSelect }),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const params = (questions: ReturnType<typeof question>[]): QuestionParams => ({ questions });
|
|
33
|
+
const valid = (): ReturnType<typeof question>[] => [question("First?", [option("Alpha"), option("Beta")])];
|
|
34
|
+
|
|
35
|
+
interface Case {
|
|
36
|
+
name: string;
|
|
37
|
+
input: ReturnType<typeof params>;
|
|
38
|
+
expected: QuestionnaireError | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCases(): Case[] {
|
|
42
|
+
return [
|
|
43
|
+
{ name: "accepts a minimal valid questionnaire", input: params(valid()), expected: undefined },
|
|
44
|
+
{
|
|
45
|
+
name: "accepts exactly one question with two options",
|
|
46
|
+
input: params([question("Solo?", [option("A"), option("B")])]),
|
|
47
|
+
expected: undefined,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "accepts exactly four questions and four options",
|
|
51
|
+
input: params([
|
|
52
|
+
question("Q1", [option("A1"), option("B1"), option("C1"), option("D1")]),
|
|
53
|
+
question("Q2", [option("A2"), option("B2")]),
|
|
54
|
+
question("Q3", [option("A3"), option("B3")]),
|
|
55
|
+
question("Q4", [option("A4"), option("B4")]),
|
|
56
|
+
]),
|
|
57
|
+
expected: undefined,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "accepts a header at the 16 character limit",
|
|
61
|
+
input: params([question("Long header?", [option("A"), option("B")], { header: "x".repeat(MAX_HEADER_LENGTH) })]),
|
|
62
|
+
expected: undefined,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "accepts a label at the 60 character limit",
|
|
66
|
+
input: params([question("Long label?", [option("y".repeat(MAX_LABEL_LENGTH)), option("B")])]),
|
|
67
|
+
expected: undefined,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "rejects an empty question list",
|
|
71
|
+
input: params([]),
|
|
72
|
+
expected: { code: "no_questions", message: "At least one question is required." },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "rejects a question with empty options",
|
|
76
|
+
input: params([question("Empty?", [])]),
|
|
77
|
+
expected: {
|
|
78
|
+
code: "empty_options",
|
|
79
|
+
message: "Question 1 must declare at least one option.",
|
|
80
|
+
questionIndex: 0,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "rejects a single option below the minimum",
|
|
85
|
+
input: params([question("One?", [option("Only")])]),
|
|
86
|
+
expected: {
|
|
87
|
+
code: "option_count",
|
|
88
|
+
message: "Question 1 must have between 2 and 4 options, received 1.",
|
|
89
|
+
questionIndex: 0,
|
|
90
|
+
count: 1,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "rejects five options above the maximum",
|
|
95
|
+
input: params([question("Five?", [option("A"), option("B"), option("C"), option("D"), option("E")])]),
|
|
96
|
+
expected: {
|
|
97
|
+
code: "option_count",
|
|
98
|
+
message: "Question 1 must have between 2 and 4 options, received 5.",
|
|
99
|
+
questionIndex: 0,
|
|
100
|
+
count: 5,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "rejects more than four questions",
|
|
105
|
+
input: params([
|
|
106
|
+
question("Q1", [option("A1"), option("B1")]),
|
|
107
|
+
question("Q2", [option("A2"), option("B2")]),
|
|
108
|
+
question("Q3", [option("A3"), option("B3")]),
|
|
109
|
+
question("Q4", [option("A4"), option("B4")]),
|
|
110
|
+
question("Q5", [option("A5"), option("B5")]),
|
|
111
|
+
]),
|
|
112
|
+
expected: {
|
|
113
|
+
code: "too_many_questions",
|
|
114
|
+
message: "A questionnaire accepts at most 4 questions, received 5.",
|
|
115
|
+
count: 5,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "rejects duplicate question text",
|
|
120
|
+
input: params([
|
|
121
|
+
question("Same?", [option("A1"), option("B1")]),
|
|
122
|
+
question("Same?", [option("A2"), option("B2")]),
|
|
123
|
+
]),
|
|
124
|
+
expected: {
|
|
125
|
+
code: "duplicate_question",
|
|
126
|
+
message: "Question 2 duplicates an earlier question text.",
|
|
127
|
+
questionIndex: 1,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "rejects a duplicate option label within one question",
|
|
132
|
+
input: params([question("Dupe?", [option("Alpha"), option("Alpha")])]),
|
|
133
|
+
expected: {
|
|
134
|
+
code: "duplicate_option_label",
|
|
135
|
+
message: 'Question 1 option 2 duplicates the label "Alpha".',
|
|
136
|
+
questionIndex: 0,
|
|
137
|
+
optionIndex: 1,
|
|
138
|
+
label: "Alpha",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "rejects a label over the 60 character limit",
|
|
143
|
+
input: params([question("Too long?", [option("z".repeat(MAX_LABEL_LENGTH + 1)), option("B")])]),
|
|
144
|
+
expected: {
|
|
145
|
+
code: "label_too_long",
|
|
146
|
+
message: "Question 1 option 1 label exceeds 60 characters.",
|
|
147
|
+
questionIndex: 0,
|
|
148
|
+
optionIndex: 0,
|
|
149
|
+
length: MAX_LABEL_LENGTH + 1,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "rejects a header over the 16 character limit",
|
|
154
|
+
input: params([question("Header too long?", [option("A"), option("B")], { header: "x".repeat(MAX_HEADER_LENGTH + 1) })]),
|
|
155
|
+
expected: {
|
|
156
|
+
code: "header_too_long",
|
|
157
|
+
message: "Question 1 header exceeds 16 characters.",
|
|
158
|
+
questionIndex: 0,
|
|
159
|
+
length: MAX_HEADER_LENGTH + 1,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "rejects the reserved Other label",
|
|
164
|
+
input: params([question("Reserved?", [option("Other"), option("B")])]),
|
|
165
|
+
expected: {
|
|
166
|
+
code: "reserved_label",
|
|
167
|
+
message: 'Question 1 option 1 uses the reserved label "Other".',
|
|
168
|
+
questionIndex: 0,
|
|
169
|
+
optionIndex: 0,
|
|
170
|
+
label: "Other",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "rejects the reserved custom-row label",
|
|
175
|
+
input: params([question("Reserved?", [option(CUSTOM_ROW_LABEL), option("B")])]),
|
|
176
|
+
expected: {
|
|
177
|
+
code: "reserved_label",
|
|
178
|
+
message: `Question 1 option 1 uses the reserved label "${CUSTOM_ROW_LABEL}".`,
|
|
179
|
+
questionIndex: 0,
|
|
180
|
+
optionIndex: 0,
|
|
181
|
+
label: CUSTOM_ROW_LABEL,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "does not treat a reserved label with different casing as reserved",
|
|
186
|
+
input: params([question("Case?", [option("other"), option("type something.")])]),
|
|
187
|
+
expected: undefined,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "reports a duplicated question before its duplicated option label",
|
|
191
|
+
input: params([
|
|
192
|
+
question("Same?", [option("A"), option("B")]),
|
|
193
|
+
question("Same?", [option("A"), option("B")]),
|
|
194
|
+
]),
|
|
195
|
+
expected: {
|
|
196
|
+
code: "duplicate_question",
|
|
197
|
+
message: "Question 2 duplicates an earlier question text.",
|
|
198
|
+
questionIndex: 1,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test("validateQuestionnaire reports the first violation for each guard", () => {
|
|
205
|
+
for (const testCase of makeCases()) {
|
|
206
|
+
const actual = validateQuestionnaire(testCase.input);
|
|
207
|
+
assert.deepEqual(actual, testCase.expected, testCase.name);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("schema constants expose the documented limits", () => {
|
|
212
|
+
assert.equal(MAX_QUESTIONS, 4);
|
|
213
|
+
assert.equal(MIN_OPTIONS, 2);
|
|
214
|
+
assert.equal(MAX_OPTIONS, 4);
|
|
215
|
+
assert.equal(MAX_HEADER_LENGTH, 16);
|
|
216
|
+
assert.equal(MAX_LABEL_LENGTH, 60);
|
|
217
|
+
assert.equal(CUSTOM_ROW_LABEL, "Type something.");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("QuestionParamsSchema pins the questionnaire parameter shape", () => {
|
|
221
|
+
const root = QuestionParamsSchema as unknown as {
|
|
222
|
+
additionalProperties?: boolean;
|
|
223
|
+
properties?: {
|
|
224
|
+
questions?: {
|
|
225
|
+
minItems?: number;
|
|
226
|
+
maxItems?: number;
|
|
227
|
+
items?: {
|
|
228
|
+
additionalProperties?: boolean;
|
|
229
|
+
properties?: Record<string, unknown>;
|
|
230
|
+
required?: string[];
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
const questions = root.properties?.questions;
|
|
236
|
+
const questionSchema = questions?.items;
|
|
237
|
+
const questionProperties = questionSchema?.properties ?? {};
|
|
238
|
+
|
|
239
|
+
assert.equal(root.additionalProperties, false);
|
|
240
|
+
assert.equal(questions?.minItems, 1);
|
|
241
|
+
assert.equal(questions?.maxItems, MAX_QUESTIONS);
|
|
242
|
+
assert.equal(questionSchema?.additionalProperties, false);
|
|
243
|
+
assert.deepEqual([...(questionSchema?.required ?? [])].sort(), ["header", "options", "question"]);
|
|
244
|
+
assert.deepEqual(Object.keys(questionProperties).sort(), ["header", "multiSelect", "options", "question"]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("schema rejects malformed questionnaire parameters", () => {
|
|
248
|
+
const validParams = params(valid());
|
|
249
|
+
assert.equal(Value.Check(QuestionParamsSchema, validParams), true);
|
|
250
|
+
|
|
251
|
+
const invalidCases: Array<[string, unknown]> = [
|
|
252
|
+
["missing questions", {}],
|
|
253
|
+
["empty questions", { questions: [] }],
|
|
254
|
+
["five questions", params([
|
|
255
|
+
question("Q1", [option("A1"), option("B1")]),
|
|
256
|
+
question("Q2", [option("A2"), option("B2")]),
|
|
257
|
+
question("Q3", [option("A3"), option("B3")]),
|
|
258
|
+
question("Q4", [option("A4"), option("B4")]),
|
|
259
|
+
question("Q5", [option("A5"), option("B5")]),
|
|
260
|
+
])],
|
|
261
|
+
["one option", params([question("One?", [option("Only")])])],
|
|
262
|
+
["label over the limit", params([question("Long?", [option("z".repeat(MAX_LABEL_LENGTH + 1)), option("B")])])],
|
|
263
|
+
["header over the limit", params([question("Long?", [option("A"), option("B")], { header: "x".repeat(MAX_HEADER_LENGTH + 1) })])],
|
|
264
|
+
["unknown root property", { questions: valid(), extra: true }],
|
|
265
|
+
];
|
|
266
|
+
for (const [name, subject] of invalidCases) {
|
|
267
|
+
assert.equal(Value.Check(QuestionParamsSchema, subject), false, name);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("schema accepts multiSelect and preview as optional fields", () => {
|
|
272
|
+
const withOptionals = params([question("Options?", [option("A", "Preview A"), option("B")], { multiSelect: true })]);
|
|
273
|
+
assert.equal(Value.Check(QuestionParamsSchema, withOptionals), true);
|
|
274
|
+
});
|