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,214 +0,0 @@
1
- /**
2
- * Tests for Phase 3b foreman write operations:
3
- * - handleRestartCommand (mocked execFileSync)
4
- * - handleDeleteCommand + executeDeleteAgent (mocked exec + FS)
5
- * - handleUpdateCommand (mocked combined exec)
6
- */
7
-
8
- import { describe, it, expect, vi, beforeEach } from 'vitest'
9
- import {
10
- handleRestartCommand,
11
- handleDeleteCommand,
12
- executeDeleteAgent,
13
- handleUpdateCommand,
14
- type SwitchroomExecFn,
15
- } from '../foreman/foreman-handlers.js'
16
- import type { execFileSync } from 'child_process'
17
-
18
- // ─── /restart ─────────────────────────────────────────────────────────────
19
-
20
- describe('foreman: handleRestartCommand — input validation', () => {
21
- it('returns usage when no agent specified', () => {
22
- const result = handleRestartCommand('')
23
- expect(result.ok).toBe(false)
24
- expect(result.text).toContain('Usage')
25
- })
26
-
27
- it('rejects invalid agent name', () => {
28
- const execFile = vi.fn()
29
- // 'BadName' has uppercase — first token is invalid
30
- const result = handleRestartCommand('BadName', execFile as never)
31
- expect(result.ok).toBe(false)
32
- expect(result.text).toBe('Invalid agent name.')
33
- expect(execFile).not.toHaveBeenCalled()
34
- })
35
-
36
- it('rejects path traversal', () => {
37
- const execFile = vi.fn()
38
- const result = handleRestartCommand('../etc/passwd', execFile as never)
39
- expect(result.ok).toBe(false)
40
- expect(execFile).not.toHaveBeenCalled()
41
- })
42
-
43
- it('rejects agent name with colon', () => {
44
- const execFile = vi.fn()
45
- const result = handleRestartCommand('bad:name', execFile as never)
46
- expect(result.ok).toBe(false)
47
- expect(execFile).not.toHaveBeenCalled()
48
- })
49
- })
50
-
51
- describe('foreman: handleRestartCommand — execFileSync calls', () => {
52
- it('calls systemctl --user restart with correct unit name', () => {
53
- const execFile = vi.fn().mockReturnValue('')
54
- const result = handleRestartCommand('gymbro', execFile as never)
55
-
56
- expect(result.ok).toBe(true)
57
- expect(execFile).toHaveBeenCalledOnce()
58
- const [cmd, args] = execFile.mock.calls[0] as [string, string[]]
59
- expect(cmd).toBe('systemctl')
60
- expect(args).toContain('--user')
61
- expect(args).toContain('restart')
62
- expect(args).toContain('switchroom-gymbro')
63
- // Must NOT be a shell string — second arg must be an array
64
- expect(Array.isArray(args)).toBe(true)
65
- })
66
-
67
- it('uses execFileSync not execSync (no shell)', () => {
68
- const execFile = vi.fn().mockReturnValue('')
69
- handleRestartCommand('gymbro', execFile as never)
70
- // The function is called with 3 args (cmd, args[], opts) not a shell string
71
- const [, args] = execFile.mock.calls[0] as [string, string[], object]
72
- expect(Array.isArray(args)).toBe(true)
73
- })
74
-
75
- it('includes agent name in success reply', () => {
76
- const execFile = vi.fn().mockReturnValue('')
77
- const result = handleRestartCommand('gymbro', execFile as never)
78
- expect(result.ok).toBe(true)
79
- expect(result.text).toContain('gymbro')
80
- })
81
-
82
- it('returns error text when systemctl fails', () => {
83
- const execFile = vi.fn().mockImplementation(() => {
84
- throw Object.assign(new Error('unit not found'), { stderr: 'Unit switchroom-gymbro.service not found.' })
85
- })
86
- const result = handleRestartCommand('gymbro', execFile as never)
87
- expect(result.ok).toBe(false)
88
- expect(result.text).toContain('restart failed')
89
- expect(result.text).toContain('gymbro')
90
- })
91
-
92
- it('handles agent name with hyphen', () => {
93
- const execFile = vi.fn().mockReturnValue('')
94
- handleRestartCommand('my-agent', execFile as never)
95
- const [, args] = execFile.mock.calls[0] as [string, string[]]
96
- expect(args).toContain('switchroom-my-agent')
97
- })
98
-
99
- it('escapes HTML in error output', () => {
100
- const execFile = vi.fn().mockImplementation(() => {
101
- throw Object.assign(new Error('err'), { stderr: 'Error: <unit> not found' })
102
- })
103
- const result = handleRestartCommand('gymbro', execFile as never)
104
- expect(result.text).not.toContain('<unit>')
105
- })
106
- })
107
-
108
- // ─── /delete first step ───────────────────────────────────────────────────
109
-
110
- describe('foreman: handleDeleteCommand — prompt', () => {
111
- it('returns usage when no agent specified', () => {
112
- const result = handleDeleteCommand('')
113
- expect(result.replies[0].text).toContain('Usage')
114
- expect(result.needsConfirm).toBeFalsy()
115
- })
116
-
117
- it('rejects invalid agent name', () => {
118
- // 'BadAgent' has uppercase — invalid
119
- const result = handleDeleteCommand('BadAgent')
120
- expect(result.replies[0].text).toBe('Invalid agent name.')
121
- expect(result.needsConfirm).toBeFalsy()
122
- })
123
-
124
- it('returns confirmation prompt for valid agent', () => {
125
- const result = handleDeleteCommand('gymbro')
126
- expect(result.replies[0].text).toContain('gymbro')
127
- expect(result.replies[0].text).toContain('YES')
128
- expect(result.needsConfirm).toBe(true)
129
- expect(result.agentForConfirm).toBe('gymbro')
130
- })
131
-
132
- it('escapes HTML in agent name in prompt', () => {
133
- // valid name, but let's use one that could trip HTML — actually all valid names
134
- // are alphanumeric so no HTML risk; just verify the name appears
135
- const result = handleDeleteCommand('my-agent')
136
- expect(result.replies[0].text).toContain('my-agent')
137
- expect(result.replies[0].html).toBe(true)
138
- })
139
- })
140
-
141
- // ─── executeDeleteAgent ───────────────────────────────────────────────────
142
-
143
- describe('foreman: executeDeleteAgent — execution', () => {
144
- it('runs CLI destroy with --yes flag', () => {
145
- const switchroomExec: SwitchroomExecFn = vi.fn().mockReturnValue('Removed unit.')
146
- const execFile = vi.fn().mockReturnValue('')
147
- const tmpDir = '/tmp/fake-agents-dir'
148
-
149
- const result = executeDeleteAgent('gymbro', switchroomExec, execFile as never, tmpDir)
150
- expect(switchroomExec).toHaveBeenCalledWith(['agent', 'destroy', '--yes', 'gymbro'])
151
- expect(result.replies[0].text).toContain('gymbro')
152
- })
153
-
154
- it('reports success without archive when dir does not exist', () => {
155
- const switchroomExec: SwitchroomExecFn = vi.fn().mockReturnValue('done')
156
- const result = executeDeleteAgent('gymbro', switchroomExec, vi.fn() as never, '/nonexistent-agents-dir')
157
- // No archive reported (dir didn't exist)
158
- expect(result.replies[0].text).not.toContain('Archived')
159
- })
160
-
161
- it('rejects invalid agent name without calling CLI', () => {
162
- const switchroomExec: SwitchroomExecFn = vi.fn()
163
- const result = executeDeleteAgent('bad name!', switchroomExec, vi.fn() as never, '/tmp')
164
- expect(result.replies[0].text).toBe('Invalid agent name.')
165
- expect(switchroomExec).not.toHaveBeenCalled()
166
- })
167
-
168
- it('reports CLI error but notes archive succeeded', () => {
169
- const switchroomExec: SwitchroomExecFn = vi.fn().mockImplementation(() => {
170
- throw Object.assign(new Error('destroy failed'), { stderr: 'switchroom error' })
171
- })
172
- // Use a dir that definitely doesn't exist so no actual rename
173
- const result = executeDeleteAgent('gymbro', switchroomExec, vi.fn() as never, '/nonexistent-agents-12345')
174
- expect(result.replies[0].text).toContain('CLI destroy failed')
175
- })
176
- })
177
-
178
- // ─── /update ──────────────────────────────────────────────────────────────
179
-
180
- describe('foreman: handleUpdateCommand', () => {
181
- it('calls switchroomExec with ["update"]', () => {
182
- const exec: SwitchroomExecFn = vi.fn().mockReturnValue('Updated successfully.')
183
- handleUpdateCommand(exec)
184
- expect(exec).toHaveBeenCalledWith(['update'])
185
- })
186
-
187
- it('returns output in a pre block', () => {
188
- const exec: SwitchroomExecFn = vi.fn().mockReturnValue('Pulled abc123. Reconciled 2 agents.')
189
- const result = handleUpdateCommand(exec)
190
- expect(result.replies[0].text).toContain('Pulled abc123')
191
- expect(result.replies[0].html).toBe(true)
192
- })
193
-
194
- it('returns error message when CLI throws', () => {
195
- const exec: SwitchroomExecFn = vi.fn().mockImplementation(() => {
196
- throw Object.assign(new Error('not a git repo'), { stderr: 'fatal: not a git repository' })
197
- })
198
- const result = handleUpdateCommand(exec)
199
- expect(result.replies[0].text).toContain('update failed')
200
- })
201
-
202
- it('returns no-output message when CLI returns empty', () => {
203
- const exec: SwitchroomExecFn = vi.fn().mockReturnValue(' \n')
204
- const result = handleUpdateCommand(exec)
205
- expect(result.replies[0].text).toContain('Update complete')
206
- })
207
-
208
- it('paginates output > 3 KB', () => {
209
- const bigOutput = 'x'.repeat(4000)
210
- const exec: SwitchroomExecFn = vi.fn().mockReturnValue(bigOutput)
211
- const result = handleUpdateCommand(exec)
212
- expect(result.replies.length).toBeGreaterThan(1)
213
- })
214
- })
@@ -1,243 +0,0 @@
1
- /**
2
- * Harness ordering invariants — table-driven scenarios.
3
- *
4
- * These tests assert cross-cutting properties that hold across a range
5
- * of turn shapes (Class A/B/C, retry, replay-dup, error-cascade). The
6
- * scenarios are deterministic and named, not random — see
7
- * HARNESS_UPGRADE_PLAN.md "scenario fuzzer" decision (skeptic finding 7:
8
- * property-based fuzzing without shrinking is worse than no fuzzing).
9
- *
10
- * Each invariant carries a `// fails when:` comment indicating the
11
- * production change that would break it. The test author should mentally
12
- * `git stash` that change and confirm the test fails — see plan
13
- * "Validation rule for each new test."
14
- *
15
- * Invariants:
16
- *
17
- * INV-1 — Terminal reaction (👍) fires AT-OR-AFTER the last user-
18
- * visible answer text. NEVER before. The Bug D/Z contract
19
- * generalized to all turn shapes.
20
- *
21
- * INV-2 — Exactly one terminal reaction fires per logical turn
22
- * (regardless of how many tool emoji ladder steps occurred
23
- * in between). Catches a future regression where setDone
24
- * fires twice.
25
- *
26
- * INV-3 — Editing a deleted message always errors. Catches a
27
- * regression in the fake's delete-vs-edit ordering, which
28
- * would let a buggy production module silently miss the
29
- * "edit-to-deleted-message" failure mode in tests.
30
- *
31
- * INV-4 — Outbound dedup window holds for the full TTL once the
32
- * cache is wired in.
33
- *
34
- * INV-5 — Hold-and-release ordering: an event fired while a
35
- * sendMessage/editMessageText is parked at `holdNext`
36
- * observes the world as it was BEFORE the held call landed.
37
- * This pins the harness's own contract — necessary for
38
- * future Bug-D-class tests to be expressible.
39
- */
40
-
41
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
42
- import { createRealGatewayHarness } from './real-gateway-harness.js'
43
- import { createFakeBotApi } from './fake-bot-api.js'
44
-
45
- const CHAT = '8248703757'
46
- const INBOUND_MSG = 100
47
-
48
- beforeEach(() => {
49
- vi.useFakeTimers()
50
- })
51
- afterEach(() => {
52
- vi.useRealTimers()
53
- })
54
-
55
- interface TurnShape {
56
- name: string
57
- /** Drive the harness through one logical turn. Resolves when turn ended. */
58
- drive: (h: ReturnType<typeof createRealGatewayHarness>) => Promise<void>
59
- }
60
-
61
- const turnShapes: TurnShape[] = [
62
- {
63
- name: 'class-A reply (sub-2s, no tools)',
64
- drive: async (h) => {
65
- h.inbound({ chatId: CHAT, messageId: INBOUND_MSG, text: 'hi' })
66
- h.feedSessionEvent({
67
- kind: 'enqueue',
68
- chatId: CHAT,
69
- messageId: '1',
70
- threadId: null,
71
- rawContent: 'hi',
72
- })
73
- await h.clock.advance(20)
74
- await h.streamReply({ chat_id: CHAT, text: 'Hello back!', done: true })
75
- await h.clock.advance(20)
76
- },
77
- },
78
- {
79
- name: 'class-B with-tools (1 tool, ~3s)',
80
- drive: async (h) => {
81
- h.inbound({ chatId: CHAT, messageId: INBOUND_MSG, text: 'work' })
82
- h.feedSessionEvent({
83
- kind: 'enqueue',
84
- chatId: CHAT,
85
- messageId: '1',
86
- threadId: null,
87
- rawContent: 'work',
88
- })
89
- await h.clock.advance(50)
90
- h.feedSessionEvent({ kind: 'thinking' })
91
- await h.clock.advance(500)
92
- h.feedSessionEvent({ kind: 'tool_use', toolName: 'Read' })
93
- await h.clock.advance(2500)
94
- await h.streamReply({ chat_id: CHAT, text: 'Here is the answer.', done: true })
95
- await h.clock.advance(20)
96
- },
97
- },
98
- {
99
- name: 'class-C subagent (long, ~10s, sub-agent emit)',
100
- drive: async (h) => {
101
- h.inbound({ chatId: CHAT, messageId: INBOUND_MSG, text: 'big task' })
102
- h.feedSessionEvent({
103
- kind: 'enqueue',
104
- chatId: CHAT,
105
- messageId: '1',
106
- threadId: null,
107
- rawContent: 'big task',
108
- })
109
- await h.clock.advance(50)
110
- h.feedSessionEvent({ kind: 'thinking' })
111
- await h.clock.advance(500)
112
- h.feedSessionEvent({ kind: 'tool_use', toolName: 'Read' })
113
- await h.clock.advance(2000)
114
- h.feedSessionEvent({ kind: 'tool_use', toolName: 'Grep' })
115
- await h.clock.advance(2000)
116
- h.feedSessionEvent({ kind: 'tool_use', toolName: 'Edit' })
117
- await h.clock.advance(5000)
118
- await h.streamReply({ chat_id: CHAT, text: 'Done — see the diff above.', done: true })
119
- await h.clock.advance(20)
120
- },
121
- },
122
- ]
123
-
124
- describe('INV-1 — terminal reaction fires AT-OR-AFTER last delivery (Bug D/Z generalized)', () => {
125
- for (const shape of turnShapes) {
126
- it(`${shape.name}: lastReactionEmojiAt >= lastAnswerTextDeliveredAt`, async () => {
127
- // fails when: a future refactor moves setDone() from the streamReply
128
- // post-await branch back to the JSONL turn_end handler — exactly
129
- // Bug D's failure mode, generalized over turn shapes.
130
- const h = createRealGatewayHarness({ gapMs: 0 })
131
- await shape.drive(h)
132
- const deliveredAt = h.lastAnswerTextDeliveredAt(CHAT)
133
- const reactionAt = h.lastReactionEmojiAt(CHAT)
134
- expect(deliveredAt, `no answer text delivered for ${shape.name}`).not.toBeNull()
135
- expect(reactionAt, `no reaction emitted for ${shape.name}`).not.toBeNull()
136
- expect(reactionAt!).toBeGreaterThanOrEqual(deliveredAt!)
137
- h.finalize()
138
- })
139
- }
140
- })
141
-
142
- describe('INV-2 — terminal 👍 fires exactly once per turn (Bug Z generalized)', () => {
143
- for (const shape of turnShapes) {
144
- it(`${shape.name}: 👍 appears exactly once across the full reaction sequence`, async () => {
145
- // fails when: a future change fires setDone() more than once
146
- // for the same turn (e.g. both the streamReply post-await branch
147
- // AND the turn_end JSONL handler call it). Specifically asserting
148
- // the COUNT of 👍 in the full sequence — not just "the last
149
- // emoji is unique" (which would be a tautology).
150
- const h = createRealGatewayHarness({ gapMs: 0 })
151
- await shape.drive(h)
152
- const seq = h.recorder.reactionSequence()
153
- expect(seq.length, `no reactions for ${shape.name}`).toBeGreaterThan(0)
154
- const thumbsUpCount = seq.filter((e) => e === '👍').length
155
- expect(thumbsUpCount).toBe(1)
156
- // And the 👍 is the LAST reaction — terminal contract.
157
- expect(seq[seq.length - 1]).toBe('👍')
158
- h.finalize()
159
- })
160
- }
161
- })
162
-
163
- describe('INV-3 — editing a deleted message always errors', () => {
164
- it('fake throws messageToEditNotFound after the message is deleted', async () => {
165
- // fails when: someone changes fake-bot-api's editMessageText to
166
- // silently succeed for deleted messages — production modules
167
- // would then look correct in tests but error in real Telegram.
168
- const bot = createFakeBotApi()
169
- const r = (await bot.api.sendMessage('c1', 'long enough text content here ok', {})) as {
170
- message_id: number
171
- }
172
- await bot.api.deleteMessage('c1', r.message_id)
173
- await expect(
174
- bot.api.editMessageText('c1', r.message_id, 'updated text content here ok', {}),
175
- ).rejects.toMatchObject({ error_code: 400 })
176
- })
177
- })
178
-
179
- describe('INV-4 — outbound dedup window holds for the full TTL', () => {
180
- // Span of "now" offsets within the TTL that should all be deduped.
181
- const inWindowOffsets = [0, 1000, 30_000, 59_000]
182
- // Span outside the TTL that should NOT be deduped.
183
- const outOfWindowOffsets = [60_001, 120_000]
184
-
185
- for (const ms of inWindowOffsets) {
186
- it(`same content at +${ms}ms is suppressed (within TTL=60s)`, async () => {
187
- // fails when: TTL is silently shortened, or the cache's eviction
188
- // sweep evicts entries before their TTL expires.
189
- const h = createRealGatewayHarness({ gapMs: 0, withDedup: true })
190
- const text = 'A long enough message to clear the 24-char dedup floor by a wide margin.'
191
- await h.send({ chat_id: CHAT, text })
192
- await h.clock.advance(ms)
193
- const id2 = await h.send({ chat_id: CHAT, text })
194
- expect(id2).toBeNull()
195
- h.finalize()
196
- })
197
- }
198
-
199
- for (const ms of outOfWindowOffsets) {
200
- it(`same content at +${ms}ms is allowed (outside TTL)`, async () => {
201
- // fails when: TTL eviction breaks (entries linger past their TTL).
202
- const h = createRealGatewayHarness({ gapMs: 0, withDedup: true })
203
- const text = 'A long enough message to clear the 24-char dedup floor by a wide margin.'
204
- await h.send({ chat_id: CHAT, text })
205
- await h.clock.advance(ms)
206
- const id2 = await h.send({ chat_id: CHAT, text })
207
- expect(id2).not.toBeNull()
208
- h.finalize()
209
- })
210
- }
211
- })
212
-
213
- describe('INV-5 — holdNext: events fired during a held call observe pre-held state', () => {
214
- it('a setMessageReaction parked at holdNext lets unrelated state mutate before its release', async () => {
215
- // fails when: a future fake-bot refactor makes holdNext block
216
- // mutations on OTHER methods, or makes release() synchronous when
217
- // it should be async.
218
- //
219
- // This is the foundational seam that makes Bug-D-class tests
220
- // expressible: "while editMessageText is pending, fire the 👍" —
221
- // the harness must let the 👍 land while the edit is parked.
222
- const bot = createFakeBotApi()
223
- const r = (await bot.api.sendMessage('c1', 'long enough text content here ok', {})) as {
224
- message_id: number
225
- }
226
- const hold = bot.holdNext('editMessageText', 'c1')
227
- // Start the edit; it parks at the gate.
228
- const editPromise = bot.api.editMessageText('c1', r.message_id, 'edited content here ok', {})
229
- // Yield a microtask so the held call enters its await.
230
- await Promise.resolve()
231
- expect(hold.triggered()).toBe(true)
232
- // Fire something else — setMessageReaction — and confirm it lands
233
- // independently while the edit is still parked.
234
- await bot.api.setMessageReaction('c1', r.message_id, [{ type: 'emoji', emoji: '👍' }])
235
- expect(bot.state.reactions.length).toBe(1)
236
- // The edit's text hasn't landed yet — the message is still original.
237
- expect(bot.textOf(r.message_id)).toBe('long enough text content here ok')
238
- // Release the edit — now the text updates.
239
- hold.release()
240
- await editPromise
241
- expect(bot.textOf(r.message_id)).toBe('edited content here ok')
242
- })
243
- })
@@ -1,124 +0,0 @@
1
- /**
2
- * Tests for the structured pin/unpin event logger — the module that
3
- * emits one JSON line per pin API interaction so operators can audit
4
- * the pin lifecycle without parsing free-form log text.
5
- *
6
- * Covers spec `docs/pinned-progress-card-reliability.md` §6.1 and T9.
7
- */
8
-
9
- import { describe, it, expect } from "vitest";
10
- import {
11
- logPinEvent,
12
- classifyPinError,
13
- errorMessage,
14
- type PinEvent,
15
- } from "../pin-event-log.js";
16
-
17
- describe("logPinEvent", () => {
18
- it("writes one JSON line prefixed with 'pin-event: '", () => {
19
- const lines: string[] = [];
20
- const ev: PinEvent = {
21
- event: "pin",
22
- chatId: "100",
23
- messageId: 42,
24
- turnKey: "100::1",
25
- outcome: "ok",
26
- durationMs: 123,
27
- };
28
- logPinEvent(ev, (line) => lines.push(line));
29
- expect(lines).toHaveLength(1);
30
- expect(lines[0]).toMatch(/^pin-event: /);
31
- expect(lines[0].endsWith("\n")).toBe(true);
32
- const payload = JSON.parse(lines[0].replace(/^pin-event: /, "").trimEnd());
33
- expect(payload).toEqual(ev);
34
- });
35
-
36
- it("preserves all optional fields that are set", () => {
37
- const lines: string[] = [];
38
- logPinEvent(
39
- {
40
- event: "unpin-retry",
41
- chatId: "c",
42
- messageId: 7,
43
- turnKey: "c::2",
44
- outcome: "rate-limited",
45
- error: "Too Many Requests",
46
- durationMs: 1050,
47
- },
48
- (line) => lines.push(line),
49
- );
50
- const payload = JSON.parse(lines[0].replace(/^pin-event: /, "").trimEnd());
51
- expect(payload.error).toBe("Too Many Requests");
52
- expect(payload.outcome).toBe("rate-limited");
53
- expect(payload.event).toBe("unpin-retry");
54
- });
55
-
56
- it("omits undefined optional fields cleanly", () => {
57
- const lines: string[] = [];
58
- logPinEvent(
59
- {
60
- event: "sweep-pin",
61
- chatId: "c",
62
- outcome: "ok",
63
- },
64
- (line) => lines.push(line),
65
- );
66
- const raw = lines[0].replace(/^pin-event: /, "").trimEnd();
67
- expect(raw).not.toContain("undefined");
68
- const payload = JSON.parse(raw);
69
- expect(payload.error).toBeUndefined();
70
- expect(payload.messageId).toBeUndefined();
71
- });
72
- });
73
-
74
- describe("classifyPinError", () => {
75
- it("returns 'rate-limited' for Telegram 429", () => {
76
- expect(
77
- classifyPinError({ error_code: 429, description: "Too Many Requests: retry after 2" }),
78
- ).toBe("rate-limited");
79
- });
80
-
81
- it("returns 'forbidden' for Telegram 403", () => {
82
- expect(classifyPinError({ error_code: 403, description: "Forbidden: bot was kicked" })).toBe(
83
- "forbidden",
84
- );
85
- });
86
-
87
- it("falls back to message substring match when error_code is absent", () => {
88
- expect(classifyPinError(new Error("Bad Request: not enough rights to manage pins"))).toBe(
89
- "forbidden",
90
- );
91
- expect(classifyPinError(new Error("connect ETIMEDOUT 1.2.3.4:443"))).toBe("timeout");
92
- });
93
-
94
- it("returns 'fail' for any unrecognised error shape", () => {
95
- expect(classifyPinError(new Error("weird unexpected failure"))).toBe("fail");
96
- expect(classifyPinError("random string")).toBe("fail");
97
- expect(classifyPinError({})).toBe("fail");
98
- });
99
-
100
- it("returns 'fail' for null/undefined", () => {
101
- expect(classifyPinError(null)).toBe("fail");
102
- expect(classifyPinError(undefined)).toBe("fail");
103
- });
104
- });
105
-
106
- describe("errorMessage", () => {
107
- it("extracts message from Error instance", () => {
108
- expect(errorMessage(new Error("boom"))).toBe("boom");
109
- });
110
-
111
- it("prefers description over message on grammy-shaped objects", () => {
112
- expect(errorMessage({ description: "Telegram says no", message: "generic" })).toBe(
113
- "Telegram says no",
114
- );
115
- });
116
-
117
- it("returns empty string for null", () => {
118
- expect(errorMessage(null)).toBe("");
119
- });
120
-
121
- it("stringifies unknown shapes", () => {
122
- expect(errorMessage(42)).toBe("42");
123
- });
124
- });
@@ -1,73 +0,0 @@
1
- /**
2
- * PR-C2 — reportApiFailure crossing its threshold while a chat is in
3
- * `pendingCompletion` state must not corrupt the deferred-completion
4
- * resolution path.
5
- *
6
- * Setup: parent turn_end while a bg sub-agent is still running →
7
- * chatState.pendingCompletion=true. Then `maxConsecutive4xx` permanent
8
- * 4xx failures arrive (the card is being abandoned locally). We then
9
- * resolve the bg sub-agent via sub_agent_turn_end. The driver must:
10
- *
11
- * - Fire onTurnComplete exactly once for the originating turnKey.
12
- * - Not double-flush.
13
- *
14
- * fails when: the terminal-apiFailure branch races with the
15
- * pendingCompletion resolution path and either swallows or duplicates
16
- * the completion callback.
17
- */
18
- import { describe, it, expect } from 'vitest'
19
- import { makeHarness, enqueue } from './_progress-card-harness.js'
20
-
21
- describe('PR-C2: API failure crossing threshold during pendingCompletion', () => {
22
- it('deferred completion still resolves exactly once; no double-flush', () => {
23
- const completions: string[] = []
24
- const { driver } = makeHarness({
25
- minIntervalMs: 0,
26
- coalesceMs: 0,
27
- heartbeatMs: 999_999, // keep heartbeat from racing the test
28
- maxConsecutive4xx: 3,
29
- promoteAfterMs: 999_999,
30
- onTurnComplete: (s) => completions.push(s.turnKey),
31
- })
32
- const maps = driver._debugGetMaps!()
33
- const CHAT = 'cA'
34
-
35
- driver.ingest(enqueue(CHAT), null)
36
- driver.ingest(
37
- {
38
- kind: 'tool_use',
39
- toolName: 'Agent',
40
- toolUseId: 'tu1',
41
- input: { prompt: 'bg', run_in_background: true },
42
- },
43
- CHAT,
44
- )
45
- driver.ingest({ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg' }, CHAT)
46
- driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
47
- driver.recordOutboundDelivered(CHAT)
48
- driver.ingest({ kind: 'turn_end', durationMs: 100 }, CHAT)
49
-
50
- expect(maps.chats.size).toBe(1)
51
- const turnKey = [...maps.chats.keys()][0]
52
- const cs = maps.chats.get(turnKey) as { pendingCompletion: boolean; apiFailures: { terminal: boolean } }
53
- expect(cs.pendingCompletion).toBe(true)
54
-
55
- // Hammer reportApiFailure past the threshold (3).
56
- for (let i = 0; i < 5; i++) {
57
- driver.reportApiFailure(turnKey, {
58
- kind: 'permanent_4xx',
59
- code: 400,
60
- description: 'bad request',
61
- })
62
- }
63
- expect(cs.apiFailures.terminal).toBe(true)
64
- // No completion fired yet — bg still running.
65
- expect(completions.length).toBe(0)
66
-
67
- // Resolve the bg sub-agent. The originating turn must complete
68
- // exactly once.
69
- driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saBG' }, CHAT)
70
- expect(completions.length).toBe(1)
71
- expect(completions[0]).toBe(turnKey)
72
- })
73
- })