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,309 +0,0 @@
1
- /**
2
- * Tests for sweepActivePins — the shared helper called by both the
3
- * startup failsafe and the /restart, /reconcile --restart, and /update
4
- * command handlers to unpin any still-pinned progress cards and clear
5
- * the sidecar.
6
- *
7
- * The real unpin path goes through `lockedBot.api.unpinChatMessage`.
8
- * These tests inject a fake unpin callback so behavior is verified
9
- * without a Telegram stub — the shape under test is:
10
- *
11
- * 1. Reads the sidecar
12
- * 2. Calls unpinFn(chatId, messageId) for each entry
13
- * 3. Swallows unpin errors (best-effort — the message may already be
14
- * gone, or the bot may have lost admin rights)
15
- * 4. Clears the sidecar regardless of success/failure
16
- * 5. Bounded by timeoutMs so a hung Telegram API can't block a
17
- * restart indefinitely
18
- */
19
-
20
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
21
- import { mkdtempSync, rmSync, existsSync } from "node:fs";
22
- import { tmpdir } from "node:os";
23
- import { join } from "node:path";
24
- import { addActivePin, readActivePins, ACTIVE_PINS_FILENAME, type ActivePin } from "../active-pins.js";
25
- import {
26
- sweepActivePins,
27
- sweepBotAuthoredPins,
28
- type PinnedMessageInfo,
29
- } from "../active-pins-sweep.js";
30
-
31
- describe("sweepActivePins", () => {
32
- let tmp: string;
33
-
34
- beforeEach(() => {
35
- tmp = mkdtempSync(join(tmpdir(), "active-pins-sweep-"));
36
- });
37
-
38
- afterEach(() => {
39
- rmSync(tmp, { recursive: true, force: true });
40
- });
41
-
42
- const makePin = (overrides: Partial<ActivePin> = {}): ActivePin => ({
43
- chatId: "100",
44
- messageId: 42,
45
- turnKey: "100:0:1",
46
- pinnedAt: 1_700_000_000_000,
47
- ...overrides,
48
- });
49
-
50
- it("is a no-op when the sidecar is empty", async () => {
51
- const calls: Array<[string, number]> = [];
52
- const result = await sweepActivePins(tmp, async (chatId, messageId) => {
53
- calls.push([chatId, messageId]);
54
- });
55
- expect(calls).toEqual([]);
56
- expect(result.swept).toEqual([]);
57
- expect(result.timedOut).toBe(false);
58
- });
59
-
60
- it("calls unpin for each sidecar entry and clears the file", async () => {
61
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1, turnKey: "A:0:1" }));
62
- addActivePin(tmp, makePin({ chatId: "B", messageId: 2, turnKey: "B:0:1" }));
63
- addActivePin(tmp, makePin({ chatId: "A", messageId: 3, turnKey: "A:0:2" }));
64
-
65
- const calls: Array<[string, number]> = [];
66
- const result = await sweepActivePins(tmp, async (chatId, messageId) => {
67
- calls.push([chatId, messageId]);
68
- });
69
-
70
- expect(calls.sort()).toEqual([
71
- ["A", 1],
72
- ["A", 3],
73
- ["B", 2],
74
- ]);
75
- expect(result.swept).toHaveLength(3);
76
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
77
- expect(readActivePins(tmp)).toEqual([]);
78
- });
79
-
80
- it("still clears the sidecar when unpin throws for every entry", async () => {
81
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
82
- addActivePin(tmp, makePin({ chatId: "B", messageId: 2, turnKey: "B:0:1" }));
83
-
84
- const errors: string[] = [];
85
- await sweepActivePins(
86
- tmp,
87
- async () => {
88
- throw new Error("message to unpin not found");
89
- },
90
- { log: (msg) => errors.push(msg) },
91
- );
92
-
93
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
94
- expect(readActivePins(tmp)).toEqual([]);
95
- // Each failure is logged — log receives one "sweeping N" line plus
96
- // one "unpin failed" line per entry.
97
- expect(errors.some((e) => e.includes("sweeping 2"))).toBe(true);
98
- expect(errors.filter((e) => e.includes("unpin failed"))).toHaveLength(2);
99
- });
100
-
101
- it("tolerates a mix of successful and failing unpins", async () => {
102
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
103
- addActivePin(tmp, makePin({ chatId: "A", messageId: 2, turnKey: "A:0:2" }));
104
-
105
- const unpinned: Array<[string, number]> = [];
106
- await sweepActivePins(tmp, async (chatId, messageId) => {
107
- if (messageId === 2) throw new Error("nope");
108
- unpinned.push([chatId, messageId]);
109
- });
110
-
111
- expect(unpinned).toEqual([["A", 1]]);
112
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
113
- });
114
-
115
- it("returns after timeoutMs even if unpin never resolves", async () => {
116
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
117
-
118
- const start = Date.now();
119
- const result = await sweepActivePins(
120
- tmp,
121
- () => new Promise(() => { /* never resolves */ }),
122
- { timeoutMs: 50 },
123
- );
124
- const elapsed = Date.now() - start;
125
-
126
- expect(result.timedOut).toBe(true);
127
- expect(elapsed).toBeLessThan(500);
128
- // Sidecar is cleared even though the unpin never landed — stale
129
- // entries get retried from Telegram's side on next boot but we
130
- // don't want the sweep to keep re-firing them forever.
131
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
132
- });
133
-
134
- it("passes the caller-provided log hook", async () => {
135
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
136
- const lines: string[] = [];
137
- await sweepActivePins(
138
- tmp,
139
- async () => { /* succeed silently */ },
140
- { log: (msg) => lines.push(msg) },
141
- );
142
- expect(lines).toContain("sweeping 1 active pin(s)");
143
- });
144
- });
145
-
146
- describe("sweepBotAuthoredPins", () => {
147
- const BOT_ID = 1000;
148
-
149
- /**
150
- * Build a fake getTopPin backed by a map of { chatId: queue }.
151
- * Each unpin pops the head of the queue — which mimics Telegram's
152
- * "unpinning the top reveals the next most recent pin" behavior.
153
- */
154
- function fakeChats(
155
- state: Record<string, Array<{ messageId: number; fromId: number | null }>>,
156
- ) {
157
- const getTopPin = async (chatId: string): Promise<PinnedMessageInfo | null> => {
158
- const q = state[chatId] ?? [];
159
- return q.length === 0 ? null : { ...q[0] };
160
- };
161
- const unpinCalls: Array<[string, number]> = [];
162
- const unpin = async (chatId: string, messageId: number): Promise<void> => {
163
- unpinCalls.push([chatId, messageId]);
164
- const q = state[chatId] ?? [];
165
- if (q[0]?.messageId === messageId) q.shift();
166
- };
167
- return { getTopPin, unpin, unpinCalls };
168
- }
169
-
170
- it("is a no-op when no chats are provided", async () => {
171
- const { getTopPin, unpin, unpinCalls } = fakeChats({});
172
- const result = await sweepBotAuthoredPins([], BOT_ID, getTopPin, unpin);
173
- expect(unpinCalls).toEqual([]);
174
- expect(result.total).toBe(0);
175
- expect(result.perChat).toEqual({});
176
- });
177
-
178
- it("is a no-op when a chat has no pinned message", async () => {
179
- const { getTopPin, unpin, unpinCalls } = fakeChats({ A: [] });
180
- const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
181
- expect(unpinCalls).toEqual([]);
182
- expect(result.total).toBe(0);
183
- });
184
-
185
- it("unpins consecutive bot-authored pins until a foreign pin is reached", async () => {
186
- const { getTopPin, unpin, unpinCalls } = fakeChats({
187
- A: [
188
- { messageId: 10, fromId: BOT_ID },
189
- { messageId: 9, fromId: BOT_ID },
190
- { messageId: 8, fromId: 42 }, // user pin — barrier
191
- { messageId: 7, fromId: BOT_ID }, // would be unpinned if barrier weren't there
192
- ],
193
- });
194
- const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
195
- expect(unpinCalls).toEqual([
196
- ["A", 10],
197
- ["A", 9],
198
- ]);
199
- expect(result.total).toBe(2);
200
- expect(result.perChat).toEqual({ A: 2 });
201
- });
202
-
203
- it("stops immediately when the top pin belongs to someone else", async () => {
204
- const { getTopPin, unpin, unpinCalls } = fakeChats({
205
- A: [
206
- { messageId: 5, fromId: 999 },
207
- { messageId: 4, fromId: BOT_ID },
208
- ],
209
- });
210
- const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
211
- expect(unpinCalls).toEqual([]);
212
- expect(result.total).toBe(0);
213
- });
214
-
215
- it("treats pins with no from.id as foreign (anonymous channel post)", async () => {
216
- const { getTopPin, unpin, unpinCalls } = fakeChats({
217
- A: [
218
- { messageId: 5, fromId: null },
219
- { messageId: 4, fromId: BOT_ID },
220
- ],
221
- });
222
- const result = await sweepBotAuthoredPins(["A"], BOT_ID, getTopPin, unpin);
223
- expect(unpinCalls).toEqual([]);
224
- expect(result.total).toBe(0);
225
- });
226
-
227
- it("iterates across multiple chats independently", async () => {
228
- const { getTopPin, unpin, unpinCalls } = fakeChats({
229
- A: [
230
- { messageId: 1, fromId: BOT_ID },
231
- { messageId: 2, fromId: BOT_ID },
232
- ],
233
- B: [{ messageId: 3, fromId: 777 }],
234
- C: [{ messageId: 4, fromId: BOT_ID }],
235
- });
236
- const result = await sweepBotAuthoredPins(
237
- ["A", "B", "C"],
238
- BOT_ID,
239
- getTopPin,
240
- unpin,
241
- );
242
- expect(unpinCalls).toEqual([
243
- ["A", 1],
244
- ["A", 2],
245
- ["C", 4],
246
- ]);
247
- expect(result.total).toBe(3);
248
- expect(result.perChat).toEqual({ A: 2, C: 1 });
249
- });
250
-
251
- it("advances to the next chat when getTopPin throws", async () => {
252
- const unpinCalls: Array<[string, number]> = [];
253
- const errors: string[] = [];
254
- const result = await sweepBotAuthoredPins(
255
- ["A", "B"],
256
- BOT_ID,
257
- async (chatId) => {
258
- if (chatId === "A") throw new Error("boom");
259
- return { messageId: 7, fromId: BOT_ID };
260
- },
261
- async (chatId, messageId) => {
262
- unpinCalls.push([chatId, messageId]);
263
- // Simulate "pin is gone after unpin" so the B loop terminates
264
- throw new Error("already gone");
265
- },
266
- { log: (msg) => errors.push(msg) },
267
- );
268
- // A errored on getChat; B attempted one unpin (which threw) then stopped
269
- expect(unpinCalls).toEqual([["B", 7]]);
270
- expect(result.total).toBe(0);
271
- expect(errors.some((e) => e.includes("getChat failed for A"))).toBe(true);
272
- expect(errors.some((e) => e.includes("unpin failed for B/7"))).toBe(true);
273
- });
274
-
275
- it("breaks out of a chat on unpin failure to avoid infinite loops", async () => {
276
- const calls: Array<[string, number]> = [];
277
- const result = await sweepBotAuthoredPins(
278
- ["A"],
279
- BOT_ID,
280
- async () => ({ messageId: 1, fromId: BOT_ID }),
281
- async (chatId, messageId) => {
282
- calls.push([chatId, messageId]);
283
- throw new Error("permission denied");
284
- },
285
- );
286
- // Without the break, the fake getTopPin would keep returning the same
287
- // pin and we'd loop until maxPerChat. With the break, we see exactly
288
- // one call and the sweep moves on.
289
- expect(calls).toEqual([["A", 1]]);
290
- expect(result.total).toBe(0);
291
- });
292
-
293
- it("is bounded by maxPerChat when pins keep re-appearing", async () => {
294
- // A pathological case: getTopPin always returns a bot-authored pin,
295
- // and unpin silently "succeeds" without removing it. Without the
296
- // maxPerChat bound the loop would be infinite.
297
- let calls = 0;
298
- await sweepBotAuthoredPins(
299
- ["A"],
300
- BOT_ID,
301
- async () => ({ messageId: 1, fromId: BOT_ID }),
302
- async () => {
303
- calls++;
304
- },
305
- { maxPerChat: 5 },
306
- );
307
- expect(calls).toBe(5);
308
- });
309
- });
@@ -1,187 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { mkdtempSync, rmSync, writeFileSync, existsSync, readFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import {
6
- readActivePins,
7
- writeActivePins,
8
- addActivePin,
9
- removeActivePin,
10
- clearActivePins,
11
- ACTIVE_PINS_FILENAME,
12
- type ActivePin,
13
- } from "../active-pins.js";
14
-
15
- describe("active-pins sidecar", () => {
16
- let tmp: string;
17
-
18
- beforeEach(() => {
19
- tmp = mkdtempSync(join(tmpdir(), "active-pins-"));
20
- });
21
-
22
- afterEach(() => {
23
- rmSync(tmp, { recursive: true, force: true });
24
- });
25
-
26
- const makePin = (overrides: Partial<ActivePin> = {}): ActivePin => ({
27
- chatId: "100",
28
- messageId: 42,
29
- turnKey: "100:0:1",
30
- pinnedAt: 1_700_000_000_000,
31
- ...overrides,
32
- });
33
-
34
- describe("readActivePins", () => {
35
- it("returns [] when the file is missing", () => {
36
- expect(readActivePins(tmp)).toEqual([]);
37
- });
38
-
39
- it("returns [] when the file is empty", () => {
40
- writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), "");
41
- expect(readActivePins(tmp)).toEqual([]);
42
- });
43
-
44
- it("returns [] when the file is not valid JSON", () => {
45
- writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), "{not json");
46
- expect(readActivePins(tmp)).toEqual([]);
47
- });
48
-
49
- it("returns [] when the JSON root is not an array", () => {
50
- writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), '{"oops": true}');
51
- expect(readActivePins(tmp)).toEqual([]);
52
- });
53
-
54
- it("round-trips valid pin entries", () => {
55
- const pin = makePin();
56
- writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), JSON.stringify([pin]));
57
- expect(readActivePins(tmp)).toEqual([pin]);
58
- });
59
-
60
- it("drops entries with missing or wrong-typed fields", () => {
61
- const good = makePin({ messageId: 7 });
62
- const bad = [
63
- good,
64
- { chatId: "x", messageId: "not-a-number", turnKey: "k", pinnedAt: 1 },
65
- { chatId: "y", messageId: 1, turnKey: "k" }, // missing pinnedAt
66
- null,
67
- "nope",
68
- ];
69
- writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), JSON.stringify(bad));
70
- expect(readActivePins(tmp)).toEqual([good]);
71
- });
72
- });
73
-
74
- describe("writeActivePins", () => {
75
- it("creates the sidecar with JSON content", () => {
76
- const pins = [makePin(), makePin({ messageId: 43, turnKey: "100:0:2" })];
77
- writeActivePins(tmp, pins);
78
- const raw = readFileSync(join(tmp, ACTIVE_PINS_FILENAME), "utf-8");
79
- expect(JSON.parse(raw)).toEqual(pins);
80
- });
81
-
82
- it("deletes the sidecar when writing an empty list", () => {
83
- writeFileSync(join(tmp, ACTIVE_PINS_FILENAME), JSON.stringify([makePin()]));
84
- writeActivePins(tmp, []);
85
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
86
- });
87
-
88
- it("is idempotent — deleting an already-missing file is a no-op", () => {
89
- writeActivePins(tmp, []);
90
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
91
- });
92
- });
93
-
94
- describe("addActivePin", () => {
95
- it("creates the sidecar on first add", () => {
96
- addActivePin(tmp, makePin());
97
- expect(readActivePins(tmp)).toHaveLength(1);
98
- });
99
-
100
- it("appends distinct pins", () => {
101
- addActivePin(tmp, makePin({ messageId: 1, turnKey: "a" }));
102
- addActivePin(tmp, makePin({ messageId: 2, turnKey: "b" }));
103
- const pins = readActivePins(tmp);
104
- expect(pins).toHaveLength(2);
105
- expect(pins.map((p) => p.messageId).sort()).toEqual([1, 2]);
106
- });
107
-
108
- it("replaces an existing entry with the same (chatId, messageId)", () => {
109
- addActivePin(tmp, makePin({ pinnedAt: 100 }));
110
- addActivePin(tmp, makePin({ pinnedAt: 200 }));
111
- const pins = readActivePins(tmp);
112
- expect(pins).toHaveLength(1);
113
- expect(pins[0].pinnedAt).toBe(200);
114
- });
115
-
116
- it("treats different chatIds as distinct even with matching messageIds", () => {
117
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1 }));
118
- addActivePin(tmp, makePin({ chatId: "B", messageId: 1 }));
119
- expect(readActivePins(tmp)).toHaveLength(2);
120
- });
121
- });
122
-
123
- describe("removeActivePin", () => {
124
- it("removes the matching entry", () => {
125
- addActivePin(tmp, makePin({ messageId: 1, turnKey: "a" }));
126
- addActivePin(tmp, makePin({ messageId: 2, turnKey: "b" }));
127
- removeActivePin(tmp, "100", 1);
128
- const pins = readActivePins(tmp);
129
- expect(pins).toHaveLength(1);
130
- expect(pins[0].messageId).toBe(2);
131
- });
132
-
133
- it("deletes the sidecar when the last entry is removed", () => {
134
- addActivePin(tmp, makePin());
135
- removeActivePin(tmp, "100", 42);
136
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
137
- });
138
-
139
- it("is a no-op when the file is missing", () => {
140
- removeActivePin(tmp, "100", 42);
141
- expect(readActivePins(tmp)).toEqual([]);
142
- });
143
-
144
- it("is a no-op when no entry matches", () => {
145
- addActivePin(tmp, makePin({ messageId: 1 }));
146
- removeActivePin(tmp, "100", 999);
147
- expect(readActivePins(tmp)).toHaveLength(1);
148
- });
149
-
150
- it("only matches on (chatId, messageId) — turnKey/pinnedAt are ignored", () => {
151
- addActivePin(tmp, makePin({ chatId: "A", messageId: 1, turnKey: "x" }));
152
- removeActivePin(tmp, "A", 1);
153
- expect(readActivePins(tmp)).toEqual([]);
154
- });
155
- });
156
-
157
- describe("clearActivePins", () => {
158
- it("deletes the sidecar outright", () => {
159
- addActivePin(tmp, makePin());
160
- addActivePin(tmp, makePin({ messageId: 43, turnKey: "b" }));
161
- clearActivePins(tmp);
162
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
163
- });
164
-
165
- it("is a no-op when the file is missing", () => {
166
- clearActivePins(tmp);
167
- expect(existsSync(join(tmp, ACTIVE_PINS_FILENAME))).toBe(false);
168
- });
169
- });
170
-
171
- describe("crash → restart simulation", () => {
172
- it("sidecar survives to be read by a fresh process", () => {
173
- addActivePin(tmp, makePin({ chatId: "C1", messageId: 10, turnKey: "C1:0:1" }));
174
- addActivePin(tmp, makePin({ chatId: "C2", messageId: 20, turnKey: "C2:0:1" }));
175
- // simulated restart — only thing a fresh process has is agentDir
176
- const recovered = readActivePins(tmp);
177
- expect(recovered).toHaveLength(2);
178
- expect(recovered.map((p) => `${p.chatId}/${p.messageId}`).sort()).toEqual([
179
- "C1/10",
180
- "C2/20",
181
- ]);
182
- // sweep: unpin each then clear
183
- clearActivePins(tmp);
184
- expect(readActivePins(tmp)).toEqual([]);
185
- });
186
- });
187
- });
@@ -1,118 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import {
3
- buildDashboardText,
4
- formatRateLimitTier,
5
- type DashboardState,
6
- type DashboardSlot,
7
- } from "../auth-dashboard";
8
-
9
- function slot(o: Partial<DashboardSlot> = {}): DashboardSlot {
10
- return { slot: "default", active: false, health: "healthy", quotaExhaustedUntil: null, fiveHourPct: null, sevenDayPct: null, ...o };
11
- }
12
-
13
- /**
14
- * 2026-04-22 — account-identity surface.
15
- *
16
- * Context: a user reauths an agent onto their Max 20x account, but the
17
- * OAuth browser flow gets hijacked by Telegram's in-app WebView (which
18
- * uses a separate cookie jar from their main browser) and the saved
19
- * token ends up for a different account (e.g. a Max 5x) instead. The
20
- * dashboard header showed 'Plan: max' \u2014 indistinguishable between
21
- * 5x and 20x \u2014 so the mismatch was silent until the user hit a quota wall
22
- * hours later.
23
- *
24
- * Fix: surface the full `rateLimitTier` string on the dashboard so a
25
- * wrong-account reauth is IMMEDIATELY visible. User expected max_20x,
26
- * sees max_5x, acts.
27
- *
28
- * Pair fixes (out of scope for these tests but covered in the PR):
29
- * - Auth response now includes a \ud83d\udccb Copy URL button so the user can
30
- * paste into their main browser instead of Telegram's WebView.
31
- * - Auth response text includes a tip about the in-app-browser pitfall.
32
- */
33
-
34
- describe("formatRateLimitTier", () => {
35
- it("shortens default_claude_max_5x to max_5x", () => {
36
- expect(formatRateLimitTier("default_claude_max_5x")).toBe("max_5x");
37
- });
38
-
39
- it("shortens default_claude_max_20x to max_20x", () => {
40
- expect(formatRateLimitTier("default_claude_max_20x")).toBe("max_20x");
41
- });
42
-
43
- it("shortens default_claude_pro to pro", () => {
44
- expect(formatRateLimitTier("default_claude_pro")).toBe("pro");
45
- });
46
-
47
- it("passes unknown tiers through unchanged", () => {
48
- // We don't pretend to understand every future tier string. Passthrough
49
- // means a new Anthropic tier name is visible verbatim until we
50
- // update the formatter.
51
- expect(formatRateLimitTier("team_custom_42")).toBe("team_custom_42");
52
- expect(formatRateLimitTier("enterprise_unlimited")).toBe("enterprise_unlimited");
53
- });
54
-
55
- it("handles empty/null-ish input gracefully", () => {
56
- expect(formatRateLimitTier("")).toBe("");
57
- });
58
- });
59
-
60
- describe("dashboard header surfaces rateLimitTier when present", () => {
61
- const base: DashboardState = {
62
- agent: "lawgpt",
63
- bankId: "lawgpt",
64
- plan: "max",
65
- slots: [slot({ active: true })],
66
- quotaHot: false,
67
- };
68
-
69
- it("shows max_20x when on the bigger plan", () => {
70
- const text = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_20x" });
71
- expect(text).toContain("Plan: <b>max_20x</b>");
72
- // Should NOT just say 'max' \u2014 that's the ambiguous label that
73
- // hid the account mismatch in the incident.
74
- expect(text).not.toContain("Plan: <b>max</b>");
75
- });
76
-
77
- it("shows max_5x when on the smaller plan", () => {
78
- const text = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_5x" });
79
- expect(text).toContain("Plan: <b>max_5x</b>");
80
- });
81
-
82
- it("falls back to plan label when rateLimitTier missing", () => {
83
- const text = buildDashboardText({ ...base, rateLimitTier: null });
84
- expect(text).toContain("Plan: <b>max</b>");
85
- });
86
-
87
- it("falls back to plan label when rateLimitTier undefined", () => {
88
- const text = buildDashboardText({ ...base });
89
- expect(text).toContain("Plan: <b>max</b>");
90
- });
91
-
92
- it("omits Plan: when neither tier nor plan are known", () => {
93
- const text = buildDashboardText({ ...base, plan: null, rateLimitTier: null });
94
- expect(text).not.toContain("Plan:");
95
- expect(text).toContain("Bank: <code>lawgpt</code>");
96
- });
97
-
98
- it("escapes HTML in tier (injection guard)", () => {
99
- const text = buildDashboardText({
100
- ...base,
101
- rateLimitTier: "<script>alert(1)</script>",
102
- });
103
- expect(text).not.toContain("<script>");
104
- expect(text).toContain("&lt;script&gt;");
105
- });
106
-
107
- it("pair assertion: user can distinguish 5x from 20x without hunting", () => {
108
- // Regression anchor: this was the exact confusion in the incident.
109
- // The user saw 'Plan: max' for both accounts and couldn't tell
110
- // which got authorized. With the tier string present, 5x and 20x
111
- // look different in a glance.
112
- const fivex = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_5x" });
113
- const twentyx = buildDashboardText({ ...base, rateLimitTier: "default_claude_max_20x" });
114
- expect(fivex).not.toBe(twentyx);
115
- expect(fivex).toContain("5x");
116
- expect(twentyx).toContain("20x");
117
- });
118
- });