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,128 +0,0 @@
1
- /**
2
- * P1 of #662 — lifecycle integration. Drive the real driver with
3
- * TWO_ZONE_CARD=1 set; assert the rendered HTML for a turn with 2
4
- * fleet members contains expected substrings (header phase, parent
5
- * bullets, fleet rows, fleet count).
6
- *
7
- * Uses the same lightweight harness pattern as
8
- * progress-card-driver-fleet-shadow.test.ts — no Telegram bot, just
9
- * record the emit calls and inspect their HTML payload.
10
- */
11
-
12
- import { describe, it, expect, beforeEach, afterEach } from 'vitest'
13
- import { createProgressDriver } from '../progress-card-driver.js'
14
- import type { SessionEvent } from '../session-tail.js'
15
-
16
- function harness() {
17
- let now = 1000
18
- const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
19
- let nextRef = 0
20
- const emits: Array<{ chatId: string; payload: string }> = []
21
- const driver = createProgressDriver({
22
- emit: (args) => {
23
- // Parent card emits only — sub-agent per-agent cards carry agentId.
24
- if ((args as { agentId?: string }).agentId == null) {
25
- emits.push({ chatId: args.chatId, payload: args.html })
26
- }
27
- },
28
- minIntervalMs: 0,
29
- coalesceMs: 0,
30
- initialDelayMs: 0,
31
- promoteAfterMs: 999_999,
32
- now: () => now,
33
- setTimeout: (fn, ms) => {
34
- const ref = nextRef++
35
- timers.push({ fireAt: now + ms, fn, ref })
36
- return { ref }
37
- },
38
- clearTimeout: (h) => {
39
- const ref = (h as { ref: number }).ref
40
- const idx = timers.findIndex((t) => t.ref === ref)
41
- if (idx !== -1) timers.splice(idx, 1)
42
- },
43
- setInterval: (fn, ms) => {
44
- const ref = nextRef++
45
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
46
- return { ref }
47
- },
48
- clearInterval: (h) => {
49
- const ref = (h as { ref: number }).ref
50
- const idx = timers.findIndex((t) => t.ref === ref)
51
- if (idx !== -1) timers.splice(idx, 1)
52
- },
53
- })
54
- return {
55
- driver,
56
- emits,
57
- advance: (ms: number) => {
58
- now += ms
59
- const due = timers.filter((t) => t.fireAt <= now)
60
- for (const t of due) {
61
- t.fn()
62
- if (t.repeat) {
63
- t.fireAt = now + t.repeat
64
- } else {
65
- const i = timers.indexOf(t)
66
- if (i >= 0) timers.splice(i, 1)
67
- }
68
- }
69
- },
70
- flush: () => {
71
- // Pump any pending timers
72
- const due = timers.filter((t) => t.fireAt <= now)
73
- for (const t of due) t.fn()
74
- },
75
- }
76
- }
77
-
78
- const enqueue = (chatId: string): SessionEvent => ({
79
- kind: 'enqueue',
80
- chatId,
81
- messageId: '1',
82
- threadId: null,
83
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
84
- })
85
-
86
- describe('two-zone-card lifecycle (TWO_ZONE_CARD=1)', () => {
87
- let prevFlag: string | undefined
88
- beforeEach(() => {
89
- prevFlag = process.env.TWO_ZONE_CARD
90
- process.env.TWO_ZONE_CARD = '1'
91
- })
92
- afterEach(() => {
93
- if (prevFlag === undefined) delete process.env.TWO_ZONE_CARD
94
- else process.env.TWO_ZONE_CARD = prevFlag
95
- })
96
-
97
- it('renders two-zone card with fleet rows when flag is on', () => {
98
- const { driver, emits, advance } = harness()
99
- const CHAT = 'c1'
100
- driver.ingest(enqueue(CHAT), null)
101
-
102
- const events: SessionEvent[] = [
103
- { kind: 'tool_use', toolUseId: 'p1', toolName: 'Read', input: { file_path: '/tmp/foo.ts' } },
104
- { kind: 'sub_agent_started', agentId: 'sa1', firstPromptText: 'do work', subagentType: 'worker' },
105
- { kind: 'sub_agent_started', agentId: 'sa2', firstPromptText: 'review', subagentType: 'reviewer' },
106
- { kind: 'sub_agent_tool_use', agentId: 'sa1', toolUseId: 't1', toolName: 'Grep', input: { pattern: 'TODO' } },
107
- ]
108
- for (const ev of events) driver.ingest(ev, CHAT)
109
- // Drain the coalesce/min-interval setTimeout queue so deferred
110
- // sub-agent emits flush. Each ingest schedules a 0-delay timer
111
- // that is only invoked when fake time advances.
112
- advance(0)
113
-
114
- // Find the most recent emitted payload — it should be a two-zone card.
115
- const last = emits[emits.length - 1]
116
- expect(last).toBeDefined()
117
- const html = last.payload
118
- // Header substrings
119
- expect(html).toMatch(/Working/)
120
- // Fleet zone present with count
121
- expect(html).toContain('FLEET (2)')
122
- expect(html).toContain('worker')
123
- expect(html).toContain('reviewer')
124
- // Fleet ids (6 chars)
125
- expect(html).toContain('sa1')
126
- expect(html).toContain('sa2')
127
- })
128
- })
@@ -1,58 +0,0 @@
1
- /**
2
- * P1 of #662 — renderer output never reintroduces raw absolute paths
3
- * or bearer-shaped tokens. Most coverage lives in fleet-state.test.ts;
4
- * this asserts the *renderer* basenames/redacts via the FleetMember's
5
- * sanitised values (i.e. it doesn't re-pull from raw input anywhere).
6
- */
7
-
8
- import { describe, it, expect } from 'vitest'
9
- import { renderTwoZoneCard } from '../two-zone-card.js'
10
- import type { FleetMember } from '../fleet-state.js'
11
- import type { ProgressCardState } from '../progress-card.js'
12
-
13
- const baseState: ProgressCardState = {
14
- turnStartedAt: 1,
15
- items: [],
16
- narratives: [],
17
- stage: 'run',
18
- thinking: false,
19
- subAgents: new Map(),
20
- pendingAgentSpawns: new Map(),
21
- tasks: [],
22
- }
23
-
24
- function fm(over: Partial<FleetMember>): FleetMember {
25
- return {
26
- agentId: 'aaaaaaaaaaaa',
27
- role: 'agent',
28
- startedAt: 0,
29
- toolCount: 1,
30
- lastActivityAt: 1000,
31
- lastTool: null,
32
- status: 'running',
33
- terminalAt: null,
34
- errorSeen: false,
35
- originatingTurnKey: 'k',
36
- ...over,
37
- }
38
- }
39
-
40
- describe('two-zone-card sanitise', () => {
41
- it('does not contain raw absolute path under /etc/secrets', () => {
42
- const fleet = new Map([['a', fm({
43
- lastTool: { name: 'Read', sanitisedArg: 'foo.key' }, // already sanitised by fleet-state
44
- })]])
45
- const out = renderTwoZoneCard({ state: baseState, fleet, now: 2000 })
46
- expect(out).not.toContain('/etc/secrets')
47
- expect(out).toContain('foo.key')
48
- })
49
-
50
- it('does not contain bearer-shaped tokens (sanitised upstream)', () => {
51
- const fleet = new Map([['a', fm({
52
- lastTool: { name: 'Bash', sanitisedArg: 'curl -H "Authorization: [redacted]" https://api' },
53
- })]])
54
- const out = renderTwoZoneCard({ state: baseState, fleet, now: 2000 })
55
- expect(out).toContain('[redacted]')
56
- expect(out).not.toMatch(/Bearer\s+[A-Za-z0-9]{16,}/)
57
- })
58
- })
@@ -1,133 +0,0 @@
1
- /**
2
- * P1 of #662 — golden output for 5 canonical card states.
3
- *
4
- * Uses explicit `toBe()` rather than `toMatchSnapshot()` so the same
5
- * test file passes under both vitest (Core tests CI step) and bun
6
- * (Plugin tests CI step) — the two snapshot formats are incompatible.
7
- */
8
-
9
- import { describe, it, expect } from 'vitest'
10
- import { renderTwoZoneCard } from '../two-zone-card.js'
11
- import type { FleetMember } from '../fleet-state.js'
12
- import type { ProgressCardState } from '../progress-card.js'
13
-
14
- function st(over: Partial<ProgressCardState> & { stage: ProgressCardState['stage'] }): ProgressCardState {
15
- return {
16
- turnStartedAt: 0,
17
- items: [],
18
- narratives: [],
19
- stage: over.stage,
20
- thinking: false,
21
- subAgents: new Map(),
22
- pendingAgentSpawns: new Map(),
23
- tasks: [],
24
- ...over,
25
- }
26
- }
27
-
28
- function fm(over: Partial<FleetMember>): FleetMember {
29
- return {
30
- agentId: 'aaaaaa00',
31
- role: 'agent',
32
- startedAt: 0,
33
- toolCount: 0,
34
- lastActivityAt: 0,
35
- lastTool: null,
36
- status: 'running',
37
- terminalAt: null,
38
- errorSeen: false,
39
- originatingTurnKey: 'k',
40
- ...over,
41
- }
42
- }
43
-
44
- const NOW = 60_000
45
-
46
- describe('two-zone-card snapshots', () => {
47
- it('empty fleet — clean clerk-style card', () => {
48
- const out = renderTwoZoneCard({
49
- state: st({ stage: 'run', turnStartedAt: NOW - 5000 }),
50
- fleet: new Map(),
51
- now: NOW,
52
- })
53
- expect(out).toBe('⚙️ <b>Working…</b> · ⏱ 00:05 · 🔧 0')
54
- })
55
-
56
- it('3 members mixed', () => {
57
- const fleet = new Map([
58
- ['a', fm({ agentId: 'aaaaaa01', role: 'researcher', status: 'running', toolCount: 4, lastActivityAt: NOW - 2000, lastTool: { name: 'Grep', sanitisedArg: 'TODO' } })],
59
- ['b', fm({ agentId: 'bbbbbb02', role: 'worker', status: 'done', toolCount: 8, lastActivityAt: NOW - 10_000, terminalAt: NOW - 10_000 })],
60
- ['c', fm({ agentId: 'cccccc03', role: 'reviewer', status: 'stuck', toolCount: 2, lastActivityAt: NOW - 70_000, lastTool: { name: 'Read', sanitisedArg: 'big.ts' } })],
61
- ])
62
- const out = renderTwoZoneCard({
63
- state: st({ stage: 'run', turnStartedAt: NOW - 30_000 }),
64
- fleet,
65
- now: NOW,
66
- })
67
- expect(out).toBe(
68
- '⚙️ <b>Working…</b> · ⏱ 00:30 · 🔧 14 · 🤖 3\n' +
69
- '\n' +
70
- '<b>FLEET (3)</b>\n' +
71
- '↻ researcher <code>aaaaaa</code> · 4t · Grep <code>TODO</code> (2s ago)\n' +
72
- '✓ worker <code>bbbbbb</code> · 8t · done 10s ago\n' +
73
- '⚠ reviewer <code>cccccc</code> · 2t · idle 1m10s ago',
74
- )
75
- })
76
-
77
- it('all-done with completed receipts', () => {
78
- const fleet = new Map([
79
- ['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'done', toolCount: 5, lastActivityAt: NOW - 10_000, terminalAt: NOW - 10_000 })],
80
- ['b', fm({ agentId: 'bbbbbb02', role: 'reviewer', status: 'done', toolCount: 3, lastActivityAt: NOW - 5000, terminalAt: NOW - 5000 })],
81
- ])
82
- const out = renderTwoZoneCard({
83
- state: st({ stage: 'done', turnStartedAt: NOW - 20_000 }),
84
- fleet,
85
- now: NOW,
86
- })
87
- expect(out).toBe(
88
- '✅ <b>Done</b> · ⏱ 00:20 · 🔧 8 · 🤖 2\n' +
89
- '\n' +
90
- '<b>FLEET (2)</b>\n' +
91
- '✓ reviewer <code>bbbbbb</code> · 3t · done 5s ago\n' +
92
- '✓ worker <code>aaaaaa</code> · 5t · done 10s ago',
93
- )
94
- })
95
-
96
- it('all-stuck', () => {
97
- const fleet = new Map([
98
- ['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'stuck', toolCount: 1, lastActivityAt: NOW - 90_000, lastTool: { name: 'Bash', sanitisedArg: 'sleep 999' } })],
99
- ['b', fm({ agentId: 'bbbbbb02', role: 'worker', status: 'stuck', toolCount: 1, lastActivityAt: NOW - 80_000, lastTool: { name: 'Bash', sanitisedArg: 'sleep 999' } })],
100
- ])
101
- const out = renderTwoZoneCard({
102
- state: st({ stage: 'run', turnStartedAt: NOW - 95_000 }),
103
- fleet,
104
- now: NOW,
105
- })
106
- expect(out).toBe(
107
- '⚠ <b>Stalled</b> · ⏱ 01:35 · 🔧 2 · 🤖 2\n' +
108
- '\n' +
109
- '<b>FLEET (2)</b>\n' +
110
- '⚠ worker <code>bbbbbb</code> · 1t · idle 1m20s ago\n' +
111
- '⚠ worker <code>aaaaaa</code> · 1t · idle 1m30s ago',
112
- )
113
- })
114
-
115
- it('background — parent done, background sub still running', () => {
116
- const fleet = new Map([
117
- ['a', fm({ agentId: 'aaaaaa01', role: 'worker', status: 'done', toolCount: 5, lastActivityAt: NOW - 30_000, terminalAt: NOW - 30_000 })],
118
- ['b', fm({ agentId: 'bbbbbb02', role: 'background', status: 'background', toolCount: 12, lastActivityAt: NOW - 1000, lastTool: { name: 'Bash', sanitisedArg: 'long-job.sh' } })],
119
- ])
120
- const out = renderTwoZoneCard({
121
- state: st({ stage: 'done', turnStartedAt: NOW - 90_000 }),
122
- fleet,
123
- now: NOW,
124
- })
125
- expect(out).toBe(
126
- '⏸ <b>Background</b> · ⏱ 01:30 · 🔧 17 · 🤖 2\n' +
127
- '\n' +
128
- '<b>FLEET (2)</b>\n' +
129
- '⏸ background <code>bbbbbb</code> · 12t · Bash <code>long-job.sh</code> (1s ago)\n' +
130
- '✓ worker <code>aaaaaa</code> · 5t · done 30s ago',
131
- )
132
- })
133
- })
@@ -1,155 +0,0 @@
1
- /**
2
- * P2 of #662 — concurrent-chat isolation. Two distinct chats each
3
- * spawn a background sub-agent. Routing must keep their fleets
4
- * completely independent: a tool_use event for chat A's bg sub-agent
5
- * must never bleed into chat B's fleet, and vice versa.
6
- */
7
-
8
- import { describe, it, expect } from 'vitest'
9
- import { createProgressDriver } from '../progress-card-driver.js'
10
- import type { SessionEvent } from '../session-tail.js'
11
-
12
- function harness() {
13
- let now = 1000
14
- const timers: Array<{ fireAt: number; fn: () => void; ref: number; repeat?: number }> = []
15
- let nextRef = 0
16
- const driver = createProgressDriver({
17
- emit: () => {},
18
- minIntervalMs: 500,
19
- coalesceMs: 400,
20
- initialDelayMs: 0,
21
- promoteAfterMs: 999_999,
22
- now: () => now,
23
- setTimeout: (fn, ms) => {
24
- const ref = nextRef++
25
- timers.push({ fireAt: now + ms, fn, ref })
26
- return { ref }
27
- },
28
- clearTimeout: (h) => {
29
- const ref = (h as { ref: number }).ref
30
- const idx = timers.findIndex((t) => t.ref === ref)
31
- if (idx !== -1) timers.splice(idx, 1)
32
- },
33
- setInterval: (fn, ms) => {
34
- const ref = nextRef++
35
- timers.push({ fireAt: now + ms, fn, ref, repeat: ms })
36
- return { ref }
37
- },
38
- clearInterval: (h) => {
39
- const ref = (h as { ref: number }).ref
40
- const idx = timers.findIndex((t) => t.ref === ref)
41
- if (idx !== -1) timers.splice(idx, 1)
42
- },
43
- })
44
- return { driver, advance: (ms: number) => { now += ms } }
45
- }
46
-
47
- const enqueue = (chatId: string): SessionEvent => ({
48
- kind: 'enqueue',
49
- chatId,
50
- messageId: '1',
51
- threadId: null,
52
- rawContent: `<channel chat_id="${chatId}">go</channel>`,
53
- })
54
-
55
- const enqueueWithThread = (chatId: string, threadId: string, msgId: string): SessionEvent => ({
56
- kind: 'enqueue',
57
- chatId,
58
- messageId: msgId,
59
- threadId,
60
- rawContent: `<channel chat_id="${chatId}" thread_id="${threadId}">go</channel>`,
61
- })
62
-
63
- describe('P2: concurrent-chat fleet isolation', () => {
64
- it('two chats with their own background sub-agents do not cross-pollinate', () => {
65
- const { driver } = harness()
66
-
67
- // Chat A
68
- driver.ingest(enqueue('cA'), null)
69
- driver.ingest(
70
- {
71
- kind: 'tool_use',
72
- toolName: 'Agent',
73
- toolUseId: 'tuA',
74
- input: { prompt: 'pA', run_in_background: true },
75
- },
76
- 'cA',
77
- )
78
- driver.ingest({ kind: 'sub_agent_started', agentId: 'saA', firstPromptText: 'pA' }, 'cA')
79
-
80
- // Chat B
81
- driver.ingest(enqueue('cB'), null)
82
- driver.ingest(
83
- {
84
- kind: 'tool_use',
85
- toolName: 'Agent',
86
- toolUseId: 'tuB',
87
- input: { prompt: 'pB', run_in_background: true },
88
- },
89
- 'cB',
90
- )
91
- driver.ingest({ kind: 'sub_agent_started', agentId: 'saB', firstPromptText: 'pB' }, 'cB')
92
-
93
- const fleetA = driver.peekFleet('cA')!
94
- const fleetB = driver.peekFleet('cB')!
95
- expect(fleetA.has('saA')).toBe(true)
96
- expect(fleetA.has('saB')).toBe(false)
97
- expect(fleetB.has('saB')).toBe(true)
98
- expect(fleetB.has('saA')).toBe(false)
99
- expect(fleetA.get('saA')!.status).toBe('background')
100
- expect(fleetB.get('saB')!.status).toBe('background')
101
- })
102
-
103
- it('PR-C2: two threads in the SAME chat (different threadId, shared chatId) — no cross-talk in per-chat state maps', () => {
104
- // Two forum-topic threads in the same Telegram chat. Same chatId,
105
- // distinct threadId. The driver must key per-turn state on the
106
- // composite (chatId, threadId) base — closing one thread's turn
107
- // must not touch the other's, and bg sub-agents must not bleed
108
- // between threads.
109
- const { driver } = harness()
110
-
111
- driver.ingest(enqueueWithThread('cShared', 'tA', '1'), null)
112
- driver.ingest(
113
- {
114
- kind: 'tool_use', toolName: 'Agent', toolUseId: 'tuA',
115
- input: { prompt: 'pA', run_in_background: true },
116
- },
117
- 'cShared',
118
- )
119
- driver.ingest({ kind: 'sub_agent_started', agentId: 'saA', firstPromptText: 'pA' }, 'cShared')
120
-
121
- driver.ingest(enqueueWithThread('cShared', 'tB', '2'), null)
122
- driver.ingest(
123
- {
124
- kind: 'tool_use', toolName: 'Agent', toolUseId: 'tuB',
125
- input: { prompt: 'pB', run_in_background: true },
126
- },
127
- 'cShared',
128
- )
129
- driver.ingest({ kind: 'sub_agent_started', agentId: 'saB', firstPromptText: 'pB' }, 'cShared')
130
-
131
- // peekFleet keys on (chatId, threadId).
132
- const fleetA = driver.peekFleet('cShared', 'tA')!
133
- const fleetB = driver.peekFleet('cShared', 'tB')!
134
- expect(fleetA).toBeDefined()
135
- expect(fleetB).toBeDefined()
136
- expect(fleetA.has('saA')).toBe(true)
137
- expect(fleetA.has('saB')).toBe(false)
138
- expect(fleetB.has('saB')).toBe(true)
139
- expect(fleetB.has('saA')).toBe(false)
140
-
141
- // baseTurnSeqs must have distinct entries for the two threads.
142
- const maps = (driver as unknown as {
143
- _debugGetMaps?: () => { baseTurnSeqs: Map<string, number>; chats: Map<string, unknown> }
144
- })._debugGetMaps!()
145
- const baseKeys = [...maps.baseTurnSeqs.keys()]
146
- // The two threads MUST resolve to distinct base keys (proves the
147
- // composite (chatId, threadId) is honoured). The exact base-key
148
- // format is (chatId:threadId) — assert distinctness without
149
- // hard-coding the format.
150
- expect(baseKeys.length).toBe(2)
151
- expect(new Set(baseKeys).size).toBe(2)
152
- // chats map: 2 in-flight turns.
153
- expect(maps.chats.size).toBe(2)
154
- })
155
- })
@@ -1,117 +0,0 @@
1
- /**
2
- * PR-A — phaseFor precedence: silent-end must be lifted above the
3
- * background/done branches but gated on parentDone (or stage===done) so
4
- * it can't fire while the parent is still in flight.
5
- *
6
- * Drives `phaseFor` across all combinations of
7
- * (parentDone, silentEnd, fleetRunning, stalledClose) and asserts the
8
- * resolved label.
9
- */
10
-
11
- import { describe, it, expect } from 'vitest'
12
- import { phaseFor } from '../two-zone-card.js'
13
- import type { FleetMember } from '../fleet-state.js'
14
- import type { ProgressCardState } from '../progress-card.js'
15
-
16
- function fm(id: string, status: FleetMember['status'], lastActivityAt: number = 100_000): FleetMember {
17
- return {
18
- agentId: id,
19
- role: 'agent',
20
- startedAt: 0,
21
- toolCount: 0,
22
- lastActivityAt,
23
- lastTool: null,
24
- status,
25
- terminalAt: status === 'done' || status === 'failed' || status === 'killed' ? lastActivityAt : null,
26
- errorSeen: status === 'failed',
27
- originatingTurnKey: 'k',
28
- }
29
- }
30
-
31
- function st(stage: ProgressCardState['stage']): ProgressCardState {
32
- return {
33
- turnStartedAt: 1,
34
- items: [],
35
- narratives: [],
36
- stage,
37
- thinking: false,
38
- subAgents: new Map(),
39
- pendingAgentSpawns: new Map(),
40
- tasks: [],
41
- }
42
- }
43
-
44
- const NOW = 100_000
45
-
46
- interface Row {
47
- parentDone: boolean
48
- silentEnd: boolean
49
- fleetRunning: boolean
50
- stalledClose: boolean
51
- expected: string
52
- }
53
-
54
- // Truth table: 2^4 = 16 combinations of the four boolean inputs.
55
- // stalledClose dominates everything → Forced close.
56
- // silentEnd fires only when parentDone is true.
57
- // When silentEnd is gated off and parent is still running, fleetRunning
58
- // alone yields Working… (parent active); when parent is done +
59
- // fleetRunning we get Background; parentDone alone yields Done.
60
- const rows: Row[] = [
61
- // stalledClose=true → always Forced close
62
- { parentDone: false, silentEnd: false, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
63
- { parentDone: false, silentEnd: false, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
64
- { parentDone: false, silentEnd: true, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
65
- { parentDone: false, silentEnd: true, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
66
- { parentDone: true, silentEnd: false, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
67
- { parentDone: true, silentEnd: false, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
68
- { parentDone: true, silentEnd: true, fleetRunning: false, stalledClose: true, expected: 'Forced close' },
69
- { parentDone: true, silentEnd: true, fleetRunning: true, stalledClose: true, expected: 'Forced close' },
70
-
71
- // stalledClose=false
72
- // parent in flight (parentDone=false)
73
- { parentDone: false, silentEnd: false, fleetRunning: false, stalledClose: false, expected: 'Working…' },
74
- { parentDone: false, silentEnd: false, fleetRunning: true, stalledClose: false, expected: 'Working…' },
75
- // silentEnd is GATED on parentDone — must not fire while parent in flight
76
- { parentDone: false, silentEnd: true, fleetRunning: false, stalledClose: false, expected: 'Working…' },
77
- { parentDone: false, silentEnd: true, fleetRunning: true, stalledClose: false, expected: 'Working…' },
78
-
79
- // parent done
80
- { parentDone: true, silentEnd: false, fleetRunning: false, stalledClose: false, expected: 'Done' },
81
- { parentDone: true, silentEnd: false, fleetRunning: true, stalledClose: false, expected: 'Background' },
82
- // silentEnd LIFTED above background/done — fires even when fleet still running
83
- { parentDone: true, silentEnd: true, fleetRunning: false, stalledClose: false, expected: 'Ended without reply' },
84
- { parentDone: true, silentEnd: true, fleetRunning: true, stalledClose: false, expected: 'Ended without reply' },
85
- ]
86
-
87
- describe('phaseFor precedence — (parentDone, silentEnd, fleetRunning, stalledClose)', () => {
88
- it.each(rows)(
89
- 'parentDone=$parentDone silentEnd=$silentEnd fleetRunning=$fleetRunning stalledClose=$stalledClose → $expected',
90
- ({ parentDone, silentEnd, fleetRunning, stalledClose, expected }) => {
91
- const stage: ProgressCardState['stage'] = parentDone ? 'done' : 'run'
92
- const fleet = new Map<string, FleetMember>()
93
- if (fleetRunning) fleet.set('a', fm('a', 'running', NOW))
94
- const opts: Record<string, unknown> = {}
95
- if (silentEnd) opts.silentEnd = true
96
- if (stalledClose) opts.stalledClose = true
97
- // parentDone is conveyed via stage; also pass the explicit flag for parity
98
- if (parentDone) opts.parentDone = true
99
-
100
- const phase = phaseFor(st(stage), fleet, NOW, opts)
101
- expect(phase.label).toBe(expected)
102
- },
103
- )
104
-
105
- // PR-C2 reviewer follow-up: add an explicit row for FleetMember.status=
106
- // 'background'. The truth-table above uses status='running' to model
107
- // "fleet still running"; `anyFleetActive` also returns true for
108
- // 'background' status, so a parentDone+background-only fleet should
109
- // resolve to "Background" rather than "Done".
110
- it('parentDone=true + fleet=[background-only] → Background (not Done)', () => {
111
- const fleet = new Map<string, FleetMember>([
112
- ['bg', fm('bg', 'background', NOW)],
113
- ])
114
- const phase = phaseFor(st('done'), fleet, NOW, { parentDone: true })
115
- expect(phase.label).toBe('Background')
116
- })
117
- })