@team-agent/installer 0.2.11 → 0.3.1

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 (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1204 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1207 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +557 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1084 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +526 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1101 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +237 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +272 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +410 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +489 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +2109 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +985 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +710 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +187 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +468 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +743 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +329 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +553 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +578 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +659 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +765 -0
  172. package/crates/team-agent/src/tmux_backend.rs +810 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +118 -112
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,310 @@
1
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2
+ use super::*;
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────
5
+ // helpers — concrete golden fixtures (Python v0.2.11 @ 439bef8)
6
+ // ─────────────────────────────────────────────────────────────────────────
7
+
8
+ /// `MessageStore.SCHEMA_VERSION == 3` (both Python & Rust db/schema.rs:13). metadata 三元之一。
9
+ const GOLDEN_SCHEMA_VERSION: i64 = 3;
10
+
11
+ fn ws() -> WorkspacePath {
12
+ WorkspacePath::new("/tmp/team-agent-coord-test")
13
+ }
14
+
15
+ fn meta(pid: u32, proto: u32, schema: i64) -> CoordinatorMetadata {
16
+ CoordinatorMetadata {
17
+ pid: Pid(pid),
18
+ protocol_version: proto,
19
+ message_store_schema_version: schema,
20
+ source: MetadataSource::Boot,
21
+ updated_at: "2026-06-02T00:00:00+00:00".to_string(),
22
+ }
23
+ }
24
+
25
+ /// `ProviderRegistry` mock — 断言**零** provider-client 调用 (MUST-NOT-13 / §84).
26
+ /// `adapter_for`/`error_lists` 各记一次调用计数;abnormal-track 只允许触碰 `error_lists`,
27
+ /// 绝不触碰 `adapter_for`(那会走真实 provider client crate)。
28
+ struct MockRegistry {
29
+ whitelist: Vec<String>,
30
+ blacklist: Vec<String>,
31
+ adapter_calls: std::cell::Cell<u32>,
32
+ error_list_calls: std::cell::Cell<u32>,
33
+ }
34
+
35
+ impl MockRegistry {
36
+ fn new(whitelist: &[&str], blacklist: &[&str]) -> Self {
37
+ Self {
38
+ whitelist: whitelist.iter().map(|s| s.to_string()).collect(),
39
+ blacklist: blacklist.iter().map(|s| s.to_string()).collect(),
40
+ adapter_calls: std::cell::Cell::new(0),
41
+ error_list_calls: std::cell::Cell::new(0),
42
+ }
43
+ }
44
+ }
45
+
46
+ impl ProviderRegistry for MockRegistry {
47
+ fn adapter_for(&self, _provider: Provider) -> Box<dyn ProviderAdapter> {
48
+ // 任何对它的调用都违反 §84 zero-injection;计数,断言保持 0。
49
+ self.adapter_calls.set(self.adapter_calls.get() + 1);
50
+ crate::provider::get_adapter(_provider)
51
+ }
52
+ fn error_lists(&self, _provider: Provider) -> ErrorLists {
53
+ self.error_list_calls.set(self.error_list_calls.get() + 1);
54
+ ErrorLists {
55
+ whitelist: self.whitelist.clone(),
56
+ blacklist: self.blacklist.clone(),
57
+ }
58
+ }
59
+ }
60
+
61
+ /// in-memory `MarkerStore` —— durable marker 落盘探针。
62
+ struct MapMarkerStore {
63
+ markers: std::collections::BTreeMap<String, Value>,
64
+ fail: bool,
65
+ }
66
+ impl MapMarkerStore {
67
+ fn ok() -> Self {
68
+ Self { markers: Default::default(), fail: false }
69
+ }
70
+ fn failing() -> Self {
71
+ Self { markers: Default::default(), fail: true }
72
+ }
73
+ }
74
+ impl MarkerStore for MapMarkerStore {
75
+ fn set_marker(&mut self, name: &str, value: Value) -> bool {
76
+ if self.fail {
77
+ return false;
78
+ }
79
+ self.markers.insert(name.to_string(), value);
80
+ true
81
+ }
82
+ }
83
+
84
+ // ─────────────────────────────────────────────────────────────────────────
85
+ // Mock `Transport` — records every method call (name) + returns canned values.
86
+ // The §84/no-tmux/no-panic tick contracts inject THIS so a Coordinator can be
87
+ // constructed in tests. `has_session` answer drives the tmux-session-missing gate.
88
+ // Recorded `calls` let a future tick-order test assert which control-plane probes
89
+ // ran. capture/query return canned text so tick's readonly detectors stay
90
+ // provider-neutral (no real subprocess, no provider client). MUST-NOT-13: this
91
+ // mock NEVER reaches a provider client crate.
92
+ // ─────────────────────────────────────────────────────────────────────────
93
+ use crate::transport::{
94
+ test_support::OfflineTransport, AttachOutcome, BackendKind, CaptureRange, CapturedText,
95
+ InjectPayload, InjectReport, Key, PaneField, PaneId, PaneInfo, PaneLiveness, SessionName,
96
+ SetEnvOutcome, SpawnResult, Target, Transport, TransportError, WindowName,
97
+ };
98
+
99
+ struct MockTransport {
100
+ inner: OfflineTransport,
101
+ calls: std::sync::Arc<std::sync::Mutex<Vec<&'static str>>>,
102
+ }
103
+
104
+ impl MockTransport {
105
+ fn new(session_present: bool) -> Self {
106
+ Self {
107
+ inner: OfflineTransport::new().with_session_present(session_present),
108
+ calls: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
109
+ }
110
+ }
111
+ fn record(&self, name: &'static str) {
112
+ self.calls.lock().unwrap().push(name);
113
+ }
114
+ fn calls(&self) -> Vec<&'static str> {
115
+ self.calls.lock().unwrap().clone()
116
+ }
117
+ }
118
+
119
+ impl Transport for MockTransport {
120
+ fn kind(&self) -> BackendKind {
121
+ self.inner.kind()
122
+ }
123
+ fn spawn_first(
124
+ &self,
125
+ s: &SessionName,
126
+ w: &WindowName,
127
+ argv: &[String],
128
+ cwd: &std::path::Path,
129
+ env: &std::collections::BTreeMap<String, String>,
130
+ ) -> Result<SpawnResult, TransportError> {
131
+ self.record("spawn_first");
132
+ self.inner.spawn_first(s, w, argv, cwd, env)
133
+ }
134
+ fn spawn_into(
135
+ &self,
136
+ s: &SessionName,
137
+ w: &WindowName,
138
+ argv: &[String],
139
+ cwd: &std::path::Path,
140
+ env: &std::collections::BTreeMap<String, String>,
141
+ ) -> Result<SpawnResult, TransportError> {
142
+ self.record("spawn_into");
143
+ self.inner.spawn_into(s, w, argv, cwd, env)
144
+ }
145
+ fn inject(
146
+ &self,
147
+ t: &Target,
148
+ p: &InjectPayload,
149
+ submit: Key,
150
+ bracketed: bool,
151
+ ) -> Result<InjectReport, TransportError> {
152
+ self.record("inject");
153
+ self.inner.inject(t, p, submit, bracketed)
154
+ }
155
+ fn send_keys(&self, t: &Target, keys: &[Key]) -> Result<(), TransportError> {
156
+ self.record("send_keys");
157
+ self.inner.send_keys(t, keys)
158
+ }
159
+ fn capture(
160
+ &self,
161
+ t: &Target,
162
+ range: CaptureRange,
163
+ ) -> Result<CapturedText, TransportError> {
164
+ self.record("capture");
165
+ self.inner.capture(t, range)
166
+ }
167
+ fn query(
168
+ &self,
169
+ t: &Target,
170
+ f: PaneField,
171
+ ) -> Result<Option<String>, TransportError> {
172
+ self.record("query");
173
+ self.inner.query(t, f)
174
+ }
175
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
176
+ self.record("liveness");
177
+ self.inner.liveness(pane)
178
+ }
179
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
180
+ self.record("list_targets");
181
+ self.inner.list_targets()
182
+ }
183
+ fn has_session(&self, s: &SessionName) -> Result<bool, TransportError> {
184
+ self.record("has_session");
185
+ self.inner.has_session(s)
186
+ }
187
+ fn list_windows(
188
+ &self,
189
+ s: &SessionName,
190
+ ) -> Result<Vec<WindowName>, TransportError> {
191
+ self.record("list_windows");
192
+ self.inner.list_windows(s)
193
+ }
194
+ fn set_session_env(
195
+ &self,
196
+ s: &SessionName,
197
+ k: &str,
198
+ v: &str,
199
+ ) -> Result<SetEnvOutcome, TransportError> {
200
+ self.record("set_session_env");
201
+ self.inner.set_session_env(s, k, v)
202
+ }
203
+ fn kill_session(&self, s: &SessionName) -> Result<(), TransportError> {
204
+ self.record("kill_session");
205
+ self.inner.kill_session(s)
206
+ }
207
+ fn kill_window(&self, t: &Target) -> Result<(), TransportError> {
208
+ self.record("kill_window");
209
+ self.inner.kill_window(t)
210
+ }
211
+ fn attach_session(
212
+ &self,
213
+ s: &SessionName,
214
+ ) -> Result<AttachOutcome, TransportError> {
215
+ self.record("attach_session");
216
+ self.inner.attach_session(s)
217
+ }
218
+ }
219
+
220
+ /// Construct a `Coordinator` over a fresh temp workspace with injected mocks.
221
+ /// Returns `(coord, transport_calls_handle)` so tick tests can assert control-plane
222
+ /// call order and that `inject` (an exploratory prompt) never fired (§84).
223
+ /// `session_present` drives the tmux-session-missing gate; `save_hook` injects a
224
+ /// forced save failure (bug-084); `recorder` captures tick side-effect ORDER.
225
+ fn coord_for_test(
226
+ session_present: bool,
227
+ save_hook: Option<SaveHook>,
228
+ recorder: Option<OrderRecorder>,
229
+ ) -> (Coordinator, std::sync::Arc<std::sync::Mutex<Vec<&'static str>>>) {
230
+ let dir = std::env::temp_dir().join(format!(
231
+ "team-agent-coord-tick-{}-{}",
232
+ std::process::id(),
233
+ std::time::SystemTime::now()
234
+ .duration_since(std::time::UNIX_EPOCH)
235
+ .unwrap()
236
+ .as_nanos()
237
+ ));
238
+ std::fs::create_dir_all(&dir).unwrap();
239
+ let ws = WorkspacePath::new(dir);
240
+ let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
241
+ let transport = MockTransport::new(session_present);
242
+ let calls = std::sync::Arc::clone(&transport.calls);
243
+ let coord = Coordinator::for_test(
244
+ ws,
245
+ reg,
246
+ Box::new(transport),
247
+ save_hook,
248
+ recorder,
249
+ );
250
+ (coord, calls)
251
+ }
252
+
253
+ /// Like [`coord_for_test`] but seeds a TRUTHY `session_name` into the workspace
254
+ /// state.json first, so the tmux-session gate actually runs. The session-missing
255
+ /// STOP path requires a truthy session_name (Python lifecycle.py:276
256
+ /// `if session_name and not _tmux_session_exists(...)`); a null/empty name skips
257
+ /// the gate entirely (see `p2_tick_skips_tmux_gate_when_session_name_absent`).
258
+ fn coord_for_test_with_session(
259
+ session_present: bool,
260
+ session_name: &str,
261
+ ) -> (Coordinator, std::sync::Arc<std::sync::Mutex<Vec<&'static str>>>) {
262
+ let dir = std::env::temp_dir().join(format!(
263
+ "team-agent-coord-tick-sess-{}-{}",
264
+ std::process::id(),
265
+ std::time::SystemTime::now()
266
+ .duration_since(std::time::UNIX_EPOCH)
267
+ .unwrap()
268
+ .as_nanos()
269
+ ));
270
+ std::fs::create_dir_all(&dir).unwrap();
271
+ crate::state::persist::save_runtime_state(
272
+ &dir,
273
+ &serde_json::json!({ "session_name": session_name }),
274
+ )
275
+ .unwrap();
276
+ let ws = WorkspacePath::new(dir);
277
+ let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
278
+ let transport = MockTransport::new(session_present);
279
+ let calls = std::sync::Arc::clone(&transport.calls);
280
+ let coord = Coordinator::for_test(ws, reg, Box::new(transport), None, None);
281
+ (coord, calls)
282
+ }
283
+
284
+ /// A save hook that always fails (bug-084 forced persistence failure).
285
+ fn failing_save_hook() -> SaveHook {
286
+ Box::new(|_ws, _state| {
287
+ Err(crate::state::StateError::SaveFailed(
288
+ "injected tick-end save failure".to_string(),
289
+ ))
290
+ })
291
+ }
292
+
293
+ fn read_event_log_dir(dir: &std::path::Path) -> Vec<serde_json::Value> {
294
+ let path = crate::model::paths::logs_dir(dir).join("events.jsonl");
295
+ match std::fs::read_to_string(&path) {
296
+ Ok(text) => text.lines().filter_map(|l| serde_json::from_str(l).ok()).collect(),
297
+ Err(_) => Vec::new(),
298
+ }
299
+ }
300
+
301
+
302
+ mod basics;
303
+ mod abnormal;
304
+ mod watch;
305
+ mod tick_core;
306
+ mod spine;
307
+ mod health_sync;
308
+ mod takeover;
309
+ mod daemon;
310
+ mod main_preserved;
@@ -0,0 +1,261 @@
1
+ use super::*;
2
+
3
+ // ═════════════════════════════════════════════════════════════════════════
4
+ // SPINE — Coordinator::tick() must drive REAL cross-subsystem side-effects
5
+ // (orchestration port, sub-phase 1, P0). Golden: coordinator/lifecycle.py:250-385
6
+ // (deliver_pending → fire_scheduled → … → save_runtime_state LAST). Today the 14
7
+ // obligation steps are bare `record_step` probes (tick.rs:171) and base_tick_report
8
+ // (tick.rs:345) fabricates empty delivered/scheduled/stuck/results vecs, so these
9
+ // integration tests — wiring REAL state.json + team.db, only the OS edge mocked —
10
+ // FAIL (RED) and pass once the porter wires tick to the real messaging fns.
11
+ // ═════════════════════════════════════════════════════════════════════════
12
+
13
+ /// Non-panicking transport: mirrors `MockTransport` but `inject` RECORDS and returns Ok
14
+ /// (a real `deliver_pending` obligation legitimately injects; the §84-guard `MockTransport`
15
+ /// panics on inject, which is correct for the no-obligation tick but not for a delivery test).
16
+ pub(super) struct DeliveringTransport {
17
+ inner: MockTransport,
18
+ }
19
+ impl DeliveringTransport {
20
+ pub(super) fn new() -> Self {
21
+ Self { inner: MockTransport::new(true) }
22
+ }
23
+ }
24
+ impl Transport for DeliveringTransport {
25
+ fn kind(&self) -> BackendKind {
26
+ self.inner.kind()
27
+ }
28
+ fn spawn_first(
29
+ &self,
30
+ s: &SessionName,
31
+ w: &WindowName,
32
+ a: &[String],
33
+ c: &std::path::Path,
34
+ e: &std::collections::BTreeMap<String, String>,
35
+ ) -> Result<SpawnResult, TransportError> {
36
+ self.inner.spawn_first(s, w, a, c, e)
37
+ }
38
+ fn spawn_into(
39
+ &self,
40
+ s: &SessionName,
41
+ w: &WindowName,
42
+ a: &[String],
43
+ c: &std::path::Path,
44
+ e: &std::collections::BTreeMap<String, String>,
45
+ ) -> Result<SpawnResult, TransportError> {
46
+ self.inner.spawn_into(s, w, a, c, e)
47
+ }
48
+ fn inject(
49
+ &self,
50
+ _t: &Target,
51
+ _p: &InjectPayload,
52
+ _submit: Key,
53
+ _bracketed: bool,
54
+ ) -> Result<InjectReport, TransportError> {
55
+ self.inner.record("inject");
56
+ Ok(InjectReport {
57
+ stage_reached: crate::transport::InjectStage::Submit,
58
+ inject_verification: crate::transport::InjectVerification::CaptureContainsToken,
59
+ submit_verification: crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck,
60
+ turn_verification: crate::transport::TurnVerification::NotYetObserved,
61
+ attempts: 1,
62
+ })
63
+ }
64
+ fn send_keys(&self, t: &Target, k: &[Key]) -> Result<(), TransportError> {
65
+ self.inner.send_keys(t, k)
66
+ }
67
+ fn capture(&self, t: &Target, r: CaptureRange) -> Result<CapturedText, TransportError> {
68
+ self.inner.capture(t, r)
69
+ }
70
+ fn query(&self, t: &Target, f: PaneField) -> Result<Option<String>, TransportError> {
71
+ self.inner.query(t, f)
72
+ }
73
+ fn liveness(&self, p: &PaneId) -> Result<PaneLiveness, TransportError> {
74
+ self.inner.liveness(p)
75
+ }
76
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
77
+ self.inner.list_targets()
78
+ }
79
+ fn has_session(&self, s: &SessionName) -> Result<bool, TransportError> {
80
+ self.inner.has_session(s)
81
+ }
82
+ fn list_windows(&self, s: &SessionName) -> Result<Vec<WindowName>, TransportError> {
83
+ self.inner.list_windows(s)
84
+ }
85
+ fn set_session_env(&self, s: &SessionName, k: &str, v: &str) -> Result<SetEnvOutcome, TransportError> {
86
+ self.inner.set_session_env(s, k, v)
87
+ }
88
+ fn kill_session(&self, s: &SessionName) -> Result<(), TransportError> {
89
+ self.inner.kill_session(s)
90
+ }
91
+ fn kill_window(&self, t: &Target) -> Result<(), TransportError> {
92
+ self.inner.kill_window(t)
93
+ }
94
+ fn attach_session(&self, s: &SessionName) -> Result<AttachOutcome, TransportError> {
95
+ self.inner.attach_session(s)
96
+ }
97
+ }
98
+
99
+ /// Build a `Coordinator` over a freshly-seeded REAL workspace: state.json carries a truthy
100
+ /// `session_name` (so the gate runs) + a worker agents map. Returns the workspace dir so the
101
+ /// test can seed/inspect the REAL team.db. `transport` is the injected OS edge; `save_hook`
102
+ /// injects the bug-084 forced save failure.
103
+ fn seeded_spine_coord(
104
+ transport: Box<dyn Transport>,
105
+ save_hook: Option<SaveHook>,
106
+ ) -> (Coordinator, std::path::PathBuf) {
107
+ let dir = std::env::temp_dir().join(format!(
108
+ "team-agent-coord-spine-{}-{}",
109
+ std::process::id(),
110
+ std::time::SystemTime::now()
111
+ .duration_since(std::time::UNIX_EPOCH)
112
+ .unwrap()
113
+ .as_nanos()
114
+ ));
115
+ std::fs::create_dir_all(&dir).unwrap();
116
+ crate::state::persist::save_runtime_state(
117
+ &dir,
118
+ &serde_json::json!({
119
+ "session_name": "team-spine",
120
+ "agents": { "w1": { "provider": "codex" } },
121
+ }),
122
+ )
123
+ .unwrap();
124
+ let ws = WorkspacePath::new(dir.clone());
125
+ let reg: Box<dyn ProviderRegistry> = Box::new(MockRegistry::new(&[], &[]));
126
+ let coord = Coordinator::for_test(ws, reg, transport, save_hook, None);
127
+ (coord, dir)
128
+ }
129
+
130
+ /// Insert ONE due `health_ping` scheduled_event (status pending, far-past due) into the REAL
131
+ /// team.db. `fire_due_scheduled_events` fully handles `health_ping` (logs, no transport inject)
132
+ /// and marks the row `done`. Returns the autoincrement id.
133
+ fn seed_due_health_ping(dir: &std::path::Path) -> i64 {
134
+ let store = MessageStore::open(dir).unwrap();
135
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
136
+ conn.execute(
137
+ "insert into scheduled_events(owner_team_id, due_at, target, kind, payload_json, status, created_at) \
138
+ values (null, '2000-01-01T00:00:00+00:00', 'w1', 'health_ping', '{}', 'pending', '2000-01-01T00:00:00+00:00')",
139
+ [],
140
+ )
141
+ .unwrap();
142
+ conn.last_insert_rowid()
143
+ }
144
+
145
+ fn scheduled_status(dir: &std::path::Path, id: i64) -> String {
146
+ let store = MessageStore::open(dir).unwrap();
147
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
148
+ conn.query_row("select status from scheduled_events where id = ?1", [id], |r| r.get::<_, String>(0))
149
+ .unwrap()
150
+ }
151
+
152
+ pub(super) fn message_status(dir: &std::path::Path, message_id: &str) -> String {
153
+ let store = MessageStore::open(dir).unwrap();
154
+ let conn = crate::db::schema::open_db(store.db_path()).unwrap();
155
+ conn.query_row(
156
+ "select status from messages where message_id = ?1",
157
+ [message_id],
158
+ |r| r.get::<_, String>(0),
159
+ )
160
+ .unwrap()
161
+ }
162
+
163
+ // P0 — tick must FIRE a due scheduled event against the REAL team.db (fire_due_scheduled_events,
164
+ // lifecycle.py:286). Observable: report.scheduled carries the id AND the db row is marked 'done'.
165
+ #[test]
166
+ fn spine_tick_fires_due_scheduled_event_and_marks_db_done() {
167
+ let (coord, dir) = seeded_spine_coord(Box::new(MockTransport::new(true)), None);
168
+ let id = seed_due_health_ping(&dir);
169
+ assert_eq!(scheduled_status(&dir, id), "pending", "precondition: the seeded row starts pending");
170
+
171
+ let report = coord.tick().expect("tick returns a typed report");
172
+
173
+ assert!(
174
+ report.scheduled.iter().any(|e| e.id == id),
175
+ "tick must report the fired scheduled event id {id} (today base_tick_report fabricates []); got {:?}",
176
+ report.scheduled
177
+ );
178
+ assert_eq!(
179
+ scheduled_status(&dir, id),
180
+ "done",
181
+ "fire_due_scheduled_events must mark the scheduled_events row 'done' in the REAL team.db"
182
+ );
183
+ }
184
+
185
+ // P1 — bug-084 save-LAST ordering with a REAL side-effect: a failing save_hook must NOT undo the
186
+ // scheduled fire. The db mutation commits BEFORE atomic_save (lifecycle.py:286 then :346), and the
187
+ // degraded report still carries `scheduled` (lifecycle.py:356). Proves save is genuinely the LAST
188
+ // mutation — AFTER the real obligation side-effects, not after no-op probes.
189
+ #[test]
190
+ fn spine_tick_save_failure_still_persists_real_scheduled_mutation() {
191
+ let (coord, dir) = seeded_spine_coord(Box::new(MockTransport::new(true)), Some(failing_save_hook()));
192
+ let id = seed_due_health_ping(&dir);
193
+
194
+ let report = coord.tick().expect("a degraded tick is Ok(TickReport), not Err");
195
+
196
+ // degraded report shape (bug-084: save failure → ok=false / persisted=false, NOT Err).
197
+ assert!(!report.ok, "save failure → ok=false");
198
+ assert_eq!(report.persisted, Some(false), "save failure → persisted=Some(false)");
199
+ assert_eq!(report.reason, Some(TickStopReason::PersistenceDegraded));
200
+ // the REAL db side-effect happened BEFORE the (failed) save.
201
+ assert_eq!(
202
+ scheduled_status(&dir, id),
203
+ "done",
204
+ "the scheduled fire must commit to the db BEFORE save — save is the LAST mutation (bug-084)"
205
+ );
206
+ // and the degraded report still carries the fired event.
207
+ assert!(
208
+ report.scheduled.iter().any(|e| e.id == id),
209
+ "the degraded report must still carry the fired scheduled event (lifecycle.py:356)"
210
+ );
211
+ }
212
+
213
+ // P0 — tick must DELIVER a pending message against the REAL team.db (deliver_pending_messages,
214
+ // lifecycle.py:285). Observable: report.delivered carries the id OR the message row advances past
215
+ // its created 'accepted' state (claimed/delivered). NOTE: this also needs deliver_pending_messages
216
+ // (messaging/delivery.rs:126, currently a stub returning []) to be implemented — it is RED both
217
+ // because tick does not call it AND because the fn is a stub; it greens when both land.
218
+ #[test]
219
+ fn spine_tick_delivers_pending_message_drives_real_db_or_report() {
220
+ let (coord, dir) = seeded_spine_coord(Box::new(DeliveringTransport::new()), None);
221
+ let store = MessageStore::open(&dir).unwrap();
222
+ let mid = store
223
+ .create_message(Some("task-1"), "leader", "w1", "do the thing", None, true, None)
224
+ .unwrap();
225
+ drop(store);
226
+ assert_eq!(message_status(&dir, &mid), "accepted", "precondition: a fresh message is 'accepted'");
227
+
228
+ let report = coord.tick().expect("tick returns a typed report");
229
+
230
+ let delivered_reported = report.delivered.iter().any(|d| d.message_id == mid);
231
+ let db_advanced = message_status(&dir, &mid) != "accepted";
232
+ assert!(
233
+ delivered_reported || db_advanced,
234
+ "tick must deliver the pending message: report.delivered should carry {mid} OR its db status \
235
+ must advance past 'accepted' (claimed/delivered). report.delivered={:?} db_status={}",
236
+ report.delivered,
237
+ message_status(&dir, &mid)
238
+ );
239
+ }
240
+
241
+ // ═════════════════════════════════════════════════════════════════════════
242
+ // SPINE-WIRING (③ review→fix) RED — tick tmux-session-missing gate observability.
243
+ // Golden lifecycle.py:277-279: emit a `coordinator.session_missing` event (session=name)
244
+ // BEFORE the stop report. /tmp/spine_divergences.md #5.
245
+ // ═════════════════════════════════════════════════════════════════════════
246
+
247
+ #[test]
248
+ fn spine_tick_session_missing_emits_event() {
249
+ // seeded_spine_coord seeds a truthy session_name ("team-spine"); MockTransport(false) makes the
250
+ // tmux gate fire → the tick stops. The gate must ALSO emit coordinator.session_missing.
251
+ let (coord, dir) = seeded_spine_coord(Box::new(MockTransport::new(false)), None);
252
+ let report = coord.tick().expect("tick returns a typed report");
253
+ assert!(report.stop, "precondition: a missing session stops the tick");
254
+ let events = read_event_log_dir(&dir);
255
+ assert!(
256
+ events
257
+ .iter()
258
+ .any(|e| e.get("event").and_then(|v| v.as_str()) == Some("coordinator.session_missing")),
259
+ "the tmux-missing gate must emit a coordinator.session_missing event before the stop report; got {events:?}"
260
+ );
261
+ }