gsd-pi 2.19.0 → 2.20.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.
Files changed (249) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  9. package/dist/resources/extensions/gsd/auto.ts +424 -30
  10. package/dist/resources/extensions/gsd/commands.ts +518 -36
  11. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  12. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  14. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  15. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  16. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  17. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  20. package/dist/resources/extensions/gsd/export.ts +81 -2
  21. package/dist/resources/extensions/gsd/files.ts +39 -9
  22. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  23. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  24. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  25. package/dist/resources/extensions/gsd/history.ts +0 -1
  26. package/dist/resources/extensions/gsd/index.ts +277 -1
  27. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  28. package/dist/resources/extensions/gsd/metrics.ts +39 -3
  29. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  30. package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  31. package/dist/resources/extensions/gsd/preferences.ts +125 -150
  32. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  33. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  34. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  35. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  36. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  37. package/dist/resources/extensions/gsd/quick.ts +156 -0
  38. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  39. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  40. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  41. package/dist/resources/extensions/gsd/state.ts +30 -0
  42. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  43. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  44. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  45. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  46. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  48. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  49. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  50. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  51. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  52. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  53. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  54. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  55. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  56. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  57. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  58. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  59. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  60. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  61. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  62. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  63. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  64. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  65. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  66. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  67. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  68. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  69. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  70. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  71. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  72. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  73. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  74. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  75. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  76. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  77. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  78. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  79. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  80. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  81. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  82. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  83. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  84. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  85. package/dist/resources/extensions/gsd/types.ts +29 -0
  86. package/dist/resources/extensions/gsd/undo.ts +0 -1
  87. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  88. package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
  89. package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  90. package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
  91. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  92. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  93. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  94. package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  95. package/dist/resources/extensions/remote-questions/format.ts +154 -8
  96. package/dist/resources/extensions/remote-questions/manager.ts +9 -7
  97. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  98. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  99. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  100. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  101. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  102. package/dist/resources/extensions/voice/index.ts +4 -3
  103. package/package.json +1 -1
  104. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  106. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  109. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  117. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  124. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  126. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  128. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  130. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  133. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  136. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  138. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  141. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  142. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  143. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  145. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  146. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  147. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  148. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  149. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  150. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  151. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  153. package/src/resources/extensions/google-search/index.ts +164 -47
  154. package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
  155. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  156. package/src/resources/extensions/gsd/auto.ts +424 -30
  157. package/src/resources/extensions/gsd/commands.ts +518 -36
  158. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  159. package/src/resources/extensions/gsd/context-store.ts +195 -0
  160. package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
  161. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  162. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  163. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  164. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  165. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  166. package/src/resources/extensions/gsd/doctor.ts +283 -2
  167. package/src/resources/extensions/gsd/export.ts +81 -2
  168. package/src/resources/extensions/gsd/files.ts +39 -9
  169. package/src/resources/extensions/gsd/git-service.ts +6 -0
  170. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  171. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  172. package/src/resources/extensions/gsd/history.ts +0 -1
  173. package/src/resources/extensions/gsd/index.ts +277 -1
  174. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  175. package/src/resources/extensions/gsd/metrics.ts +39 -3
  176. package/src/resources/extensions/gsd/notifications.ts +0 -1
  177. package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
  178. package/src/resources/extensions/gsd/preferences.ts +125 -150
  179. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  180. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  181. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  182. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  183. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  184. package/src/resources/extensions/gsd/quick.ts +156 -0
  185. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  186. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  187. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  188. package/src/resources/extensions/gsd/state.ts +30 -0
  189. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  190. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  191. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  192. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  193. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  194. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  195. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  196. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  197. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  198. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  199. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  200. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  201. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  202. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  203. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  204. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  205. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  206. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  207. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  208. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  209. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  211. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  212. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  213. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  214. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  215. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  216. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  217. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  218. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
  219. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  220. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  221. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  222. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  223. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  224. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  225. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  226. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
  227. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  228. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
  229. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  230. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  231. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  232. package/src/resources/extensions/gsd/types.ts +29 -0
  233. package/src/resources/extensions/gsd/undo.ts +0 -1
  234. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  235. package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
  236. package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
  237. package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
  238. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  239. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  240. package/src/resources/extensions/remote-questions/config.ts +4 -2
  241. package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
  242. package/src/resources/extensions/remote-questions/format.ts +154 -8
  243. package/src/resources/extensions/remote-questions/manager.ts +9 -7
  244. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  245. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  246. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  247. package/src/resources/extensions/remote-questions/types.ts +2 -1
  248. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  249. package/src/resources/extensions/voice/index.ts +4 -3
@@ -0,0 +1,385 @@
1
+ // prompt-db: Tests for DB-aware inline helpers (inlineDecisionsFromDb, inlineRequirementsFromDb, inlineProjectFromDb)
2
+ //
3
+ // Validates:
4
+ // (a) DB-aware helpers return scoped content when DB has data
5
+ // (b) Helpers fall back to non-null output when DB unavailable
6
+ // (c) Scoped filtering actually reduces content
7
+
8
+ import { createTestContext } from './test-helpers.ts';
9
+ import {
10
+ openDatabase,
11
+ closeDatabase,
12
+ isDbAvailable,
13
+ insertDecision,
14
+ insertRequirement,
15
+ insertArtifact,
16
+ } from '../gsd-db.ts';
17
+ import {
18
+ queryDecisions,
19
+ queryRequirements,
20
+ queryProject,
21
+ formatDecisionsForPrompt,
22
+ formatRequirementsForPrompt,
23
+ } from '../context-store.ts';
24
+
25
+ const { assertEq, assertTrue, assertMatch, assertNoMatch, report } = createTestContext();
26
+
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+ // prompt-db: DB-aware decisions helper returns scoped content
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+
31
+ console.log('\n=== prompt-db: scoped decisions from DB ===');
32
+ {
33
+ openDatabase(':memory:');
34
+
35
+ // Insert decisions across 3 milestones
36
+ for (let i = 1; i <= 10; i++) {
37
+ const milestoneNum = ((i - 1) % 3) + 1;
38
+ insertDecision({
39
+ id: `D${String(i).padStart(3, '0')}`,
40
+ when_context: `M00${milestoneNum}/S01`,
41
+ scope: 'architecture',
42
+ decision: `decision ${i}`,
43
+ choice: `choice ${i}`,
44
+ rationale: `rationale ${i}`,
45
+ revisable: 'yes',
46
+ superseded_by: null,
47
+ });
48
+ }
49
+
50
+ // Query scoped to M001
51
+ const m001Decisions = queryDecisions({ milestoneId: 'M001' });
52
+ assertTrue(m001Decisions.length > 0, 'M001 decisions should exist');
53
+ assertTrue(m001Decisions.length < 10, `scoped query should return fewer than 10 (got ${m001Decisions.length})`);
54
+
55
+ // Verify all returned decisions are for M001
56
+ for (const d of m001Decisions) {
57
+ assertMatch(d.when_context, /M001/, `decision ${d.id} should be for M001`);
58
+ }
59
+
60
+ // Format and verify wrapping
61
+ const formatted = formatDecisionsForPrompt(m001Decisions);
62
+ assertTrue(formatted.length > 0, 'formatted decisions should be non-empty');
63
+ assertMatch(formatted, /\| # \| When \| Scope/, 'formatted decisions have table header');
64
+
65
+ // Verify the expected wrapper format that inlineDecisionsFromDb would produce
66
+ const wrapped = `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${formatted}`;
67
+ assertMatch(wrapped, /^### Decisions/, 'wrapped decisions start with ### Decisions');
68
+ assertMatch(wrapped, /Source:.*DECISIONS\.md/, 'wrapped decisions have source path');
69
+
70
+ closeDatabase();
71
+ }
72
+
73
+ // ═══════════════════════════════════════════════════════════════════════════
74
+ // prompt-db: DB-aware requirements helper returns scoped content
75
+ // ═══════════════════════════════════════════════════════════════════════════
76
+
77
+ console.log('\n=== prompt-db: scoped requirements from DB ===');
78
+ {
79
+ openDatabase(':memory:');
80
+
81
+ // Insert requirements across different slices
82
+ insertRequirement({
83
+ id: 'R001', class: 'functional', status: 'active',
84
+ description: 'feature A', why: 'needed', source: 'M001', primary_owner: 'S01',
85
+ supporting_slices: '', validation: 'test', notes: '', full_content: '',
86
+ superseded_by: null,
87
+ });
88
+ insertRequirement({
89
+ id: 'R002', class: 'functional', status: 'active',
90
+ description: 'feature B', why: 'needed', source: 'M001', primary_owner: 'S02',
91
+ supporting_slices: 'S01', validation: 'test', notes: '', full_content: '',
92
+ superseded_by: null,
93
+ });
94
+ insertRequirement({
95
+ id: 'R003', class: 'functional', status: 'active',
96
+ description: 'feature C', why: 'needed', source: 'M001', primary_owner: 'S03',
97
+ supporting_slices: '', validation: 'test', notes: '', full_content: '',
98
+ superseded_by: null,
99
+ });
100
+
101
+ // Query scoped to S01 — should get R001 (primary) and R002 (supporting)
102
+ const s01Reqs = queryRequirements({ sliceId: 'S01' });
103
+ assertEq(s01Reqs.length, 2, 'S01 requirements should be 2 (primary + supporting)');
104
+ const ids = s01Reqs.map(r => r.id).sort();
105
+ assertEq(ids, ['R001', 'R002'], 'S01 owns R001 and supports R002');
106
+
107
+ // Unscoped query returns all 3
108
+ const allReqs = queryRequirements();
109
+ assertEq(allReqs.length, 3, 'unscoped requirements should return all 3');
110
+
111
+ // Format and verify wrapping
112
+ const formatted = formatRequirementsForPrompt(s01Reqs);
113
+ assertTrue(formatted.length > 0, 'formatted requirements should be non-empty');
114
+ assertMatch(formatted, /### R001/, 'formatted requirements include R001');
115
+ assertMatch(formatted, /### R002/, 'formatted requirements include R002');
116
+ assertNoMatch(formatted, /### R003/, 'formatted requirements exclude R003');
117
+
118
+ // Verify the expected wrapper format that inlineRequirementsFromDb would produce
119
+ const wrapped = `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${formatted}`;
120
+ assertMatch(wrapped, /^### Requirements/, 'wrapped requirements start with ### Requirements');
121
+ assertMatch(wrapped, /Source:.*REQUIREMENTS\.md/, 'wrapped requirements have source path');
122
+
123
+ closeDatabase();
124
+ }
125
+
126
+ // ═══════════════════════════════════════════════════════════════════════════
127
+ // prompt-db: DB-aware project helper returns content from DB
128
+ // ═══════════════════════════════════════════════════════════════════════════
129
+
130
+ console.log('\n=== prompt-db: project content from DB ===');
131
+ {
132
+ openDatabase(':memory:');
133
+
134
+ insertArtifact({
135
+ path: 'PROJECT.md',
136
+ artifact_type: 'project',
137
+ milestone_id: null,
138
+ slice_id: null,
139
+ task_id: null,
140
+ full_content: '# Test Project\n\nThis is the project description.',
141
+ });
142
+
143
+ const content = queryProject();
144
+ assertEq(content, '# Test Project\n\nThis is the project description.', 'queryProject returns content');
145
+
146
+ // Verify the expected wrapper format that inlineProjectFromDb would produce
147
+ const wrapped = `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${content}`;
148
+ assertMatch(wrapped, /^### Project/, 'wrapped project starts with ### Project');
149
+ assertMatch(wrapped, /Source:.*PROJECT\.md/, 'wrapped project has source path');
150
+ assertMatch(wrapped, /# Test Project/, 'wrapped project includes content');
151
+
152
+ closeDatabase();
153
+ }
154
+
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+ // prompt-db: fallback when DB unavailable
157
+ // ═══════════════════════════════════════════════════════════════════════════
158
+
159
+ console.log('\n=== prompt-db: fallback when DB unavailable ===');
160
+ {
161
+ closeDatabase();
162
+ assertTrue(!isDbAvailable(), 'DB should not be available');
163
+
164
+ // queryDecisions returns [] when DB closed — helper would fall back
165
+ const decisions = queryDecisions({ milestoneId: 'M001' });
166
+ assertEq(decisions, [], 'queryDecisions returns [] when DB closed');
167
+
168
+ // queryRequirements returns [] when DB closed — helper would fall back
169
+ const requirements = queryRequirements({ sliceId: 'S01' });
170
+ assertEq(requirements, [], 'queryRequirements returns [] when DB closed');
171
+
172
+ // queryProject returns null when DB closed — helper would fall back
173
+ const project = queryProject();
174
+ assertEq(project, null, 'queryProject returns null when DB closed');
175
+
176
+ // formatDecisionsForPrompt returns '' for empty input
177
+ const formatted = formatDecisionsForPrompt([]);
178
+ assertEq(formatted, '', 'formatDecisionsForPrompt returns empty for empty input');
179
+
180
+ // formatRequirementsForPrompt returns '' for empty input
181
+ const formattedReqs = formatRequirementsForPrompt([]);
182
+ assertEq(formattedReqs, '', 'formatRequirementsForPrompt returns empty for empty input');
183
+ }
184
+
185
+ // ═══════════════════════════════════════════════════════════════════════════
186
+ // prompt-db: scoped filtering reduces content vs unscoped
187
+ // ═══════════════════════════════════════════════════════════════════════════
188
+
189
+ console.log('\n=== prompt-db: scoped filtering reduces content ===');
190
+ {
191
+ openDatabase(':memory:');
192
+
193
+ // Insert 10 decisions across 3 milestones
194
+ for (let i = 1; i <= 10; i++) {
195
+ const milestoneNum = ((i - 1) % 3) + 1;
196
+ insertDecision({
197
+ id: `D${String(i).padStart(3, '0')}`,
198
+ when_context: `M00${milestoneNum}/S01`,
199
+ scope: 'architecture',
200
+ decision: `decision ${i} with some lengthy description for token measurement`,
201
+ choice: `choice ${i}`,
202
+ rationale: `rationale ${i} with additional context`,
203
+ revisable: 'yes',
204
+ superseded_by: null,
205
+ });
206
+ }
207
+
208
+ const allDecisions = queryDecisions();
209
+ const m001Decisions = queryDecisions({ milestoneId: 'M001' });
210
+
211
+ assertEq(allDecisions.length, 10, 'unscoped returns all 10 decisions');
212
+ assertTrue(m001Decisions.length < 10, `M001-scoped returns fewer than 10 (got ${m001Decisions.length})`);
213
+ assertTrue(m001Decisions.length > 0, 'M001-scoped returns at least 1');
214
+
215
+ // Format both and compare sizes — scoped should be shorter
216
+ const allFormatted = formatDecisionsForPrompt(allDecisions);
217
+ const scopedFormatted = formatDecisionsForPrompt(m001Decisions);
218
+
219
+ assertTrue(
220
+ scopedFormatted.length < allFormatted.length,
221
+ `scoped content (${scopedFormatted.length} chars) should be shorter than unscoped (${allFormatted.length} chars)`,
222
+ );
223
+
224
+ // Insert requirements across 4 slices
225
+ for (let i = 1; i <= 8; i++) {
226
+ const sliceNum = ((i - 1) % 4) + 1;
227
+ insertRequirement({
228
+ id: `R${String(i).padStart(3, '0')}`,
229
+ class: 'functional',
230
+ status: 'active',
231
+ description: `requirement ${i} with detailed description`,
232
+ why: `justification ${i}`,
233
+ source: 'M001',
234
+ primary_owner: `S0${sliceNum}`,
235
+ supporting_slices: '',
236
+ validation: `validation ${i}`,
237
+ notes: '',
238
+ full_content: '',
239
+ superseded_by: null,
240
+ });
241
+ }
242
+
243
+ const allReqs = queryRequirements();
244
+ const s01Reqs = queryRequirements({ sliceId: 'S01' });
245
+
246
+ assertEq(allReqs.length, 8, 'unscoped returns all 8 requirements');
247
+ assertTrue(s01Reqs.length < 8, `S01-scoped returns fewer than 8 (got ${s01Reqs.length})`);
248
+ assertTrue(s01Reqs.length > 0, 'S01-scoped returns at least 1');
249
+
250
+ const allReqsFormatted = formatRequirementsForPrompt(allReqs);
251
+ const scopedReqsFormatted = formatRequirementsForPrompt(s01Reqs);
252
+
253
+ assertTrue(
254
+ scopedReqsFormatted.length < allReqsFormatted.length,
255
+ `scoped requirements (${scopedReqsFormatted.length} chars) should be shorter than unscoped (${allReqsFormatted.length} chars)`,
256
+ );
257
+
258
+ closeDatabase();
259
+ }
260
+
261
+ // ═══════════════════════════════════════════════════════════════════════════
262
+ // prompt-db: DB helpers produce correct wrapper format
263
+ // ═══════════════════════════════════════════════════════════════════════════
264
+
265
+ console.log('\n=== prompt-db: DB helpers wrapper format matches expected pattern ===');
266
+ {
267
+ openDatabase(':memory:');
268
+
269
+ insertDecision({
270
+ id: 'D001', when_context: 'M001/S01', scope: 'architecture',
271
+ decision: 'use SQLite', choice: 'better-sqlite3', rationale: 'fast',
272
+ revisable: 'yes', superseded_by: null,
273
+ });
274
+
275
+ insertRequirement({
276
+ id: 'R001', class: 'functional', status: 'active',
277
+ description: 'persist decisions', why: 'memory', source: 'M001',
278
+ primary_owner: 'S01', supporting_slices: '', validation: 'test',
279
+ notes: '', full_content: '', superseded_by: null,
280
+ });
281
+
282
+ insertArtifact({
283
+ path: 'PROJECT.md',
284
+ artifact_type: 'project',
285
+ milestone_id: null,
286
+ slice_id: null,
287
+ task_id: null,
288
+ full_content: '# Project Name\n\nDescription.',
289
+ });
290
+
291
+ // Simulate what inlineDecisionsFromDb does
292
+ const decisions = queryDecisions({ milestoneId: 'M001' });
293
+ assertTrue(decisions.length === 1, 'got 1 decision for M001');
294
+ const dFormatted = formatDecisionsForPrompt(decisions);
295
+ const dWrapped = `### Decisions\nSource: \`.gsd/DECISIONS.md\`\n\n${dFormatted}`;
296
+ assertMatch(dWrapped, /^### Decisions\nSource: `.gsd\/DECISIONS\.md`\n\n\| #/, 'decisions wrapper format correct');
297
+
298
+ // Simulate what inlineRequirementsFromDb does
299
+ const reqs = queryRequirements({ sliceId: 'S01' });
300
+ assertTrue(reqs.length === 1, 'got 1 requirement for S01');
301
+ const rFormatted = formatRequirementsForPrompt(reqs);
302
+ const rWrapped = `### Requirements\nSource: \`.gsd/REQUIREMENTS.md\`\n\n${rFormatted}`;
303
+ assertMatch(rWrapped, /^### Requirements\nSource: `.gsd\/REQUIREMENTS\.md`\n\n### R001/, 'requirements wrapper format correct');
304
+
305
+ // Simulate what inlineProjectFromDb does
306
+ const project = queryProject();
307
+ assertTrue(project !== null, 'project content exists');
308
+ const pWrapped = `### Project\nSource: \`.gsd/PROJECT.md\`\n\n${project}`;
309
+ assertMatch(pWrapped, /^### Project\nSource: `.gsd\/PROJECT\.md`\n\n# Project Name/, 'project wrapper format correct');
310
+
311
+ closeDatabase();
312
+ }
313
+
314
+ // ═══════════════════════════════════════════════════════════════════════════
315
+ // prompt-db: re-import updates DB when source markdown changes
316
+ // ═══════════════════════════════════════════════════════════════════════════
317
+
318
+ import { mkdtempSync, writeFileSync, mkdirSync } from 'node:fs';
319
+ import { join } from 'node:path';
320
+ import { tmpdir } from 'node:os';
321
+ import { migrateFromMarkdown } from '../md-importer.ts';
322
+
323
+ console.log('\n=== prompt-db: re-import updates DB when source markdown changes ===');
324
+ {
325
+ // Create a temp dir simulating a project with .gsd/DECISIONS.md
326
+ const tmpDir = mkdtempSync(join(tmpdir(), 'prompt-db-reimport-'));
327
+ const gsdDir = join(tmpDir, '.gsd');
328
+ mkdirSync(gsdDir, { recursive: true });
329
+
330
+ // Write initial DECISIONS.md with 2 decisions
331
+ const initialDecisions = `# Decisions Register
332
+
333
+ | # | When | Scope | Decision | Choice | Rationale | Revisable? |
334
+ |---|------|-------|----------|--------|-----------|------------|
335
+ | D001 | M001/S01 | architecture | use SQLite | better-sqlite3 | fast and embedded | yes |
336
+ | D002 | M001/S01 | tooling | use vitest | vitest | modern test runner | yes |
337
+ `;
338
+ writeFileSync(join(gsdDir, 'DECISIONS.md'), initialDecisions);
339
+
340
+ // Open in-memory DB and do initial import
341
+ openDatabase(':memory:');
342
+ migrateFromMarkdown(tmpDir);
343
+
344
+ // Verify initial state: 2 decisions
345
+ const initial = queryDecisions();
346
+ assertEq(initial.length, 2, 're-import: initial import has 2 decisions');
347
+ const initialIds = initial.map(d => d.id).sort();
348
+ assertEq(initialIds, ['D001', 'D002'], 're-import: initial decisions are D001, D002');
349
+
350
+ // Now "the LLM modifies DECISIONS.md" — add a third decision
351
+ const updatedDecisions = `# Decisions Register
352
+
353
+ | # | When | Scope | Decision | Choice | Rationale | Revisable? |
354
+ |---|------|-------|----------|--------|-----------|------------|
355
+ | D001 | M001/S01 | architecture | use SQLite | better-sqlite3 | fast and embedded | yes |
356
+ | D002 | M001/S01 | tooling | use vitest | vitest | modern test runner | yes |
357
+ | D003 | M001/S02 | runtime | dynamic imports | D014 pattern | lazy loading | yes |
358
+ `;
359
+ writeFileSync(join(gsdDir, 'DECISIONS.md'), updatedDecisions);
360
+
361
+ // Re-import (simulating what handleAgentEnd does)
362
+ migrateFromMarkdown(tmpDir);
363
+
364
+ // Verify DB now has 3 decisions
365
+ const afterReimport = queryDecisions();
366
+ assertEq(afterReimport.length, 3, 're-import: after re-import has 3 decisions');
367
+ const afterIds = afterReimport.map(d => d.id).sort();
368
+ assertEq(afterIds, ['D001', 'D002', 'D003'], 're-import: decisions are D001, D002, D003');
369
+
370
+ // Verify the new decision has correct data
371
+ const d003 = afterReimport.find(d => d.id === 'D003');
372
+ assertTrue(d003 !== undefined, 're-import: D003 exists');
373
+ assertEq(d003!.when_context, 'M001/S02', 're-import: D003 when_context is M001/S02');
374
+ assertEq(d003!.scope, 'runtime', 're-import: D003 scope is runtime');
375
+ assertEq(d003!.choice, 'D014 pattern', 're-import: D003 choice is D014 pattern');
376
+
377
+ // Verify scoped query picks up the new decision
378
+ const m001Scoped = queryDecisions({ milestoneId: 'M001' });
379
+ assertTrue(m001Scoped.length === 3, 're-import: all 3 decisions are for M001');
380
+
381
+ closeDatabase();
382
+ }
383
+
384
+ // ─── Final Report ──────────────────────────────────────────────────────────
385
+ report();
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { join, dirname } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { parseSlackReply, parseDiscordResponse, formatForDiscord } from "../../remote-questions/format.ts";
6
+ import { parseSlackReply, parseDiscordResponse, formatForDiscord, formatForSlack, parseSlackReactionResponse, formatForTelegram, parseTelegramResponse } from "../../remote-questions/format.ts";
7
7
  import { resolveRemoteConfig, isValidChannelId } from "../../remote-questions/config.ts";
8
8
  import { sanitizeError } from "../../remote-questions/manager.ts";
9
9
 
@@ -94,6 +94,21 @@ test("parseDiscordResponse rejects multi-question reaction parsing", () => {
94
94
  assert.match(String(result.answers.second.user_note), /single-question prompts/i);
95
95
  });
96
96
 
97
+ test("parseSlackReactionResponse handles single-question reactions", () => {
98
+ const result = parseSlackReactionResponse(["two"], [{
99
+ id: "choice",
100
+ header: "Choice",
101
+ question: "Pick one",
102
+ allowMultiple: false,
103
+ options: [
104
+ { label: "Alpha", description: "A" },
105
+ { label: "Beta", description: "B" },
106
+ ],
107
+ }]);
108
+
109
+ assert.deepEqual(result, { answers: { choice: { answers: ["Beta"] } } });
110
+ });
111
+
97
112
  test("parseSlackReply truncates user_note longer than 500 chars", () => {
98
113
  const longText = "x".repeat(600);
99
114
  const result = parseSlackReply(longText, [{
@@ -189,6 +204,65 @@ test("formatForDiscord includes context source in footer when present", () => {
189
204
  assert.ok(embeds[0].footer?.text.includes("auto-mode-dispatch"), "footer should include context source");
190
205
  });
191
206
 
207
+ test("formatForSlack includes context source when present", () => {
208
+ const blocks = formatForSlack({
209
+ id: "slack-1",
210
+ channel: "slack",
211
+ createdAt: Date.now(),
212
+ timeoutAt: Date.now() + 60000,
213
+ pollIntervalMs: 5000,
214
+ context: { source: "ask_user_questions" },
215
+ questions: [{
216
+ id: "q1",
217
+ header: "Confirm",
218
+ question: "Proceed?",
219
+ options: [
220
+ { label: "Yes", description: "Continue" },
221
+ { label: "No", description: "Stop" },
222
+ ],
223
+ allowMultiple: false,
224
+ }],
225
+ });
226
+
227
+ const sourceBlock = blocks.find((block) => block.type === "context" && block.elements?.some((el) => el.text.includes("Source:")));
228
+ assert.ok(sourceBlock, "Slack blocks should include a context source block");
229
+ });
230
+
231
+ test("formatForSlack multi-question prompts explain semicolon and newline reply format", () => {
232
+ const blocks = formatForSlack({
233
+ id: "slack-2",
234
+ channel: "slack",
235
+ createdAt: Date.now(),
236
+ timeoutAt: Date.now() + 60000,
237
+ pollIntervalMs: 5000,
238
+ questions: [
239
+ {
240
+ id: "q1",
241
+ header: "First",
242
+ question: "Pick one",
243
+ options: [
244
+ { label: "Alpha", description: "A" },
245
+ { label: "Beta", description: "B" },
246
+ ],
247
+ allowMultiple: false,
248
+ },
249
+ {
250
+ id: "q2",
251
+ header: "Second",
252
+ question: "Explain",
253
+ options: [
254
+ { label: "Gamma", description: "G" },
255
+ { label: "Delta", description: "D" },
256
+ ],
257
+ allowMultiple: false,
258
+ },
259
+ ],
260
+ });
261
+
262
+ const instructionBlock = blocks.find((block) => block.type === "context" && block.elements?.some((el) => el.text.includes("one line per question")));
263
+ assert.ok(instructionBlock, "Slack multi-question prompts should explain one-line or semicolon reply format");
264
+ });
265
+
192
266
  test("formatForDiscord omits source from footer when context is absent", () => {
193
267
  const prompt = {
194
268
  id: "test-2",
@@ -356,6 +430,27 @@ test("DiscordAdapter source-level: acknowledgeAnswer method exists", () => {
356
430
  assert.ok(adapterSrc.includes("✅"), "should use checkmark emoji for acknowledgement");
357
431
  });
358
432
 
433
+ test("SlackAdapter source-level: supports reaction polling and acknowledgement", () => {
434
+ const adapterSrc = readFileSync(
435
+ join(__dirname, "..", "..", "remote-questions", "slack-adapter.ts"),
436
+ "utf-8",
437
+ );
438
+ assert.ok(adapterSrc.includes("reactions.get"), "should poll Slack reactions");
439
+ assert.ok(adapterSrc.includes("reactions.add"), "should add Slack reactions");
440
+ assert.ok(adapterSrc.includes("async acknowledgeAnswer"), "should acknowledge Slack answers");
441
+ assert.ok(adapterSrc.includes("white_check_mark"), "should use a checkmark acknowledgement reaction");
442
+ });
443
+
444
+ test("Slack setup source-level: offers channel picker with manual fallback", () => {
445
+ const commandSrc = readFileSync(
446
+ join(__dirname, "..", "..", "remote-questions", "remote-command.ts"),
447
+ "utf-8",
448
+ );
449
+ assert.ok(commandSrc.includes("users.conversations"), "Slack setup should query Slack channels");
450
+ assert.ok(commandSrc.includes("Select a Slack channel"), "Slack setup should present a channel picker");
451
+ assert.ok(commandSrc.includes("Enter channel ID manually"), "Slack setup should preserve manual fallback");
452
+ });
453
+
359
454
  test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
360
455
  const adapterSrc = readFileSync(
361
456
  join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),
@@ -369,6 +464,172 @@ test("DiscordAdapter source-level: resolves guild ID for message URLs", () => {
369
464
  );
370
465
  });
371
466
 
467
+ // ═══════════════════════════════════════════════════════════════════════════
468
+ // Telegram Tests
469
+ // ═══════════════════════════════════════════════════════════════════════════
470
+
471
+ test("formatForTelegram single-question produces inline keyboard", () => {
472
+ const prompt = {
473
+ id: "tg-1",
474
+ channel: "telegram" as const,
475
+ createdAt: Date.now(),
476
+ timeoutAt: Date.now() + 60000,
477
+ pollIntervalMs: 5000,
478
+ questions: [{
479
+ id: "q1",
480
+ header: "Confirm",
481
+ question: "Proceed?",
482
+ options: [
483
+ { label: "Yes", description: "Continue" },
484
+ { label: "No", description: "Stop" },
485
+ ],
486
+ allowMultiple: false,
487
+ }],
488
+ };
489
+
490
+ const msg = formatForTelegram(prompt);
491
+ assert.equal(msg.parse_mode, "HTML");
492
+ assert.ok(msg.text.includes("<b>GSD needs your input</b>"));
493
+ assert.ok(msg.text.includes("<b>Confirm</b>"));
494
+ assert.ok(msg.reply_markup, "single-question should have inline keyboard");
495
+ assert.equal(msg.reply_markup!.inline_keyboard.length, 2, "should have 2 button rows");
496
+ assert.equal(msg.reply_markup!.inline_keyboard[0][0].callback_data, "tg-1:0");
497
+ assert.equal(msg.reply_markup!.inline_keyboard[1][0].callback_data, "tg-1:1");
498
+ });
499
+
500
+ test("formatForTelegram multi-question omits inline keyboard", () => {
501
+ const prompt = {
502
+ id: "tg-2",
503
+ channel: "telegram" as const,
504
+ createdAt: Date.now(),
505
+ timeoutAt: Date.now() + 60000,
506
+ pollIntervalMs: 5000,
507
+ questions: [
508
+ {
509
+ id: "q1",
510
+ header: "First",
511
+ question: "Pick",
512
+ options: [{ label: "A", description: "a" }],
513
+ allowMultiple: false,
514
+ },
515
+ {
516
+ id: "q2",
517
+ header: "Second",
518
+ question: "Pick",
519
+ options: [{ label: "B", description: "b" }],
520
+ allowMultiple: false,
521
+ },
522
+ ],
523
+ };
524
+
525
+ const msg = formatForTelegram(prompt);
526
+ assert.equal(msg.reply_markup, undefined, "multi-question should not have inline keyboard");
527
+ assert.ok(msg.text.includes("1/2"), "should show question position");
528
+ assert.ok(msg.text.includes("2/2"), "should show question position");
529
+ });
530
+
531
+ test("formatForTelegram escapes HTML in user content", () => {
532
+ const prompt = {
533
+ id: "tg-3",
534
+ channel: "telegram" as const,
535
+ createdAt: Date.now(),
536
+ timeoutAt: Date.now() + 60000,
537
+ pollIntervalMs: 5000,
538
+ questions: [{
539
+ id: "q1",
540
+ header: "Test <script>",
541
+ question: "Is 5 > 3 & 2 < 4?",
542
+ options: [{ label: "<b>Yes</b>", description: "it's true" }],
543
+ allowMultiple: false,
544
+ }],
545
+ };
546
+
547
+ const msg = formatForTelegram(prompt);
548
+ assert.ok(msg.text.includes("&lt;script&gt;"), "should escape < > in header");
549
+ assert.ok(msg.text.includes("5 &gt; 3 &amp; 2 &lt; 4"), "should escape in question");
550
+ assert.ok(msg.text.includes("&lt;b&gt;Yes&lt;/b&gt;"), "should escape in option label");
551
+ });
552
+
553
+ test("parseTelegramResponse handles callback_data button press", () => {
554
+ const questions = [{
555
+ id: "choice",
556
+ header: "Pick",
557
+ question: "Choose",
558
+ allowMultiple: false,
559
+ options: [
560
+ { label: "Alpha", description: "A" },
561
+ { label: "Beta", description: "B" },
562
+ ],
563
+ }];
564
+
565
+ const result = parseTelegramResponse("prompt-123:1", null, questions, "prompt-123");
566
+ assert.deepEqual(result, { answers: { choice: { answers: ["Beta"] } } });
567
+ });
568
+
569
+ test("parseTelegramResponse handles text reply delegation", () => {
570
+ const questions = [{
571
+ id: "choice",
572
+ header: "Pick",
573
+ question: "Choose",
574
+ allowMultiple: false,
575
+ options: [
576
+ { label: "Alpha", description: "A" },
577
+ { label: "Beta", description: "B" },
578
+ ],
579
+ }];
580
+
581
+ const result = parseTelegramResponse(null, "1", questions, "prompt-123");
582
+ assert.deepEqual(result, { answers: { choice: { answers: ["Alpha"] } } });
583
+ });
584
+
585
+ test("parseTelegramResponse handles multi-question semicolons", () => {
586
+ const questions = [
587
+ {
588
+ id: "first",
589
+ header: "First",
590
+ question: "Pick",
591
+ allowMultiple: false,
592
+ options: [
593
+ { label: "Alpha", description: "A" },
594
+ { label: "Beta", description: "B" },
595
+ ],
596
+ },
597
+ {
598
+ id: "second",
599
+ header: "Second",
600
+ question: "Pick",
601
+ allowMultiple: false,
602
+ options: [
603
+ { label: "Gamma", description: "G" },
604
+ { label: "Delta", description: "D" },
605
+ ],
606
+ },
607
+ ];
608
+
609
+ const result = parseTelegramResponse(null, "2;1", questions, "prompt-123");
610
+ assert.deepEqual(result.answers.first.answers, ["Beta"]);
611
+ assert.deepEqual(result.answers.second.answers, ["Gamma"]);
612
+ });
613
+
614
+ test("isValidChannelId validates Telegram chat IDs", () => {
615
+ // Valid positive ID
616
+ assert.equal(isValidChannelId("telegram", "12345"), true);
617
+ // Valid negative group ID
618
+ assert.equal(isValidChannelId("telegram", "-1001234567890"), true);
619
+ // Too short
620
+ assert.equal(isValidChannelId("telegram", "1234"), false);
621
+ // Non-numeric
622
+ assert.equal(isValidChannelId("telegram", "abc12345"), false);
623
+ // URL injection
624
+ assert.equal(isValidChannelId("telegram", "https://evil.com"), false);
625
+ });
626
+
627
+ test("sanitizeError strips Telegram bot token patterns", () => {
628
+ const fakeToken = "1234567890:ABCdefGHIjklMNOpqrSTUvwxyz12345678";
629
+ const result = sanitizeError(`Token: ${fakeToken}`);
630
+ assert.ok(!result.includes("1234567890:ABC"), "should strip Telegram bot token");
631
+ });
632
+
372
633
  test("DiscordAdapter source-level: sendPrompt sets threadUrl in ref", () => {
373
634
  const adapterSrc = readFileSync(
374
635
  join(__dirname, "..", "..", "remote-questions", "discord-adapter.ts"),