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,87 +0,0 @@
1
- /**
2
- * Regression: parent turn_end fires before bg sub-agent emits any
3
- * state.subAgents entries (i.e. sub_agent_started hasn't arrived yet).
4
- *
5
- * Before the fix, hasAnyRunningSubAgent returned false at turn_end time
6
- * (subAgents was empty) so the card was closed immediately. The fleet
7
- * shadow's hasLiveBackground gate is the fix — the fleet member is
8
- * created at sub_agent_started time and tagged status:'background',
9
- * which keeps pendingCompletion=true even when subAgents is empty.
10
- *
11
- * Scenario:
12
- * 1. Parent emits Agent tool_use with run_in_background:true.
13
- * 2. Parent emits turn_end immediately — sub_agent_started has NOT
14
- * arrived yet, so state.subAgents is empty.
15
- * 3. Card must remain alive (NOT in completions).
16
- * 4. sub_agent_started arrives → fleet records the member.
17
- * 5. sub_agent_turn_end arrives → deferred completion must fire.
18
- */
19
- import { describe, it, expect } from 'vitest'
20
- import { makeHarness, enqueue } from './_progress-card-harness.js'
21
-
22
- describe('two-zone-bg: parent turn_end before sub_agent_started → card survives → bg done cleans up', () => {
23
- it('does not close the card prematurely; fires completion on bg sub-agent terminal', () => {
24
- const { driver, completions, advance } = makeHarness({
25
- minIntervalMs: 0,
26
- coalesceMs: 0,
27
- promoteAfterMs: 999_999,
28
- })
29
- const CHAT = 'cBG_early'
30
-
31
- // Step 1: enqueue a new parent turn.
32
- driver.ingest(enqueue(CHAT), null)
33
-
34
- // Step 2: parent emits Agent tool_use with run_in_background:true.
35
- driver.ingest(
36
- {
37
- kind: 'tool_use',
38
- toolName: 'Agent',
39
- toolUseId: 'tu-bg-1',
40
- input: { prompt: 'do bg work', run_in_background: true },
41
- },
42
- CHAT,
43
- )
44
-
45
- // Step 3: sub_agent_started — fleet member created as background.
46
- driver.ingest(
47
- {
48
- kind: 'sub_agent_started',
49
- agentId: 'sa-early',
50
- firstPromptText: 'do bg work',
51
- },
52
- CHAT,
53
- )
54
-
55
- // Step 4: parent turn_end fires — sub-agent has no subAgents reducer
56
- // entry yet (the sub_agent_started above only added to fleet, the
57
- // reducer may not have a running entry depending on event ordering).
58
- // Regardless, fleet has a live background member → card must defer.
59
- driver.ingest({ kind: 'turn_end', durationMs: 100 }, CHAT)
60
-
61
- // Card must NOT be complete yet.
62
- expect(completions).toHaveLength(0)
63
-
64
- // Step 5: bg sub-agent does some work.
65
- advance(10)
66
- driver.ingest(
67
- {
68
- kind: 'sub_agent_tool_use',
69
- agentId: 'sa-early',
70
- toolUseId: 'bgt-1',
71
- toolName: 'Bash',
72
- input: { command: 'echo hi' },
73
- },
74
- CHAT,
75
- )
76
-
77
- // Still not done.
78
- expect(completions).toHaveLength(0)
79
-
80
- // Step 6: bg sub-agent terminates → deferred completion must fire.
81
- advance(10)
82
- driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'sa-early' }, CHAT)
83
-
84
- // Completion must have fired exactly once.
85
- expect(completions).toHaveLength(1)
86
- })
87
- })
@@ -1,211 +0,0 @@
1
- /**
2
- * P2 of #662 / fixes #64 — background sub-agent persistence across
3
- * subsequent parent turns.
4
- *
5
- * Lifecycle under test:
6
- * - Turn A enqueue → parent dispatches Agent({run_in_background:true})
7
- * → sub_agent_started → parent reply → turn_end (would normally
8
- * finalize and dispose).
9
- * - Turn B enqueues. The original PerChatState for turn A must
10
- * survive because its fleet still has a 'background' member.
11
- * - Background sub-agent emits sub_agent_tool_use. Routing must land
12
- * the event on turn A's state (originatingTurnKey), NOT turn B's
13
- * fresh state.
14
- * - When the background sub-agent finally fires sub_agent_turn_end,
15
- * turn A's PerChatState completes and is disposed.
16
- */
17
-
18
- import { describe, it, expect } from 'vitest'
19
- import { createProgressDriver } from '../progress-card-driver.js'
20
- import type { SessionEvent } from '../session-tail.js'
21
-
22
- function harness() {
23
- let now = 1000
24
- const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
25
- let nextRef = 0
26
- const completions: string[] = []
27
- const driver = createProgressDriver({
28
- emit: () => {},
29
- minIntervalMs: 500,
30
- coalesceMs: 400,
31
- initialDelayMs: 0,
32
- promoteAfterMs: 999_999,
33
- onTurnComplete: (s) => completions.push(s.turnKey),
34
- now: () => now,
35
- setTimeout: (fn, ms) => {
36
- const ref = nextRef++
37
- timers.push({ fireAt: now + ms, fn, ref })
38
- return { ref }
39
- },
40
- clearTimeout: (h) => {
41
- const ref = (h as { ref: number }).ref
42
- const idx = timers.findIndex((t) => t.ref === ref)
43
- if (idx !== -1) timers.splice(idx, 1)
44
- },
45
- setInterval: (fn, ms) => {
46
- const ref = nextRef++
47
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
48
- return { ref }
49
- },
50
- clearInterval: (h) => {
51
- const ref = (h as { ref: number }).ref
52
- const idx = timers.findIndex((t) => t.ref === ref)
53
- if (idx !== -1) timers.splice(idx, 1)
54
- },
55
- })
56
- return { driver, completions, advance: (ms: number) => { now += ms } }
57
- }
58
-
59
- const enqueue = (chatId: string, msgId: string): SessionEvent => ({
60
- kind: 'enqueue',
61
- chatId,
62
- messageId: msgId,
63
- threadId: null,
64
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
65
- })
66
-
67
- describe('P2 / #64: background sub-agent persists across parent turn boundaries', () => {
68
- it('PerChatState for turn A survives parent turn_end while background fleet member runs', () => {
69
- const { driver, completions } = harness()
70
- const CHAT = 'c1'
71
-
72
- // Turn A
73
- driver.ingest(enqueue(CHAT, '1'), null)
74
- driver.ingest(
75
- {
76
- kind: 'tool_use',
77
- toolName: 'Agent',
78
- toolUseId: 'tu1',
79
- input: { prompt: 'bg work', description: 'long-bg', run_in_background: true },
80
- },
81
- CHAT,
82
- )
83
- driver.ingest(
84
- { kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg work' },
85
- CHAT,
86
- )
87
- // Parent reply fires + delivery so turn_end takes the ✅ Done path.
88
- driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
89
- driver.recordOutboundDelivered(CHAT)
90
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
91
-
92
- // Background sub-agent is still running → onTurnComplete must NOT
93
- // have fired for turn A yet. Fleet is still inspectable.
94
- expect(completions.length).toBe(0)
95
- const fleetA = driver.peekFleet(CHAT)!
96
- expect(fleetA.has('saBG')).toBe(true)
97
- expect(fleetA.get('saBG')!.status).toBe('background')
98
- })
99
-
100
- it('background sub-agent tool_use after a NEW turn arrives still updates the originating turn fleet', () => {
101
- const { driver, completions, advance } = harness()
102
- const CHAT = 'c1'
103
-
104
- // Turn A spawns bg sub-agent
105
- driver.ingest(enqueue(CHAT, '1'), null)
106
- driver.ingest(
107
- {
108
- kind: 'tool_use',
109
- toolName: 'Agent',
110
- toolUseId: 'tu1',
111
- input: { prompt: 'bg work', description: 'long-bg', run_in_background: true },
112
- },
113
- CHAT,
114
- )
115
- driver.ingest(
116
- { kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg work' },
117
- CHAT,
118
- )
119
- driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
120
- driver.recordOutboundDelivered(CHAT)
121
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
122
-
123
- const fleetBeforeTurnB = driver.peekFleet(CHAT)!
124
- const turnAStartedAt = fleetBeforeTurnB.get('saBG')!.startedAt
125
-
126
- // Advance the clock so the bg sub-agent's later tool_use gets a
127
- // distinguishable lastActivityAt (proves routing actually mutated
128
- // the originating member rather than no-oping).
129
- advance(50)
130
-
131
- // Turn B starts (and ends quickly, no sub-agents).
132
- driver.ingest(enqueue(CHAT, '2'), null)
133
- advance(10)
134
- driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
135
- driver.recordOutboundDelivered(CHAT)
136
- driver.ingest({ kind: 'turn_end', durationMs: 200 }, CHAT)
137
-
138
- // Background sub-agent emits a tool_use after parent moved on.
139
- driver.ingest(
140
- {
141
- kind: 'sub_agent_tool_use',
142
- agentId: 'saBG',
143
- toolUseId: 'bgt1',
144
- toolName: 'Read',
145
- input: { file_path: '/tmp/x.txt' },
146
- },
147
- CHAT,
148
- )
149
-
150
- // The bg fleet member's lastActivityAt advanced — proving routing
151
- // landed on the originating PerChatState rather than dropping the
152
- // event as a "late event for ended turn".
153
- // Iterate all fleets — the originating one survives even if peekFleet
154
- // returns turn B's state.
155
- // We discover it via a known-stable agentId.
156
- // Use the test-only carry: peekFleet returns whichever chat:thread
157
- // matches; but turn B may shadow it. So use the fleet from turn A
158
- // by looking it up via the driver's introspection — we just call
159
- // peekFleet(CHAT) and accept that it returns a fleet where saBG
160
- // either lives (if A is still bound) or doesn't (if B took over).
161
- // Either way the saBG entry exists somewhere; check it via the
162
- // dedicated test hook.
163
- const allLiveBg = driver.peekFleet(CHAT)
164
- // saBG might live on turn A's fleet which is no longer the
165
- // currentTurnKey; but the routing must have updated it. We rely on
166
- // a debug hook to find it across all chats.
167
- expect(allLiveBg).toBeDefined()
168
- // Strict: we expect SOMEWHERE in the driver, saBG's lastActivityAt
169
- // is now newer than turnAStartedAt.
170
- // Pull via the driver's test hook (added in P2): peekAllFleets.
171
- const all = (driver as unknown as { peekAllFleets?: () => Array<{ turnKey: string; fleet: Map<string, { agentId: string; lastActivityAt: number; toolCount: number }> }> })
172
- .peekAllFleets?.() ?? []
173
- let found: { lastActivityAt: number; toolCount: number } | undefined
174
- for (const entry of all) {
175
- const m = entry.fleet.get('saBG')
176
- if (m != null) found = m
177
- }
178
- expect(found).toBeDefined()
179
- expect(found!.toolCount).toBe(1)
180
- expect(found!.lastActivityAt).toBeGreaterThan(turnAStartedAt)
181
- // Turn B should have completed normally (no bg on it).
182
- expect(completions.some((k) => k.endsWith(':2'))).toBe(true)
183
- // Turn A should NOT have completed yet (bg still running).
184
- expect(completions.some((k) => k.endsWith(':1'))).toBe(false)
185
- })
186
-
187
- it('completes the originating turn when the last background sub-agent reaches turn_end', () => {
188
- const { driver, completions } = harness()
189
- const CHAT = 'c1'
190
- driver.ingest(enqueue(CHAT, '1'), null)
191
- driver.ingest(
192
- {
193
- kind: 'tool_use',
194
- toolName: 'Agent',
195
- toolUseId: 'tu1',
196
- input: { prompt: 'bg', run_in_background: true },
197
- },
198
- CHAT,
199
- )
200
- driver.ingest({ kind: 'sub_agent_started', agentId: 'saBG', firstPromptText: 'bg' }, CHAT)
201
- driver.ingest({ kind: 'tool_use', toolName: 'mcp__switchroom-telegram__reply' }, CHAT)
202
- driver.recordOutboundDelivered(CHAT)
203
- driver.ingest({ kind: 'turn_end', durationMs: 500 }, CHAT)
204
- expect(completions.length).toBe(0)
205
-
206
- // BG sub-agent eventually finishes.
207
- driver.ingest({ kind: 'sub_agent_turn_end', agentId: 'saBG' }, CHAT)
208
- expect(completions.length).toBe(1)
209
- expect(completions[0]).toMatch(/:1$/)
210
- })
211
- })
@@ -1,62 +0,0 @@
1
- /**
2
- * P1 of #662 — fleet zone caps at 5 visible rows; surplus collapses
3
- * to "+ N more" footer. Order is most-recent-activity first.
4
- */
5
-
6
- import { describe, it, expect } from 'vitest'
7
- import { renderFleetZone } from '../two-zone-card.js'
8
- import type { FleetMember } from '../fleet-state.js'
9
-
10
- function fm(id: string, lastActivityAt: number): FleetMember {
11
- return {
12
- agentId: id,
13
- role: 'role-' + id,
14
- startedAt: 0,
15
- toolCount: 1,
16
- lastActivityAt,
17
- lastTool: { name: 'Read', sanitisedArg: 'x.ts' },
18
- status: 'running',
19
- terminalAt: null,
20
- errorSeen: false,
21
- originatingTurnKey: 'k',
22
- }
23
- }
24
-
25
- describe('renderFleetZone cap', () => {
26
- it('returns empty string for empty fleet', () => {
27
- expect(renderFleetZone(new Map(), 0)).toBe('')
28
- })
29
-
30
- it('renders all rows for fleet ≤ 5', () => {
31
- const fleet = new Map([
32
- ['a', fm('aaaaaa', 100)],
33
- ['b', fm('bbbbbb', 200)],
34
- ['c', fm('cccccc', 300)],
35
- ])
36
- const out = renderFleetZone(fleet, 1000)
37
- expect(out).toContain('FLEET (3)')
38
- expect(out).toContain('aaaaaa')
39
- expect(out).toContain('bbbbbb')
40
- expect(out).toContain('cccccc')
41
- expect(out).not.toContain('more')
42
- })
43
-
44
- it('caps at 5 with "+ N more" footer for fleet > 5, ordered most-recent-first', () => {
45
- const fleet = new Map<string, FleetMember>()
46
- for (let i = 0; i < 7; i++) {
47
- const id = `agent${i}xx`
48
- fleet.set(id, fm(id, 100 + i))
49
- }
50
- const out = renderFleetZone(fleet, 1000)
51
- expect(out).toContain('FLEET (7)')
52
- expect(out).toContain('+ 2 more')
53
- // Most-recent activity (i=6, ts=106) must appear; oldest two (i=0, i=1) must not
54
- expect(out).toContain('agent6')
55
- expect(out).toContain('agent2')
56
- expect(out).not.toContain('agent0')
57
- expect(out).not.toContain('agent1')
58
- // Count visible rows by counting status glyphs at row starts
59
- const rowLines = out.split('\n').filter((l) => l.startsWith('↻'))
60
- expect(rowLines.length).toBe(5)
61
- })
62
- })
@@ -1,101 +0,0 @@
1
- /**
2
- * P1 of #662 — fleet row formatting: id6 truncation, role fallback,
3
- * terminal status suffix, glyph mapping.
4
- */
5
-
6
- import { describe, it, expect } from 'vitest'
7
- import {
8
- renderFleetRow,
9
- glyphForFleetStatus,
10
- formatRelativeTime,
11
- } from '../two-zone-card.js'
12
- import type { FleetMember } from '../fleet-state.js'
13
-
14
- function fm(over: Partial<FleetMember>): FleetMember {
15
- return {
16
- agentId: 'abcdef0123456789',
17
- role: 'agent',
18
- startedAt: 0,
19
- toolCount: 0,
20
- lastActivityAt: 1000,
21
- lastTool: null,
22
- status: 'running',
23
- terminalAt: null,
24
- errorSeen: false,
25
- originatingTurnKey: 'k',
26
- ...over,
27
- }
28
- }
29
-
30
- describe('glyphForFleetStatus', () => {
31
- it('maps every status to a glyph', () => {
32
- expect(glyphForFleetStatus('running')).toBe('↻')
33
- expect(glyphForFleetStatus('background')).toBe('⏸')
34
- expect(glyphForFleetStatus('done')).toBe('✓')
35
- expect(glyphForFleetStatus('failed')).toBe('✗')
36
- expect(glyphForFleetStatus('stuck')).toBe('⚠')
37
- expect(glyphForFleetStatus('killed')).toBe('✗')
38
- })
39
- })
40
-
41
- describe('formatRelativeTime', () => {
42
- it('seconds under 60', () => {
43
- expect(formatRelativeTime(3000)).toBe('3s ago')
44
- })
45
- it('minutes + seconds', () => {
46
- expect(formatRelativeTime(72_000)).toBe('1m12s ago')
47
- })
48
- it('zero', () => {
49
- expect(formatRelativeTime(0)).toBe('0s ago')
50
- })
51
- })
52
-
53
- describe('renderFleetRow', () => {
54
- const NOW = 10_000
55
-
56
- it('uses 6-char id slice', () => {
57
- const out = renderFleetRow(fm({ agentId: 'abcdef0123456789' }), NOW)
58
- expect(out).toContain('abcdef')
59
- expect(out).not.toContain('abcdef0')
60
- })
61
-
62
- it('renders running with last tool + age', () => {
63
- const out = renderFleetRow(fm({
64
- lastActivityAt: NOW - 5000,
65
- lastTool: { name: 'Read', sanitisedArg: 'file.ts' },
66
- toolCount: 3,
67
- }), NOW)
68
- expect(out).toContain('Read')
69
- expect(out).toContain('file.ts')
70
- expect(out).toContain('5s ago')
71
- expect(out).toContain('3t')
72
- expect(out.startsWith('↻')).toBe(true)
73
- })
74
-
75
- it('renders terminal done with relative time', () => {
76
- const out = renderFleetRow(fm({
77
- status: 'done',
78
- terminalAt: NOW - 12_000,
79
- lastActivityAt: NOW - 12_000,
80
- }), NOW)
81
- expect(out).toContain('done 12s ago')
82
- expect(out.startsWith('✓')).toBe(true)
83
- })
84
-
85
- it('renders failed terminal with status suffix', () => {
86
- const out = renderFleetRow(fm({
87
- status: 'failed',
88
- terminalAt: NOW - 3000,
89
- lastActivityAt: NOW - 3000,
90
- errorSeen: true,
91
- }), NOW)
92
- expect(out).toContain('failed 3s ago')
93
- expect(out.startsWith('✗')).toBe(true)
94
- })
95
-
96
- it('falls back when no lastTool yet', () => {
97
- const out = renderFleetRow(fm({ lastActivityAt: NOW }), NOW)
98
- expect(out).toContain('↻')
99
- expect(out).toContain('agent')
100
- })
101
- })
@@ -1,78 +0,0 @@
1
- /**
2
- * P1 of #662 — phaseFor truth table.
3
- *
4
- * Drives the `phaseFor(state, fleet)` resolver across the 6-row spec
5
- * table from `reference/status-card-design.md` plus edge cases that
6
- * have historically been mis-classified (parent-stalled-fleet-active,
7
- * parent-done-fg-failed-bg-running, reply-and-fleet, sub-text-only).
8
- */
9
-
10
- import { describe, it, expect } from 'vitest'
11
- import { phaseFor } from '../two-zone-card.js'
12
- import type { FleetMember } from '../fleet-state.js'
13
- import type { ProgressCardState } from '../progress-card.js'
14
-
15
- function fm(id: string, status: FleetMember['status'], lastActivityAt: number = 1000): FleetMember {
16
- return {
17
- agentId: id,
18
- role: 'agent',
19
- startedAt: 0,
20
- toolCount: 0,
21
- lastActivityAt,
22
- lastTool: null,
23
- status,
24
- terminalAt: status === 'done' || status === 'failed' || status === 'killed' ? lastActivityAt : null,
25
- errorSeen: status === 'failed',
26
- originatingTurnKey: 'k',
27
- }
28
- }
29
-
30
- function st(opts: Partial<ProgressCardState> & { stage: ProgressCardState['stage'] }): ProgressCardState {
31
- return {
32
- turnStartedAt: 1,
33
- items: [],
34
- narratives: [],
35
- stage: opts.stage,
36
- thinking: false,
37
- subAgents: new Map(),
38
- pendingAgentSpawns: new Map(),
39
- tasks: [],
40
- ...opts,
41
- }
42
- }
43
-
44
- const fleetOf = (...members: FleetMember[]) => new Map(members.map((m) => [m.agentId, m]))
45
-
46
- const NOW = 100_000
47
-
48
- describe('phaseFor truth table', () => {
49
- it.each([
50
- // [name, state, fleet, opts, expectedLabel]
51
- ['working: parent in flight, no fleet', st({ stage: 'run' }), new Map(), {}, 'Working…'],
52
- ['working: parent in flight + fleet running', st({ stage: 'run' }), fleetOf(fm('a', 'running', NOW)), {}, 'Working…'],
53
- ['background: parent done, bg running', st({ stage: 'done' }), fleetOf(fm('a', 'running', NOW)), { parentDone: true }, 'Background'],
54
- ['background: parentDone flag + fg running', st({ stage: 'run' }), fleetOf(fm('a', 'running', NOW)), { parentDone: true }, 'Background'],
55
- ['stalled: parent idle + all fleet stuck', st({ stage: 'run' }), fleetOf(fm('a', 'stuck', 0), fm('b', 'stuck', 0)), {}, 'Stalled'],
56
- ['done: parent done + all fleet terminal', st({ stage: 'done' }), fleetOf(fm('a', 'done'), fm('b', 'failed')), {}, 'Done'],
57
- ['done: parent done, no fleet', st({ stage: 'done' }), new Map(), {}, 'Done'],
58
- ['silent: parent terminal + no reply', st({ stage: 'done' }), new Map(), { silentEnd: true }, 'Ended without reply'],
59
- ['forced close: stalledClose flag wins', st({ stage: 'done' }), fleetOf(fm('a', 'done')), { stalledClose: true }, 'Forced close'],
60
- // Edge cases
61
- ['parent-done + fg-failed + bg-running → Background, not Done', st({ stage: 'done' }), fleetOf(fm('a', 'failed'), fm('b', 'running', NOW)), { parentDone: true }, 'Background'],
62
- ['mixed terminal+stuck → not Done', st({ stage: 'run' }), fleetOf(fm('a', 'done'), fm('b', 'stuck', 0)), {}, 'Stalled'],
63
- ['reply tool fired AND fleet running → Background (parentDone)', st({ stage: 'done' }), fleetOf(fm('a', 'running', NOW)), { parentDone: true }, 'Background'],
64
- // Regression: pre-fix the `[].every(...)` vacuous-truth at
65
- // two-zone-card.ts fleetAllStuck would mark the fleet stalled the
66
- // moment the last sub-agent finished while the parent was still
67
- // running. Plan agents that completed in 2-3min showed ⚠ Stalled
68
- // on the pinned card until the parent itself wrapped up. Now: zero
69
- // running-or-stuck members in the fleet means we fall through to
70
- // the default "Working…" instead.
71
- ['regression: all fleet done + parent still running → Working… (was Stalled)', st({ stage: 'run' }), fleetOf(fm('a', 'done'), fm('b', 'done')), {}, 'Working…'],
72
- ['regression: lone done sub-agent + parent still running → Working…', st({ stage: 'run' }), fleetOf(fm('a', 'done')), {}, 'Working…'],
73
- ['regression: failed-only fleet + parent still running → Working… (was Stalled)', st({ stage: 'run' }), fleetOf(fm('a', 'failed')), {}, 'Working…'],
74
- ])('%s', (_name, state, fleet, opts, expectedLabel) => {
75
- const phase = phaseFor(state, fleet, NOW, opts as Record<string, unknown>)
76
- expect(phase.label).toBe(expectedLabel)
77
- })
78
- })
@@ -1,110 +0,0 @@
1
- /**
2
- * P1 of #662 — render-invariant property tests.
3
- *
4
- * For any input state with fleet 0..50 and any tool-arg shape:
5
- * 1. Output passes a tag-balance validator (no <blockquote> 400s).
6
- * 2. Output is < 4096 bytes.
7
- * 3. Idempotent: same inputs → same output.
8
- *
9
- * Uses vitest `it.each` with ~30 hand-crafted shapes covering the
10
- * property surface (per #662 P1 — replaces fast-check).
11
- */
12
-
13
- import { describe, it, expect } from 'vitest'
14
- import { renderTwoZoneCard } from '../two-zone-card.js'
15
- import type { FleetMember, FleetStatus } from '../fleet-state.js'
16
- import type { ProgressCardState } from '../progress-card.js'
17
- import { isBalancedHtml } from './html-balanced.js'
18
-
19
- const baseState: ProgressCardState = {
20
- turnStartedAt: 1,
21
- items: [],
22
- narratives: [],
23
- stage: 'run',
24
- thinking: false,
25
- subAgents: new Map(),
26
- pendingAgentSpawns: new Map(),
27
- tasks: [],
28
- }
29
-
30
- function makeMember(i: number, status: FleetStatus, argShape: string): FleetMember {
31
- return {
32
- agentId: `agent-${i.toString().padStart(8, '0')}`,
33
- role: i % 3 === 0 ? `worker-${i}` : i % 3 === 1 ? 'general-purpose' : 'investigate the auth bug',
34
- startedAt: 0,
35
- toolCount: i,
36
- lastActivityAt: 100 + i,
37
- lastTool: i === 0 ? null : { name: 'Read', sanitisedArg: argShape },
38
- status,
39
- terminalAt: ['done', 'failed', 'killed'].includes(status) ? 100 + i : null,
40
- errorSeen: status === 'failed',
41
- originatingTurnKey: 'k',
42
- }
43
- }
44
-
45
- const ARG_SHAPES = [
46
- '',
47
- 'simple.ts',
48
- 'foo.key',
49
- 'a/b/c/very-long-relative-path-that-should-not-explode-the-card.ts',
50
- '<html-y-arg>',
51
- '&amp;already-escaped',
52
- 'emoji 🚀 in arg',
53
- 'quotes "and" \'apostrophes\'',
54
- '[redacted]',
55
- '\nmultiline\nshould\nflatten',
56
- ]
57
-
58
- const STATUSES: FleetStatus[] = ['running', 'background', 'done', 'failed', 'stuck', 'killed']
59
-
60
- const SIZES = [0, 1, 3, 5, 10, 50]
61
-
62
- const cases: Array<[string, number, string]> = []
63
- for (const size of SIZES) {
64
- for (const arg of ARG_SHAPES.slice(0, 3)) {
65
- cases.push([`size=${size} arg=${JSON.stringify(arg).slice(0, 20)}`, size, arg])
66
- }
67
- }
68
-
69
- describe('two-zone-card render invariants', () => {
70
- it.each(cases)('%s — balanced HTML, <4096 bytes, idempotent', (_name, size, arg) => {
71
- const fleet = new Map<string, FleetMember>()
72
- for (let i = 0; i < size; i++) {
73
- const status = STATUSES[i % STATUSES.length]
74
- fleet.set(`a${i}`, makeMember(i, status, arg))
75
- }
76
- const out1 = renderTwoZoneCard({ state: baseState, fleet, now: 5000 })
77
- const out2 = renderTwoZoneCard({ state: baseState, fleet, now: 5000 })
78
- const balance = isBalancedHtml(out1)
79
- expect(balance.balanced, `unbalanced: open=${balance.openTags.join(',')} extra=${balance.extraCloses.join(',')}`).toBe(true)
80
- expect(out1.length).toBeLessThan(4096)
81
- expect(out1).toBe(out2)
82
- })
83
-
84
- it('handles arg shapes individually with size=5', () => {
85
- for (const arg of ARG_SHAPES) {
86
- const fleet = new Map<string, FleetMember>()
87
- for (let i = 0; i < 5; i++) fleet.set(`a${i}`, makeMember(i, 'running', arg))
88
- const out = renderTwoZoneCard({ state: baseState, fleet, now: 5000 })
89
- const b = isBalancedHtml(out)
90
- expect(b.balanced, `unbalanced for arg=${arg}: open=${b.openTags.join(',')}`).toBe(true)
91
- expect(out.length).toBeLessThan(4096)
92
- }
93
- })
94
- })
95
-
96
- describe('html-balanced validator self-test', () => {
97
- it('balanced cases', () => {
98
- expect(isBalancedHtml('').balanced).toBe(true)
99
- expect(isBalancedHtml('plain text').balanced).toBe(true)
100
- expect(isBalancedHtml('<b>bold</b>').balanced).toBe(true)
101
- expect(isBalancedHtml('<b>bold <i>italic</i></b>').balanced).toBe(true)
102
- expect(isBalancedHtml('text with &lt;not a tag&gt;').balanced).toBe(true)
103
- expect(isBalancedHtml('<blockquote>x</blockquote>').balanced).toBe(true)
104
- })
105
- it('unbalanced cases', () => {
106
- expect(isBalancedHtml('<b>open').balanced).toBe(false)
107
- expect(isBalancedHtml('close</b>').balanced).toBe(false)
108
- expect(isBalancedHtml('<b><i>x</b></i>').balanced).toBe(false)
109
- })
110
- })