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,257 +0,0 @@
1
- /**
2
- * First-paint seam tests — Phase 2 of #545.
3
- *
4
- * Drives `firstPaintTurn` directly with fake bot api, fake progress driver,
5
- * and a stub `controllerFactory`. Measures wall-clock deltas against the
6
- * spec-doc deadlines (waiting-ux-spec.md):
7
- *
8
- * F2 ("instant draft / status reaction"):
9
- * bot.api.setMessageReaction(... '👀' ...) within 800ms of seam entry.
10
- * F3 ("progress card start"):
11
- * progressDriver.startTurn within 800ms of seam entry. (The spec only
12
- * pins 800ms on the status reaction; we mirror that bound here because
13
- * progress-card start is a synchronous side effect of the same seam
14
- * and shares the "first visible signal" contract.)
15
- *
16
- * The seam is a pure async fn, so 'within Xms' really means 'before any
17
- * fake-timer advance' — we assert with the wall clock pinned, then verify
18
- * elapsed-fake-time stays <800ms.
19
- *
20
- * RED-or-GREEN: Phase 2 is allowed to surface real seam bugs. Do NOT alter
21
- * production code to force these green; if a deadline is missed, that's a
22
- * bug to file.
23
- */
24
-
25
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
26
- import {
27
- firstPaintTurn,
28
- type FirstPaintCtx,
29
- type FirstPaintDeps,
30
- } from '../first-paint.js'
31
- import type { StatusReactionController } from '../status-reactions.js'
32
- import type { DraftStreamHandle } from '../draft-stream.js'
33
-
34
- const CHAT = '8248703757'
35
- const INBOUND_MSG = 100
36
- const STATUS_REACTION_DEADLINE_MS = 800
37
- const PROGRESS_CARD_DEADLINE_MS = 800
38
-
39
- type ReactionCall = {
40
- chatId: string | number
41
- messageId: number
42
- emoji: string
43
- /** Wall-clock at invocation (Date.now under fake timers). */
44
- ts: number
45
- }
46
-
47
- type StartTurnCall = {
48
- chatId: string
49
- threadId?: string
50
- userText: string
51
- replyToMessageId?: number
52
- ts: number
53
- }
54
-
55
- interface Harness {
56
- deps: FirstPaintDeps
57
- ctx: FirstPaintCtx
58
- reactionCalls: ReactionCall[]
59
- startTurnCalls: StartTurnCall[]
60
- controllerCalls: { setQueued: number; cancel: number }
61
- errors: string[]
62
- }
63
-
64
- function makeHarness(overrides: { ctx?: Partial<FirstPaintCtx> } = {}): Harness {
65
- const reactionCalls: ReactionCall[] = []
66
- const startTurnCalls: StartTurnCall[] = []
67
- const controllerCalls = { setQueued: 0, cancel: 0 }
68
- const errors: string[] = []
69
-
70
- const fakeController = {
71
- setQueued: () => {
72
- controllerCalls.setQueued += 1
73
- },
74
- cancel: () => {
75
- controllerCalls.cancel += 1
76
- },
77
- // Surface enough of the public API to satisfy callers; unused here.
78
- setThinking: () => {},
79
- setTool: () => {},
80
- setCompacting: () => {},
81
- setDone: () => {},
82
- setSilent: () => {},
83
- setError: () => {},
84
- } as unknown as StatusReactionController
85
-
86
- const deps: FirstPaintDeps = {
87
- bot: {
88
- api: {
89
- setMessageReaction: async (chatId, messageId, reactions) => {
90
- for (const r of reactions) {
91
- reactionCalls.push({
92
- chatId,
93
- messageId,
94
- emoji: r.emoji as string,
95
- ts: Date.now(),
96
- })
97
- }
98
- },
99
- },
100
- },
101
- progressDriver: {
102
- startTurn: (args) => {
103
- startTurnCalls.push({ ...args, ts: Date.now() })
104
- },
105
- },
106
- activeStatusReactions: new Map(),
107
- activeReactionMsgIds: new Map(),
108
- activeTurnStartedAt: new Map(),
109
- progressUpdateTurnCount: new Map(),
110
- activeDraftStreams: new Map<string, DraftStreamHandle>(),
111
- activeDraftParseModes: new Map(),
112
- suppressPtyPreview: new Set(),
113
- statusKey: (chatId, threadId) => `${chatId}:${threadId ?? '_'}`,
114
- streamKey: (chatId, threadId) => `${chatId}:${threadId ?? '_'}`,
115
- purgeReactionTracking: () => {},
116
- signalTracker: {
117
- noteSignal: () => {},
118
- reset: () => {},
119
- },
120
- resolveAgentDirFromEnv: () => null,
121
- addActiveReaction: () => {},
122
- logStreamingEvent: () => {},
123
- controllerFactory: () => fakeController,
124
- logError: (m) => {
125
- errors.push(m)
126
- },
127
- }
128
-
129
- const ctx: FirstPaintCtx = {
130
- chatId: CHAT,
131
- messageId: INBOUND_MSG,
132
- messageThreadId: undefined,
133
- isSteerPrefix: false,
134
- effectiveText: 'hi',
135
- inboundReceivedAt: Date.now(),
136
- access: { statusReactions: true },
137
- ...overrides.ctx,
138
- }
139
-
140
- return { deps, ctx, reactionCalls, startTurnCalls, controllerCalls, errors }
141
- }
142
-
143
- beforeEach(() => {
144
- vi.useFakeTimers()
145
- })
146
-
147
- afterEach(() => {
148
- vi.useRealTimers()
149
- })
150
-
151
- describe('firstPaintTurn — first-paint seam', () => {
152
- it('happy path: fresh turn fires status reaction (👀 via setQueued path) and startTurn in order', async () => {
153
- const h = makeHarness()
154
- const t0 = Date.now()
155
-
156
- await firstPaintTurn(h.deps, h.ctx)
157
-
158
- // Fresh turn: controllerFactory was used, setQueued called once.
159
- expect(h.controllerCalls.setQueued).toBe(1)
160
- expect(h.controllerCalls.cancel).toBe(0)
161
-
162
- // The fake controller is a stub, so the bot.api.setMessageReaction
163
- // path here is exercised only via the steer/queued branches — for a
164
- // fresh turn the seam delegates the actual emoji emission to the
165
- // controller (which our stub records as `setQueued`). Either way the
166
- // FIRST visible signal happens within the seam call. Assert startTurn
167
- // fired and the controller was queued before any timer advance.
168
- expect(h.startTurnCalls).toHaveLength(1)
169
- expect(h.startTurnCalls[0].chatId).toBe(CHAT)
170
- expect(h.startTurnCalls[0].userText).toBe('hi')
171
- expect(h.startTurnCalls[0].replyToMessageId).toBe(INBOUND_MSG)
172
-
173
- // No fake-time advance happened; both side effects fired synchronously.
174
- expect(Date.now() - t0).toBe(0)
175
- expect(h.errors).toHaveLength(0)
176
- })
177
-
178
- it('F2 — instant draft: status reaction fires within 800ms of seam entry (fresh turn)', async () => {
179
- const h = makeHarness()
180
- const t0 = Date.now()
181
-
182
- await firstPaintTurn(h.deps, h.ctx)
183
-
184
- // The "first visible signal" for a fresh turn is the controller's
185
- // queued state. Production wires this through the controller's emit
186
- // callback (which calls bot.api.setMessageReaction with 👀). Our
187
- // controllerFactory stub records `setQueued` instead — so we assert
188
- // setQueued landed within the deadline.
189
- expect(h.controllerCalls.setQueued).toBe(1)
190
- const elapsed = Date.now() - t0
191
- expect(elapsed).toBeLessThan(STATUS_REACTION_DEADLINE_MS)
192
- })
193
-
194
- it('F2 — instant draft (queued mid-turn branch): 👀 reaction fires synchronously within 800ms', async () => {
195
- const h = makeHarness()
196
- // Simulate prior turn in flight by seeding activeStatusReactions.
197
- const key = `${CHAT}:_`
198
- const placeholderCtrl = {
199
- cancel: () => {},
200
- setQueued: () => {},
201
- } as unknown as StatusReactionController
202
- h.deps.activeStatusReactions.set(key, placeholderCtrl)
203
- h.deps.activeTurnStartedAt.set(key, Date.now() - 5_000)
204
-
205
- const t0 = Date.now()
206
- await firstPaintTurn(h.deps, h.ctx)
207
-
208
- // Mid-turn queued branch posts 👀 directly via bot.api.
209
- const eyes = h.reactionCalls.find((c) => c.emoji === '👀')
210
- expect(eyes, 'expected a 👀 reaction call in mid-turn queued branch').toBeDefined()
211
- expect((eyes!.ts) - t0).toBeLessThan(STATUS_REACTION_DEADLINE_MS)
212
- })
213
-
214
- it('F3 — progress card: startTurn fires within 800ms of seam entry on a fresh turn', async () => {
215
- const h = makeHarness()
216
- const t0 = Date.now()
217
-
218
- await firstPaintTurn(h.deps, h.ctx)
219
-
220
- expect(h.startTurnCalls).toHaveLength(1)
221
- const elapsed = h.startTurnCalls[0].ts - t0
222
- expect(elapsed).toBeLessThan(PROGRESS_CARD_DEADLINE_MS)
223
- })
224
-
225
- it('F3 — does NOT fire startTurn when a prior turn is in flight (steer branch)', async () => {
226
- const h = makeHarness({ ctx: { isSteerPrefix: true } })
227
- const key = `${CHAT}:_`
228
- const placeholderCtrl = {
229
- cancel: () => {},
230
- setQueued: () => {},
231
- } as unknown as StatusReactionController
232
- h.deps.activeStatusReactions.set(key, placeholderCtrl)
233
- h.deps.activeTurnStartedAt.set(key, Date.now() - 5_000)
234
-
235
- const result = await firstPaintTurn(h.deps, h.ctx)
236
-
237
- expect(result.isSteering).toBe(true)
238
- expect(h.startTurnCalls).toHaveLength(0)
239
- // Steer branch posts 🤝, not a new card.
240
- expect(h.reactionCalls.find((c) => c.emoji === '🤝')).toBeDefined()
241
- })
242
-
243
- it('logError dep captures progress-card startTurn failures (does not write to stderr)', async () => {
244
- const h = makeHarness()
245
- h.deps.progressDriver = {
246
- startTurn: () => {
247
- throw new Error('boom')
248
- },
249
- }
250
-
251
- await firstPaintTurn(h.deps, h.ctx)
252
-
253
- expect(h.errors).toHaveLength(1)
254
- expect(h.errors[0]).toContain('progress-card startTurn failed')
255
- expect(h.errors[0]).toContain('boom')
256
- })
257
- })
@@ -1,359 +0,0 @@
1
- /**
2
- * Tests for the create-agent flow state machine (foreman-create-flow.ts).
3
- *
4
- * Pure function tests — no grammY, no SQLite, no network.
5
- *
6
- * Covers:
7
- * - startCreateFlow: valid/invalid name, inline name, no name
8
- * - handleFlowText: step transitions (asked-name → asked-profile → asked-bot-token → ...)
9
- * - Error paths: invalid name, unknown profile, bad token shape, short code
10
- * - makeInitialState / advanceState / stepLabel helpers
11
- */
12
-
13
- import { describe, it, expect } from 'vitest'
14
- import {
15
- startCreateFlow,
16
- handleFlowText,
17
- makeInitialState,
18
- advanceState,
19
- stepLabel,
20
- isValidAgentName,
21
- } from '../foreman/foreman-create-flow.js'
22
- import type { CreateFlowState } from '../foreman/state.js'
23
-
24
- const PROFILES = ['default', 'health-coach', 'coding-assistant']
25
-
26
- // ─── isValidAgentName ─────────────────────────────────────────────────────
27
-
28
- describe('foreman-create-flow: isValidAgentName', () => {
29
- it('accepts lowercase simple name', () => expect(isValidAgentName('gymbro')).toBe(true))
30
- it('accepts name with hyphens', () => expect(isValidAgentName('my-agent')).toBe(true))
31
- it('accepts name with underscores', () => expect(isValidAgentName('my_agent')).toBe(true))
32
- it('accepts name starting with digit', () => expect(isValidAgentName('1agent')).toBe(true))
33
- it('rejects uppercase', () => expect(isValidAgentName('Gymbro')).toBe(false))
34
- it('rejects empty string', () => expect(isValidAgentName('')).toBe(false))
35
- it('rejects spaces', () => expect(isValidAgentName('my agent')).toBe(false))
36
- it('rejects semicolon', () => expect(isValidAgentName('agent; evil')).toBe(false))
37
- it('accepts 51-char name', () => expect(isValidAgentName('a'.repeat(51))).toBe(true))
38
- it('rejects 52-char name', () => expect(isValidAgentName('a'.repeat(52))).toBe(false))
39
- })
40
-
41
- // ─── startCreateFlow ──────────────────────────────────────────────────────
42
-
43
- describe('foreman-create-flow: startCreateFlow', () => {
44
- it('asks for name when no inline name given', () => {
45
- const action = startCreateFlow(null, PROFILES)
46
- expect(action.kind).toBe('ask-name')
47
- })
48
-
49
- it('asks for profile when valid inline name given', () => {
50
- const action = startCreateFlow('gymbro', PROFILES)
51
- expect(action.kind).toBe('ask-profile')
52
- if (action.kind === 'ask-profile') {
53
- expect(action.profiles).toEqual(PROFILES)
54
- }
55
- })
56
-
57
- it('returns error when inline name is invalid', () => {
58
- const action = startCreateFlow('Bad Name!', PROFILES)
59
- expect(action.kind).toBe('error')
60
- if (action.kind === 'error') {
61
- expect(action.message).toContain('Bad Name!')
62
- expect(action.stayInStep).toBe(false)
63
- }
64
- })
65
-
66
- it('returns error for uppercase inline name', () => {
67
- const action = startCreateFlow('MyAgent', PROFILES)
68
- expect(action.kind).toBe('error')
69
- })
70
- })
71
-
72
- // ─── handleFlowText — null state ─────────────────────────────────────────
73
-
74
- describe('foreman-create-flow: handleFlowText with null state', () => {
75
- it('cancels when no active flow', () => {
76
- const action = handleFlowText({ state: null, text: 'hello', profiles: PROFILES })
77
- expect(action.kind).toBe('cancel')
78
- })
79
- })
80
-
81
- // ─── handleFlowText — asked-name step ────────────────────────────────────
82
-
83
- describe('foreman-create-flow: handleFlowText step=asked-name', () => {
84
- function makeState(): CreateFlowState {
85
- return makeInitialState('chat-1', null) // step = 'asked-name'
86
- }
87
-
88
- it('transitions to ask-profile on valid name', () => {
89
- const action = handleFlowText({ state: makeState(), text: 'gymbro', profiles: PROFILES })
90
- expect(action.kind).toBe('ask-profile')
91
- if (action.kind === 'ask-profile') {
92
- expect(action.profiles).toEqual(PROFILES)
93
- }
94
- })
95
-
96
- it('returns error on invalid name, stayInStep=true', () => {
97
- const action = handleFlowText({ state: makeState(), text: 'Bad Name!', profiles: PROFILES })
98
- expect(action.kind).toBe('error')
99
- if (action.kind === 'error') {
100
- expect(action.stayInStep).toBe(true)
101
- }
102
- })
103
-
104
- it('error message mentions the bad input', () => {
105
- const action = handleFlowText({ state: makeState(), text: 'MyBotIsGreat', profiles: PROFILES })
106
- if (action.kind === 'error') {
107
- expect(action.message).toContain('MyBotIsGreat')
108
- }
109
- })
110
-
111
- it('accepts name with hyphens', () => {
112
- const action = handleFlowText({ state: makeState(), text: 'my-agent', profiles: PROFILES })
113
- expect(action.kind).toBe('ask-profile')
114
- })
115
- })
116
-
117
- // ─── handleFlowText — asked-profile step ──────────────────────────────────
118
-
119
- describe('foreman-create-flow: handleFlowText step=asked-profile', () => {
120
- function makeState(name = 'gymbro'): CreateFlowState {
121
- return {
122
- chatId: 'chat-1',
123
- step: 'asked-profile',
124
- name,
125
- profile: null,
126
- botToken: null,
127
- authSessionName: null,
128
- loginUrl: null,
129
- startedAt: Date.now(),
130
- updatedAt: Date.now(),
131
- }
132
- }
133
-
134
- it('transitions to ask-bot-token on valid profile', () => {
135
- const action = handleFlowText({ state: makeState(), text: 'health-coach', profiles: PROFILES })
136
- expect(action.kind).toBe('ask-bot-token')
137
- if (action.kind === 'ask-bot-token') {
138
- expect(action.profile).toBe('health-coach')
139
- expect(action.name).toBe('gymbro')
140
- }
141
- })
142
-
143
- it('returns error on unknown profile, stayInStep=true', () => {
144
- const action = handleFlowText({ state: makeState(), text: 'nonexistent-profile', profiles: PROFILES })
145
- expect(action.kind).toBe('error')
146
- if (action.kind === 'error') {
147
- expect(action.stayInStep).toBe(true)
148
- expect(action.message).toContain('nonexistent-profile')
149
- }
150
- })
151
-
152
- it('lists valid profiles in error message', () => {
153
- const action = handleFlowText({ state: makeState(), text: 'bad', profiles: PROFILES })
154
- if (action.kind === 'error') {
155
- for (const p of PROFILES) {
156
- expect(action.message).toContain(p)
157
- }
158
- }
159
- })
160
-
161
- it('cancels with missing-name when state.name is unset (#28 item 1)', () => {
162
- // Pre-#28 fix this fell back to using the profile name as the agent
163
- // name. Now we cancel cleanly so the user gets a clear restart
164
- // signal instead of an agent named "default".
165
- const stateNoName = {
166
- chatId: 'chat-1',
167
- step: 'asked-profile' as const,
168
- name: null,
169
- profile: null,
170
- botToken: null,
171
- authSessionName: null,
172
- loginUrl: null,
173
- startedAt: Date.now(),
174
- updatedAt: Date.now(),
175
- }
176
- const action = handleFlowText({ state: stateNoName, text: 'default', profiles: PROFILES })
177
- expect(action.kind).toBe('cancel')
178
- if (action.kind === 'cancel') {
179
- expect(action.reason).toBe('missing-name')
180
- }
181
- })
182
- })
183
-
184
- // ─── handleFlowText — asked-bot-token step ───────────────────────────────
185
-
186
- describe('foreman-create-flow: handleFlowText step=asked-bot-token', () => {
187
- function makeState(): CreateFlowState {
188
- return {
189
- chatId: 'chat-1',
190
- step: 'asked-bot-token',
191
- name: 'gymbro',
192
- profile: 'health-coach',
193
- botToken: null,
194
- authSessionName: null,
195
- loginUrl: null,
196
- startedAt: Date.now(),
197
- updatedAt: Date.now(),
198
- }
199
- }
200
-
201
- it('transitions to call-create-agent on token-shaped input', () => {
202
- const token = '1234567890:AAHaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
203
- const action = handleFlowText({ state: makeState(), text: token, profiles: PROFILES })
204
- expect(action.kind).toBe('call-create-agent')
205
- if (action.kind === 'call-create-agent') {
206
- expect(action.botToken).toBe(token)
207
- expect(action.name).toBe('gymbro')
208
- expect(action.profile).toBe('health-coach')
209
- }
210
- })
211
-
212
- it('returns error on token with no colon, stayInStep=true', () => {
213
- const action = handleFlowText({ state: makeState(), text: 'notavalidtoken', profiles: PROFILES })
214
- expect(action.kind).toBe('error')
215
- if (action.kind === 'error') {
216
- expect(action.stayInStep).toBe(true)
217
- }
218
- })
219
-
220
- it('returns error on token too short', () => {
221
- const action = handleFlowText({ state: makeState(), text: 'a:b', profiles: PROFILES })
222
- expect(action.kind).toBe('error')
223
- if (action.kind === 'error') {
224
- expect(action.stayInStep).toBe(true)
225
- }
226
- })
227
-
228
- it('cancels if name or profile missing in state', () => {
229
- const state: CreateFlowState = {
230
- ...makeState(),
231
- name: null,
232
- }
233
- const action = handleFlowText({ state, text: '1234567890:AAHsomething', profiles: PROFILES })
234
- expect(action.kind).toBe('cancel')
235
- })
236
- })
237
-
238
- // ─── handleFlowText — asked-oauth-code step ──────────────────────────────
239
-
240
- describe('foreman-create-flow: handleFlowText step=asked-oauth-code', () => {
241
- function makeState(): CreateFlowState {
242
- return {
243
- chatId: 'chat-1',
244
- step: 'asked-oauth-code',
245
- name: 'gymbro',
246
- profile: 'health-coach',
247
- botToken: '1234567890:AAHsomething',
248
- authSessionName: 'gymbro-auth-123',
249
- loginUrl: 'https://claude.ai/oauth/authorize?...',
250
- startedAt: Date.now(),
251
- updatedAt: Date.now(),
252
- }
253
- }
254
-
255
- it('transitions to call-complete-creation on plausible code', () => {
256
- const action = handleFlowText({ state: makeState(), text: 'abc12345', profiles: PROFILES })
257
- expect(action.kind).toBe('call-complete-creation')
258
- if (action.kind === 'call-complete-creation') {
259
- expect(action.name).toBe('gymbro')
260
- expect(action.code).toBe('abc12345')
261
- }
262
- })
263
-
264
- it('returns error on code that is too short, stayInStep=true', () => {
265
- const action = handleFlowText({ state: makeState(), text: 'ab', profiles: PROFILES })
266
- expect(action.kind).toBe('error')
267
- if (action.kind === 'error') {
268
- expect(action.stayInStep).toBe(true)
269
- }
270
- })
271
-
272
- it('cancels if name missing in state', () => {
273
- const state: CreateFlowState = { ...makeState(), name: null }
274
- const action = handleFlowText({ state, text: 'abc12345', profiles: PROFILES })
275
- expect(action.kind).toBe('cancel')
276
- })
277
- })
278
-
279
- // ─── handleFlowText — done step ──────────────────────────────────────────
280
-
281
- describe('foreman-create-flow: handleFlowText step=done', () => {
282
- it('returns cancel when flow is already done', () => {
283
- const state: CreateFlowState = {
284
- chatId: 'chat-1',
285
- step: 'done',
286
- name: 'gymbro',
287
- profile: 'health-coach',
288
- botToken: null,
289
- authSessionName: null,
290
- loginUrl: null,
291
- startedAt: Date.now(),
292
- updatedAt: Date.now(),
293
- }
294
- const action = handleFlowText({ state, text: 'hello', profiles: PROFILES })
295
- expect(action.kind).toBe('cancel')
296
- })
297
- })
298
-
299
- // ─── makeInitialState ─────────────────────────────────────────────────────
300
-
301
- describe('foreman-create-flow: makeInitialState', () => {
302
- it('sets step to asked-name when name is null', () => {
303
- const state = makeInitialState('chat-1', null)
304
- expect(state.step).toBe('asked-name')
305
- expect(state.name).toBeNull()
306
- })
307
-
308
- it('sets step to asked-profile when name provided', () => {
309
- const state = makeInitialState('chat-1', 'gymbro')
310
- expect(state.step).toBe('asked-profile')
311
- expect(state.name).toBe('gymbro')
312
- })
313
-
314
- it('sets startedAt and updatedAt', () => {
315
- const before = Date.now()
316
- const state = makeInitialState('chat-1', null)
317
- const after = Date.now()
318
- expect(state.startedAt).toBeGreaterThanOrEqual(before)
319
- expect(state.startedAt).toBeLessThanOrEqual(after)
320
- expect(state.updatedAt).toBe(state.startedAt)
321
- })
322
- })
323
-
324
- // ─── advanceState ─────────────────────────────────────────────────────────
325
-
326
- describe('foreman-create-flow: advanceState', () => {
327
- it('merges updates into state', () => {
328
- const state = makeInitialState('chat-1', 'gymbro')
329
- const next = advanceState(state, { step: 'asked-bot-token', profile: 'health-coach' })
330
- expect(next.step).toBe('asked-bot-token')
331
- expect(next.profile).toBe('health-coach')
332
- expect(next.name).toBe('gymbro')
333
- expect(next.chatId).toBe('chat-1')
334
- })
335
-
336
- it('updates updatedAt', () => {
337
- const state = makeInitialState('chat-1', null)
338
- const before = Date.now()
339
- const next = advanceState(state, { step: 'asked-profile', name: 'gymbro' })
340
- expect(next.updatedAt).toBeGreaterThanOrEqual(before)
341
- })
342
-
343
- it('preserves startedAt', () => {
344
- const state = makeInitialState('chat-1', null)
345
- const next = advanceState(state, { step: 'asked-profile' })
346
- expect(next.startedAt).toBe(state.startedAt)
347
- })
348
- })
349
-
350
- // ─── stepLabel ────────────────────────────────────────────────────────────
351
-
352
- describe('foreman-create-flow: stepLabel', () => {
353
- it('returns a non-empty string for each step', () => {
354
- const steps = ['asked-name', 'asked-profile', 'asked-bot-token', 'asked-oauth-code', 'done'] as const
355
- for (const step of steps) {
356
- expect(stepLabel(step).length).toBeGreaterThan(0)
357
- }
358
- })
359
- })