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
@@ -0,0 +1,191 @@
1
+ /**
2
+ * UAT scenario for #1115 — `vault.broker.approvalAuth: telegram-id`
3
+ * single-factor approve path.
4
+ *
5
+ * **What this exercises that unit tests cannot.** The schema test, the
6
+ * resolver fuzz, and the source-text contracts all live inside the
7
+ * gateway process. They prove the wiring is shaped right. They do NOT
8
+ * prove that:
9
+ * - the *live* Telegram callback for the Approve button is routed
10
+ * to `handleVaultRequestAccessCallback`,
11
+ * - the gateway-cached `AUTO_UNLOCK_PASSPHRASE` is what the broker
12
+ * actually accepts,
13
+ * - the broker mints a real grant token end-to-end with no
14
+ * passphrase prompt visible in chat,
15
+ * - the success card carries the single-factor footer
16
+ * (`Approver verified by Telegram identity`).
17
+ *
18
+ * This scenario closes that gap by round-tripping a real Telegram tap
19
+ * against a real broker on a host configured with
20
+ * `vault.broker.approvalAuth: telegram-id`.
21
+ *
22
+ * Sibling: `vault-request-access-end-to-end-dm.test.ts` exercises the
23
+ * same agent-initiated path under the DEFAULT `passphrase` posture
24
+ * (two-factor). The two scenarios together pin both rungs of the
25
+ * posture matrix.
26
+ *
27
+ * **Skipped by default.** To unskip:
28
+ *
29
+ * 1. Standard UAT preflight (`uat/SETUP.md` §5-6) — test-harness agent
30
+ * live, driver session auth'd, env vars set.
31
+ *
32
+ * 2. **Host posture flipped to single-factor.** Edit `switchroom.yaml`:
33
+ *
34
+ * vault:
35
+ * broker:
36
+ * autoUnlock: true
37
+ * approvalAuth: telegram-id
38
+ *
39
+ * Then `switchroom update` (or `apply` + restart gateway). The
40
+ * scenario refuses to run if `switchroom doctor` reports the
41
+ * passphrase posture — running it under passphrase mode would
42
+ * block on a passphrase prompt the scenario doesn't send.
43
+ *
44
+ * 3. **Sacrificial vault key.** Same convention as the sibling:
45
+ *
46
+ * TMPF=$(mktemp) && printf '%s' 'sentinel-1115-value' > "$TMPF" && \
47
+ * switchroom vault set uat/req-access-target-tid --file "$TMPF" \
48
+ * --format string ; shred -u "$TMPF"
49
+ *
50
+ * 4. Remove `describe.skip` below.
51
+ *
52
+ * Why skipped: (a) mutates host vault state (mints a 30-day grant on
53
+ * test-harness), (b) requires the operator to flip the live posture
54
+ * — opt-in only. Cleanup is operator-side
55
+ * (`switchroom vault revoke <grant-id>` after the run, then revert
56
+ * the posture if desired).
57
+ */
58
+
59
+ import { describe, expect, it } from "vitest";
60
+ import { spinUp } from "../harness.js";
61
+
62
+ const SENTINEL_VALUE = "sentinel-1115-value";
63
+ const TARGET_KEY = "uat/req-access-target-tid";
64
+
65
+ describe.skip("uat: vault_request_access — telegram-id (single-factor) posture (#1115)", () => {
66
+ it(
67
+ "agent calls tool → operator taps Approve → silent mint, no passphrase prompt, single-factor footer present",
68
+ async () => {
69
+ const sc = await spinUp({ agent: "test-harness" });
70
+ try {
71
+ // 1. Trigger the agent-side MCP tool call. The agent's reply
72
+ // is what emits the approval card.
73
+ await sc.sendDM(
74
+ `Please call your vault_request_access MCP tool with ` +
75
+ `key="${TARGET_KEY}", scope="read", reason="UAT regression for #1115 ` +
76
+ `(telegram-id single-factor posture)". Then attempt to read the key ` +
77
+ `once the operator confirms.`,
78
+ );
79
+
80
+ // 2. Wait for the bot's approval card. Anchor on the headline
81
+ // emoji + tool-specific copy — same as the passphrase
82
+ // sibling scenario, since the card itself is identical
83
+ // regardless of posture (it's the Approve-tap behaviour
84
+ // that diverges).
85
+ const card = await sc.expectMessage(/🔐.*wants vault access/, {
86
+ from: "bot",
87
+ timeout: 60_000,
88
+ });
89
+
90
+ // 3. Confirm the inline keyboard has the Approve button and
91
+ // locate its callback_data.
92
+ const kb = await sc.driver.getKeyboard(sc.botUserId, card.messageId);
93
+ expect(kb).not.toBeNull();
94
+ const approveButton = kb!
95
+ .flat()
96
+ .find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
97
+ expect(approveButton, "card should have an [✅ Approve] button").toBeDefined();
98
+
99
+ // 4. Tap Approve. Under `telegram-id` posture this MUST take
100
+ // us straight to the "Approved by @ … — minting…" state —
101
+ // no passphrase prompt should appear at any point.
102
+ await sc.driver.pressButton(
103
+ sc.botUserId,
104
+ card.messageId,
105
+ approveButton!.callbackData!,
106
+ );
107
+
108
+ // 5. Expect the immediate "minting" edit (with operator
109
+ // @username) — the load-bearing UX signal that the
110
+ // passphrase prompt was skipped.
111
+ await sc.expectMessage(/Approved by @.*minting/i, {
112
+ from: "bot",
113
+ timeout: 15_000,
114
+ });
115
+
116
+ // 6. Expect the success card with the SINGLE-FACTOR footer —
117
+ // `performVaultAccessApproval` annotates the card with
118
+ // "Approver verified by Telegram identity (broker
119
+ // auto-unlocked at startup)" only under telegram-id mode.
120
+ // If posture flipped silently to passphrase between steps
121
+ // 1 and 5, the footer wouldn't say this and the regex
122
+ // misses → the scenario fails with a posture-state
123
+ // diagnosis.
124
+ const granted = await sc.expectMessage(
125
+ /Granted.*read access[\s\S]*Approver verified by Telegram identity/i,
126
+ { from: "bot", timeout: 30_000 },
127
+ );
128
+ expect(granted.text).toMatch(/broker auto-unlocked at startup/i);
129
+
130
+ // 7. **Negative invariant** — *implicit*. The scenario sends
131
+ // no passphrase between steps 4 (tap) and 6 (Granted). If
132
+ // the gateway had actually fallen back to the passphrase
133
+ // branch (`Reply with your passphrase` prompt), the flow
134
+ // would stall waiting for an operator reply and the
135
+ // `expectMessage(/Granted .../)` in step 6 would time out.
136
+ // The single-factor-footer regex in step 6 is the
137
+ // explicit positive gate that we landed in the
138
+ // telegram-id branch and not, say, an unlocked-cache
139
+ // shortcut.
140
+
141
+ // 8. Load-bearing functional assertion: the freshly-minted
142
+ // grant actually works. Mirrors the sibling scenario's
143
+ // final assertion so a future regression that breaks
144
+ // the token-write path is caught here too.
145
+ await sc.sendDM(
146
+ `Now run: switchroom vault get ${TARGET_KEY} — and tell me ` +
147
+ `exactly what the command printed (including any error markers).`,
148
+ );
149
+ const replyAfterGet = await sc.expectMessage(
150
+ new RegExp(SENTINEL_VALUE),
151
+ { from: "bot", timeout: 60_000 },
152
+ );
153
+ expect(replyAfterGet.text).toContain(SENTINEL_VALUE);
154
+ expect(replyAfterGet.text).not.toMatch(/VAULT-BROKER-DENIED/);
155
+ } finally {
156
+ await sc.tearDown();
157
+ }
158
+ },
159
+ 300_000,
160
+ );
161
+
162
+ it(
163
+ "doctor reports the single-factor posture so operators can verify the host before merging",
164
+ async () => {
165
+ // This second-tier check exists to flush out the failure mode
166
+ // where the YAML lands `approvalAuth: telegram-id` but the
167
+ // gateway either didn't reload or the schema-validation gate
168
+ // silently rejected it. Without this check, scenario 1 still
169
+ // passes when the gateway has reverted to passphrase posture
170
+ // (the operator would just be looking at a separate gateway
171
+ // process state and we'd never know).
172
+ const sc = await spinUp({ agent: "test-harness" });
173
+ try {
174
+ await sc.sendDM(
175
+ `Run: switchroom doctor — and quote exactly the line containing ` +
176
+ `"Approval auth:".`,
177
+ );
178
+ const doctorReply = await sc.expectMessage(
179
+ /Approval auth:\s*telegram-id/i,
180
+ { from: "bot", timeout: 60_000 },
181
+ );
182
+ expect(doctorReply.text).toMatch(
183
+ /single-factor.*Telegram account security/i,
184
+ );
185
+ } finally {
186
+ await sc.tearDown();
187
+ }
188
+ },
189
+ 120_000,
190
+ );
191
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Vault UX scenario — operator DMs `/vault audit`, taps `[Allow]`
3
+ * on a recent denial, agent re-attempts the vault read and succeeds.
4
+ *
5
+ * Part of: https://github.com/switchroom/switchroom/issues/866
6
+ * Exercises: #969 P2b one-tap allow flow.
7
+ *
8
+ * **Gated by operator setup.** To unskip:
9
+ *
10
+ * 1. **Driver must be admin on `test-harness`.** `agent add
11
+ * --allow-from $DRIVER_UID` already includes the driver in
12
+ * `access.json:allowFrom`. Confirm the `/vault` commands also
13
+ * work — they may require an explicit `admin_chat_id` setting
14
+ * in the agent's switchroom.yaml. Look for
15
+ * "VAULT-AUDIT-FORBIDDEN" or similar in the gateway log when
16
+ * the driver DMs `/vault audit`.
17
+ *
18
+ * 2. **Sacrificial vault keys for the test.** The scenario writes
19
+ * and reads `uat-test-denial` under a deliberately empty
20
+ * `--allow` scope so the agent's first read fails with
21
+ * VAULT-BROKER-DENIED — that denial is what shows up in
22
+ * `/vault audit`. Pre-create the key on the host:
23
+ *
24
+ * ```bash
25
+ * TMPF=$(mktemp) && printf '%s' 'sentinel-uat-value' > "$TMPF" && \
26
+ * switchroom vault set uat-test-denial --file "$TMPF" \
27
+ * --format string ; shred -u "$TMPF"
28
+ * ```
29
+ *
30
+ * The scenario then triggers a denial, taps Allow (scopes the
31
+ * key to `test-harness`), and asserts the agent can now read it.
32
+ * Cleanup at the end is operator-side: re-narrow the scope or
33
+ * remove the key.
34
+ *
35
+ * 3. Remove the `describe.skip` below.
36
+ *
37
+ * Why skipped by default: the scenario mutates host vault state
38
+ * (broker ACL) — opt-in only.
39
+ */
40
+
41
+ import { describe, expect, it } from "vitest";
42
+ import { spinUp } from "../harness.js";
43
+
44
+ describe.skip("uat: /vault audit one-tap allow", () => {
45
+ it("driver taps [Allow] on a recent denial; agent's next read succeeds", async () => {
46
+ const sc = await spinUp({ agent: "test-harness" });
47
+ try {
48
+ // 1. Trigger a denial by asking the agent to read a key the
49
+ // agent isn't yet scoped for.
50
+ await sc.sendDM(
51
+ "Please run `switchroom vault get uat-test-denial` and tell me the value.",
52
+ );
53
+
54
+ // The bot's denial trace finishes the turn — wait for the
55
+ // turn-end message, then proceed to /vault audit.
56
+ await sc.expectMessage(/VAULT-BROKER-DENIED|denied|cannot/i, {
57
+ from: "bot",
58
+ timeout: 60_000,
59
+ });
60
+
61
+ // 2. DM /vault audit. The bot replies with a recent-denials
62
+ // summary + inline [Allow] / [Deny] buttons per denied key.
63
+ await sc.sendDM("/vault audit");
64
+ const auditCard = await sc.expectMessage(/Recent denials|uat-test-denial/, {
65
+ from: "bot",
66
+ timeout: 30_000,
67
+ });
68
+
69
+ // 3. Find and press the [Allow] button.
70
+ const kb = await sc.driver.getKeyboard(sc.botUserId, auditCard.messageId);
71
+ expect(kb).not.toBeNull();
72
+ const allowButton = kb!
73
+ .flat()
74
+ .find(
75
+ (b) =>
76
+ b.callbackData !== undefined &&
77
+ /allow/i.test(b.text) &&
78
+ b.callbackData.includes("uat-test-denial"),
79
+ );
80
+ expect(allowButton).toBeDefined();
81
+ await sc.driver.pressButton(
82
+ sc.botUserId,
83
+ auditCard.messageId,
84
+ allowButton!.callbackData!,
85
+ );
86
+
87
+ // 4. Confirmation comes back via card edit; assert it lands.
88
+ // (The gateway typically edits the audit card in-place to
89
+ // show "allowed" status.)
90
+ await sc.expectMessage(/allowed|✓|scope updated/i, {
91
+ from: "bot",
92
+ timeout: 15_000,
93
+ });
94
+
95
+ // 5. Re-attempt the read. Should now succeed.
96
+ await sc.sendDM(
97
+ "Try `switchroom vault get uat-test-denial` again now.",
98
+ );
99
+ const success = await sc.expectMessage(/sentinel-uat-value/, {
100
+ from: "bot",
101
+ timeout: 60_000,
102
+ });
103
+ expect(success.text).toContain("sentinel-uat-value");
104
+ } finally {
105
+ await sc.tearDown();
106
+ }
107
+ });
108
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * End-to-end UAT scenario for #1052 — agent auto-resumes its task
3
+ * after operator approves a vault_request_access card.
4
+ *
5
+ * Pre-fix: agent fired vault_request_access → ended its turn → operator
6
+ * approved later → grant minted → agent did nothing further until
7
+ * operator messaged again. Operator had to manually nudge the agent
8
+ * to resume work the agent itself had flagged.
9
+ *
10
+ * Fix: gateway injects a synthetic InboundMessage after successful
11
+ * mint (via the existing inject_inbound IPC primitive cron uses).
12
+ * Agent's bridge receives the channel event, starts a new turn, and
13
+ * resumes the task.
14
+ *
15
+ * Load-bearing assertion: after the operator's passphrase reply +
16
+ * "Granted" card edit, the DRIVER sees a NEW bot turn (a substantive
17
+ * reply that uses the just-granted key) WITHOUT the driver sending
18
+ * any further message.
19
+ *
20
+ * **Skipped by default.** To unskip:
21
+ *
22
+ * 1. Standard UAT preflight (`uat/SETUP.md` §5-6).
23
+ * 2. `TELEGRAM_UAT_VAULT_PASSPHRASE` set in env.
24
+ * 3. Pre-create a sacrificial vault key:
25
+ *
26
+ * ```bash
27
+ * TMPF=$(mktemp) && printf '%s' 'sentinel-1052-value' > "$TMPF" && \
28
+ * switchroom vault set uat/auto-resume-key --file "$TMPF" \
29
+ * --format string ; shred -u "$TMPF"
30
+ * ```
31
+ *
32
+ * 4. Remove `describe.skip` below.
33
+ *
34
+ * Why skipped: mutates vault state. Cleanup is operator-side post-run.
35
+ */
36
+
37
+ import { describe, expect, it } from "vitest";
38
+ import { spinUp } from "../harness.js";
39
+
40
+ const KEY = "uat/auto-resume-key";
41
+ const SENTINEL = "sentinel-1052-value";
42
+
43
+ describe.skip("uat: agent auto-resumes after vault grant approval (#1052)", () => {
44
+ it(
45
+ "agent fires card, operator approves, agent emits new turn WITHOUT a nudge",
46
+ async () => {
47
+ const passphrase = process.env.TELEGRAM_UAT_VAULT_PASSPHRASE;
48
+ if (!passphrase) {
49
+ throw new Error(
50
+ "TELEGRAM_UAT_VAULT_PASSPHRASE must be set in env (see uat/SETUP.md).",
51
+ );
52
+ }
53
+ const sc = await spinUp({ agent: "test-harness" });
54
+ try {
55
+ // 1. Ask the agent to fetch the key — it'll hit DENIED first,
56
+ // fire vault_request_access, then end its turn.
57
+ await sc.sendDM(
58
+ `Please run \`switchroom vault get ${KEY}\`. If you get ` +
59
+ `VAULT-BROKER-DENIED, call your vault_request_access MCP tool ` +
60
+ `for that key (read, 30d, reason "UAT for #1052 auto-resume"), ` +
61
+ `END YOUR TURN cleanly, and when the operator approves you should ` +
62
+ `automatically resume by re-running the vault get and reporting ` +
63
+ `the value.`,
64
+ );
65
+
66
+ // 2. Wait for the approval card.
67
+ const card = await sc.expectMessage(/🔐.*wants vault access/, {
68
+ from: "bot",
69
+ timeout: 120_000,
70
+ });
71
+ const kb = await sc.driver.getKeyboard(sc.botUserId, card.messageId);
72
+ const approveButton = kb!
73
+ .flat()
74
+ .find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
75
+ expect(approveButton).toBeDefined();
76
+
77
+ // 3. Tap Approve. Triggers passphrase prompt.
78
+ await sc.driver.pressButton(sc.botUserId, card.messageId, approveButton!.callbackData!);
79
+ await sc.expectMessage(/Vault is locked.*Reply with your passphrase/, {
80
+ from: "bot",
81
+ timeout: 15_000,
82
+ });
83
+
84
+ // 4. Send passphrase. Card edits to "Granted ...".
85
+ const lastDriverMsg = await sc.sendDM(passphrase);
86
+ const lastDriverMsgId = lastDriverMsg.messageId;
87
+ await sc.expectMessage(/Granted.*read access/, {
88
+ from: "bot",
89
+ timeout: 30_000,
90
+ });
91
+
92
+ // 5. THE LOAD-BEARING #1052 ASSERTION: the agent should
93
+ // auto-resume WITHOUT the driver sending another message.
94
+ // We wait for a substantive bot reply containing the
95
+ // sentinel value, OR matching the auto-resume channel
96
+ // source marker the gateway stamps.
97
+ //
98
+ // Pre-fix: this assertion times out (no synthetic
99
+ // injection → agent's bridge never received a new turn
100
+ // → agent stayed idle).
101
+ //
102
+ // Post-fix: the gateway's ipcServer.sendToAgent fires a
103
+ // synthetic inbound with meta.source="vault_grant_approved".
104
+ // The agent's bridge starts a new turn, re-runs vault get,
105
+ // and reports the value.
106
+ const autoResumeReply = await sc.expectMessage(
107
+ new RegExp(SENTINEL),
108
+ { from: "bot", timeout: 180_000 },
109
+ );
110
+ expect(autoResumeReply.text).toContain(SENTINEL);
111
+ expect(
112
+ autoResumeReply.messageId,
113
+ "auto-resume reply must be a NEW message, not the granted-card edit",
114
+ ).toBeGreaterThan(lastDriverMsgId);
115
+ } finally {
116
+ await sc.tearDown();
117
+ }
118
+ },
119
+ 420_000,
120
+ );
121
+ });
@@ -0,0 +1,161 @@
1
+ /**
2
+ * End-to-end UAT scenario for the #1051 fix — concurrent
3
+ * vault_request_access cards must BOTH end up readable by the agent.
4
+ *
5
+ * #1051 had two failure modes:
6
+ * (a) `.vault-token` overwrite — each new grant strands the prior
7
+ * (agent can read only the most-recently-approved key).
8
+ * (b) pending-op race — second Approve tap before first passphrase
9
+ * reply orphans the first stage entirely (no second grant
10
+ * even minted).
11
+ *
12
+ * Both fixed by the gateway-side grant-union path: list existing
13
+ * grants → union keys → mint a consolidated grant. PLUS the
14
+ * pending-op shape extended to a queue so concurrent taps don't
15
+ * overwrite.
16
+ *
17
+ * This scenario covers (a) — the most-likely real-world repro
18
+ * (gymbro fires two cards back-to-back; operator approves both
19
+ * sequentially). Covering (b) cleanly needs precise tap-timing
20
+ * that's harder to script — the static-source test
21
+ * `telegram-plugin/tests/vault-grant-union.test.ts` pins it at the
22
+ * code level instead.
23
+ *
24
+ * **Skipped by default.** To unskip:
25
+ *
26
+ * 1. Standard UAT preflight (`uat/SETUP.md` §5-6).
27
+ * 2. `TELEGRAM_UAT_VAULT_PASSPHRASE` set in env.
28
+ * 3. Pre-create two sacrificial vault keys:
29
+ *
30
+ * ```bash
31
+ * for k in uat/concurrent-a uat/concurrent-b ; do
32
+ * TMPF=$(mktemp); printf '%s' "sentinel-$(basename "$k")" > "$TMPF"
33
+ * switchroom vault set "$k" --file "$TMPF" --format string
34
+ * shred -u "$TMPF"
35
+ * done
36
+ * ```
37
+ *
38
+ * 4. Remove `describe.skip` below.
39
+ *
40
+ * Why skipped: mutates vault state (mints a grant covering both keys
41
+ * on test-harness). Cleanup is operator-side post-run.
42
+ */
43
+
44
+ import { describe, expect, it } from "vitest";
45
+ import { spinUp } from "../harness.js";
46
+
47
+ const KEY_A = "uat/concurrent-a";
48
+ const KEY_B = "uat/concurrent-b";
49
+ const SENTINEL_A = "sentinel-concurrent-a";
50
+ const SENTINEL_B = "sentinel-concurrent-b";
51
+
52
+ describe.skip("uat: concurrent vault_request_access approvals — both grants survive (#1051)", () => {
53
+ it(
54
+ "agent fires two cards back-to-back, operator approves both → agent can read both keys",
55
+ async () => {
56
+ const passphrase = process.env.TELEGRAM_UAT_VAULT_PASSPHRASE;
57
+ if (!passphrase) {
58
+ throw new Error(
59
+ "TELEGRAM_UAT_VAULT_PASSPHRASE must be set in env (see uat/SETUP.md).",
60
+ );
61
+ }
62
+ const sc = await spinUp({ agent: "test-harness" });
63
+ try {
64
+ // 1. Tell the agent to fire TWO vault_request_access calls
65
+ // in the same turn for two distinct keys. The natural way
66
+ // to express this is one prompt that describes both
67
+ // needs; the agent then makes both tool calls.
68
+ await sc.sendDM(
69
+ `Please call your vault_request_access MCP tool TWICE — ` +
70
+ `once for key="${KEY_A}" and once for key="${KEY_B}" — ` +
71
+ `both scope="read", both reason="UAT for #1051 concurrent". ` +
72
+ `Then attempt to read BOTH keys via switchroom vault get and ` +
73
+ `print the values in your reply.`,
74
+ );
75
+
76
+ // 2. Wait for the FIRST approval card.
77
+ const cardA = await sc.expectMessage(
78
+ new RegExp(`🔐.*wants vault access[\\s\\S]*${KEY_A.replace("/", "\\/")}`),
79
+ { from: "bot", timeout: 90_000 },
80
+ );
81
+ const cardB = await sc.expectMessage(
82
+ new RegExp(`🔐.*wants vault access[\\s\\S]*${KEY_B.replace("/", "\\/")}`),
83
+ { from: "bot", timeout: 30_000 },
84
+ );
85
+
86
+ // 3. Tap Approve on card A.
87
+ const kbA = await sc.driver.getKeyboard(sc.botUserId, cardA.messageId);
88
+ const approveA = kbA!
89
+ .flat()
90
+ .find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
91
+ expect(approveA).toBeDefined();
92
+ await sc.driver.pressButton(sc.botUserId, cardA.messageId, approveA!.callbackData!);
93
+
94
+ // Wait for the passphrase prompt on card A.
95
+ await sc.expectMessage(/Vault is locked.*Reply with your passphrase/, {
96
+ from: "bot",
97
+ timeout: 15_000,
98
+ });
99
+
100
+ // 4. Tap Approve on card B BEFORE typing the passphrase.
101
+ // This exercises the pending-op queueing path (bug B).
102
+ // The card should edit to "Queued behind an earlier card."
103
+ const kbB = await sc.driver.getKeyboard(sc.botUserId, cardB.messageId);
104
+ const approveB = kbB!
105
+ .flat()
106
+ .find((b) => b.callbackData !== undefined && /approve/i.test(b.text));
107
+ expect(approveB).toBeDefined();
108
+ await sc.driver.pressButton(sc.botUserId, cardB.messageId, approveB!.callbackData!);
109
+ await sc.expectMessage(/Queued behind an earlier card|Queued.*one passphrase/i, {
110
+ from: "bot",
111
+ timeout: 15_000,
112
+ });
113
+
114
+ // 5. Send passphrase. Gateway drains the queue, mints a
115
+ // unioned grant for {KEY_A, KEY_B}, writes a single
116
+ // .vault-token. BOTH cards edit to "Granted".
117
+ await sc.sendDM(passphrase);
118
+ await sc.expectMessage(new RegExp(`Granted[\\s\\S]*${KEY_A.replace("/", "\\/")}`), {
119
+ from: "bot",
120
+ timeout: 30_000,
121
+ });
122
+ await sc.expectMessage(new RegExp(`Granted[\\s\\S]*${KEY_B.replace("/", "\\/")}`), {
123
+ from: "bot",
124
+ timeout: 30_000,
125
+ });
126
+
127
+ // 6. Ask the agent to read BOTH keys. The load-bearing
128
+ // assertion: pre-fix, the agent could only read ONE
129
+ // (.vault-token had been overwritten with the second
130
+ // grant's token, which only covered KEY_B). Post-fix,
131
+ // the agent has a single token whose grant covers
132
+ // BOTH keys, so BOTH gets succeed.
133
+ await sc.sendDM(
134
+ `Now run: switchroom vault get ${KEY_A} && switchroom vault get ${KEY_B} ` +
135
+ `— and paste the output verbatim. Include any error markers if either fails.`,
136
+ );
137
+ const finalReply = await sc.expectMessage(
138
+ new RegExp(SENTINEL_A),
139
+ { from: "bot", timeout: 90_000 },
140
+ );
141
+ expect(
142
+ finalReply.text,
143
+ `agent must read KEY_A — pre-fix this was the SECOND grant's key and would have succeeded; the union now covers it`,
144
+ ).toContain(SENTINEL_A);
145
+ expect(
146
+ finalReply.text,
147
+ `agent must read KEY_B — pre-fix this would have FAILED with VAULT-BROKER-DENIED because .vault-token was overwritten`,
148
+ ).toContain(SENTINEL_B);
149
+ expect(
150
+ finalReply.text,
151
+ `neither key should denied`,
152
+ ).not.toMatch(/VAULT-BROKER-DENIED/);
153
+ } finally {
154
+ await sc.tearDown();
155
+ }
156
+ },
157
+ 420_000, // 7 min — covers two cards rendering + two approves +
158
+ // passphrase round-trip + drain queue + two
159
+ // mints + two vault gets.
160
+ );
161
+ });