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,347 +0,0 @@
1
- /**
2
- * Tests for telegram-plugin/foreman/foreman-handlers.ts
3
- *
4
- * Tests the real handler implementations imported from foreman-handlers.ts,
5
- * using injected mocks for execFileSync and switchroomExecJson rather than
6
- * re-implementing the logic locally.
7
- *
8
- * Covers:
9
- * - assertSafeAgentName: valid and invalid agent names
10
- * - handleLogsCommand: agent name validation, --tail parsing, execFileSync args,
11
- * bad-name rejection, empty output, paginated output
12
- * - buildFleetSummary: calls switchroomExecJson(['agent', 'list']),
13
- * formats HTML output correctly
14
- * - private-chat guard: middleware rejects non-private chats
15
- * - parseTailN: tail-N parsing rules
16
- * - chunkText: pagination boundary logic
17
- */
18
-
19
- import { describe, it, expect, vi } from 'vitest'
20
- import {
21
- assertSafeAgentName,
22
- handleLogsCommand,
23
- buildFleetSummary,
24
- parseTailN,
25
- chunkText,
26
- type SwitchroomExecJsonFn,
27
- } from '../foreman/foreman-handlers.js'
28
- import { isAllowedSender } from '../shared/bot-runtime.js'
29
- import type { Context } from 'grammy'
30
-
31
- // ─── assertSafeAgentName ──────────────────────────────────────────────────
32
-
33
- describe('foreman-handlers: assertSafeAgentName', () => {
34
- it('accepts simple lowercase names', () => {
35
- expect(() => assertSafeAgentName('gymbro')).not.toThrow()
36
- })
37
-
38
- it('accepts names with hyphens', () => {
39
- expect(() => assertSafeAgentName('my-agent')).not.toThrow()
40
- })
41
-
42
- it('accepts names with underscores', () => {
43
- expect(() => assertSafeAgentName('my_agent')).not.toThrow()
44
- })
45
-
46
- it('accepts lowercase names with digits', () => {
47
- expect(() => assertSafeAgentName('agent1')).not.toThrow()
48
- })
49
-
50
- it('rejects uppercase names', () => {
51
- expect(() => assertSafeAgentName('Agent1')).toThrow('invalid agent name')
52
- })
53
-
54
- it('accepts 51-char name (Telegram callback_data max)', () => {
55
- expect(() => assertSafeAgentName('a'.repeat(51))).not.toThrow()
56
- })
57
-
58
- it('rejects 52-char name (exceeds callback_data budget)', () => {
59
- expect(() => assertSafeAgentName('a'.repeat(52))).toThrow('invalid agent name')
60
- })
61
-
62
- it('rejects empty name', () => {
63
- expect(() => assertSafeAgentName('')).toThrow('invalid agent name')
64
- })
65
-
66
- it('rejects name with space', () => {
67
- expect(() => assertSafeAgentName('my agent')).toThrow('invalid agent name')
68
- })
69
-
70
- it('rejects name with semicolon (shell injection attempt)', () => {
71
- expect(() => assertSafeAgentName('agent; rm -rf /')).toThrow('invalid agent name')
72
- })
73
-
74
- it('rejects name with dollar sign', () => {
75
- expect(() => assertSafeAgentName('agent$(evil)')).toThrow('invalid agent name')
76
- })
77
-
78
- it('rejects path traversal', () => {
79
- expect(() => assertSafeAgentName('../etc/passwd')).toThrow('invalid agent name')
80
- })
81
-
82
- it('rejects name with colon', () => {
83
- expect(() => assertSafeAgentName('agent:bad')).toThrow('invalid agent name')
84
- })
85
- })
86
-
87
- // ─── parseTailN ─────────────────────────────────────────────────────────
88
-
89
- describe('foreman-handlers: parseTailN', () => {
90
- it('defaults to 50 when no --tail', () => {
91
- expect(parseTailN(['gymbro'])).toBe(50)
92
- })
93
-
94
- it('parses explicit --tail N', () => {
95
- expect(parseTailN(['gymbro', '--tail', '100'])).toBe(100)
96
- })
97
-
98
- it('clamps to 500 max', () => {
99
- expect(parseTailN(['gymbro', '--tail', '9999'])).toBe(500)
100
- })
101
-
102
- it('ignores --tail without value', () => {
103
- expect(parseTailN(['gymbro', '--tail'])).toBe(50)
104
- })
105
-
106
- it('ignores non-numeric --tail value', () => {
107
- expect(parseTailN(['gymbro', '--tail', 'abc'])).toBe(50)
108
- })
109
-
110
- it('ignores zero --tail value', () => {
111
- expect(parseTailN(['gymbro', '--tail', '0'])).toBe(50)
112
- })
113
-
114
- it('ignores negative --tail value', () => {
115
- expect(parseTailN(['gymbro', '--tail', '-10'])).toBe(50)
116
- })
117
- })
118
-
119
- // ─── chunkText ───────────────────────────────────────────────────────────
120
-
121
- describe('foreman-handlers: chunkText', () => {
122
- it('returns single chunk when under limit', () => {
123
- const text = 'x'.repeat(3800)
124
- expect(chunkText(text, 3800)).toHaveLength(1)
125
- })
126
-
127
- it('splits into two chunks when over limit', () => {
128
- const text = 'x'.repeat(4097)
129
- const chunks = chunkText(text, 4096)
130
- expect(chunks).toHaveLength(2)
131
- expect(chunks[0]).toHaveLength(4096)
132
- expect(chunks[1]).toHaveLength(1)
133
- })
134
-
135
- it('all chunks reconstruct the original', () => {
136
- const text = 'abcdefgh'.repeat(1000)
137
- const chunks = chunkText(text, 3000)
138
- expect(chunks.join('')).toBe(text)
139
- })
140
-
141
- it('handles exactly limit-length text', () => {
142
- const text = 'x'.repeat(4096)
143
- expect(chunkText(text, 4096)).toHaveLength(1)
144
- })
145
- })
146
-
147
- // ─── handleLogsCommand ───────────────────────────────────────────────────
148
-
149
- describe('foreman-handlers: handleLogsCommand — agent name validation', () => {
150
- it('returns usage when no args', () => {
151
- const result = handleLogsCommand('')
152
- expect(result.replies).toHaveLength(1)
153
- expect(result.replies[0].text).toContain('Usage')
154
- })
155
-
156
- it('rejects a bad agent name and returns Invalid agent name', () => {
157
- const execFile = vi.fn()
158
- const result = handleLogsCommand('agent; rm -rf /', execFile as never)
159
- expect(result.replies[0].text).toBe('Invalid agent name.')
160
- expect(execFile).not.toHaveBeenCalled()
161
- })
162
-
163
- it('rejects agent name with colon (callback_data delimiter)', () => {
164
- const execFile = vi.fn()
165
- const result = handleLogsCommand('bad:name', execFile as never)
166
- expect(result.replies[0].text).toBe('Invalid agent name.')
167
- expect(execFile).not.toHaveBeenCalled()
168
- })
169
-
170
- it('accepts a valid agent name with hyphens', () => {
171
- const execFile = vi.fn().mockReturnValue('log line 1\nlog line 2\n')
172
- const result = handleLogsCommand('my-agent', execFile as never)
173
- expect(execFile).toHaveBeenCalled()
174
- expect(result.replies[0].text).toContain('log line 1')
175
- })
176
-
177
- it('accepts a valid agent name with underscores', () => {
178
- const execFile = vi.fn().mockReturnValue('some log\n')
179
- const result = handleLogsCommand('my_agent', execFile as never)
180
- expect(execFile).toHaveBeenCalled()
181
- })
182
- })
183
-
184
- describe('foreman-handlers: handleLogsCommand — execFileSync args', () => {
185
- it('calls journalctl with correct argv array (no shell)', () => {
186
- const execFile = vi.fn().mockReturnValue('line1\n')
187
- handleLogsCommand('gymbro', execFile as never)
188
-
189
- expect(execFile).toHaveBeenCalledOnce()
190
- const [cmd, args] = execFile.mock.calls[0] as [string, string[]]
191
- expect(cmd).toBe('journalctl')
192
- expect(args).toContain('--user')
193
- expect(args).toContain('-u')
194
- expect(args).toContain('switchroom-gymbro')
195
- expect(args).toContain('-n')
196
- expect(args).toContain('50') // default tail
197
- expect(args).toContain('--no-pager')
198
- // Must NOT be a shell string — the second arg must be an array
199
- expect(Array.isArray(args)).toBe(true)
200
- })
201
-
202
- it('passes --tail N to journalctl -n', () => {
203
- const execFile = vi.fn().mockReturnValue('line\n')
204
- handleLogsCommand('gymbro --tail 200', execFile as never)
205
-
206
- const [, args] = execFile.mock.calls[0] as [string, string[]]
207
- const nIdx = args.indexOf('-n')
208
- expect(nIdx).toBeGreaterThan(-1)
209
- expect(args[nIdx + 1]).toBe('200')
210
- })
211
-
212
- it('clamps --tail above 500 to 500', () => {
213
- const execFile = vi.fn().mockReturnValue('line\n')
214
- handleLogsCommand('gymbro --tail 9999', execFile as never)
215
-
216
- const [, args] = execFile.mock.calls[0] as [string, string[]]
217
- const nIdx = args.indexOf('-n')
218
- expect(args[nIdx + 1]).toBe('500')
219
- })
220
-
221
- it('unit name includes agent name', () => {
222
- const execFile = vi.fn().mockReturnValue('line\n')
223
- handleLogsCommand('my-agent', execFile as never)
224
-
225
- const [, args] = execFile.mock.calls[0] as [string, string[]]
226
- const uIdx = args.indexOf('-u')
227
- expect(args[uIdx + 1]).toBe('switchroom-my-agent')
228
- })
229
- })
230
-
231
- describe('foreman-handlers: handleLogsCommand — output handling', () => {
232
- it('returns empty-log message when journalctl returns blank', () => {
233
- const execFile = vi.fn().mockReturnValue(' \n')
234
- const result = handleLogsCommand('gymbro', execFile as never)
235
- expect(result.replies[0].text).toContain('No logs found')
236
- })
237
-
238
- it('returns error message when execFileSync throws', () => {
239
- const execFile = vi.fn().mockImplementation(() => {
240
- throw Object.assign(new Error('no such unit'), { stderr: 'Unit not found.' })
241
- })
242
- const result = handleLogsCommand('gymbro', execFile as never)
243
- expect(result.replies[0].text).toContain('logs failed for')
244
- })
245
-
246
- it('returns paginated replies for large output', () => {
247
- const bigOutput = 'x'.repeat(4000) // > 3 KB
248
- const execFile = vi.fn().mockReturnValue(bigOutput)
249
- const result = handleLogsCommand('gymbro', execFile as never)
250
- // Should be chunked into multiple replies
251
- expect(result.replies.length).toBeGreaterThan(1)
252
- })
253
- })
254
-
255
- // ─── buildFleetSummary ────────────────────────────────────────────────────
256
-
257
- describe('foreman-handlers: buildFleetSummary — calls switchroomExecJson correctly', () => {
258
- it('calls execJson with ["agent", "list"]', () => {
259
- const mockExecJson = vi.fn().mockReturnValue({
260
- agents: [{ name: 'gymbro', status: 'active', uptime: '1h' }],
261
- }) as SwitchroomExecJsonFn
262
- buildFleetSummary(mockExecJson)
263
- expect(mockExecJson).toHaveBeenCalledWith(['agent', 'list'])
264
- })
265
-
266
- it('renders fleet HTML with agent name and status', () => {
267
- const mockExecJson = vi.fn().mockReturnValue({
268
- agents: [{ name: 'gymbro', status: 'active', uptime: '2h' }],
269
- }) as SwitchroomExecJsonFn
270
- const html = buildFleetSummary(mockExecJson)
271
- expect(html).toContain('gymbro')
272
- expect(html).toContain('active')
273
- expect(html).toContain('Fleet status')
274
- })
275
-
276
- it('returns empty message when no agents', () => {
277
- const mockExecJson = vi.fn().mockReturnValue({ agents: [] }) as SwitchroomExecJsonFn
278
- const html = buildFleetSummary(mockExecJson)
279
- expect(html).toContain('No agents defined')
280
- })
281
-
282
- it('handles execJson throwing (CLI unreachable)', () => {
283
- const mockExecJson = vi.fn().mockImplementation(() => {
284
- throw new Error('switchroom CLI not found')
285
- }) as SwitchroomExecJsonFn
286
- const html = buildFleetSummary(mockExecJson)
287
- expect(html).toContain('agent list failed')
288
- })
289
-
290
- it('escapes HTML-unsafe characters in agent names', () => {
291
- const mockExecJson = vi.fn().mockReturnValue({
292
- agents: [{ name: '<script>', status: 'active', uptime: '1h' }],
293
- }) as SwitchroomExecJsonFn
294
- const html = buildFleetSummary(mockExecJson)
295
- expect(html).not.toContain('<script>')
296
- expect(html).toContain('&lt;script&gt;')
297
- })
298
- })
299
-
300
- // ─── private-chat guard ───────────────────────────────────────────────────
301
- // The guard lives in foreman.ts middleware but we test the isAllowedSender
302
- // helper that it composes with, plus verify the type check logic directly.
303
-
304
- describe('foreman-handlers: private-chat guard (middleware logic)', () => {
305
- function makeCtx(chatType: string | undefined, userId: number | undefined): Context {
306
- return {
307
- chat: chatType != null ? { type: chatType } : undefined,
308
- from: userId != null ? { id: userId } : undefined,
309
- } as unknown as Context
310
- }
311
-
312
- it('isAllowedSender allows configured user in private chat', () => {
313
- const ctx = makeCtx('private', 42)
314
- expect(isAllowedSender(ctx, ['42'])).toBe(true)
315
- })
316
-
317
- it('isAllowedSender blocks configured user in group chat', () => {
318
- // The middleware checks chat.type !== 'private' BEFORE isAllowedSender.
319
- // Here we just verify that the chat type is the signal to bail.
320
- const ctx = makeCtx('group', 42)
321
- // Simulate middleware: if not private, return early (do NOT call isAllowedSender)
322
- const isPrivate = ctx.chat?.type === 'private'
323
- expect(isPrivate).toBe(false)
324
- // isAllowedSender itself would allow the user — the guard is in middleware
325
- expect(isAllowedSender(ctx, ['42'])).toBe(true) // guard is upstream
326
- })
327
-
328
- it('isAllowedSender blocks unknown user in private chat', () => {
329
- const ctx = makeCtx('private', 99)
330
- expect(isAllowedSender(ctx, ['42'])).toBe(false)
331
- })
332
-
333
- it('isAllowedSender blocks when ctx.from is missing', () => {
334
- const ctx = makeCtx('private', undefined)
335
- expect(isAllowedSender(ctx, ['42'])).toBe(false)
336
- })
337
-
338
- it('group chat is detected as non-private (middleware would bail)', () => {
339
- const ctx = makeCtx('supergroup', 42)
340
- expect(ctx.chat?.type !== 'private').toBe(true)
341
- })
342
-
343
- it('undefined chat type is treated as non-private (middleware would bail)', () => {
344
- const ctx = makeCtx(undefined, 42)
345
- expect(ctx.chat?.type !== 'private').toBe(true)
346
- })
347
- })
@@ -1,164 +0,0 @@
1
- /**
2
- * Tests for telegram-plugin/foreman/state.ts — SQLite-backed conversation state.
3
- *
4
- * Uses bun:test (not vitest) because it imports bun:sqlite.
5
- * Run with: bun test telegram-plugin/tests/foreman-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
- // We override SWITCHROOM_FOREMAN_DIR before importing state so each test
13
- // gets a fresh DB in a temp directory.
14
-
15
- let tmpDir: string
16
-
17
- beforeEach(() => {
18
- tmpDir = mkdtempSync(tmpdir() + '/foreman-state-test-')
19
- process.env.SWITCHROOM_FOREMAN_DIR = tmpDir
20
- })
21
-
22
- afterEach(async () => {
23
- // Must reset the DB singleton between tests so the next test gets a fresh one
24
- const { _resetDbForTest } = await import('../foreman/state.js')
25
- _resetDbForTest()
26
- delete process.env.SWITCHROOM_FOREMAN_DIR
27
- try { rmSync(tmpDir, { recursive: true, force: true }) } catch { /* ignore */ }
28
- })
29
-
30
- // ─── Round-trip: setState + getState ─────────────────────────────────────
31
-
32
- describe('foreman-state: setState + getState round-trip', () => {
33
- it('returns null for unknown chat', async () => {
34
- const { getState } = await import('../foreman/state.js')
35
- const result = getState('unknown-chat')
36
- expect(result).toBeNull()
37
- })
38
-
39
- it('persists and retrieves state', async () => {
40
- const { setState, getState } = await import('../foreman/state.js')
41
- const now = Date.now()
42
- setState({
43
- chatId: 'chat-1',
44
- step: 'asked-name',
45
- name: null,
46
- profile: null,
47
- botToken: null,
48
- authSessionName: null,
49
- loginUrl: null,
50
- startedAt: now,
51
- updatedAt: now,
52
- })
53
- const retrieved = getState('chat-1')
54
- expect(retrieved).not.toBeNull()
55
- expect(retrieved!.chatId).toBe('chat-1')
56
- expect(retrieved!.step).toBe('asked-name')
57
- expect(retrieved!.name).toBeNull()
58
- expect(retrieved!.startedAt).toBe(now)
59
- })
60
-
61
- it('persists all fields', async () => {
62
- const { setState, getState } = await import('../foreman/state.js')
63
- const now = Date.now()
64
- setState({
65
- chatId: 'chat-2',
66
- step: 'asked-oauth-code',
67
- name: 'gymbro',
68
- profile: 'health-coach',
69
- botToken: '1234567890:AAH...',
70
- authSessionName: 'gymbro-auth-session',
71
- loginUrl: 'https://example.com/oauth',
72
- startedAt: now - 5000,
73
- updatedAt: now,
74
- })
75
- const retrieved = getState('chat-2')
76
- expect(retrieved!.step).toBe('asked-oauth-code')
77
- expect(retrieved!.name).toBe('gymbro')
78
- expect(retrieved!.profile).toBe('health-coach')
79
- expect(retrieved!.botToken).toBe('1234567890:AAH...')
80
- expect(retrieved!.authSessionName).toBe('gymbro-auth-session')
81
- expect(retrieved!.loginUrl).toBe('https://example.com/oauth')
82
- })
83
-
84
- it('upserts on conflict (same chat_id)', async () => {
85
- const { setState, getState } = await import('../foreman/state.js')
86
- const now = Date.now()
87
- setState({ chatId: 'chat-3', step: 'asked-name', name: null, profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now, updatedAt: now })
88
- setState({ chatId: 'chat-3', step: 'asked-profile', name: 'gymbro', profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now, updatedAt: now + 100 })
89
-
90
- const retrieved = getState('chat-3')
91
- expect(retrieved!.step).toBe('asked-profile')
92
- expect(retrieved!.name).toBe('gymbro')
93
- })
94
- })
95
-
96
- // ─── clearState ───────────────────────────────────────────────────────────
97
-
98
- describe('foreman-state: clearState', () => {
99
- it('removes state so getState returns null', async () => {
100
- const { setState, getState, clearState } = await import('../foreman/state.js')
101
- const now = Date.now()
102
- setState({ chatId: 'chat-4', step: 'asked-name', name: null, profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now, updatedAt: now })
103
- clearState('chat-4')
104
- expect(getState('chat-4')).toBeNull()
105
- })
106
-
107
- it('is idempotent on unknown chat', async () => {
108
- const { clearState } = await import('../foreman/state.js')
109
- expect(() => clearState('nonexistent-chat')).not.toThrow()
110
- })
111
- })
112
-
113
- // ─── listActiveFlows ──────────────────────────────────────────────────────
114
-
115
- describe('foreman-state: listActiveFlows', () => {
116
- it('returns empty when no flows', async () => {
117
- const { listActiveFlows } = await import('../foreman/state.js')
118
- expect(listActiveFlows()).toHaveLength(0)
119
- })
120
-
121
- it('returns in-progress flows updated within window', async () => {
122
- const { setState, listActiveFlows } = await import('../foreman/state.js')
123
- const now = Date.now()
124
- setState({ chatId: 'chat-5', step: 'asked-bot-token', name: 'gymbro', profile: 'health-coach', botToken: null, authSessionName: null, loginUrl: null, startedAt: now - 1000, updatedAt: now - 500 })
125
-
126
- const flows = listActiveFlows(60 * 60 * 1000)
127
- expect(flows).toHaveLength(1)
128
- expect(flows[0].chatId).toBe('chat-5')
129
- expect(flows[0].step).toBe('asked-bot-token')
130
- })
131
-
132
- it('excludes flows with step=done', async () => {
133
- const { setState, listActiveFlows } = await import('../foreman/state.js')
134
- const now = Date.now()
135
- setState({ chatId: 'chat-6', step: 'done', name: 'gymbro', profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now - 1000, updatedAt: now - 500 })
136
-
137
- const flows = listActiveFlows(60 * 60 * 1000)
138
- const match = flows.find(f => f.chatId === 'chat-6')
139
- expect(match).toBeUndefined()
140
- })
141
-
142
- it('excludes flows older than maxAgeMs', async () => {
143
- const { setState, listActiveFlows } = await import('../foreman/state.js')
144
- const now = Date.now()
145
- // updated_at is 2 hours ago
146
- setState({ chatId: 'chat-7', step: 'asked-oauth-code', name: 'gymbro', profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now - 2 * 3600 * 1000, updatedAt: now - 2 * 3600 * 1000 })
147
-
148
- const flows = listActiveFlows(60 * 60 * 1000) // 1 hour window
149
- const match = flows.find(f => f.chatId === 'chat-7')
150
- expect(match).toBeUndefined()
151
- })
152
-
153
- it('returns multiple in-progress flows', async () => {
154
- const { setState, listActiveFlows } = await import('../foreman/state.js')
155
- const now = Date.now()
156
- setState({ chatId: 'chat-8', step: 'asked-name', name: null, profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now, updatedAt: now })
157
- setState({ chatId: 'chat-9', step: 'asked-profile', name: 'agent2', profile: null, botToken: null, authSessionName: null, loginUrl: null, startedAt: now, updatedAt: now })
158
-
159
- const flows = listActiveFlows()
160
- const chatIds = flows.map(f => f.chatId)
161
- expect(chatIds).toContain('chat-8')
162
- expect(chatIds).toContain('chat-9')
163
- })
164
- })