@team-agent/installer 0.2.11 → 0.3.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 (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 +1077 -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 +1141 -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 +436 -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 +1063 -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 +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -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 +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -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 +253 -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 +487 -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 +1833 -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 +933 -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 +685 -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 +159 -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 +388 -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 +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -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 +537 -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 +582 -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 +656 -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 +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -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 +90 -106
  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,271 @@
1
+ use super::*;
2
+
3
+ // =====================================================================
4
+ // 8. idle-takeover 接线 — build_idle_nodes / leader_node(unimplemented → RED)
5
+ // 命门:rollout_path=None → Unknown → never idle;leader path/provider 缺 → 省略;
6
+ // MUST-NOT-13:经 TurnStateClassifier mock,断言零 provider client 直连。
7
+ // =====================================================================
8
+
9
+ // build_idle_nodes:一个 worker 有 rollout_path(可读)→ classifier.classify 被调一次;
10
+ // 经注入分类器(零 provider client)。stopped/paused 跳过。
11
+ #[test]
12
+ fn build_idle_nodes_uses_injected_classifier_no_provider_client() {
13
+ // 真实 session 文件,使 _read_session_tail 读到非空 → classifier 返回注入 state。
14
+ let dir = std::env::temp_dir().join(format!("ta_rs_idle_{}", std::process::id()));
15
+ std::fs::create_dir_all(&dir).unwrap();
16
+ let log = dir.join("w1.jsonl");
17
+ std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
18
+ let state = serde_json::json!({
19
+ "agents": {
20
+ "w1": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "running"},
21
+ "w_stopped": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "stopped"},
22
+ }
23
+ });
24
+ let clf = CountingClassifier::new(TurnState::Idle);
25
+ let nodes = build_idle_nodes(&state, &clf).unwrap();
26
+ // stopped 被跳过(__init__ wiring:29)→ 仅 w1。
27
+ assert_eq!(nodes.len(), 1);
28
+ assert_eq!(nodes[0].node_id, "w1");
29
+ assert_eq!(nodes[0].role, NodeRole::Worker);
30
+ assert_eq!(nodes[0].state, TurnState::Idle);
31
+ // MUST-NOT-13:分类只经注入 classifier(此 mock 计数==1),零 provider client 直连。
32
+ assert_eq!(clf.calls.get(), 1, "每个 live node 恰调一次注入 classify");
33
+ }
34
+
35
+ // bug-085:rollout_path=None → 读到空串 → Unknown(不当 idle)。
36
+ #[test]
37
+ fn build_idle_nodes_none_rollout_path_yields_unknown_not_idle() {
38
+ let state = serde_json::json!({
39
+ "agents": {
40
+ "w1": {"provider": "codex", "status": "running"} // 无 rollout_path。
41
+ }
42
+ });
43
+ // 即使 mock 默认想返 Idle,空 session-log → classifier 返 Unknown(见 mock 逻辑)。
44
+ let clf = CountingClassifier::new(TurnState::Idle);
45
+ let nodes = build_idle_nodes(&state, &clf).unwrap();
46
+ assert_eq!(nodes.len(), 1);
47
+ assert_eq!(nodes[0].state, TurnState::Unknown, "None rollout_path → Unknown,绝不 idle");
48
+ assert!(
49
+ !nodes[0].state.is_idle_for_takeover(),
50
+ "unknown ≠ idle:不得对 takeover 放行"
51
+ );
52
+ }
53
+
54
+ // _leader_node:leader path 或 provider 缺 → None(省略而非猜 idle)。
55
+ #[test]
56
+ fn leader_node_omitted_when_path_or_provider_missing() {
57
+ let clf = CountingClassifier::new(TurnState::Idle);
58
+ // 既无 leader.rollout_path 也无 receiver.rollout_path → None。
59
+ let state_no_path = serde_json::json!({
60
+ "leader": {"id": "leader", "provider": "codex"},
61
+ "leader_receiver": {"provider": "codex"}
62
+ });
63
+ assert!(leader_node(&state_no_path, &clf).unwrap().is_none(), "缺 path → 省略 leader 节点");
64
+ // 有 path 但无 provider → None。
65
+ let dir = std::env::temp_dir().join(format!("ta_rs_lnode_{}", std::process::id()));
66
+ std::fs::create_dir_all(&dir).unwrap();
67
+ let log = dir.join("leader.jsonl");
68
+ std::fs::write(&log, b"{\"type\":\"turn_open\"}\n").unwrap();
69
+ let state_no_provider = serde_json::json!({
70
+ "leader": {"id": "leader", "rollout_path": log.to_string_lossy()}
71
+ });
72
+ assert!(
73
+ leader_node(&state_no_provider, &clf).unwrap().is_none(),
74
+ "缺 provider → 省略 leader 节点(不猜 idle)"
75
+ );
76
+ }
77
+
78
+ // _leader_node:path+provider 齐 → 经 classifier 产 role=leader 节点(C13)。
79
+ #[test]
80
+ fn leader_node_classified_via_injected_classifier() {
81
+ let dir = std::env::temp_dir().join(format!("ta_rs_lnode2_{}", std::process::id()));
82
+ std::fs::create_dir_all(&dir).unwrap();
83
+ let log = dir.join("leader.jsonl");
84
+ std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
85
+ let state = serde_json::json!({
86
+ "leader": {"id": "leader", "provider": "codex", "rollout_path": log.to_string_lossy()}
87
+ });
88
+ let clf = CountingClassifier::new(TurnState::Working);
89
+ let node = leader_node(&state, &clf).unwrap().expect("path+provider 齐 → 有 leader 节点");
90
+ assert_eq!(node.role, NodeRole::Leader);
91
+ assert_eq!(node.node_id, "leader");
92
+ assert_eq!(node.state, TurnState::Working);
93
+ assert_eq!(clf.calls.get(), 1, "leader 分类经注入 classifier 一次,零 provider client");
94
+ }
95
+
96
+ // =====================================================================
97
+ // 9. classify_provider_turn_state 门面(unimplemented → RED)
98
+ // unknown/abnormal 且有 event_sink → 写 idle_takeover.classify。
99
+ // =====================================================================
100
+
101
+ // 门面经注入 classifier 分类;空文本 → Unknown(本 mock),验证返回 TurnClassification。
102
+ #[test]
103
+ fn classify_provider_turn_state_returns_classification_via_injected_classifier() {
104
+ let clf = CountingClassifier::new(TurnState::Idle);
105
+ let c = classify_provider_turn_state(Provider::Codex, "{\"type\":\"turn_complete\"}", &clf, None).unwrap();
106
+ assert_eq!(c.state, TurnState::Idle);
107
+ assert_eq!(clf.calls.get(), 1);
108
+ // 空文本 → Unknown(unknown ≠ idle 命门下游)。
109
+ let clf2 = CountingClassifier::new(TurnState::Idle);
110
+ let c2 = classify_provider_turn_state(Provider::Codex, "", &clf2, None).unwrap();
111
+ assert_eq!(c2.state, TurnState::Unknown);
112
+ assert!(!c2.state.is_idle_for_takeover());
113
+ }
114
+
115
+ // event_sink + unknown/abnormal → 写 idle_takeover.classify(事件名字节锁)。
116
+ #[test]
117
+ fn classify_event_name_is_idle_takeover_classify() {
118
+ assert_eq!(LeaderEvent::IdleTakeoverClassify.name(), "idle_takeover.classify");
119
+ }
120
+
121
+ // =====================================================================
122
+ // 10. push_idle_reminder(unimplemented → RED):!should_ping → no-op。
123
+ // =====================================================================
124
+
125
+ #[test]
126
+ fn push_idle_reminder_noop_when_should_not_ping() {
127
+ let ws = std::env::temp_dir().join(format!("ta_rs_push_{}", std::process::id()));
128
+ std::fs::create_dir_all(&ws).unwrap();
129
+ let event_log = crate::event_log::EventLog::new(&ws);
130
+ let state = serde_json::json!({"leader": {"id": "leader"}});
131
+ let result = TakeoverReminderResult {
132
+ should_ping: false,
133
+ message: None,
134
+ interrupted_nodes: vec![],
135
+ reason: Some("not_armed_no_worker_turn".into()),
136
+ };
137
+ // should_ping=false → no-op,返回 Ok(())。现 unimplemented → RED。
138
+ push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
139
+ // 强化:no-op 必须真的什么都不做 —— 不写 idle_takeover.reminder 事件(EventLog 无该事件)。
140
+ let events = event_log.tail(50).unwrap();
141
+ assert!(
142
+ !events.iter().any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
143
+ "should_ping=false 时绝不写 reminder 事件"
144
+ );
145
+ }
146
+
147
+ // #236 nag_removal (N35) — push_idle_reminder is now a no-op shim.
148
+ // [OLD] assertion: should_ping=true → writes idle_takeover.reminder event (with
149
+ // interrupted/reason byte-locked golden payload).
150
+ // [NEW] assertion: even when should_ping=true, push_idle_reminder writes NO event
151
+ // and emits NO leader-bound message; ownership/handover happens only via explicit
152
+ // `claim-leader` / `takeover` commands. The function signature is preserved so
153
+ // existing callers (coordinator/tick.rs, lifecycle wiring) still resolve.
154
+ #[test]
155
+ fn push_idle_reminder_is_silent_no_op_under_n35_even_when_should_ping_true() {
156
+ let ws = std::env::temp_dir().join(format!("ta_rs_push2_{}", std::process::id()));
157
+ std::fs::create_dir_all(&ws).unwrap();
158
+ let event_log = crate::event_log::EventLog::new(&ws);
159
+ let state = serde_json::json!({"leader": {"id": "leader"}});
160
+ let result = TakeoverReminderResult {
161
+ should_ping: true,
162
+ message: Some("neutral reminder body".into()),
163
+ interrupted_nodes: vec!["w1".into()],
164
+ reason: Some("armed_all_idle".into()),
165
+ };
166
+ push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
167
+ let events = event_log.tail(50).unwrap();
168
+ assert!(
169
+ !events.iter().any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
170
+ "#236 N35: push_idle_reminder must no longer emit the reminder nag event; got {events:?}"
171
+ );
172
+ }
173
+
174
+ // idle_takeover.reminder / push_failed 事件名字节锁。
175
+ #[test]
176
+ fn idle_takeover_event_names_byte_locked() {
177
+ assert_eq!(LeaderEvent::IdleTakeoverReminder.name(), "idle_takeover.reminder");
178
+ assert_eq!(LeaderEvent::IdleTakeoverPushFailed.name(), "idle_takeover.push_failed");
179
+ assert_eq!(LeaderEvent::IdleTakeoverPing.name(), "idle_takeover.ping");
180
+ }
181
+
182
+ // =====================================================================
183
+ // 11. struct 构造 / 序列化形态 + key 插入序证据(纯数据,不依赖 body)
184
+ // =====================================================================
185
+
186
+ // LeaderReceiver:所有可选字段 Option(bug-085 半状态合法);序列化保字段名。
187
+ #[test]
188
+ fn leader_receiver_struct_serializes_with_python_field_names() {
189
+ let recv = LeaderReceiver {
190
+ mode: ReceiverMode::DirectTmux,
191
+ status: ReceiverStatus::Attached,
192
+ provider: Provider::ClaudeCode,
193
+ pane_id: PaneId::new("%648"),
194
+ session_name: Some(SessionName::new("S")),
195
+ window_index: Some("1".into()),
196
+ window_name: Some(WindowName::new("W")),
197
+ pane_index: Some("2".into()),
198
+ pane_tty: Some("/dev/ttys001".into()),
199
+ pane_current_command: Some("claude".into()),
200
+ fingerprint: Some("fp".into()),
201
+ leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
202
+ owner_epoch: Some(OwnerEpoch(3)),
203
+ attached_at: Some("2026-06-02T00:00:00+00:00".into()),
204
+ discovery: Some(Discovery::ClaimLeader),
205
+ requested_provider: None,
206
+ warning: None,
207
+ };
208
+ let v = serde_json::to_value(&recv).unwrap();
209
+ assert_eq!(v["mode"], serde_json::json!("direct_tmux"));
210
+ assert_eq!(v["status"], serde_json::json!("attached"));
211
+ assert_eq!(v["provider"], serde_json::json!("claude_code"));
212
+ assert_eq!(v["pane_id"], serde_json::json!("%648"));
213
+ assert_eq!(v["owner_epoch"], serde_json::json!(3));
214
+ assert_eq!(v["discovery"], serde_json::json!("claim_leader"));
215
+ // bug-085:None 字段序列化为 null(半状态合法,不崩)。
216
+ assert_eq!(v["requested_provider"], serde_json::Value::Null);
217
+ assert_eq!(v["warning"], serde_json::Value::Null);
218
+ }
219
+
220
+ // TeamOwner:claimed_via kebab + owner_epoch int;os_user Option(Family A 才写)。
221
+ #[test]
222
+ fn team_owner_struct_serializes_with_python_shape() {
223
+ let owner = TeamOwner {
224
+ pane_id: PaneId::new("%9"),
225
+ provider: Provider::Codex,
226
+ machine_fingerprint: "fp".into(),
227
+ leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
228
+ owner_epoch: OwnerEpoch(1),
229
+ claimed_at: "2026-06-02T00:00:00+00:00".into(),
230
+ claimed_via: ClaimedVia::ClaimLeader,
231
+ os_user: Some("alice".into()),
232
+ };
233
+ let v = serde_json::to_value(&owner).unwrap();
234
+ assert_eq!(v["claimed_via"], serde_json::json!("claim-leader"));
235
+ assert_eq!(v["owner_epoch"], serde_json::json!(1));
236
+ assert_eq!(v["provider"], serde_json::json!("codex"));
237
+ assert_eq!(v["os_user"], serde_json::json!("alice"));
238
+ }
239
+
240
+ // LeaderIdentity:source 用 leader-plan 枚举值(Override→"override");team_id 透明串。
241
+ #[test]
242
+ fn leader_identity_struct_serializes_with_leader_plan_source() {
243
+ let id = LeaderIdentity {
244
+ leader_session_uuid: uuid("fp", "/ws", "u", "default"),
245
+ leader_session_uuid_source: LeaderSessionUuidSource::Override,
246
+ machine_fingerprint: "fp".into(),
247
+ workspace_abspath: std::path::PathBuf::from("/ws"),
248
+ os_user: "u".into(),
249
+ team_id: TeamKey::new("default"),
250
+ };
251
+ let v = serde_json::to_value(&id).unwrap();
252
+ assert_eq!(v["leader_session_uuid_source"], serde_json::json!("override"));
253
+ assert_eq!(v["team_id"], serde_json::json!("default"));
254
+ }
255
+
256
+ // IdleNode:bug-085 rollout_path Option;state 是 TurnState(穷尽,Unknown 非 idle)。
257
+ #[test]
258
+ fn idle_node_unknown_state_is_not_idle() {
259
+ let n = IdleNode {
260
+ node_id: "w1".into(),
261
+ role: NodeRole::Worker,
262
+ state: TurnState::Unknown,
263
+ turn_id: None,
264
+ annotations: vec![],
265
+ provider: Some(Provider::Codex),
266
+ auth_mode: None,
267
+ rollout_path: None, // bug-085:None 合法 → 该 node Unknown。
268
+ };
269
+ assert!(!n.state.is_idle_for_takeover(), "Unknown 不当 idle");
270
+ assert!(n.rollout_path.is_none());
271
+ }
@@ -0,0 +1,225 @@
1
+ use super::*;
2
+
3
+ // =====================================================================
4
+ // 7. 五条 lease 路径签名 + 返回 LeaseResult 形态(unimplemented → RED)
5
+ // =====================================================================
6
+
7
+ // attach_leader:手动 CLI attach(__init__.py:19-58 → attach_leader_to_state:276 →
8
+ // _resolve_leader_pane)。在无 live tmux 的测试环境,指定一个不存在的 pane %1 →
9
+ // _resolve_leader_pane raise RuntimeError("tmux pane not found: %1")(_legacy_pane_discovery.py:153),
10
+ // 映射到 LeaderError::Validation。golden(probe_attach.py 已验:真跑即 raise)。
11
+ // 强化:钉具体的 Err 形态 + 错误串含 pane id;并断言失败时绝不留下半绑定 state(无 team_owner)。
12
+ // unimplemented → RED(unimplemented panic ≠ 期望的 Validation,且后续 is_err 断言不会被求值)。
13
+ #[test]
14
+ fn attach_leader_errors_when_pane_not_resolvable() {
15
+ let ws = std::env::temp_dir().join(format!("ta_rs_attach_{}", std::process::id()));
16
+ std::fs::create_dir_all(&ws).unwrap();
17
+ let r = attach_leader(&ws, Some(&PaneId::new("%1")), Provider::Codex);
18
+ // 不可解析 pane → Err(Validation),错误串提及 pane not found。
19
+ match r {
20
+ Err(LeaderError::Validation(msg)) => {
21
+ assert!(msg.contains("%1"), "Validation 错误须含目标 pane id,got {msg}");
22
+ assert!(msg.contains("not found"), "须是 pane-not-found 形态,got {msg}");
23
+ }
24
+ Err(other) => panic!("期望 Validation(pane not found),got {other:?}"),
25
+ Ok(v) => panic!("无 live tmux pane 时不该成功 attach,got {v:?}"),
26
+ }
27
+ // 失败不留半绑定:state.json 无 team_owner。
28
+ let st = crate::state::persist::load_runtime_state(&ws).unwrap();
29
+ assert!(st.get("team_owner").is_none(), "resolve 失败不得落 team_owner");
30
+ }
31
+
32
+ // attach_leader 成功 post-state(需 live tmux pane + cross-lane _resolve_leader_pane):
33
+ // vacant acquire → status=Claimed、reason=vacant_acquired、owner_epoch 0→1、owner/receiver 绑同 pane、
34
+ // 且 workspace state.json 真被持久化。golden(_claim_lease_no_incident:81/102/139-143)。
35
+ // real-machine-gated(无 live tmux 无法驱动 pane resolver)。
36
+ #[test]
37
+ #[ignore = "needs a live tmux pane + cross-lane _resolve_leader_pane (step 9/11)"]
38
+ fn attach_leader_binds_pane_advances_epoch_and_persists() {
39
+ let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
40
+ std::fs::create_dir_all(&ws).unwrap();
41
+ let pane = PaneId::new("%1");
42
+ let r = attach_leader(&ws, Some(&pane), Provider::Codex).unwrap();
43
+ assert!(r.ok);
44
+ assert_eq!(r.status, LeaseStatus::Claimed);
45
+ let owner = r.owner.as_ref().expect("attach 成功必带 owner");
46
+ assert_eq!(owner.pane_id, pane);
47
+ assert_eq!(owner.owner_epoch, OwnerEpoch(1), "vacant acquire 后 epoch=1");
48
+ assert_eq!(owner.provider, Provider::Codex);
49
+ let receiver = r.receiver.as_ref().expect("attach 成功必带 receiver");
50
+ assert_eq!(receiver.pane_id, pane);
51
+ assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
52
+ let persisted = crate::state::persist::load_runtime_state(&ws).unwrap();
53
+ assert_eq!(persisted["team_owner"]["pane_id"], serde_json::json!("%1"));
54
+ assert_eq!(persisted["team_owner"]["owner_epoch"], serde_json::json!(1));
55
+ }
56
+
57
+ // autobind:$TMUX_PANE 缺 → Ok(None)(__init__.py:885-887,锁前直接返回,不开锁)。
58
+ // 强化:断言这是 lock-not-acquired 早退 —— 不写 state.json(无 receiver/owner 落盘),
59
+ // 不发任何 leader_receiver.* 事件(锁前 return,连 EventLog 都不构造)。
60
+ #[test]
61
+ fn autobind_returns_none_when_tmux_pane_missing() {
62
+ if std::env::var_os("TMUX_PANE").is_some() {
63
+ return; // 在 tmux 内:走绑定路径,本用例只验缺失分支。
64
+ }
65
+ let ws = std::env::temp_dir().join(format!("ta_rs_auto_{}", std::process::id()));
66
+ std::fs::create_dir_all(&ws).unwrap();
67
+ let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
68
+ assert!(r.is_none(), "$TMUX_PANE 缺 → autobind 返回 Ok(None)");
69
+ // lock-not-acquired 早退:state.json 未被写(load 兜底成空 state,无 team_owner/receiver)。
70
+ let st = crate::state::persist::load_runtime_state(&ws).unwrap();
71
+ assert!(st.get("leader_receiver").is_none(), "skip 路径不应落 receiver");
72
+ assert!(st.get("team_owner").is_none(), "skip 路径不应落 owner");
73
+ // 早退发生在 EventLog 构造前 → 无任何事件文件。
74
+ let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
75
+ assert!(events.is_empty(), "skip 路径绝不写审计事件");
76
+ }
77
+
78
+ // autobind 成功支:$TMUX_PANE 命中 → 锁内 attach_leader_to_state → Ok(Some(receiver))。
79
+ // receiver.pane_id == 注入 pane,discovery==EnvPane($TMUX_PANE 直接命中)。
80
+ // env 变更进程全局且与并行 test race,且依赖跨 lane 的 live pane resolver,故 real-machine-gated。
81
+ #[test]
82
+ #[ignore = "needs live $TMUX_PANE + cross-lane _resolve_leader_pane; env mutation races parallel tests"]
83
+ fn autobind_binds_env_pane_on_success() {
84
+ let ws = std::env::temp_dir().join(format!("ta_rs_auto_ok_{}", std::process::id()));
85
+ std::fs::create_dir_all(&ws).unwrap();
86
+ // SAFETY: #[ignore]d,单独 `--ignored --test-threads=1` 跑,不与并行 test race。
87
+ unsafe { std::env::set_var("TMUX_PANE", "%42") };
88
+ let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
89
+ unsafe { std::env::remove_var("TMUX_PANE") };
90
+ let receiver = r.expect("$TMUX_PANE 命中 → Ok(Some(receiver))");
91
+ assert_eq!(receiver.pane_id, PaneId::new("%42"));
92
+ assert_eq!(receiver.discovery, Some(Discovery::EnvPane), "$TMUX_PANE 命中 → discovery=env_pane");
93
+ }
94
+
95
+ // claim_leader:无 ambiguous incident → 走 claim_lease_no_incident 直接 acquire/CAS。
96
+ // 现 unimplemented → RED;锁住返回 LeaseResult。
97
+ #[test]
98
+ fn claim_leader_returns_lease_result() {
99
+ let ws = std::env::temp_dir().join(format!("ta_rs_claim_{}", std::process::id()));
100
+ std::fs::create_dir_all(&ws).unwrap();
101
+ let r = claim_leader(&ws, None, false).unwrap();
102
+ // 无 caller pane(测试进程无 TMUX_PANE)→ refused not_in_tmux_pane(__init__.py:616-618)。
103
+ if std::env::var_os("TMUX_PANE").is_none() {
104
+ assert!(!r.ok);
105
+ assert_eq!(r.status, LeaseStatus::Refused);
106
+ assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
107
+ assert!(r.action.is_some());
108
+ }
109
+ }
110
+
111
+ // write_lease_dual_state:同一锁内双写(C17,__init__.py:588-596);unimplemented → RED。
112
+ // 强化:带 session_name 时必须落 BOTH —— workspace state.json + team/<session> snapshot,
113
+ // 两份 team_owner.pane_id / owner_epoch 必须一致(永不分叉)。空 body Ok(()) 会被这里抓。
114
+ #[test]
115
+ fn write_lease_dual_state_persists_both_locations_without_divergence() {
116
+ let ws = std::env::temp_dir().join(format!("ta_rs_dual_{}", std::process::id()));
117
+ std::fs::create_dir_all(&ws).unwrap();
118
+ let state = serde_json::json!({
119
+ "session_name": "team-sess",
120
+ "team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
121
+ "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
122
+ });
123
+ write_lease_dual_state(&ws, &state).unwrap();
124
+ // (1) workspace state.json(<ws>/.team/runtime/state.json)被写,team_owner.pane_id==%1。
125
+ let ws_path = crate::state::persist::runtime_state_path(&ws);
126
+ let ws_state: serde_json::Value =
127
+ serde_json::from_str(&std::fs::read_to_string(&ws_path).expect("workspace state.json 必须存在")).unwrap();
128
+ assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%1"));
129
+ assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(2));
130
+ // (2) team-level snapshot(<ws>/.team/runtime/teams/<session>/state.json)被写。
131
+ let snap_path = crate::model::paths::runtime_dir(&ws)
132
+ .join("teams")
133
+ .join("team-sess")
134
+ .join("state.json");
135
+ let snap_state: serde_json::Value =
136
+ serde_json::from_str(&std::fs::read_to_string(&snap_path).expect("team snapshot state.json 必须存在")).unwrap();
137
+ // (3) 两份永不分叉:owner pane_id / owner_epoch 必须相等(C17 核心不变量)。
138
+ assert_eq!(
139
+ ws_state["team_owner"]["pane_id"], snap_state["team_owner"]["pane_id"],
140
+ "workspace 与 team snapshot 的 owner pane 不得分叉"
141
+ );
142
+ assert_eq!(
143
+ ws_state["team_owner"]["owner_epoch"], snap_state["team_owner"]["owner_epoch"],
144
+ "workspace 与 team snapshot 的 owner_epoch 不得分叉"
145
+ );
146
+ }
147
+
148
+ // detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
149
+ #[test]
150
+ fn detect_dual_state_divergence_none_without_session_name() {
151
+ let ws = std::env::temp_dir().join(format!("ta_rs_div_{}", std::process::id()));
152
+ std::fs::create_dir_all(&ws).unwrap();
153
+ let state = serde_json::json!({"team_owner": {"pane_id": "%1"}}); // 无 session_name。
154
+ let d = detect_dual_state_divergence(&ws, &state).unwrap();
155
+ assert!(d.is_none(), "无 session_name → 无 snapshot 可比 → None");
156
+ }
157
+
158
+ // C18 核心:workspace state.json 与 team snapshot 在 owner pane 上分叉 → Some(具体分叉字段)。
159
+ // golden(probe_leader_strengthen.py):workspace owner=%1 / snapshot owner=%9 →
160
+ // {workspace_owner_pane:%1, team_owner_pane:%9, workspace_receiver_pane:%1, team_receiver_pane:%9}。
161
+ // unimplemented → RED。
162
+ #[test]
163
+ fn detect_dual_state_divergence_reports_diverging_panes() {
164
+ let ws = std::env::temp_dir().join(format!("ta_rs_divx_{}", std::process::id()));
165
+ std::fs::create_dir_all(&ws).unwrap();
166
+ let state = serde_json::json!({
167
+ "session_name": "team-sess",
168
+ "team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
169
+ "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
170
+ });
171
+ // 写一份分叉 snapshot:owner/receiver pane 都是 %9(不同于 workspace 的 %1)。
172
+ let snap_dir = crate::model::paths::runtime_dir(&ws).join("teams").join("team-sess");
173
+ std::fs::create_dir_all(&snap_dir).unwrap();
174
+ let snap = serde_json::json!({
175
+ "session_name": "team-sess",
176
+ "team_owner": {"pane_id": "%9", "leader_session_uuid": "uuuu", "owner_epoch": 2},
177
+ "leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
178
+ });
179
+ std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
180
+ let d = detect_dual_state_divergence(&ws, &state).unwrap().expect("分叉 → Some(details)");
181
+ assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
182
+ assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
183
+ assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
184
+ assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
185
+
186
+ // 匹配 snapshot(与 workspace 一致)→ None(无分叉)。
187
+ std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&state).unwrap()).unwrap();
188
+ assert!(
189
+ detect_dual_state_divergence(&ws, &state).unwrap().is_none(),
190
+ "两份一致 → 无分叉 → None"
191
+ );
192
+ }
193
+
194
+ // R8 D4 (c-lite offline byte-lock): the leader_receiver.requeued_exhausted_watchers payload-build,
195
+ // extracted from the real-tmux attach flow into a pure helper, must produce golden shape.
196
+ // golden leader/__init__.py:39-44: EXACTLY {watcher_ids, count, trigger:"attach_leader"}.
197
+ #[test]
198
+ fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
199
+ let notices = vec![crate::messaging::WatcherNotice {
200
+ watcher_id: "w1".to_string(),
201
+ result_id: Some("r1".to_string()),
202
+ ok: true,
203
+ status: Some("notify_failed".to_string()),
204
+ notified_message_id: None,
205
+ primary_watcher_id: None,
206
+ prior_state: Some("delivery_exhausted".to_string()),
207
+ error: None,
208
+ }];
209
+ let payload = crate::leader::lease::requeued_exhausted_watchers_event_payload(
210
+ &crate::transport::PaneId::new("%leader"),
211
+ &crate::model::ids::TeamKey::new("team-a"),
212
+ &notices,
213
+ );
214
+ let keys: std::collections::BTreeSet<&str> =
215
+ payload.as_object().unwrap().keys().map(String::as_str).collect();
216
+ let expected: std::collections::BTreeSet<&str> = ["watcher_ids", "count", "trigger"].into_iter().collect();
217
+ assert_eq!(keys, expected,
218
+ "D4: leader_receiver.requeued_exhausted_watchers payload must be golden {{watcher_ids, count, trigger}} \
219
+ (leader/__init__.py:39-44), not the Rust {{pane_id, team_id, watcher_ids, requeued}}; got {keys:?}");
220
+ assert_eq!(
221
+ payload.get("watcher_ids").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>()),
222
+ Some(vec!["w1"]), "watcher_ids must be the string list of requeued ids");
223
+ assert_eq!(payload.get("count").and_then(|v| v.as_u64()), Some(1), "count == number of requeued watchers");
224
+ assert_eq!(payload.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
225
+ }