switchroom 0.7.15 → 0.10.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 (301) hide show
  1. package/README.md +51 -59
  2. package/bin/run-hook.sh +27 -11
  3. package/bin/timezone-hook.sh +9 -7
  4. package/dist/agent-scheduler/index.js +410 -133
  5. package/dist/auth-broker/index.js +13932 -0
  6. package/dist/cli/switchroom.js +26937 -5601
  7. package/dist/host-control/main.js +12702 -0
  8. package/dist/vault/approvals/kernel-server.js +467 -184
  9. package/dist/vault/broker/server.js +1430 -724
  10. package/examples/minimal.yaml +63 -0
  11. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  12. package/examples/personal-google-workspace-mcp/README.md +194 -0
  13. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  14. package/examples/switchroom.yaml +220 -0
  15. package/package.json +7 -4
  16. package/profiles/_base/settings.json.hbs +20 -5
  17. package/profiles/_base/start.sh.hbs +16 -3
  18. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  19. package/profiles/_shared/telegram-style.md.hbs +20 -90
  20. package/profiles/_shared/vault-protocol.md.hbs +68 -0
  21. package/profiles/default/CLAUDE.md +50 -96
  22. package/profiles/default/CLAUDE.md.hbs +36 -6
  23. package/profiles/default/workspace/SOUL.md.hbs +12 -5
  24. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  25. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  26. package/skills/buildkite-api/SKILL.md +31 -8
  27. package/skills/buildkite-cli/SKILL.md +27 -9
  28. package/skills/buildkite-migration/SKILL.md +22 -9
  29. package/skills/buildkite-pipelines/SKILL.md +26 -9
  30. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  31. package/skills/buildkite-test-engine/SKILL.md +25 -8
  32. package/skills/docx/SKILL.md +1 -1
  33. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
  34. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
  35. package/skills/file-bug/SKILL.md +34 -6
  36. package/skills/humanizer/SKILL.md +15 -0
  37. package/skills/humanizer-calibrate/SKILL.md +7 -1
  38. package/skills/mcp-builder/SKILL.md +1 -1
  39. package/skills/pdf/SKILL.md +1 -1
  40. package/skills/pptx/SKILL.md +1 -1
  41. package/skills/skill-creator/SKILL.md +21 -1
  42. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  44. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  45. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  46. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  47. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  48. package/skills/switchroom-cli/SKILL.md +63 -64
  49. package/skills/switchroom-health/SKILL.md +23 -10
  50. package/skills/switchroom-install/SKILL.md +3 -3
  51. package/skills/switchroom-manage/SKILL.md +26 -19
  52. package/skills/switchroom-runtime/SKILL.md +191 -0
  53. package/skills/switchroom-status/SKILL.md +27 -2
  54. package/skills/telegram-test-harness/SKILL.md +3 -0
  55. package/skills/token-helpers/SKILL.md +24 -1
  56. package/skills/webapp-testing/SKILL.md +31 -1
  57. package/skills/xlsx/SKILL.md +1 -1
  58. package/telegram-plugin/admin-commands/index.ts +7 -5
  59. package/telegram-plugin/analytics-posthog.ts +191 -0
  60. package/telegram-plugin/bridge/bridge.ts +69 -0
  61. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  62. package/telegram-plugin/dist/bridge/bridge.js +194 -119
  63. package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
  64. package/telegram-plugin/dist/server.js +245 -189
  65. package/telegram-plugin/first-paint.ts +3 -24
  66. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  67. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  68. package/telegram-plugin/gateway/auth-command.ts +794 -0
  69. package/telegram-plugin/gateway/auth-line.ts +123 -0
  70. package/telegram-plugin/gateway/boot-card.ts +169 -40
  71. package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
  72. package/telegram-plugin/gateway/boot-probes.ts +166 -123
  73. package/telegram-plugin/gateway/boot-reason.ts +41 -7
  74. package/telegram-plugin/gateway/boot-version.ts +66 -0
  75. package/telegram-plugin/gateway/gateway.ts +3499 -1885
  76. package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
  77. package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
  78. package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
  79. package/telegram-plugin/gateway/quarantine.ts +69 -0
  80. package/telegram-plugin/gateway/quota-cache.ts +9 -4
  81. package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
  82. package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
  83. package/telegram-plugin/gateway/recent-denials.ts +77 -0
  84. package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
  85. package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
  86. package/telegram-plugin/history.ts +91 -0
  87. package/telegram-plugin/hooks/hooks.json +10 -0
  88. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
  89. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
  90. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
  91. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  92. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  93. package/telegram-plugin/inbound-classifier.ts +50 -0
  94. package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
  95. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  96. package/telegram-plugin/package.json +4 -2
  97. package/telegram-plugin/permission-rule.ts +51 -0
  98. package/telegram-plugin/permission-title.ts +56 -0
  99. package/telegram-plugin/quota-check.ts +19 -41
  100. package/telegram-plugin/registry/reaper.ts +223 -0
  101. package/telegram-plugin/retry-api-call.ts +80 -0
  102. package/telegram-plugin/runtime-metrics.ts +177 -0
  103. package/telegram-plugin/scripts/build.mjs +0 -1
  104. package/telegram-plugin/secret-detect/index.ts +24 -0
  105. package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
  106. package/telegram-plugin/secret-detect/vault-error.ts +78 -11
  107. package/telegram-plugin/secret-detect/vault-write.ts +14 -2
  108. package/telegram-plugin/server.js +41795 -0
  109. package/telegram-plugin/session-tail.ts +6 -1
  110. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  111. package/telegram-plugin/silence-poke.ts +420 -0
  112. package/telegram-plugin/silent-end.ts +174 -0
  113. package/telegram-plugin/stream-controller.ts +13 -0
  114. package/telegram-plugin/stream-reply-handler.ts +7 -0
  115. package/telegram-plugin/subagent-watcher.ts +213 -4
  116. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  117. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  118. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  119. package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
  120. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
  121. package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
  122. package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
  123. package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
  124. package/telegram-plugin/tests/boot-probes.test.ts +216 -10
  125. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  126. package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
  127. package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
  128. package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
  129. package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
  130. package/telegram-plugin/tests/history-reaper.test.ts +378 -0
  131. package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
  132. package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
  133. package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
  134. package/telegram-plugin/tests/issues-card.test.ts +49 -0
  135. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
  136. package/telegram-plugin/tests/permission-rule.test.ts +80 -1
  137. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  138. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  139. package/telegram-plugin/tests/races.test.ts +179 -0
  140. package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
  141. package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
  142. package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
  143. package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
  144. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
  145. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
  146. package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
  147. package/telegram-plugin/tests/silence-poke.test.ts +493 -0
  148. package/telegram-plugin/tests/silent-end.test.ts +206 -0
  149. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
  150. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
  151. package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
  152. package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
  153. package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
  154. package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
  155. package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
  156. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
  157. package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
  158. package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
  159. package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
  160. package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
  161. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
  162. package/telegram-plugin/turn-signal-tracker.ts +100 -24
  163. package/telegram-plugin/uat/SETUP.md +210 -35
  164. package/telegram-plugin/uat/assertions.ts +264 -37
  165. package/telegram-plugin/uat/driver-info.ts +57 -0
  166. package/telegram-plugin/uat/driver.ts +590 -51
  167. package/telegram-plugin/uat/harness.ts +140 -94
  168. package/telegram-plugin/uat/load-env.test.ts +72 -0
  169. package/telegram-plugin/uat/load-env.ts +48 -0
  170. package/telegram-plugin/uat/login.ts +96 -53
  171. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  172. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  173. package/telegram-plugin/uat/runners/report.ts +150 -0
  174. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  175. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  176. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  177. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  178. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  179. package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
  180. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
  181. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
  182. package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
  183. package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
  184. package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
  185. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
  186. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
  187. package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
  188. package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
  189. package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
  190. package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
  191. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
  192. package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
  193. package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
  194. package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
  195. package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
  196. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
  197. package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
  198. package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
  199. package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
  200. package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
  201. package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
  202. package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
  203. package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
  204. package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
  205. package/telegram-plugin/vault-approval-posture.ts +42 -0
  206. package/telegram-plugin/welcome-text.ts +1 -0
  207. package/telegram-plugin/active-pins-sweep.ts +0 -204
  208. package/telegram-plugin/active-pins.ts +0 -146
  209. package/telegram-plugin/auth-dashboard.ts +0 -1104
  210. package/telegram-plugin/auth-slot-parser.ts +0 -497
  211. package/telegram-plugin/card-event-log.ts +0 -138
  212. package/telegram-plugin/dist/foreman/foreman.js +0 -31106
  213. package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
  214. package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
  215. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  216. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  217. package/telegram-plugin/foreman/foreman.ts +0 -1165
  218. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  219. package/telegram-plugin/foreman/setup-state.ts +0 -239
  220. package/telegram-plugin/foreman/state.ts +0 -203
  221. package/telegram-plugin/pin-event-log.ts +0 -76
  222. package/telegram-plugin/progress-card-driver.ts +0 -2886
  223. package/telegram-plugin/progress-card-pin-manager.ts +0 -589
  224. package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
  225. package/telegram-plugin/progress-card.ts +0 -1409
  226. package/telegram-plugin/tests/HARNESS.md +0 -340
  227. package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
  228. package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
  229. package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
  230. package/telegram-plugin/tests/active-pins.test.ts +0 -187
  231. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  232. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  233. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  234. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  235. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  236. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  237. package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
  238. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  239. package/telegram-plugin/tests/card-event-log.test.ts +0 -145
  240. package/telegram-plugin/tests/first-paint.test.ts +0 -257
  241. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  242. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  243. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  244. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  245. package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
  246. package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
  247. package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
  248. package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
  249. package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
  250. package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
  251. package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
  252. package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
  253. package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
  254. package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
  255. package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
  256. package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
  257. package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
  258. package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
  259. package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
  260. package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
  261. package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
  262. package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
  263. package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
  264. package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
  265. package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
  266. package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
  267. package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
  268. package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
  269. package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
  270. package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
  271. package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
  272. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  273. package/telegram-plugin/tests/setup-state.test.ts +0 -146
  274. package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
  275. package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
  276. package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
  277. package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
  278. package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
  279. package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
  280. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
  281. package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
  282. package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
  283. package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
  284. package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
  285. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
  286. package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
  287. package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
  288. package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
  289. package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
  290. package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
  291. package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
  292. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
  293. package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
  294. package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
  295. package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
  296. package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
  297. package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
  298. package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
  299. package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
  300. package/telegram-plugin/two-zone-card.ts +0 -269
  301. package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
@@ -1,510 +0,0 @@
1
- /**
2
- * Tests for the /setup wizard state machine (setup-flow.ts).
3
- *
4
- * Pure function tests — no grammY, no SQLite, no network.
5
- *
6
- * Covers:
7
- * - startSetupFlow: no slug, valid slug, invalid slug
8
- * - handleSetupText: full happy-path step transitions
9
- * - Validator helpers: isValidSlug, isValidPersonaName, isValidModel, isValidEmoji
10
- * - Skip / cancel / error paths at each step
11
- * - makeSetupInitialState / advanceSetupState / setupStepLabel helpers
12
- */
13
-
14
- import { describe, it, expect } from 'vitest'
15
- import {
16
- startSetupFlow,
17
- handleSetupText,
18
- makeSetupInitialState,
19
- advanceSetupState,
20
- setupStepLabel,
21
- isValidSlug,
22
- isValidPersonaName,
23
- isValidModel,
24
- isValidEmoji,
25
- isSkip,
26
- isCancel,
27
- } from '../foreman/setup-flow.js'
28
- import type { SetupFlowState } from '../foreman/setup-state.js'
29
-
30
- const CALLER = '12345678'
31
-
32
- // #190: setup-flow now needs the operator's profile list. Tests that don't
33
- // care about the profile validation just pass `default` so the asked-profile
34
- // step accepts any input that maps to one of these.
35
- const PROFILES = ['default', 'coding', 'health-coach', 'executive-assistant']
36
-
37
- function makeState(overrides: Partial<SetupFlowState> = {}): SetupFlowState {
38
- return {
39
- chatId: 'chat1',
40
- step: 'asked-slug',
41
- slug: null,
42
- persona: null,
43
- model: null,
44
- emoji: null,
45
- profile: null,
46
- botToken: null,
47
- allowedUserId: null,
48
- authSessionName: null,
49
- loginUrl: null,
50
- startedAt: 1000,
51
- updatedAt: 1000,
52
- ...overrides,
53
- }
54
- }
55
-
56
- /**
57
- * Test helper — wraps handleSetupText with a default profiles list so the
58
- * 30+ existing call sites don't need to be rewritten one-by-one. Tests that
59
- * specifically exercise the asked-profile validation pass their own
60
- * profiles via the `opts` argument.
61
- */
62
- function call(
63
- state: SetupFlowState | null,
64
- text: string,
65
- opts: { callerId?: string; profiles?: string[] } = {},
66
- ) {
67
- return handleSetupText({
68
- state,
69
- text,
70
- callerId: opts.callerId ?? CALLER,
71
- profiles: opts.profiles ?? PROFILES,
72
- })
73
- }
74
-
75
- // ─── isValidSlug ─────────────────────────────────────────────────────────
76
-
77
- describe('isValidSlug', () => {
78
- it('accepts simple lowercase', () => expect(isValidSlug('gymbro')).toBe(true))
79
- it('accepts hyphens', () => expect(isValidSlug('gym-bro')).toBe(true))
80
- it('accepts underscores', () => expect(isValidSlug('gym_bro')).toBe(true))
81
- it('accepts leading digit', () => expect(isValidSlug('1agent')).toBe(true))
82
- it('rejects uppercase', () => expect(isValidSlug('GymBro')).toBe(false))
83
- it('rejects spaces', () => expect(isValidSlug('gym bro')).toBe(false))
84
- it('rejects empty', () => expect(isValidSlug('')).toBe(false))
85
- it('accepts 51-char slug', () => expect(isValidSlug('a'.repeat(51))).toBe(true))
86
- it('rejects 52-char slug', () => expect(isValidSlug('a'.repeat(52))).toBe(false))
87
- })
88
-
89
- // ─── isValidPersonaName ───────────────────────────────────────────────────
90
-
91
- describe('isValidPersonaName', () => {
92
- it('accepts normal name', () => expect(isValidPersonaName('Gym Bro')).toBe(true))
93
- it('accepts emoji in name', () => expect(isValidPersonaName('Clerk 💼')).toBe(true))
94
- it('rejects empty string', () => expect(isValidPersonaName('')).toBe(false))
95
- it('rejects control char', () => expect(isValidPersonaName('bad\x00name')).toBe(false))
96
- it('rejects 81-char name', () => expect(isValidPersonaName('a'.repeat(81))).toBe(false))
97
- it('accepts 80-char name', () => expect(isValidPersonaName('a'.repeat(80))).toBe(true))
98
- })
99
-
100
- // ─── isValidModel ─────────────────────────────────────────────────────────
101
-
102
- describe('isValidModel', () => {
103
- it('accepts sonnet alias', () => expect(isValidModel('sonnet')).toBe(true))
104
- it('accepts opus alias', () => expect(isValidModel('opus')).toBe(true))
105
- it('accepts haiku alias', () => expect(isValidModel('haiku')).toBe(true))
106
- it('accepts inherit alias', () => expect(isValidModel('inherit')).toBe(true))
107
- it('accepts full model ID', () => expect(isValidModel('claude-sonnet-4-5')).toBe(true))
108
- it('rejects spaces', () => expect(isValidModel('bad model')).toBe(false))
109
- it('rejects empty', () => expect(isValidModel('')).toBe(false))
110
- })
111
-
112
- // ─── isValidEmoji ─────────────────────────────────────────────────────────
113
-
114
- describe('isValidEmoji', () => {
115
- it('accepts single emoji', () => expect(isValidEmoji('🏋️')).toBe(true))
116
- it('accepts simple ascii (single char)', () => expect(isValidEmoji('x')).toBe(true))
117
- it('rejects empty string', () => expect(isValidEmoji('')).toBe(false))
118
- it('rejects only whitespace', () => expect(isValidEmoji(' ')).toBe(false))
119
- })
120
-
121
- // ─── isSkip / isCancel ────────────────────────────────────────────────────
122
-
123
- describe('isSkip', () => {
124
- it('matches "skip"', () => expect(isSkip('skip')).toBe(true))
125
- it('matches "s"', () => expect(isSkip('s')).toBe(true))
126
- it('matches "-"', () => expect(isSkip('-')).toBe(true))
127
- it('ignores case', () => expect(isSkip('SKIP')).toBe(true))
128
- it('does not match other words', () => expect(isSkip('no')).toBe(false))
129
- })
130
-
131
- describe('isCancel', () => {
132
- it('matches "cancel"', () => expect(isCancel('cancel')).toBe(true))
133
- it('matches "/cancel"', () => expect(isCancel('/cancel')).toBe(true))
134
- it('matches "abort"', () => expect(isCancel('abort')).toBe(true))
135
- it('ignores case', () => expect(isCancel('CANCEL')).toBe(true))
136
- it('does not match "yes"', () => expect(isCancel('yes')).toBe(false))
137
- })
138
-
139
- // ─── startSetupFlow ───────────────────────────────────────────────────────
140
-
141
- describe('startSetupFlow', () => {
142
- it('asks for slug when no inline arg', () => {
143
- const action = startSetupFlow(null)
144
- expect(action.kind).toBe('ask-slug')
145
- })
146
-
147
- it('asks for persona when valid inline slug given', () => {
148
- const action = startSetupFlow('gymbro')
149
- expect(action.kind).toBe('ask-persona')
150
- if (action.kind === 'ask-persona') expect(action.slug).toBe('gymbro')
151
- })
152
-
153
- it('returns error for invalid inline slug', () => {
154
- const action = startSetupFlow('INVALID SLUG!')
155
- expect(action.kind).toBe('error')
156
- })
157
- })
158
-
159
- // ─── handleSetupText: cancel at any step ─────────────────────────────────
160
-
161
- describe('handleSetupText: cancel', () => {
162
- const steps = [
163
- 'asked-slug', 'asked-persona', 'asked-model', 'asked-emoji',
164
- 'asked-bot-token', 'confirming-allowlist',
165
- ] as const
166
-
167
- for (const step of steps) {
168
- it(`cancels at step ${step}`, () => {
169
- const state = makeState({ step, slug: 'gymbro', persona: 'Gym Bro' })
170
- const action = handleSetupText({ state, text: 'cancel', callerId: CALLER })
171
- expect(action.kind).toBe('cancel')
172
- if (action.kind === 'cancel') expect(action.reason).toBe('user-cancelled')
173
- })
174
- }
175
- })
176
-
177
- // ─── handleSetupText: null state ──────────────────────────────────────────
178
-
179
- describe('handleSetupText: null state', () => {
180
- it('returns cancel with no-active-flow reason', () => {
181
- const action = handleSetupText({ state: null, text: 'gymbro', callerId: CALLER })
182
- expect(action.kind).toBe('cancel')
183
- if (action.kind === 'cancel') expect(action.reason).toBe('no-active-flow')
184
- })
185
- })
186
-
187
- // ─── handleSetupText: step asked-slug ────────────────────────────────────
188
-
189
- describe('handleSetupText: asked-slug', () => {
190
- it('advances to ask-persona on valid slug', () => {
191
- const state = makeState({ step: 'asked-slug' })
192
- const action = handleSetupText({ state, text: 'gymbro', callerId: CALLER })
193
- expect(action.kind).toBe('ask-persona')
194
- if (action.kind === 'ask-persona') expect(action.slug).toBe('gymbro')
195
- })
196
-
197
- it('returns error on invalid slug', () => {
198
- const state = makeState({ step: 'asked-slug' })
199
- const action = handleSetupText({ state, text: 'BAD SLUG', callerId: CALLER })
200
- expect(action.kind).toBe('error')
201
- if (action.kind === 'error') expect(action.stayInStep).toBe(true)
202
- })
203
- })
204
-
205
- // ─── handleSetupText: step asked-persona ─────────────────────────────────
206
-
207
- describe('handleSetupText: asked-persona', () => {
208
- it('advances to ask-model on valid persona', () => {
209
- const state = makeState({ step: 'asked-persona', slug: 'gymbro' })
210
- const action = handleSetupText({ state, text: 'Gym Bro', callerId: CALLER })
211
- expect(action.kind).toBe('ask-model')
212
- if (action.kind === 'ask-model') {
213
- expect(action.slug).toBe('gymbro')
214
- expect(action.persona).toBe('Gym Bro')
215
- }
216
- })
217
-
218
- it('returns error on empty persona', () => {
219
- const state = makeState({ step: 'asked-persona', slug: 'gymbro' })
220
- const action = handleSetupText({ state, text: '', callerId: CALLER })
221
- expect(action.kind).toBe('error')
222
- })
223
- })
224
-
225
- // ─── handleSetupText: step asked-model ───────────────────────────────────
226
-
227
- describe('handleSetupText: asked-model', () => {
228
- it('advances to ask-emoji with skip', () => {
229
- const state = makeState({ step: 'asked-model', slug: 'gymbro', persona: 'Gym Bro' })
230
- const action = handleSetupText({ state, text: 'skip', callerId: CALLER })
231
- expect(action.kind).toBe('ask-emoji')
232
- if (action.kind === 'ask-emoji') expect(action.model).toBeNull()
233
- })
234
-
235
- it('advances to ask-emoji with valid model', () => {
236
- const state = makeState({ step: 'asked-model', slug: 'gymbro', persona: 'Gym Bro' })
237
- const action = handleSetupText({ state, text: 'sonnet', callerId: CALLER })
238
- expect(action.kind).toBe('ask-emoji')
239
- if (action.kind === 'ask-emoji') expect(action.model).toBe('sonnet')
240
- })
241
-
242
- it('returns error on model string with spaces', () => {
243
- const state = makeState({ step: 'asked-model', slug: 'gymbro', persona: 'Gym Bro' })
244
- // Spaces are not allowed in model IDs
245
- const action = handleSetupText({ state, text: 'bad model name', callerId: CALLER })
246
- expect(action.kind).toBe('error')
247
- if (action.kind === 'error') expect(action.stayInStep).toBe(true)
248
- })
249
- })
250
-
251
- // ─── handleSetupText: step asked-emoji ───────────────────────────────────
252
-
253
- describe('handleSetupText: asked-emoji (#190 — now transitions to ask-profile)', () => {
254
- it('advances to ask-profile with skip (no emoji)', () => {
255
- const state = makeState({ step: 'asked-emoji', slug: 'gymbro', persona: 'Gym Bro', model: 'sonnet' })
256
- const action = call(state, 'skip')
257
- expect(action.kind).toBe('ask-profile')
258
- if (action.kind === 'ask-profile') {
259
- expect(action.emoji).toBeNull()
260
- expect(action.profiles).toEqual(PROFILES)
261
- }
262
- })
263
-
264
- it('advances to ask-profile carrying the emoji', () => {
265
- const state = makeState({ step: 'asked-emoji', slug: 'gymbro', persona: 'Gym Bro', model: null })
266
- const action = call(state, '🏋️')
267
- expect(action.kind).toBe('ask-profile')
268
- if (action.kind === 'ask-profile') expect(action.emoji).toBe('🏋️')
269
- })
270
- })
271
-
272
- // ─── handleSetupText: step asked-profile (#190) ──────────────────────────
273
-
274
- describe('handleSetupText: asked-profile (#190)', () => {
275
- function profileState(): SetupFlowState {
276
- return makeState({
277
- step: 'asked-profile',
278
- slug: 'gymbro',
279
- persona: 'Gym Bro',
280
- model: 'sonnet',
281
- emoji: '🏋️',
282
- })
283
- }
284
-
285
- it('advances to ask-bot-token when profile is in the live list', () => {
286
- const action = call(profileState(), 'health-coach')
287
- expect(action.kind).toBe('ask-bot-token')
288
- if (action.kind === 'ask-bot-token') {
289
- expect(action.profile).toBe('health-coach')
290
- expect(action.slug).toBe('gymbro')
291
- expect(action.emoji).toBe('🏋️')
292
- }
293
- })
294
-
295
- it('returns error stayInStep when profile is unknown', () => {
296
- const action = call(profileState(), 'nonexistent')
297
- expect(action.kind).toBe('error')
298
- if (action.kind === 'error') {
299
- expect(action.stayInStep).toBe(true)
300
- expect(action.message).toContain('nonexistent')
301
- }
302
- })
303
-
304
- it('lists valid profiles in error message', () => {
305
- const action = call(profileState(), 'bogus')
306
- if (action.kind === 'error') {
307
- for (const p of PROFILES) expect(action.message).toContain(p)
308
- }
309
- })
310
-
311
- it('cancels when slug or persona missing', () => {
312
- const action = call(makeState({ step: 'asked-profile' }), 'default')
313
- expect(action.kind).toBe('cancel')
314
- })
315
-
316
- it('respects a different profiles list per call', () => {
317
- const action = call(profileState(), 'tiny-bundle', { profiles: ['tiny-bundle'] })
318
- expect(action.kind).toBe('ask-bot-token')
319
- })
320
- })
321
-
322
- // ─── handleSetupText: step asked-bot-token ───────────────────────────────
323
-
324
- describe('handleSetupText: asked-bot-token', () => {
325
- it('advances to confirm-allowlist with valid token shape', () => {
326
- const state = makeState({
327
- step: 'asked-bot-token',
328
- slug: 'gymbro',
329
- persona: 'Gym Bro',
330
- model: null,
331
- emoji: null,
332
- })
333
- const action = handleSetupText({ state, text: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx', callerId: CALLER })
334
- expect(action.kind).toBe('confirm-allowlist')
335
- if (action.kind === 'confirm-allowlist') expect(action.callerId).toBe(CALLER)
336
- })
337
-
338
- it('returns error on bad token shape', () => {
339
- const state = makeState({
340
- step: 'asked-bot-token',
341
- slug: 'gymbro',
342
- persona: 'Gym Bro',
343
- })
344
- const action = handleSetupText({ state, text: 'notavalidtoken', callerId: CALLER })
345
- expect(action.kind).toBe('error')
346
- if (action.kind === 'error') expect(action.stayInStep).toBe(true)
347
- })
348
-
349
- it('returns cancel when slug or persona is missing', () => {
350
- const state = makeState({
351
- step: 'asked-bot-token',
352
- slug: null,
353
- persona: null,
354
- })
355
- const action = handleSetupText({ state, text: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx', callerId: CALLER })
356
- expect(action.kind).toBe('cancel')
357
- })
358
- })
359
-
360
- // ─── handleSetupText: step confirming-allowlist ───────────────────────────
361
-
362
- describe('handleSetupText: confirming-allowlist (#189 — now transitions to call-create-agent)', () => {
363
- const baseState = makeState({
364
- step: 'confirming-allowlist',
365
- slug: 'gymbro',
366
- persona: 'Gym Bro',
367
- model: null,
368
- emoji: null,
369
- profile: 'default',
370
- botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
371
- })
372
-
373
- it('advances to call-create-agent on "yes"', () => {
374
- const action = call(baseState, 'yes')
375
- expect(action.kind).toBe('call-create-agent')
376
- if (action.kind === 'call-create-agent') {
377
- expect(action.allowedUserId).toBe(CALLER)
378
- expect(action.slug).toBe('gymbro')
379
- expect(action.persona).toBe('Gym Bro')
380
- expect(action.profile).toBe('default')
381
- }
382
- })
383
-
384
- it('advances to call-create-agent on "y"', () => {
385
- const action = call(baseState, 'y')
386
- expect(action.kind).toBe('call-create-agent')
387
- if (action.kind === 'call-create-agent') expect(action.allowedUserId).toBe(CALLER)
388
- })
389
-
390
- it('uses custom user_id when not "yes"', () => {
391
- const action = call(baseState, '99999999')
392
- expect(action.kind).toBe('call-create-agent')
393
- if (action.kind === 'call-create-agent') expect(action.allowedUserId).toBe('99999999')
394
- })
395
-
396
- it('falls back to "default" profile for legacy in-flight flows where profile is null', () => {
397
- // Simulates a flow that started before the #190 schema migration —
398
- // SQLite returns NULL for the new `profile` column, the wizard
399
- // shouldn't break.
400
- const legacy = makeState({
401
- step: 'confirming-allowlist',
402
- slug: 'oldgymbro',
403
- persona: 'Old Gym',
404
- botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
405
- profile: null,
406
- })
407
- const action = call(legacy, 'yes')
408
- expect(action.kind).toBe('call-create-agent')
409
- if (action.kind === 'call-create-agent') expect(action.profile).toBe('default')
410
- })
411
- })
412
-
413
- // ─── handleSetupText: step asked-oauth-code (#189) ───────────────────────
414
-
415
- describe('handleSetupText: asked-oauth-code (#189)', () => {
416
- function oauthState(): SetupFlowState {
417
- return makeState({
418
- step: 'asked-oauth-code',
419
- slug: 'gymbro',
420
- persona: 'Gym Bro',
421
- profile: 'default',
422
- botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
423
- authSessionName: 'gymbro-foreman',
424
- loginUrl: 'https://example.com/login',
425
- })
426
- }
427
-
428
- it('advances to call-complete-creation with a valid-shape code', () => {
429
- const action = call(oauthState(), 'a1b2c3d4e5')
430
- expect(action.kind).toBe('call-complete-creation')
431
- if (action.kind === 'call-complete-creation') {
432
- expect(action.slug).toBe('gymbro')
433
- expect(action.code).toBe('a1b2c3d4e5')
434
- }
435
- })
436
-
437
- it('returns error stayInStep on too-short code', () => {
438
- const action = call(oauthState(), 'abc')
439
- expect(action.kind).toBe('error')
440
- if (action.kind === 'error') expect(action.stayInStep).toBe(true)
441
- })
442
-
443
- it('cancels when slug missing (corrupt state)', () => {
444
- const action = call(makeState({ step: 'asked-oauth-code' }), 'a1b2c3d4e5')
445
- expect(action.kind).toBe('cancel')
446
- })
447
- })
448
-
449
- // ─── handleSetupText: terminal steps ─────────────────────────────────────
450
-
451
- describe('handleSetupText: terminal steps', () => {
452
- it('returns cancel for reconciling step', () => {
453
- const state = makeState({ step: 'reconciling' })
454
- const action = handleSetupText({ state, text: 'anything', callerId: CALLER })
455
- expect(action.kind).toBe('cancel')
456
- })
457
-
458
- it('returns cancel for done step', () => {
459
- const state = makeState({ step: 'done' })
460
- const action = handleSetupText({ state, text: 'anything', callerId: CALLER })
461
- expect(action.kind).toBe('cancel')
462
- })
463
- })
464
-
465
- // ─── makeSetupInitialState ────────────────────────────────────────────────
466
-
467
- describe('makeSetupInitialState', () => {
468
- it('sets step to asked-slug when no slug', () => {
469
- const s = makeSetupInitialState('chat1', null)
470
- expect(s.step).toBe('asked-slug')
471
- expect(s.slug).toBeNull()
472
- })
473
-
474
- it('sets step to asked-persona when slug provided', () => {
475
- const s = makeSetupInitialState('chat1', 'gymbro')
476
- expect(s.step).toBe('asked-persona')
477
- expect(s.slug).toBe('gymbro')
478
- })
479
- })
480
-
481
- // ─── advanceSetupState ────────────────────────────────────────────────────
482
-
483
- describe('advanceSetupState', () => {
484
- it('merges updates and bumps updatedAt', () => {
485
- const original = makeState({ updatedAt: 1000 })
486
- const advanced = advanceSetupState(original, { step: 'asked-persona', slug: 'gymbro' })
487
- expect(advanced.step).toBe('asked-persona')
488
- expect(advanced.slug).toBe('gymbro')
489
- expect(advanced.chatId).toBe(original.chatId)
490
- expect(advanced.updatedAt).toBeGreaterThanOrEqual(original.updatedAt)
491
- })
492
- })
493
-
494
- // ─── setupStepLabel ───────────────────────────────────────────────────────
495
-
496
- describe('setupStepLabel', () => {
497
- const cases: [import('../foreman/setup-state.js').SetupFlowStep, string][] = [
498
- ['asked-slug', 'waiting for agent slug'],
499
- ['asked-persona', 'waiting for persona name'],
500
- ['asked-model', 'waiting for model choice'],
501
- ['asked-emoji', 'waiting for emoji'],
502
- ['asked-bot-token', 'waiting for BotFather token'],
503
- ['confirming-allowlist', 'waiting for allowlist confirmation'],
504
- ['reconciling', 'provisioning agent'],
505
- ['done', 'done'],
506
- ]
507
- for (const [step, expected] of cases) {
508
- it(`labels ${step} correctly`, () => expect(setupStepLabel(step)).toBe(expected))
509
- }
510
- })
@@ -1,146 +0,0 @@
1
- /**
2
- * Tests for /setup wizard SQLite state (setup-state.ts).
3
- *
4
- * Uses bun:test (not vitest) because setup-state.ts imports bun:sqlite.
5
- * Run with: bun test telegram-plugin/tests/setup-state.test.ts
6
- */
7
-
8
- import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
9
- import { mkdtempSync, rmSync } from 'fs'
10
- import { tmpdir } from 'os'
11
-
12
- let tmpDir: string
13
-
14
- beforeEach(() => {
15
- tmpDir = mkdtempSync(tmpdir() + '/setup-state-test-')
16
- process.env.SWITCHROOM_FOREMAN_DIR = tmpDir
17
- })
18
-
19
- afterEach(async () => {
20
- const { _resetSetupDbForTest } = await import('../foreman/setup-state.js')
21
- _resetSetupDbForTest()
22
- delete process.env.SWITCHROOM_FOREMAN_DIR
23
- try { rmSync(tmpDir, { recursive: true, force: true }) } catch { /* ignore */ }
24
- })
25
-
26
- function makeState(chatId = 'chat1') {
27
- const now = Date.now()
28
- return {
29
- chatId,
30
- step: 'asked-slug' as const,
31
- slug: null,
32
- persona: null,
33
- model: null,
34
- emoji: null,
35
- botToken: null,
36
- allowedUserId: null,
37
- startedAt: now,
38
- updatedAt: now,
39
- }
40
- }
41
-
42
- // ─── Round-trip: setSetupState + getSetupState ────────────────────────────
43
-
44
- describe('setup-state: round-trip', () => {
45
- it('stores and retrieves initial state', async () => {
46
- const { setSetupState, getSetupState } = await import('../foreman/setup-state.js')
47
- const state = makeState()
48
- setSetupState(state)
49
- const retrieved = getSetupState('chat1')
50
- expect(retrieved).not.toBeNull()
51
- expect(retrieved?.step).toBe('asked-slug')
52
- expect(retrieved?.slug).toBeNull()
53
- expect(retrieved?.chatId).toBe('chat1')
54
- })
55
-
56
- it('returns null for unknown chatId', async () => {
57
- const { getSetupState } = await import('../foreman/setup-state.js')
58
- expect(getSetupState('nonexistent')).toBeNull()
59
- })
60
-
61
- it('upserts state on repeat setSetupState', async () => {
62
- const { setSetupState, getSetupState } = await import('../foreman/setup-state.js')
63
- const state = makeState()
64
- setSetupState(state)
65
- setSetupState({ ...state, step: 'asked-persona', slug: 'gymbro' })
66
- const retrieved = getSetupState('chat1')
67
- expect(retrieved?.step).toBe('asked-persona')
68
- expect(retrieved?.slug).toBe('gymbro')
69
- })
70
-
71
- it('stores all fields', async () => {
72
- const { setSetupState, getSetupState } = await import('../foreman/setup-state.js')
73
- const now = Date.now()
74
- setSetupState({
75
- chatId: 'chat2',
76
- step: 'asked-bot-token',
77
- slug: 'myagent',
78
- persona: 'My Agent',
79
- model: 'sonnet',
80
- emoji: '🤖',
81
- botToken: '1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx',
82
- allowedUserId: '99999999',
83
- startedAt: now - 1000,
84
- updatedAt: now,
85
- })
86
- const retrieved = getSetupState('chat2')
87
- expect(retrieved?.slug).toBe('myagent')
88
- expect(retrieved?.persona).toBe('My Agent')
89
- expect(retrieved?.model).toBe('sonnet')
90
- expect(retrieved?.emoji).toBe('🤖')
91
- expect(retrieved?.botToken).toBe('1234567890:AAHxxxxxxxxxxxxxxxxxxxxxxx')
92
- expect(retrieved?.allowedUserId).toBe('99999999')
93
- })
94
- })
95
-
96
- // ─── clearSetupState ──────────────────────────────────────────────────────
97
-
98
- describe('setup-state: clearSetupState', () => {
99
- it('removes state for given chat', async () => {
100
- const { setSetupState, clearSetupState, getSetupState } = await import('../foreman/setup-state.js')
101
- setSetupState(makeState('chatA'))
102
- setSetupState(makeState('chatB'))
103
- clearSetupState('chatA')
104
- expect(getSetupState('chatA')).toBeNull()
105
- expect(getSetupState('chatB')).not.toBeNull()
106
- })
107
-
108
- it('is a no-op when chat has no state', async () => {
109
- const { clearSetupState, getSetupState } = await import('../foreman/setup-state.js')
110
- // Should not throw
111
- clearSetupState('nobody')
112
- expect(getSetupState('nobody')).toBeNull()
113
- })
114
- })
115
-
116
- // ─── listActiveSetupFlows ─────────────────────────────────────────────────
117
-
118
- describe('setup-state: listActiveSetupFlows', () => {
119
- it('returns only non-done flows within maxAge', async () => {
120
- const { setSetupState, listActiveSetupFlows } = await import('../foreman/setup-state.js')
121
- const now = Date.now()
122
-
123
- setSetupState({ ...makeState('chat1'), step: 'asked-slug', updatedAt: now })
124
- setSetupState({ ...makeState('chat2'), step: 'done', updatedAt: now })
125
- setSetupState({ ...makeState('chat3'), step: 'asked-persona', updatedAt: now - 2 * 60 * 60 * 1000 }) // 2 hours old
126
-
127
- const active = listActiveSetupFlows(60 * 60 * 1000) // 1 hour
128
- expect(active.length).toBe(1)
129
- expect(active[0].chatId).toBe('chat1')
130
- })
131
-
132
- it('returns empty list when nothing active', async () => {
133
- const { listActiveSetupFlows } = await import('../foreman/setup-state.js')
134
- const active = listActiveSetupFlows()
135
- expect(active).toEqual([])
136
- })
137
-
138
- it('returns multiple active flows', async () => {
139
- const { setSetupState, listActiveSetupFlows } = await import('../foreman/setup-state.js')
140
- const now = Date.now()
141
- setSetupState({ ...makeState('c1'), updatedAt: now })
142
- setSetupState({ ...makeState('c2'), step: 'asked-persona', updatedAt: now })
143
- const active = listActiveSetupFlows()
144
- expect(active.length).toBe(2)
145
- })
146
- })