@terreno/blocks 57.10.1
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/LICENSE +21 -0
- package/README.md +62 -0
- package/biome.jsonc +18 -0
- package/bunfig.toml +6 -0
- package/dist/asks/errors.d.ts +77 -0
- package/dist/asks/errors.js +364 -0
- package/dist/asks/files.d.ts +53 -0
- package/dist/asks/files.js +171 -0
- package/dist/asks/formValues.d.ts +32 -0
- package/dist/asks/formValues.js +229 -0
- package/dist/asks/headless.d.ts +227 -0
- package/dist/asks/headless.js +94 -0
- package/dist/asks/jsonSchema.d.ts +6 -0
- package/dist/asks/jsonSchema.js +62 -0
- package/dist/asks/limits.d.ts +58 -0
- package/dist/asks/limits.js +61 -0
- package/dist/asks/prompt.d.ts +10 -0
- package/dist/asks/prompt.js +144 -0
- package/dist/asks/schema.d.ts +826 -0
- package/dist/asks/schema.js +864 -0
- package/dist/asks/simpleCard.d.ts +85 -0
- package/dist/asks/simpleCard.js +279 -0
- package/dist/asks/validateInput.d.ts +13 -0
- package/dist/asks/validateInput.js +19 -0
- package/dist/asks/validateResponse.d.ts +13 -0
- package/dist/asks/validateResponse.js +278 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +58 -0
- package/dist/errors.d.ts +51 -0
- package/dist/errors.js +106 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +111 -0
- package/dist/jsonSchema.d.ts +5 -0
- package/dist/jsonSchema.js +12 -0
- package/dist/limits.d.ts +27 -0
- package/dist/limits.js +30 -0
- package/dist/lint.d.ts +21 -0
- package/dist/lint.js +388 -0
- package/dist/parse.d.ts +16 -0
- package/dist/parse.js +95 -0
- package/dist/parsePartial.d.ts +12 -0
- package/dist/parsePartial.js +105 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +95 -0
- package/dist/schema.d.ts +199 -0
- package/dist/schema.js +277 -0
- package/dist/validate.d.ts +17 -0
- package/dist/validate.js +331 -0
- package/package.json +60 -0
- package/schemas/pendingAsksResponse.schema.json +195 -0
- package/schemas/simpleCard.schema.json +135 -0
- package/schemas/turnRequest.schema.json +108 -0
- package/schemas/turnResponse.schema.json +204 -0
- package/scripts/writeJsonSchemas.ts +11 -0
- package/src/asks/docParity.test.ts +65 -0
- package/src/asks/errors.test.ts +250 -0
- package/src/asks/errors.ts +449 -0
- package/src/asks/files.test.ts +294 -0
- package/src/asks/files.ts +216 -0
- package/src/asks/fixtures/invalid/choice-blank-text.json +20 -0
- package/src/asks/fixtures/invalid/choice-default-not-in-options.json +13 -0
- package/src/asks/fixtures/invalid/choice-default-selects-two.json +16 -0
- package/src/asks/fixtures/invalid/choice-description-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-duplicate-ids.json +17 -0
- package/src/asks/fixtures/invalid/choice-label-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-not-whole.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-bounds-out-of-range.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-duplicate.json +17 -0
- package/src/asks/fixtures/invalid/choice-many-default-too-many.json +15 -0
- package/src/asks/fixtures/invalid/choice-many-max-above-options.json +14 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-choices.json +13 -0
- package/src/asks/fixtures/invalid/choice-many-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/choice-missing-required.json +14 -0
- package/src/asks/fixtures/invalid/choice-mixed-errors.json +19 -0
- package/src/asks/fixtures/invalid/choice-not-an-object.json +5 -0
- package/src/asks/fixtures/invalid/choice-one-with-many-fields.json +19 -0
- package/src/asks/fixtures/invalid/choice-option-id-format.json +35 -0
- package/src/asks/fixtures/invalid/choice-options-not-objects.json +12 -0
- package/src/asks/fixtures/invalid/choice-other-label-too-long.json +25 -0
- package/src/asks/fixtures/invalid/choice-other-label-without-allow-other.json +14 -0
- package/src/asks/fixtures/invalid/choice-prompt-empty.json +12 -0
- package/src/asks/fixtures/invalid/choice-prompt-too-long.json +23 -0
- package/src/asks/fixtures/invalid/choice-select-unknown.json +12 -0
- package/src/asks/fixtures/invalid/choice-submit-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/choice-title-too-long.json +24 -0
- package/src/asks/fixtures/invalid/choice-too-few-options.json +9 -0
- package/src/asks/fixtures/invalid/choice-too-many-options.json +219 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-option.json +15 -0
- package/src/asks/fixtures/invalid/choice-unknown-key-root.json +13 -0
- package/src/asks/fixtures/invalid/choice-wrong-types.json +16 -0
- package/src/asks/fixtures/invalid/confirm-emoji-label-too-long.json +13 -0
- package/src/asks/fixtures/invalid/confirm-label-too-long.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike-after-trim.json +14 -0
- package/src/asks/fixtures/invalid/confirm-labels-alike.json +13 -0
- package/src/asks/fixtures/invalid/confirm-missing-prompt-blank-label.json +16 -0
- package/src/asks/fixtures/invalid/confirm-unknown-keys.json +23 -0
- package/src/asks/fixtures/invalid/confirm-wrong-types.json +18 -0
- package/src/asks/fixtures/invalid/files-accept-duplicate.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-empty.json +13 -0
- package/src/asks/fixtures/invalid/files-accept-unknown.json +13 -0
- package/src/asks/fixtures/invalid/files-counts-out-of-range.json +19 -0
- package/src/asks/fixtures/invalid/files-min-above-max.json +15 -0
- package/src/asks/fixtures/invalid/files-missing-accept-unknown-key.json +17 -0
- package/src/asks/fixtures/invalid/form-default-bad-format.json +40 -0
- package/src/asks/fixtures/invalid/form-default-invalid-date.json +40 -0
- package/src/asks/fixtures/invalid/form-default-not-in-options.json +58 -0
- package/src/asks/fixtures/invalid/form-default-out-of-range.json +33 -0
- package/src/asks/fixtures/invalid/form-default-text-bounds.json +42 -0
- package/src/asks/fixtures/invalid/form-default-wrong-type.json +30 -0
- package/src/asks/fixtures/invalid/form-duplicate-field-ids.json +24 -0
- package/src/asks/fixtures/invalid/form-field-id-format.json +19 -0
- package/src/asks/fixtures/invalid/form-field-text-limits.json +33 -0
- package/src/asks/fixtures/invalid/form-keys-of-another-type.json +39 -0
- package/src/asks/fixtures/invalid/form-length-above-cap.json +30 -0
- package/src/asks/fixtures/invalid/form-min-above-max.json +32 -0
- package/src/asks/fixtures/invalid/form-no-fields.json +13 -0
- package/src/asks/fixtures/invalid/form-select-options.json +44 -0
- package/src/asks/fixtures/invalid/form-too-many-fields.json +59 -0
- package/src/asks/fixtures/invalid/form-unknown-field-type.json +27 -0
- package/src/asks/fixtures/invalid/markdown-bounds-out-of-range.json +18 -0
- package/src/asks/fixtures/invalid/markdown-initial-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-max-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-cap.json +13 -0
- package/src/asks/fixtures/invalid/markdown-min-above-max.json +14 -0
- package/src/asks/fixtures/invalid/markdown-placeholder-too-long.json +13 -0
- package/src/asks/fixtures/invalid/markdown-wrong-types-unknown-key.json +27 -0
- package/src/asks/fixtures/valid/choice-emoji-label-cut.json +32 -0
- package/src/asks/fixtures/valid/choice-labels-collide-after-cut.json +21 -0
- package/src/asks/fixtures/valid/choice-long-text-cut.json +41 -0
- package/src/asks/fixtures/valid/choice-many-default-below-min.json +24 -0
- package/src/asks/fixtures/valid/choice-many-every-choice-with-other.json +24 -0
- package/src/asks/fixtures/valid/choice-many-no-default.json +21 -0
- package/src/asks/fixtures/valid/choice-many-optional-no-buttons.json +20 -0
- package/src/asks/fixtures/valid/choice-many-options-default-label-collides.json +23 -0
- package/src/asks/fixtures/valid/choice-many-options-no-buttons.json +21 -0
- package/src/asks/fixtures/valid/choice-many-options-no-default.json +22 -0
- package/src/asks/fixtures/valid/choice-many-options-with-default.json +30 -0
- package/src/asks/fixtures/valid/choice-many-required-with-default.json +29 -0
- package/src/asks/fixtures/valid/choice-many-single-pick-or-other.json +29 -0
- package/src/asks/fixtures/valid/choice-many-toppings-with-default.json +36 -0
- package/src/asks/fixtures/valid/choice-max-limits.json +245 -0
- package/src/asks/fixtures/valid/choice-plan-with-default.json +42 -0
- package/src/asks/fixtures/valid/choice-two-options-no-decline.json +32 -0
- package/src/asks/fixtures/valid/choice-two-options.json +32 -0
- package/src/asks/fixtures/valid/confirm-allow-decline.json +40 -0
- package/src/asks/fixtures/valid/confirm-default-labels.json +36 -0
- package/src/asks/fixtures/valid/confirm-destructive-delete.json +41 -0
- package/src/asks/fixtures/valid/confirm-emoji-labels.json +38 -0
- package/src/asks/fixtures/valid/confirm-long-text-cut.json +40 -0
- package/src/asks/fixtures/valid/files-every-type.json +28 -0
- package/src/asks/fixtures/valid/files-one-csv-no-decline.json +16 -0
- package/src/asks/fixtures/valid/files-receipt-photos.json +24 -0
- package/src/asks/fixtures/valid/form-boolean-newsletter.json +30 -0
- package/src/asks/fixtures/valid/form-date-due.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-meeting.json +44 -0
- package/src/asks/fixtures/valid/form-datetime-minutes-offset.json +44 -0
- package/src/asks/fixtures/valid/form-eight-fields.json +71 -0
- package/src/asks/fixtures/valid/form-email-contact.json +44 -0
- package/src/asks/fixtures/valid/form-invoice-details.json +68 -0
- package/src/asks/fixtures/valid/form-multiselect-channels.json +64 -0
- package/src/asks/fixtures/valid/form-number-budget.json +45 -0
- package/src/asks/fixtures/valid/form-phone-callback.json +44 -0
- package/src/asks/fixtures/valid/form-select-shipping.json +58 -0
- package/src/asks/fixtures/valid/form-text-required-default.json +47 -0
- package/src/asks/fixtures/valid/form-textarea-notes.json +23 -0
- package/src/asks/fixtures/valid/form-time-reminder.json +44 -0
- package/src/asks/fixtures/valid/form-url-website.json +30 -0
- package/src/asks/fixtures/valid/markdown-announcement-draft.json +39 -0
- package/src/asks/fixtures/valid/markdown-empty-draft.json +34 -0
- package/src/asks/fixtures/valid/markdown-initial-blank-below-min.json +24 -0
- package/src/asks/fixtures/valid/markdown-initial-over-max.json +24 -0
- package/src/asks/fixtures/valid/markdown-long-text-cut.json +37 -0
- package/src/asks/fixtures/valid/markdown-no-decline.json +28 -0
- package/src/asks/fixtures/valid/markdown-no-initial-with-min.json +25 -0
- package/src/asks/formValues.ts +320 -0
- package/src/asks/headless.test.ts +146 -0
- package/src/asks/headless.ts +130 -0
- package/src/asks/jsonSchema.test.ts +171 -0
- package/src/asks/jsonSchema.ts +89 -0
- package/src/asks/limits.ts +58 -0
- package/src/asks/prompt.test.ts +200 -0
- package/src/asks/prompt.ts +173 -0
- package/src/asks/schema.test.ts +198 -0
- package/src/asks/schema.ts +1203 -0
- package/src/asks/simpleCard.test.ts +998 -0
- package/src/asks/simpleCard.ts +341 -0
- package/src/asks/validateInput.test.ts +875 -0
- package/src/asks/validateInput.ts +24 -0
- package/src/asks/validateResponse.test.ts +740 -0
- package/src/asks/validateResponse.ts +341 -0
- package/src/blocksDocParity.test.ts +21 -0
- package/src/cli.test.ts +134 -0
- package/src/cli.ts +65 -0
- package/src/display.test.ts +68 -0
- package/src/errors.test.ts +29 -0
- package/src/errors.ts +125 -0
- package/src/fixtures/invalid/column-not-found.yaml +14 -0
- package/src/fixtures/invalid/column-type.yaml +14 -0
- package/src/fixtures/invalid/dataset-not-found.yaml +7 -0
- package/src/fixtures/invalid/dataset-too-large.yaml +20 -0
- package/src/fixtures/invalid/depth-exceeded.yaml +10 -0
- package/src/fixtures/invalid/duplicate-id.yaml +6 -0
- package/src/fixtures/invalid/invalid-enum.yaml +5 -0
- package/src/fixtures/invalid/key-order.yaml +4 -0
- package/src/fixtures/invalid/missing-required.yaml +3 -0
- package/src/fixtures/invalid/not-a-document.txt +1 -0
- package/src/fixtures/invalid/row-arity.yaml +11 -0
- package/src/fixtures/invalid/select-target.yaml +12 -0
- package/src/fixtures/invalid/table-too-wide.yaml +9 -0
- package/src/fixtures/invalid/too-many-points.yaml +8 -0
- package/src/fixtures/invalid/unknown-key.yaml +5 -0
- package/src/fixtures/invalid/unsupported-version.yaml +4 -0
- package/src/fixtures/invalid/yaml-anchor.yaml +6 -0
- package/src/fixtures/partial/cut-after-divider.yaml +5 -0
- package/src/fixtures/valid/actions.yaml +32 -0
- package/src/fixtures/valid/chart.yaml +20 -0
- package/src/fixtures/valid/layout.yaml +26 -0
- package/src/fixtures/valid/prose.yaml +4 -0
- package/src/html.test.ts +52 -0
- package/src/index.test.ts +114 -0
- package/src/index.ts +175 -0
- package/src/jsonSchema.test.ts +33 -0
- package/src/jsonSchema.ts +11 -0
- package/src/limits.ts +27 -0
- package/src/lint.test.ts +289 -0
- package/src/lint.ts +484 -0
- package/src/parse.test.ts +281 -0
- package/src/parse.ts +104 -0
- package/src/parsePartial.test.ts +53 -0
- package/src/parsePartial.ts +116 -0
- package/src/prompt.test.ts +24 -0
- package/src/prompt.ts +104 -0
- package/src/schema.ts +509 -0
- package/src/tests/askFixtures.ts +40 -0
- package/src/validate.perf.test.ts +43 -0
- package/src/validate.ts +369 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "Here is a draft announcement. Edit anything, then send it back.",
|
|
5
|
+
"title": "Launch announcement",
|
|
6
|
+
"initial": "# We're live\n\nToday we launched **Terreno Asks**: your agent can now ask you a question and wait for the answer.\n\n- Pick from options\n- Approve or deny\n- Edit a draft like this one\n",
|
|
7
|
+
"maxLength": 2000,
|
|
8
|
+
"submitLabel": "Send it back"
|
|
9
|
+
},
|
|
10
|
+
"simple": {
|
|
11
|
+
"buttons": [
|
|
12
|
+
{
|
|
13
|
+
"id": "approve",
|
|
14
|
+
"label": "Approve draft",
|
|
15
|
+
"response": {
|
|
16
|
+
"action": "accept",
|
|
17
|
+
"content": {
|
|
18
|
+
"changed": false,
|
|
19
|
+
"markdown": "# We're live\n\nToday we launched **Terreno Asks**: your agent can now ask you a question and wait for the answer.\n\n- Pick from options\n- Approve or deny\n- Edit a draft like this one\n"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"style": "primary"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "cancel",
|
|
26
|
+
"label": "Cancel",
|
|
27
|
+
"response": {
|
|
28
|
+
"action": "decline"
|
|
29
|
+
},
|
|
30
|
+
"style": "cancel"
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"handoff": true,
|
|
34
|
+
"kind": "markdown",
|
|
35
|
+
"text": "Here is a draft announcement. Edit anything, then send it back.",
|
|
36
|
+
"title": "Launch announcement",
|
|
37
|
+
"toolCallId": "call_fixture"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "Write the message you want me to send to the team."
|
|
5
|
+
},
|
|
6
|
+
"simple": {
|
|
7
|
+
"buttons": [
|
|
8
|
+
{
|
|
9
|
+
"id": "approve",
|
|
10
|
+
"label": "Approve draft",
|
|
11
|
+
"response": {
|
|
12
|
+
"action": "accept",
|
|
13
|
+
"content": {
|
|
14
|
+
"changed": false,
|
|
15
|
+
"markdown": ""
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"style": "primary"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "cancel",
|
|
22
|
+
"label": "Cancel",
|
|
23
|
+
"response": {
|
|
24
|
+
"action": "decline"
|
|
25
|
+
},
|
|
26
|
+
"style": "cancel"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"handoff": true,
|
|
30
|
+
"kind": "markdown",
|
|
31
|
+
"text": "Write the message you want me to send to the team.",
|
|
32
|
+
"toolCallId": "call_fixture"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "Add a note for the delivery driver.",
|
|
5
|
+
"initial": " ",
|
|
6
|
+
"minLength": 1
|
|
7
|
+
},
|
|
8
|
+
"simple": {
|
|
9
|
+
"buttons": [
|
|
10
|
+
{
|
|
11
|
+
"id": "cancel",
|
|
12
|
+
"label": "Cancel",
|
|
13
|
+
"response": {
|
|
14
|
+
"action": "decline"
|
|
15
|
+
},
|
|
16
|
+
"style": "cancel"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"handoff": true,
|
|
20
|
+
"kind": "markdown",
|
|
21
|
+
"text": "Add a note for the delivery driver.",
|
|
22
|
+
"toolCallId": "call_fixture"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "This summary is too long for the 80-character slot. Trim it, then send it back.",
|
|
5
|
+
"initial": "Our quarterly results beat every forecast, driven by strong growth in the team plan and lower churn.",
|
|
6
|
+
"maxLength": 80
|
|
7
|
+
},
|
|
8
|
+
"simple": {
|
|
9
|
+
"buttons": [
|
|
10
|
+
{
|
|
11
|
+
"id": "cancel",
|
|
12
|
+
"label": "Cancel",
|
|
13
|
+
"response": {
|
|
14
|
+
"action": "decline"
|
|
15
|
+
},
|
|
16
|
+
"style": "cancel"
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"handoff": true,
|
|
20
|
+
"kind": "markdown",
|
|
21
|
+
"text": "This summary is too long for the 80-character slot. Trim it, then send it back.",
|
|
22
|
+
"toolCallId": "call_fixture"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "I drafted the quarterly update for the board. It covers revenue, hiring, the product roadmap for next quarter, and the two risks we discussed on Monday. Edit anything that reads wrong, then send it back.",
|
|
5
|
+
"title": "Quarterly board update for the October meeting",
|
|
6
|
+
"initial": "# Q3 update\n\nRevenue grew 18%.\n"
|
|
7
|
+
},
|
|
8
|
+
"simple": {
|
|
9
|
+
"buttons": [
|
|
10
|
+
{
|
|
11
|
+
"id": "approve",
|
|
12
|
+
"label": "Approve draft",
|
|
13
|
+
"response": {
|
|
14
|
+
"action": "accept",
|
|
15
|
+
"content": {
|
|
16
|
+
"changed": false,
|
|
17
|
+
"markdown": "# Q3 update\n\nRevenue grew 18%.\n"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"style": "primary"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "cancel",
|
|
24
|
+
"label": "Cancel",
|
|
25
|
+
"response": {
|
|
26
|
+
"action": "decline"
|
|
27
|
+
},
|
|
28
|
+
"style": "cancel"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"handoff": true,
|
|
32
|
+
"kind": "markdown",
|
|
33
|
+
"text": "I drafted the quarterly update for the board. It covers revenue, hiring, the product roadmap for next quarter, and the two risks we…",
|
|
34
|
+
"title": "Quarterly board update for the October…",
|
|
35
|
+
"toolCallId": "call_fixture"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "Review the release notes before I publish them.",
|
|
5
|
+
"initial": "## 2.3.0\n\n- Markdown asks\n",
|
|
6
|
+
"allowDecline": false
|
|
7
|
+
},
|
|
8
|
+
"simple": {
|
|
9
|
+
"buttons": [
|
|
10
|
+
{
|
|
11
|
+
"id": "approve",
|
|
12
|
+
"label": "Approve draft",
|
|
13
|
+
"response": {
|
|
14
|
+
"action": "accept",
|
|
15
|
+
"content": {
|
|
16
|
+
"changed": false,
|
|
17
|
+
"markdown": "## 2.3.0\n\n- Markdown asks\n"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"style": "primary"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"handoff": true,
|
|
24
|
+
"kind": "markdown",
|
|
25
|
+
"text": "Review the release notes before I publish them.",
|
|
26
|
+
"toolCallId": "call_fixture"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"kind": "markdown",
|
|
3
|
+
"input": {
|
|
4
|
+
"prompt": "Write a short bio for your profile page.",
|
|
5
|
+
"placeholder": "A sentence or two about you",
|
|
6
|
+
"minLength": 20,
|
|
7
|
+
"maxLength": 400
|
|
8
|
+
},
|
|
9
|
+
"simple": {
|
|
10
|
+
"buttons": [
|
|
11
|
+
{
|
|
12
|
+
"id": "cancel",
|
|
13
|
+
"label": "Cancel",
|
|
14
|
+
"response": {
|
|
15
|
+
"action": "decline"
|
|
16
|
+
},
|
|
17
|
+
"style": "cancel"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"handoff": true,
|
|
21
|
+
"kind": "markdown",
|
|
22
|
+
"text": "Write a short bio for your profile page.",
|
|
23
|
+
"toolCallId": "call_fixture"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import {z} from "zod";
|
|
2
|
+
import {type AskErrorCode, type AskErrorDraft, formatAskPath, quoteValue} from "./errors";
|
|
3
|
+
import {ASK_LIMITS} from "./limits";
|
|
4
|
+
import type {FormAskInput, FormField, FormFieldType} from "./schema";
|
|
5
|
+
|
|
6
|
+
/** A value a form answer holds for one field. */
|
|
7
|
+
export type FormValue = boolean | number | string | string[];
|
|
8
|
+
|
|
9
|
+
const emailSchema = z.email();
|
|
10
|
+
const urlSchema = z.url({protocol: /^https?$/});
|
|
11
|
+
const PHONE_CHARACTERS = /^\+?[0-9 ().-]+$/;
|
|
12
|
+
const NON_DIGITS = /\D/g;
|
|
13
|
+
|
|
14
|
+
/** The ISO 8601 shape each date field type takes, with an example for fixes. */
|
|
15
|
+
const DATE_FORMATS = {
|
|
16
|
+
date: {example: "2026-10-01", format: "YYYY-MM-DD", schema: z.iso.date()},
|
|
17
|
+
datetime: {
|
|
18
|
+
example: "2026-10-01T09:30:00Z",
|
|
19
|
+
format: "an ISO 8601 date and time with Z or an offset such as +02:00 (seconds optional)",
|
|
20
|
+
schema: z.union([
|
|
21
|
+
z.iso.datetime({offset: true, precision: -1}),
|
|
22
|
+
z.iso.datetime({offset: true}),
|
|
23
|
+
]),
|
|
24
|
+
},
|
|
25
|
+
time: {example: "09:30", format: "HH:mm (24-hour)", schema: z.iso.time({precision: -1})},
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
const TYPE_NAMES: Record<FormFieldType, string> = {
|
|
29
|
+
boolean: "true or false",
|
|
30
|
+
date: "a date string",
|
|
31
|
+
datetime: "a datetime string",
|
|
32
|
+
email: "an email address",
|
|
33
|
+
multiselect: "a list of option ids",
|
|
34
|
+
number: "a number",
|
|
35
|
+
phone: "a phone number",
|
|
36
|
+
select: "an option id",
|
|
37
|
+
text: "a string",
|
|
38
|
+
textarea: "a string",
|
|
39
|
+
time: "a time string",
|
|
40
|
+
url: "an http or https URL",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const characters = (count: number): string => (count === 1 ? "1 character" : `${count} characters`);
|
|
44
|
+
|
|
45
|
+
/** The longest value a field accepts, in UTF-16 code units. */
|
|
46
|
+
export const formTextMaxLength = (field: FormField): number => {
|
|
47
|
+
if (field.type === "textarea") {
|
|
48
|
+
return field.maxLength ?? ASK_LIMITS.form.textareaMaxLength;
|
|
49
|
+
}
|
|
50
|
+
if (field.type === "text") {
|
|
51
|
+
return field.maxLength ?? ASK_LIMITS.form.textMaxLength;
|
|
52
|
+
}
|
|
53
|
+
return ASK_LIMITS.form.textMaxLength;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Whether a value counts as unanswered: a string with no visible characters, or an empty list.
|
|
58
|
+
* `false` and `0` are answers.
|
|
59
|
+
*/
|
|
60
|
+
export const isBlankFormValue = (value: unknown): boolean =>
|
|
61
|
+
(typeof value === "string" && value.trim() === "") ||
|
|
62
|
+
(Array.isArray(value) && value.length === 0);
|
|
63
|
+
|
|
64
|
+
interface CheckContext {
|
|
65
|
+
field: FormField;
|
|
66
|
+
/** True when the value is the field's `default` in an ask, not a user's answer. */
|
|
67
|
+
isDefault: boolean;
|
|
68
|
+
segments: readonly PropertyKey[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const draft = (
|
|
72
|
+
{segments}: CheckContext,
|
|
73
|
+
code: AskErrorCode,
|
|
74
|
+
message: string,
|
|
75
|
+
fix: string
|
|
76
|
+
): AskErrorDraft => ({code, fix, message, segments});
|
|
77
|
+
|
|
78
|
+
const mismatch = (context: CheckContext, detail?: string): AskErrorDraft => {
|
|
79
|
+
const path = formatAskPath(context.segments);
|
|
80
|
+
const expected = TYPE_NAMES[context.field.type];
|
|
81
|
+
return draft(
|
|
82
|
+
context,
|
|
83
|
+
"FIELD_TYPE_MISMATCH",
|
|
84
|
+
detail ??
|
|
85
|
+
`${path} must be ${expected} for the ${context.field.type} field "${context.field.id}".`,
|
|
86
|
+
`Make ${path} ${expected}.`
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const checkLength = (context: CheckContext, value: string): AskErrorDraft[] => {
|
|
91
|
+
const path = formatAskPath(context.segments);
|
|
92
|
+
const max = formTextMaxLength(context.field);
|
|
93
|
+
const drafts: AskErrorDraft[] = [];
|
|
94
|
+
if (value.length > max) {
|
|
95
|
+
drafts.push(
|
|
96
|
+
draft(
|
|
97
|
+
context,
|
|
98
|
+
"TOO_LONG",
|
|
99
|
+
`${path} is ${characters(value.length)}, but the field allows at most ${max}.`,
|
|
100
|
+
`Shorten ${path} to ${characters(max)} or fewer.`
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const {field} = context;
|
|
105
|
+
const min = field.type === "text" || field.type === "textarea" ? (field.minLength ?? 0) : 0;
|
|
106
|
+
const visibleLength = value.trim().length;
|
|
107
|
+
if (visibleLength === 0 && context.isDefault) {
|
|
108
|
+
drafts.push(
|
|
109
|
+
draft(
|
|
110
|
+
context,
|
|
111
|
+
"TOO_SHORT",
|
|
112
|
+
`${path} must contain visible text.`,
|
|
113
|
+
`Write at least one visible character in ${path}, or remove it.`
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
} else if (visibleLength < min) {
|
|
117
|
+
drafts.push(
|
|
118
|
+
draft(
|
|
119
|
+
context,
|
|
120
|
+
"TOO_SHORT",
|
|
121
|
+
`${path} is ${characters(visibleLength)}, but the field needs at least ${min}.`,
|
|
122
|
+
`Write at least ${characters(min)} in ${path}.`
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return drafts;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const checkStringFormat = (context: CheckContext, value: string): AskErrorDraft[] => {
|
|
130
|
+
const path = formatAskPath(context.segments);
|
|
131
|
+
const {type} = context.field;
|
|
132
|
+
if (type === "email" && !emailSchema.safeParse(value).success) {
|
|
133
|
+
return [mismatch(context, `${path} ${quoteValue(value)} is not a valid email address.`)];
|
|
134
|
+
}
|
|
135
|
+
if (type === "url" && !urlSchema.safeParse(value).success) {
|
|
136
|
+
return [mismatch(context, `${path} ${quoteValue(value)} is not an http or https URL.`)];
|
|
137
|
+
}
|
|
138
|
+
if (type === "phone") {
|
|
139
|
+
const digits = value.replace(NON_DIGITS, "").length;
|
|
140
|
+
const {phoneDigitsMax, phoneDigitsMin} = ASK_LIMITS.form;
|
|
141
|
+
if (!PHONE_CHARACTERS.test(value) || digits < phoneDigitsMin || digits > phoneDigitsMax) {
|
|
142
|
+
return [
|
|
143
|
+
draft(
|
|
144
|
+
context,
|
|
145
|
+
"FIELD_TYPE_MISMATCH",
|
|
146
|
+
`${path} ${quoteValue(value)} is not a valid phone number.`,
|
|
147
|
+
`Make ${path} ${phoneDigitsMin}-${phoneDigitsMax} digits, optionally starting with "+", with only spaces, dots, dashes, or parentheses between them.`
|
|
148
|
+
),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return [];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const checkNumber = (context: CheckContext, value: unknown): AskErrorDraft[] => {
|
|
156
|
+
const {field} = context;
|
|
157
|
+
if (field.type !== "number") {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
161
|
+
return [mismatch(context)];
|
|
162
|
+
}
|
|
163
|
+
const path = formatAskPath(context.segments);
|
|
164
|
+
if (field.integer === true && !Number.isInteger(value)) {
|
|
165
|
+
return [
|
|
166
|
+
draft(
|
|
167
|
+
context,
|
|
168
|
+
"FIELD_TYPE_MISMATCH",
|
|
169
|
+
`${path} is ${value}, but the field takes whole numbers only.`,
|
|
170
|
+
`Make ${path} a whole number.`
|
|
171
|
+
),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
if (field.min !== undefined && value < field.min) {
|
|
175
|
+
return [
|
|
176
|
+
draft(
|
|
177
|
+
context,
|
|
178
|
+
"OUT_OF_RANGE",
|
|
179
|
+
`${path} is ${value}, below the field's min of ${field.min}.`,
|
|
180
|
+
`Make ${path} at least ${field.min}.`
|
|
181
|
+
),
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
if (field.max !== undefined && value > field.max) {
|
|
185
|
+
return [
|
|
186
|
+
draft(
|
|
187
|
+
context,
|
|
188
|
+
"OUT_OF_RANGE",
|
|
189
|
+
`${path} is ${value}, above the field's max of ${field.max}.`,
|
|
190
|
+
`Make ${path} at most ${field.max}.`
|
|
191
|
+
),
|
|
192
|
+
];
|
|
193
|
+
}
|
|
194
|
+
return [];
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const notOffered = (context: CheckContext, id: string, segments: PropertyKey[]): AskErrorDraft =>
|
|
198
|
+
context.isDefault
|
|
199
|
+
? {
|
|
200
|
+
code: "DEFAULT_NOT_IN_OPTIONS",
|
|
201
|
+
fix: `Use an id from fields[${String(context.segments[1])}].options, or remove it from the default.`,
|
|
202
|
+
message: `Default ${quoteValue(id)} is not the id of any option of the field.`,
|
|
203
|
+
segments,
|
|
204
|
+
}
|
|
205
|
+
: {
|
|
206
|
+
code: "OPTION_NOT_OFFERED",
|
|
207
|
+
fix: `Use the id of one of the options of the field "${context.field.id}".`,
|
|
208
|
+
message: `${quoteValue(id)} is not one of the field's options.`,
|
|
209
|
+
segments,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const checkOptions = (context: CheckContext, value: unknown): AskErrorDraft[] => {
|
|
213
|
+
const {field} = context;
|
|
214
|
+
if (field.type !== "select" && field.type !== "multiselect") {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const offered = new Set(field.options.map((option) => option.id));
|
|
218
|
+
if (field.type === "select") {
|
|
219
|
+
if (typeof value !== "string") {
|
|
220
|
+
return [mismatch(context)];
|
|
221
|
+
}
|
|
222
|
+
return offered.has(value) ? [] : [notOffered(context, value, [...context.segments])];
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(value) || value.some((id) => typeof id !== "string")) {
|
|
225
|
+
return [mismatch(context)];
|
|
226
|
+
}
|
|
227
|
+
const path = formatAskPath(context.segments);
|
|
228
|
+
if (value.length > ASK_LIMITS.choice.optionsMax) {
|
|
229
|
+
return [
|
|
230
|
+
draft(
|
|
231
|
+
context,
|
|
232
|
+
"TOO_MANY",
|
|
233
|
+
`${path} has more than ${ASK_LIMITS.choice.optionsMax} items.`,
|
|
234
|
+
`Remove items from ${path} until it has ${ASK_LIMITS.choice.optionsMax} or fewer.`
|
|
235
|
+
),
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
const firstIndexById = new Map<string, number>();
|
|
239
|
+
return (value as string[]).flatMap((id, index): AskErrorDraft[] => {
|
|
240
|
+
const segments = [...context.segments, index];
|
|
241
|
+
const firstIndex = firstIndexById.get(id);
|
|
242
|
+
if (firstIndex !== undefined) {
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
code: "DUPLICATE_ID",
|
|
246
|
+
fix: `List each option id in ${path} once.`,
|
|
247
|
+
message: `${quoteValue(id)} is already listed at ${formatAskPath([...context.segments, firstIndex])}.`,
|
|
248
|
+
segments,
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
firstIndexById.set(id, index);
|
|
253
|
+
return offered.has(id) ? [] : [notOffered(context, id, segments)];
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Checks one present value against its field: its JSON type, then the type's own rules (length,
|
|
259
|
+
* format, range, calendar date, offered options). Used for every answer value and every default,
|
|
260
|
+
* so a default is always a value the field accepts. Blank values are the caller's concern, except
|
|
261
|
+
* that a blank default string fails with `TOO_SHORT`.
|
|
262
|
+
*/
|
|
263
|
+
export const checkFormValue = ({
|
|
264
|
+
field,
|
|
265
|
+
isDefault,
|
|
266
|
+
segments,
|
|
267
|
+
value,
|
|
268
|
+
}: CheckContext & {value: unknown}): AskErrorDraft[] => {
|
|
269
|
+
const context: CheckContext = {field, isDefault, segments};
|
|
270
|
+
switch (field.type) {
|
|
271
|
+
case "boolean":
|
|
272
|
+
return typeof value === "boolean" ? [] : [mismatch(context)];
|
|
273
|
+
case "number":
|
|
274
|
+
return checkNumber(context, value);
|
|
275
|
+
case "select":
|
|
276
|
+
case "multiselect":
|
|
277
|
+
return checkOptions(context, value);
|
|
278
|
+
case "date":
|
|
279
|
+
case "time":
|
|
280
|
+
case "datetime": {
|
|
281
|
+
if (typeof value !== "string") {
|
|
282
|
+
return [mismatch(context)];
|
|
283
|
+
}
|
|
284
|
+
const {example, format, schema} = DATE_FORMATS[field.type];
|
|
285
|
+
if (schema.safeParse(value).success) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const path = formatAskPath(segments);
|
|
289
|
+
return [
|
|
290
|
+
draft(
|
|
291
|
+
context,
|
|
292
|
+
"INVALID_DATE",
|
|
293
|
+
`${path} ${quoteValue(value)} is not a real ${field.type} in ${format}.`,
|
|
294
|
+
`Write ${path} as ${format}, such as "${example}".`
|
|
295
|
+
),
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
default: {
|
|
299
|
+
if (typeof value !== "string") {
|
|
300
|
+
return [mismatch(context)];
|
|
301
|
+
}
|
|
302
|
+
const lengthDrafts = checkLength(context, value);
|
|
303
|
+
if (lengthDrafts.length > 0 || value.trim() === "") {
|
|
304
|
+
return lengthDrafts;
|
|
305
|
+
}
|
|
306
|
+
return checkStringFormat(context, value);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The values a form starts with: each field's `default`, keyed by field id. Fields without a
|
|
313
|
+
* default are left out.
|
|
314
|
+
*/
|
|
315
|
+
export const formDefaultValues = (input: FormAskInput): Record<string, FormValue> =>
|
|
316
|
+
Object.fromEntries(
|
|
317
|
+
input.fields.flatMap((field) =>
|
|
318
|
+
field.default === undefined ? [] : [[field.id, field.default as FormValue]]
|
|
319
|
+
)
|
|
320
|
+
);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {describe, expect, it} from "bun:test";
|
|
2
|
+
import {pendingAskListItemSchema, turnRequestSchema, turnResultSchema} from "./headless";
|
|
3
|
+
import {toSimpleCard} from "./simpleCard";
|
|
4
|
+
|
|
5
|
+
const SHAPES = "prompt, askResponse, or toolCallId with buttonId";
|
|
6
|
+
|
|
7
|
+
const requestIssues = (body: unknown): {code: string; message: string; path: string}[] => {
|
|
8
|
+
const result = turnRequestSchema.safeParse(body);
|
|
9
|
+
if (result.success) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return result.error.issues.map(({code, message, path}) => ({
|
|
13
|
+
code,
|
|
14
|
+
message,
|
|
15
|
+
path: path.join("."),
|
|
16
|
+
}));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CARD = toSimpleCard({
|
|
20
|
+
input: {
|
|
21
|
+
options: [
|
|
22
|
+
{id: "yes", label: "Yes"},
|
|
23
|
+
{id: "no", label: "No"},
|
|
24
|
+
],
|
|
25
|
+
prompt: "Ship it?",
|
|
26
|
+
select: "one",
|
|
27
|
+
},
|
|
28
|
+
kind: "choice",
|
|
29
|
+
toolCallId: "call_1",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("turnRequestSchema", () => {
|
|
33
|
+
it("accepts each request shape, with or without a surface", () => {
|
|
34
|
+
const bodies = [
|
|
35
|
+
{prompt: "Help me pick a plan"},
|
|
36
|
+
{prompt: "Help me pick a plan", surface: "compact"},
|
|
37
|
+
{
|
|
38
|
+
askResponse: {action: "accept", content: {selected: ["team"]}, toolCallId: "call_1"},
|
|
39
|
+
surface: "full",
|
|
40
|
+
},
|
|
41
|
+
{askResponse: {action: "decline", toolCallId: "call_1"}},
|
|
42
|
+
{askResponse: {action: "cancel", reason: "Changed my mind", toolCallId: "call_1"}},
|
|
43
|
+
{buttonId: "option:team", surface: "compact", toolCallId: "call_1"},
|
|
44
|
+
];
|
|
45
|
+
for (const body of bodies) {
|
|
46
|
+
expect(requestIssues(body)).toEqual([]);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("asks for one shape when the body has none", () => {
|
|
51
|
+
const expected = [{code: "custom", message: `Send one of ${SHAPES}.`, path: "prompt"}];
|
|
52
|
+
expect(requestIssues({})).toEqual(expected);
|
|
53
|
+
expect(requestIssues({surface: "compact"})).toEqual(expected);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("names every field of a body that mixes shapes", () => {
|
|
57
|
+
const message = `Send only one of ${SHAPES}.`;
|
|
58
|
+
expect(requestIssues({buttonId: "yes", prompt: "Hi", toolCallId: "call_1"})).toEqual([
|
|
59
|
+
{code: "custom", message, path: "buttonId"},
|
|
60
|
+
{code: "custom", message, path: "prompt"},
|
|
61
|
+
{code: "custom", message, path: "toolCallId"},
|
|
62
|
+
]);
|
|
63
|
+
expect(
|
|
64
|
+
requestIssues({askResponse: {action: "decline", toolCallId: "call_1"}, prompt: "Hi"})
|
|
65
|
+
).toEqual([
|
|
66
|
+
{code: "custom", message, path: "askResponse"},
|
|
67
|
+
{code: "custom", message, path: "prompt"},
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("asks for the missing half of a button press", () => {
|
|
72
|
+
expect(requestIssues({buttonId: "option:yes"})).toEqual([
|
|
73
|
+
{code: "custom", message: "Send toolCallId with buttonId.", path: "toolCallId"},
|
|
74
|
+
]);
|
|
75
|
+
expect(requestIssues({toolCallId: "call_1"})).toEqual([
|
|
76
|
+
{code: "custom", message: "Send buttonId with toolCallId.", path: "buttonId"},
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("rejects unknown fields, an unknown surface, empty strings, and an answer without toolCallId", () => {
|
|
81
|
+
expect(requestIssues({historyId: "abc", prompt: "Hi"}).map(({code}) => code)).toEqual([
|
|
82
|
+
"unrecognized_keys",
|
|
83
|
+
]);
|
|
84
|
+
expect(requestIssues({prompt: "Hi", surface: "watch"}).map(({path}) => path)).toEqual([
|
|
85
|
+
"surface",
|
|
86
|
+
]);
|
|
87
|
+
expect(requestIssues({prompt: ""}).map(({path}) => path)).toEqual(["prompt"]);
|
|
88
|
+
expect(requestIssues({buttonId: "", toolCallId: "call_1"}).map(({path}) => path)).toEqual([
|
|
89
|
+
"buttonId",
|
|
90
|
+
]);
|
|
91
|
+
expect(requestIssues({askResponse: {action: "decline"}}).map(({path}) => path)).toEqual([
|
|
92
|
+
"askResponse.toolCallId",
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("turnResultSchema", () => {
|
|
98
|
+
it("accepts a finished turn and a turn that paused on an ask", () => {
|
|
99
|
+
expect(turnResultSchema.safeParse({historyId: "h1", text: "Done."}).success).toBe(true);
|
|
100
|
+
expect(
|
|
101
|
+
turnResultSchema.safeParse({
|
|
102
|
+
historyId: "h1",
|
|
103
|
+
pendingAsk: {kind: "choice", simple: CARD, toolCallId: "call_1"},
|
|
104
|
+
text: "",
|
|
105
|
+
title: "Shipping",
|
|
106
|
+
}).success
|
|
107
|
+
).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("accepts a turn that failed after it started", () => {
|
|
111
|
+
expect(
|
|
112
|
+
turnResultSchema.safeParse({error: "The model is unavailable.", historyId: "h1", text: ""})
|
|
113
|
+
.success
|
|
114
|
+
).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("requires historyId and text", () => {
|
|
118
|
+
expect(turnResultSchema.safeParse({text: "Done."}).success).toBe(false);
|
|
119
|
+
expect(turnResultSchema.safeParse({historyId: "h1"}).success).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("pendingAskListItemSchema", () => {
|
|
124
|
+
const item = {
|
|
125
|
+
created: "2026-09-27T12:00:00.000Z",
|
|
126
|
+
historyId: "h1",
|
|
127
|
+
kind: "choice",
|
|
128
|
+
simple: CARD,
|
|
129
|
+
title: "Shipping",
|
|
130
|
+
toolCallId: "call_1",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
it("accepts a pending ask with its card", () => {
|
|
134
|
+
expect(pendingAskListItemSchema.safeParse(item).success).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("requires created to be an ISO 8601 UTC timestamp", () => {
|
|
138
|
+
expect(pendingAskListItemSchema.safeParse({...item, created: "yesterday"}).success).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("rejects a card that is not a simple card", () => {
|
|
142
|
+
expect(
|
|
143
|
+
pendingAskListItemSchema.safeParse({...item, simple: {...CARD, buttons: "none"}}).success
|
|
144
|
+
).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
});
|