@selesai/code 0.8.4 → 0.8.6

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.
Files changed (147) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +7 -1
  3. package/dist/core/slash-commands.js +1 -0
  4. package/dist/defaults/models.json +46 -55
  5. package/dist/defaults/settings.json +7 -8
  6. package/dist/extensions/agent-browser.test.ts +178 -2
  7. package/dist/extensions/agent-browser.ts +2 -0
  8. package/dist/extensions/copy-turn.test.ts +229 -0
  9. package/dist/extensions/copy-turn.ts +11 -0
  10. package/dist/extensions/grep-app/index.test.ts +356 -1
  11. package/dist/extensions/grep-app/index.ts +6 -1
  12. package/dist/extensions/handoff-new.test.ts +351 -161
  13. package/dist/extensions/handoff-new.ts +3 -0
  14. package/dist/extensions/inline-skills.test.ts +93 -0
  15. package/dist/extensions/inline-skills.ts +3 -0
  16. package/dist/extensions/pi-subagents/CHANGELOG.md +26 -3
  17. package/dist/extensions/pi-subagents/README.md +2 -0
  18. package/dist/extensions/pi-subagents/agents/builder.md +5 -2
  19. package/dist/extensions/pi-subagents/agents/commentator.md +1 -1
  20. package/dist/extensions/pi-subagents/docs/configuration.md +54 -2
  21. package/dist/extensions/pi-subagents/docs/observability.md +1 -1
  22. package/dist/extensions/pi-subagents/docs/tool-reference.md +1 -1
  23. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  24. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  25. package/dist/extensions/pi-subagents/package.json +1 -1
  26. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +1 -1
  27. package/dist/extensions/pi-subagents/src/agents/agents.ts +9 -3
  28. package/dist/extensions/pi-subagents/src/extension/config.ts +12 -0
  29. package/dist/extensions/pi-subagents/src/extension/doctor.ts +40 -0
  30. package/dist/extensions/pi-subagents/src/extension/index.ts +104 -14
  31. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +5 -0
  32. package/dist/extensions/pi-subagents/src/extension/rpc.ts +3 -9
  33. package/dist/extensions/pi-subagents/src/extension/schemas.ts +2 -2
  34. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -1
  35. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +4 -7
  36. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -4
  37. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +6 -2
  38. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +374 -0
  39. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +9 -5
  40. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +117 -29
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +12 -6
  42. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -1
  43. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +14 -5
  44. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +33 -15
  45. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +5 -2
  46. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +104 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +17 -3
  48. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +19 -1
  49. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +2 -2
  50. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +5 -2
  51. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +12 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -3
  53. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +84 -36
  54. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +2 -1
  55. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +37 -2
  56. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +135 -22
  57. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +12 -0
  58. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -4
  59. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +171 -0
  60. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +653 -196
  61. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +13 -4
  62. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +286 -0
  63. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +23 -0
  64. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
  65. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +44 -1
  66. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +280 -0
  67. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +4 -2
  68. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +19 -3
  69. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +17 -5
  70. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +71 -0
  71. package/dist/extensions/pi-subagents/src/shared/types.ts +108 -0
  72. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -1
  73. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +7 -5
  74. package/dist/extensions/pi-subagents/src/tui/fleet.ts +226 -13
  75. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +22 -5
  76. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +28 -0
  77. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +87 -0
  78. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +96 -6
  79. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +28 -5
  80. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +8 -5
  81. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +83 -2
  82. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +211 -15
  83. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +41 -3
  84. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +314 -10
  85. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +31 -1
  86. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +317 -0
  87. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +21 -2
  88. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +16 -1
  89. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +6 -1
  90. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +75 -4
  91. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +18 -0
  92. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +29 -1
  93. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +31 -5
  94. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +139 -1
  95. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
  96. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +103 -0
  97. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +22 -0
  98. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +2 -1
  99. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +171 -0
  100. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +30 -4
  101. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +14 -0
  102. package/dist/extensions/pi-subagents/test/unit/owned-process-tree.test.ts +69 -0
  103. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +83 -0
  104. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +2 -2
  105. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +55 -2
  106. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +12 -1
  107. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +7 -7
  108. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +121 -0
  109. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +9 -0
  110. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -3
  111. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +73 -0
  112. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +28 -0
  113. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +10 -1
  114. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +39 -0
  115. package/dist/extensions/pi-subagents/test/unit/timeout-defaults.test.ts +65 -0
  116. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +45 -0
  117. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +61 -0
  118. package/dist/extensions/ponytail/index.js +11 -0
  119. package/dist/extensions/ponytail/ponytail-config.cjs +2 -0
  120. package/dist/extensions/ponytail/ponytail-instructions.cjs +6 -0
  121. package/dist/extensions/ponytail/test/extension.test.js +274 -140
  122. package/dist/extensions/ponytail/test/helpers.test.js +280 -92
  123. package/dist/extensions/question/index.ts +18 -0
  124. package/dist/extensions/question/question-list.ts +22 -0
  125. package/dist/extensions/question/tests/batch.test.ts +103 -65
  126. package/dist/extensions/question/tests/helpers.test.ts +68 -35
  127. package/dist/extensions/question/tests/question-list.test.ts +506 -204
  128. package/dist/extensions/question/tests/row-layout.test.ts +180 -114
  129. package/dist/extensions/question/tests/schemas.test.ts +42 -19
  130. package/dist/extensions/question/tests/shortcuts.test.ts +106 -84
  131. package/dist/extensions/question/tests/wizard.test.ts +771 -50
  132. package/dist/extensions/rtk.test.ts +180 -1
  133. package/dist/extensions/tokenin-onboarding.ts +3 -0
  134. package/dist/extensions/undo.test.ts +720 -0
  135. package/dist/extensions/undo.ts +6 -0
  136. package/dist/extensions/web-agent-onboarding.test.ts +415 -0
  137. package/dist/extensions/workflow/extension.ts +3 -3
  138. package/dist/extensions/workflow/modes.ts +41 -19
  139. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  140. package/dist/modes/interactive/interactive-mode.js +48 -1
  141. package/dist/skills/pi-subagents/references/execution-controls.md +1 -1
  142. package/docs/plans/workflow-autoloop-reference.md +1 -2
  143. package/docs/plans/workflow-handoff-carryover.md +108 -0
  144. package/docs/quickstart.md +27 -43
  145. package/docs/settings.md +4 -0
  146. package/docs/usage.md +1 -0
  147. package/package.json +5 -1
@@ -1,75 +1,113 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
1
+ import { describe, expect, it } from "vitest";
3
2
 
4
3
  import { buildQuestionAnswers, prepareQuestions, saveDraftQuestion } from "../batch.ts";
5
4
  import type { DraftQuestionState } from "../types.ts";
6
5
 
7
- test("prepareQuestions: normalizes typed questions and generated ids", () => {
8
- const result = prepareQuestions([
9
- { type: "select", question: "Pick a color", options: [{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }] },
10
- { type: "text", id: "notes", label: "Notes", question: "Anything else?" },
11
- ]);
12
- assert.ok("questions" in result);
13
- if (!("questions" in result)) return;
14
- assert.deepEqual(result.questions, [
15
- {
16
- id: "q1",
17
- label: "Q1",
18
- question: "Pick a color",
19
- context: undefined,
6
+ describe("question batch helpers", () => {
7
+ it("prepareQuestions: normalizes typed questions and generated ids", () => {
8
+ const result = prepareQuestions([
9
+ { type: "select", question: "Pick a color", options: [{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }] },
10
+ { type: "text", id: "notes", label: "Notes", question: "Anything else?" },
11
+ ]);
12
+ expect("questions" in result).toBe(true);
13
+ if (!("questions" in result)) return;
14
+ expect(result.questions).toEqual([
15
+ {
16
+ id: "q1",
17
+ label: "Q1",
18
+ question: "Pick a color",
19
+ context: undefined,
20
+ type: "select",
21
+ options: [
22
+ { value: "red", label: "Red", description: undefined },
23
+ { value: "blue", label: "Blue", description: undefined },
24
+ ],
25
+ allowOther: false,
26
+ },
27
+ {
28
+ id: "notes",
29
+ label: "Notes",
30
+ question: "Anything else?",
31
+ context: undefined,
32
+ type: "text",
33
+ options: [],
34
+ allowOther: false,
35
+ },
36
+ ]);
37
+ });
38
+
39
+ it("prepareQuestions: rejects empty batches, generated-id collisions, and duplicate option values", () => {
40
+ expect(prepareQuestions([])).toEqual({ error: "questions must contain at least one question." });
41
+ expect((prepareQuestions([
42
+ { type: "text", id: "q2", question: "One" },
43
+ { type: "text", question: "Two" },
44
+ ]) as { error: string }).error).toMatch(/duplicate id: q2/);
45
+ expect((prepareQuestions([{
20
46
  type: "select",
21
- options: [
22
- { value: "red", label: "Red", description: undefined },
23
- { value: "blue", label: "Blue", description: undefined },
24
- ],
25
- allowOther: false,
26
- },
27
- {
28
- id: "notes",
29
- label: "Notes",
30
- question: "Anything else?",
31
- context: undefined,
32
- type: "text",
33
- options: [],
47
+ question: "Choose",
48
+ options: [{ value: "same", label: "A" }, { value: "same", label: "B" }],
49
+ }]) as { error: string }).error).toMatch(/duplicate value: same/);
50
+ });
51
+
52
+ it("prepareQuestions: rejects missing question, bad type, empty options, and bad option fields", () => {
53
+ expect((prepareQuestions([{ type: "text" }]) as { error: string }).error).toMatch(/non-empty question/);
54
+ expect((prepareQuestions([{ type: "radio", question: "X", options: [{ value: "a", label: "A" }] }]) as { error: string }).error).toMatch(/type must be select, multiselect, or text/);
55
+ expect((prepareQuestions([{ type: "select", question: "X", options: [] }]) as { error: string }).error).toMatch(/at least one option/);
56
+ expect((prepareQuestions([{ type: "select", question: "X", options: [{ value: "", label: "A" }] }]) as { error: string }).error).toMatch(/value and label must be non-empty/);
57
+ expect((prepareQuestions([{ type: "select", question: "X", options: [{ value: "a", label: "" }] }]) as { error: string }).error).toMatch(/value and label must be non-empty/);
58
+ });
59
+
60
+ it("prepareQuestions: trims id/label/context and defaults allowOther", () => {
61
+ const result = prepareQuestions([
62
+ { type: "multiselect", id: " m1 ", label: " Multi ", question: " Pick many ", context: " ctx ", options: [{ value: "a", label: "A", description: " desc " }] },
63
+ ]);
64
+ expect("questions" in result).toBe(true);
65
+ if (!("questions" in result)) return;
66
+ expect(result.questions[0]).toEqual({
67
+ id: "m1",
68
+ label: "Multi",
69
+ question: "Pick many",
70
+ context: "ctx",
71
+ type: "multiselect",
72
+ options: [{ value: "a", label: "A", description: "desc" }],
34
73
  allowOther: false,
35
- },
36
- ]);
37
- });
74
+ });
75
+ });
38
76
 
39
- test("prepareQuestions: rejects empty batches, generated-id collisions, and duplicate option values", () => {
40
- assert.deepEqual(prepareQuestions([]), { error: "questions must contain at least one question." });
41
- assert.match((prepareQuestions([
42
- { type: "text", id: "q2", question: "One" },
43
- { type: "text", question: "Two" },
44
- ]) as { error: string }).error, /duplicate id: q2/);
45
- assert.match((prepareQuestions([{
46
- type: "select",
47
- question: "Choose",
48
- options: [{ value: "same", label: "A" }, { value: "same", label: "B" }],
49
- }]) as { error: string }).error, /duplicate value: same/);
50
- });
77
+ it("saveDraftQuestion: clearing an answer while navigating back leaves it unanswered", () => {
78
+ const states = new Map<string, DraftQuestionState>([["q1", { status: "answered", response: { kind: "text", text: "old" } }]]);
79
+ saveDraftQuestion(states, "q1", null, false);
80
+ expect(states.has("q1")).toBe(false);
81
+ });
51
82
 
52
- test("saveDraftQuestion: clearing an answer while navigating back leaves it unanswered", () => {
53
- const states = new Map<string, DraftQuestionState>([["q1", { status: "answered", response: { kind: "text", text: "old" } }]]);
54
- saveDraftQuestion(states, "q1", null, false);
55
- assert.equal(states.has("q1"), false);
56
- });
83
+ it("saveDraftQuestion: blank answer with markBlankSkipped marks skipped", () => {
84
+ const states = new Map<string, DraftQuestionState>();
85
+ saveDraftQuestion(states, "q1", null, true);
86
+ expect(states.get("q1")).toEqual({ status: "skipped" });
87
+ });
88
+
89
+ it("saveDraftQuestion: response marks answered", () => {
90
+ const states = new Map<string, DraftQuestionState>();
91
+ saveDraftQuestion(states, "q1", { kind: "text", text: "answer" }, true);
92
+ expect(states.get("q1")).toEqual({ status: "answered", response: { kind: "text", text: "answer" } });
93
+ });
57
94
 
58
- test("buildQuestionAnswers: preserves answered, skipped, and unvisited states", () => {
59
- const prepared = prepareQuestions([
60
- { type: "text", id: "a", question: "A" },
61
- { type: "text", id: "b", question: "B" },
62
- { type: "text", id: "c", question: "C" },
63
- ]);
64
- assert.ok("questions" in prepared);
65
- if (!("questions" in prepared)) return;
66
- const states = new Map<string, DraftQuestionState>([
67
- ["a", { status: "answered", response: { kind: "text", text: "answer" } }],
68
- ["b", { status: "skipped" }],
69
- ]);
70
- assert.deepEqual(buildQuestionAnswers(prepared.questions, states), [
71
- { id: "a", status: "answered", response: { kind: "text", text: "answer" } },
72
- { id: "b", status: "skipped" },
73
- { id: "c", status: "unanswered" },
74
- ]);
95
+ it("buildQuestionAnswers: preserves answered, skipped, and unvisited states", () => {
96
+ const prepared = prepareQuestions([
97
+ { type: "text", id: "a", question: "A" },
98
+ { type: "text", id: "b", question: "B" },
99
+ { type: "text", id: "c", question: "C" },
100
+ ]);
101
+ expect("questions" in prepared).toBe(true);
102
+ if (!("questions" in prepared)) return;
103
+ const states = new Map<string, DraftQuestionState>([
104
+ ["a", { status: "answered", response: { kind: "text", text: "answer" } }],
105
+ ["b", { status: "skipped" }],
106
+ ]);
107
+ expect(buildQuestionAnswers(prepared.questions, states)).toEqual([
108
+ { id: "a", status: "answered", response: { kind: "text", text: "answer" } },
109
+ { id: "b", status: "skipped" },
110
+ { id: "c", status: "unanswered" },
111
+ ]);
112
+ });
75
113
  });
@@ -1,53 +1,86 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
1
+ import { describe, expect, it } from "vitest";
3
2
 
4
3
  import {
5
4
  createSelectionResponse,
6
5
  createTextResponse,
7
6
  formatQuestionResponse,
8
7
  formatResponseSummary,
8
+ formatOptionsForMessage,
9
9
  oneLine,
10
10
  previewText,
11
+ trimText,
11
12
  } from "../helpers.ts";
12
13
  import type { PreparedQuestion } from "../types.ts";
13
14
 
14
- test("createTextResponse trims edges and rejects blank text", () => {
15
- assert.equal(createTextResponse(" "), null);
16
- assert.deepEqual(createTextResponse(" hello\nworld "), { kind: "text", text: "hello\nworld" });
17
- });
15
+ describe("question helpers", () => {
16
+ it("trimText trims and returns undefined for blank", () => {
17
+ expect(trimText(" ")).toBeUndefined();
18
+ expect(trimText(undefined)).toBeUndefined();
19
+ expect(trimText(null)).toBeUndefined();
20
+ expect(trimText(" hello ")).toBe("hello");
21
+ });
18
22
 
19
- test("createSelectionResponse supports values plus Other and rejects empty", () => {
20
- assert.equal(createSelectionResponse([], " "), null);
21
- assert.deepEqual(createSelectionResponse([" a ", "b"], " other "), {
22
- kind: "selection",
23
- values: ["a", "b"],
24
- otherText: "other",
23
+ it("createTextResponse trims edges and rejects blank text", () => {
24
+ expect(createTextResponse(" ")).toBeNull();
25
+ expect(createTextResponse(undefined)).toBeNull();
26
+ expect(createTextResponse(" hello\nworld ")).toEqual({ kind: "text", text: "hello\nworld" });
25
27
  });
26
- });
27
28
 
28
- test("formatResponseSummary returns machine values", () => {
29
- assert.equal(formatResponseSummary({ kind: "selection", values: ["a", "b"], otherText: "c" }), "a, b, c");
30
- assert.equal(formatResponseSummary({ kind: "text", text: "hello" }), "hello");
31
- });
29
+ it("createSelectionResponse supports values plus Other and rejects empty", () => {
30
+ expect(createSelectionResponse([], " ")).toBeNull();
31
+ expect(createSelectionResponse([], undefined)).toBeNull();
32
+ expect(createSelectionResponse([" a ", "b"], " other ")).toEqual({
33
+ kind: "selection",
34
+ values: ["a", "b"],
35
+ otherText: "other",
36
+ });
37
+ expect(createSelectionResponse(["a"], undefined)).toEqual({ kind: "selection", values: ["a"] });
38
+ });
32
39
 
33
- test("formatQuestionResponse maps stable values to labels", () => {
34
- const question: PreparedQuestion = {
35
- id: "q1",
36
- label: "Q1",
37
- question: "Pick",
38
- type: "multiselect",
39
- options: [{ value: "a", label: "Alpha" }, { value: "b", label: "Beta" }],
40
- allowOther: true,
41
- };
42
- assert.equal(formatQuestionResponse(question, { kind: "selection", values: ["b"], otherText: "Gamma" }), "Beta, Other: Gamma");
43
- });
40
+ it("formatResponseSummary returns machine values", () => {
41
+ expect(formatResponseSummary({ kind: "selection", values: ["a", "b"], otherText: "c" })).toBe("a, b, c");
42
+ expect(formatResponseSummary({ kind: "selection", values: ["a"] })).toBe("a");
43
+ expect(formatResponseSummary({ kind: "text", text: "hello" })).toBe("hello");
44
+ });
44
45
 
45
- test("previewText bounds multiline summaries", () => {
46
- assert.equal(previewText("one\ntwo\nthree", 160, 2), "one\ntwo…");
47
- assert.equal(previewText("a".repeat(200), 10, 2), `${"a".repeat(9)}…`);
48
- });
46
+ it("formatQuestionResponse maps stable values to labels", () => {
47
+ const question: PreparedQuestion = {
48
+ id: "q1",
49
+ label: "Q1",
50
+ question: "Pick",
51
+ type: "multiselect",
52
+ options: [{ value: "a", label: "Alpha" }, { value: "b", label: "Beta" }],
53
+ allowOther: true,
54
+ };
55
+ expect(formatQuestionResponse(question, { kind: "selection", values: ["b"], otherText: "Gamma" })).toBe("Beta, Other: Gamma");
56
+ expect(formatQuestionResponse(question, { kind: "selection", values: ["unknown"] })).toBe("unknown");
57
+ expect(formatQuestionResponse(question, { kind: "text", text: "plain" })).toBe("plain");
58
+ });
59
+
60
+ it("formatOptionsForMessage numbers options with descriptions", () => {
61
+ expect(formatOptionsForMessage([
62
+ { value: "a", label: "Alpha" },
63
+ { value: "b", label: "Beta", description: "the beta" },
64
+ ])).toBe("1. Alpha\n2. Beta — the beta");
65
+ });
66
+
67
+ it("previewText bounds multiline summaries", () => {
68
+ expect(previewText("one\ntwo\nthree", 160, 2)).toBe("one\ntwo…");
69
+ expect(previewText("a".repeat(200), 10, 2)).toBe(`${"a".repeat(9)}…`);
70
+ expect(previewText("short", 160, 2)).toBe("short");
71
+ expect(previewText("", 160, 2)).toBe("");
72
+ });
73
+
74
+ it("oneLine collapses whitespace and truncates", () => {
75
+ expect(oneLine(" hello world ")).toBe("hello world");
76
+ expect(oneLine("a".repeat(20), 10)).toBe(`${"a".repeat(9)}…`);
77
+ expect(oneLine(undefined)).toBe("");
78
+ expect(oneLine(42)).toBe("42");
79
+ });
49
80
 
50
- test("oneLine collapses whitespace and truncates", () => {
51
- assert.equal(oneLine(" hello world "), "hello world");
52
- assert.equal(oneLine("a".repeat(20), 10), `${"a".repeat(9)}…`);
81
+ it("previewText handles single-line and exact-boundary text", () => {
82
+ expect(previewText("one line", 160, 2)).toBe("one line");
83
+ expect(previewText("a".repeat(10), 10, 2)).toBe("a".repeat(10));
84
+ expect(previewText("a".repeat(11), 10, 2)).toBe(`${"a".repeat(9)}…`);
85
+ });
53
86
  });