@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,183 @@
1
+ use super::*;
2
+
3
+ // =====================================================================
4
+ // 1. 锁名 / 闭枚举 / serde 字节对齐(纯数据,不依赖 unimplemented body)
5
+ // =====================================================================
6
+
7
+ // LEADER_OWNERSHIP_LOCK = "send"(__init__.py:393):attach/claim/takeover/autobind 共享同一临界区。
8
+ #[test]
9
+ fn ownership_lock_is_send() {
10
+ assert_eq!(LEADER_OWNERSHIP_LOCK, "send");
11
+ }
12
+
13
+ // LeaseReason 闭枚举的 serde 字节必须与 _LEASE_REASON_ENUM ∪ {caller_pane_missing} 一致。
14
+ // golden(probe_leader.py):enum 8 值(snake)+ binding 的 caller_pane_missing。
15
+ #[test]
16
+ fn lease_reason_serializes_to_exact_python_strings() {
17
+ let cases = [
18
+ (LeaseReason::VacantAcquired, "\"vacant_acquired\""),
19
+ (LeaseReason::PreviousOwnerPaneDead, "\"previous_owner_pane_dead\""),
20
+ (LeaseReason::PreviousOwnerAliveRefused, "\"previous_owner_alive_refused\""),
21
+ (LeaseReason::OwnerEpochAdvanced, "\"owner_epoch_advanced\""),
22
+ (LeaseReason::ForceConfirmRequired, "\"force_confirm_required\""),
23
+ (LeaseReason::CallerNotLeaderShaped, "\"caller_not_leader_shaped\""),
24
+ (LeaseReason::CallerCwdMismatch, "\"caller_cwd_mismatch\""),
25
+ (LeaseReason::NotInTmuxPane, "\"not_in_tmux_pane\""),
26
+ (LeaseReason::CallerPaneMissing, "\"caller_pane_missing\""),
27
+ ];
28
+ for (r, want) in cases {
29
+ assert_eq!(serde_json::to_string(&r).unwrap(), want, "{r:?}");
30
+ }
31
+ }
32
+
33
+ // _LEASE_REBIND_REQUIRED_REASONS = {not_in_tmux_pane, caller_not_leader_shaped, caller_cwd_mismatch}
34
+ // (__init__.py:384-386)→ 决定 refusal 事件名 rebind_required vs claim_refused。
35
+ // is_rebind_required 是 unimplemented!() → 调用即 panic = RED。
36
+ #[test]
37
+ fn lease_reason_rebind_required_membership_matches_python() {
38
+ // 三个 rebind-required(golden probe_leader.py rebind_required 集)。
39
+ assert!(LeaseReason::NotInTmuxPane.is_rebind_required());
40
+ assert!(LeaseReason::CallerNotLeaderShaped.is_rebind_required());
41
+ assert!(LeaseReason::CallerCwdMismatch.is_rebind_required());
42
+ // 其余一律 claim_refused(非 rebind-required)。
43
+ assert!(!LeaseReason::VacantAcquired.is_rebind_required());
44
+ assert!(!LeaseReason::PreviousOwnerPaneDead.is_rebind_required());
45
+ assert!(!LeaseReason::PreviousOwnerAliveRefused.is_rebind_required());
46
+ assert!(!LeaseReason::OwnerEpochAdvanced.is_rebind_required());
47
+ assert!(!LeaseReason::ForceConfirmRequired.is_rebind_required());
48
+ assert!(!LeaseReason::CallerPaneMissing.is_rebind_required());
49
+ }
50
+
51
+ // LeaseStatus serde(probe_leader3.py):already_bound / claimed / refused / dry_run。
52
+ #[test]
53
+ fn lease_status_serializes_to_exact_python_strings() {
54
+ assert_eq!(serde_json::to_string(&LeaseStatus::AlreadyBound).unwrap(), "\"already_bound\"");
55
+ assert_eq!(serde_json::to_string(&LeaseStatus::Claimed).unwrap(), "\"claimed\"");
56
+ assert_eq!(serde_json::to_string(&LeaseStatus::Refused).unwrap(), "\"refused\"");
57
+ assert_eq!(serde_json::to_string(&LeaseStatus::DryRun).unwrap(), "\"dry_run\"");
58
+ }
59
+
60
+ // Discovery serde(probe_leader3.py):attach_readopt/claim_leader/env_pane/explicit_pane/current_pane。
61
+ #[test]
62
+ fn discovery_serializes_to_exact_python_strings() {
63
+ assert_eq!(serde_json::to_string(&Discovery::AttachReadopt).unwrap(), "\"attach_readopt\"");
64
+ assert_eq!(serde_json::to_string(&Discovery::ClaimLeader).unwrap(), "\"claim_leader\"");
65
+ assert_eq!(serde_json::to_string(&Discovery::EnvPane).unwrap(), "\"env_pane\"");
66
+ assert_eq!(serde_json::to_string(&Discovery::ExplicitPane).unwrap(), "\"explicit_pane\"");
67
+ assert_eq!(serde_json::to_string(&Discovery::CurrentPane).unwrap(), "\"current_pane\"");
68
+ }
69
+
70
+ // ClaimedVia 是 kebab-case(__init__.py:545/693/876 等写 "attach-leader"/"claim-leader")。
71
+ #[test]
72
+ fn claimed_via_serializes_kebab_case() {
73
+ assert_eq!(serde_json::to_string(&ClaimedVia::ClaimLeader).unwrap(), "\"claim-leader\"");
74
+ assert_eq!(serde_json::to_string(&ClaimedVia::AttachLeader).unwrap(), "\"attach-leader\"");
75
+ }
76
+
77
+ // LeaseSource serde:launch/quick_start/restart/manual(__init__.py source 取值)。
78
+ #[test]
79
+ fn lease_source_serializes_snake_case() {
80
+ assert_eq!(serde_json::to_string(&LeaseSource::Launch).unwrap(), "\"launch\"");
81
+ assert_eq!(serde_json::to_string(&LeaseSource::QuickStart).unwrap(), "\"quick_start\"");
82
+ assert_eq!(serde_json::to_string(&LeaseSource::Restart).unwrap(), "\"restart\"");
83
+ assert_eq!(serde_json::to_string(&LeaseSource::Manual).unwrap(), "\"manual\"");
84
+ }
85
+
86
+ // ReceiverMode/Status:direct_tmux / attached(__init__.py:283-284)。
87
+ #[test]
88
+ fn receiver_mode_and_status_serialize_to_python_strings() {
89
+ assert_eq!(serde_json::to_string(&ReceiverMode::DirectTmux).unwrap(), "\"direct_tmux\"");
90
+ assert_eq!(serde_json::to_string(&ReceiverStatus::Attached).unwrap(), "\"attached\"");
91
+ }
92
+
93
+ // LeaderStartMode:exec_provider/new_tmux_session/attach_existing(leader_start_plan)。
94
+ #[test]
95
+ fn leader_start_mode_serializes_to_python_strings() {
96
+ assert_eq!(serde_json::to_string(&LeaderStartMode::ExecProvider).unwrap(), "\"exec_provider\"");
97
+ assert_eq!(serde_json::to_string(&LeaderStartMode::NewTmuxSession).unwrap(), "\"new_tmux_session\"");
98
+ assert_eq!(serde_json::to_string(&LeaderStartMode::AttachExisting).unwrap(), "\"attach_existing\"");
99
+ }
100
+
101
+ // NodeRole:worker/leader(build_idle_nodes role 字段)。
102
+ #[test]
103
+ fn node_role_serializes_to_python_strings() {
104
+ assert_eq!(serde_json::to_string(&NodeRole::Worker).unwrap(), "\"worker\"");
105
+ assert_eq!(serde_json::to_string(&NodeRole::Leader).unwrap(), "\"leader\"");
106
+ }
107
+
108
+ // RereadReason:wake.py 五值(probe_leader.py)。
109
+ #[test]
110
+ fn reread_reason_serializes_to_python_strings() {
111
+ assert_eq!(serde_json::to_string(&RereadReason::NoFile).unwrap(), "\"no_file\"");
112
+ assert_eq!(serde_json::to_string(&RereadReason::NeverClassified).unwrap(), "\"never_classified\"");
113
+ assert_eq!(serde_json::to_string(&RereadReason::FileChanged).unwrap(), "\"file_changed\"");
114
+ assert_eq!(
115
+ serde_json::to_string(&RereadReason::QuiescentAlreadyClassified).unwrap(),
116
+ "\"quiescent_already_classified\""
117
+ );
118
+ assert_eq!(serde_json::to_string(&RereadReason::Unchanged).unwrap(), "\"unchanged\"");
119
+ }
120
+
121
+ // =====================================================================
122
+ // 2. LeaderSessionUuidSource — 漂移 NOTE(card §168-179)
123
+ // =====================================================================
124
+
125
+ // leader plan 侧 _leader_identity_context(__init__.py:206)只产 "override"/"derived";
126
+ // identity lane(state.py:332)用 "explicit-override"/"env"/"derived"。
127
+ // 此 enum 的 serde 字节当对齐 LEADER 侧:Override→"override"。Env→"env" 也在闭集。
128
+ #[test]
129
+ fn leader_session_uuid_source_serializes_leader_plan_strings() {
130
+ assert_eq!(serde_json::to_string(&LeaderSessionUuidSource::Derived).unwrap(), "\"derived\"");
131
+ // NOTE(drift):leader plan 写裸 "override"(非 identity lane 的 "explicit-override")。
132
+ assert_eq!(serde_json::to_string(&LeaderSessionUuidSource::Override).unwrap(), "\"override\"");
133
+ assert_eq!(serde_json::to_string(&LeaderSessionUuidSource::Env).unwrap(), "\"env\"");
134
+ }
135
+
136
+ // 漂移可观测:identity lane 的 caller dict 用 "explicit-override",二者不可互换。
137
+ // 这是 leader 集成时必须 reconcile 的 NOTE —— 锁死 leader-plan 侧字节,防被误统一成
138
+ // identity 串。serde override≠"explicit-override"。
139
+ #[test]
140
+ fn leader_plan_override_is_not_identity_explicit_override_string() {
141
+ let leader_plan = serde_json::to_string(&LeaderSessionUuidSource::Override).unwrap();
142
+ assert_eq!(leader_plan, "\"override\"");
143
+ assert_ne!(leader_plan, "\"explicit-override\"", "drift NOTE: leader plan 用 'override',不可写成 identity 的 'explicit-override'");
144
+ }
145
+
146
+ // =====================================================================
147
+ // 3. LeaderEvent::name() — §40 字节级一致(unimplemented → RED)
148
+ // =====================================================================
149
+
150
+ // 全部 leader 审计事件名(probe_leader2.py FOUND 验证;ping 来自 idle_predicate 跨 lane,亦 golden)。
151
+ // 注:ReceiverClaimLeaderNotification 在 v0.2.11 任何 source 都查无 "leader_receiver.claim_leader_notification"
152
+ // 字面 → 见 deferred,此处不钉该串(只钉可验证的 23 个)。
153
+ #[test]
154
+ fn leader_event_names_match_python_byte_for_byte() {
155
+ let cases = [
156
+ (LeaderEvent::ReceiverAttached, "leader_receiver.attached"),
157
+ (LeaderEvent::ReceiverRebindApplied, "leader_receiver.rebind_applied"),
158
+ (LeaderEvent::ReceiverClaimApplied, "leader_receiver.claim_applied"),
159
+ (LeaderEvent::ReceiverClaimRefused, "leader_receiver.claim_refused"),
160
+ (LeaderEvent::ReceiverRebindRequired, "leader_receiver.rebind_required"),
161
+ (LeaderEvent::ReceiverAttachFailed, "leader_receiver.attach_failed"),
162
+ (LeaderEvent::ReceiverStateDivergenceRepaired, "leader_receiver.state_divergence_repaired"),
163
+ (LeaderEvent::ReceiverFirstTimeEnvSeeded, "leader_receiver.first_time_env_seeded"),
164
+ (LeaderEvent::ReceiverAutobindSkipped, "leader_receiver.autobind_skipped"),
165
+ (LeaderEvent::ReceiverRequeuedExhaustedWatchers, "leader_receiver.requeued_exhausted_watchers"),
166
+ (LeaderEvent::ReceiverAmbiguousCandidates, "leader_receiver.ambiguous_candidates"),
167
+ (LeaderEvent::ReceiverClaimRequeue, "leader_receiver.claim_requeue"),
168
+ (LeaderEvent::OwnerAdoptedOnRestart, "owner.adopted_on_restart"),
169
+ (LeaderEvent::OwnerBoundFromCallerPane, "owner.bound_from_caller_pane"),
170
+ (LeaderEvent::OwnerBindRefused, "owner.bind_refused"),
171
+ (LeaderEvent::OwnerEpochAdvanced, "owner_epoch_advanced"),
172
+ (LeaderEvent::LeaderSessionUuidOverride, "leader_session_uuid.override"),
173
+ (LeaderEvent::LeaderStart, "leader.start"),
174
+ (LeaderEvent::ResultWatcherRequeued, "result_watcher.requeued"),
175
+ (LeaderEvent::IdleTakeoverClassify, "idle_takeover.classify"),
176
+ (LeaderEvent::IdleTakeoverPing, "idle_takeover.ping"),
177
+ (LeaderEvent::IdleTakeoverReminder, "idle_takeover.reminder"),
178
+ (LeaderEvent::IdleTakeoverPushFailed, "idle_takeover.push_failed"),
179
+ ];
180
+ for (ev, want) in cases {
181
+ assert_eq!(ev.name(), want, "{ev:?}");
182
+ }
183
+ }
@@ -0,0 +1,237 @@
1
+ use super::*;
2
+
3
+ #[test]
4
+ #[serial_test::serial(env)]
5
+ fn d1_claim_team_owner_includes_os_user() {
6
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
7
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
8
+ let ws = p2_temp_ws("d1_owner_keys");
9
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
10
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
11
+ &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
12
+ assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
13
+ let owner = state["team_owner"].as_object().expect("team_owner object");
14
+ assert!(owner.contains_key("os_user"),
15
+ "golden claim-path team_owner carries os_user even when empty; keys={:?}", owner.keys().collect::<Vec<_>>());
16
+ let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
17
+ assert_eq!(keys, vec!["pane_id","provider","machine_fingerprint","leader_session_uuid","owner_epoch","claimed_at","claimed_via","os_user"],
18
+ "golden new_owner includes os_user");
19
+ }
20
+
21
+ // D2 [BLOCK] — claim-path leader_receiver is golden's 15 keys in golden order; NO
22
+ // fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
23
+ // Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
24
+ // (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
25
+ // deferred real-tmux seam, see d2_receiver_populated_from_caller_target_seam; the KEY-SET + ORDER
26
+ // locked here are unchanged by that scan.)
27
+ #[test]
28
+ #[serial_test::serial(env)]
29
+ fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
30
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
31
+ let _e = EnvGuard::apply(&[
32
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
33
+ ("TMUX", Some("/tmp/tmux-501/default,123,0")),
34
+ ]);
35
+ let ws = p2_temp_ws("d2_recv_keys");
36
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
37
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
38
+ &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
39
+ assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
40
+ let recv = state["leader_receiver"].as_object().expect("leader_receiver object");
41
+ for extra in ["fingerprint", "requested_provider", "warning"] {
42
+ assert!(!recv.contains_key(extra),
43
+ "golden leader_receiver has NO '{extra}' key (Rust must add skip_serializing_if); keys={:?}", recv.keys().collect::<Vec<_>>());
44
+ }
45
+ let keys: Vec<&str> = recv.keys().map(String::as_str).collect();
46
+ assert_eq!(keys, vec![
47
+ "mode","status","provider","pane_id","session_name","window_index","window_name",
48
+ "pane_index","pane_tty","pane_current_command","tmux_socket","leader_session_uuid",
49
+ "owner_epoch","attached_at","discovery",
50
+ ], "golden _receiver_from_claim_target 15-key set + ORDER (__init__.py:861-877 + BUG-4 socket-qualified receiver)");
51
+ }
52
+
53
+ // D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
54
+ // pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
55
+ // target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
56
+ #[test]
57
+ #[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
58
+ caller target via core_list_targets (golden _receiver_from_claim_target); Rust has no \
59
+ scan (values null). Porter wires the target scan; this asserts the populated values."]
60
+ fn d2_receiver_populated_from_caller_target_seam() {
61
+ // Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
62
+ // tmux target list (core_list_targets) — out of in-process scope.
63
+ }
64
+
65
+ // D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
66
+ // (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
67
+ // (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.
68
+ #[test]
69
+ #[serial_test::serial(env)]
70
+ fn d3_bound_pane_receiver_first_reclaim_from_receiver_pane_is_already_bound() {
71
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
72
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
73
+ let ws = p2_temp_ws("d3_receiver_first");
74
+ let mut state = serde_json::json!({
75
+ "session_name": "team-agent-x",
76
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
77
+ "leader_receiver": {"pane_id":"%9","owner_epoch":3,"leader_session_uuid":"U"},
78
+ });
79
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
80
+ &PaneId::new("%9"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%9","%1"])).unwrap();
81
+ assert!(r.ok, "a re-claim from the bound RECEIVER pane (%9) is idempotent ok=true; got {r:?}");
82
+ assert_eq!(r.status, LeaseStatus::AlreadyBound,
83
+ "golden :624 receiver-first: bound_pane=receiver(%9)==caller(%9) -> already_bound, NOT force_confirm");
84
+ }
85
+
86
+ // D4 [WARN] — precheck_epoch uses Python truthiness: owner_epoch=0 is FALSY -> falls through to
87
+ // receiver_epoch=5 (golden _lease_epoch :400 int(owner.epoch or recv.epoch or 0)). Rust
88
+ // current_owner_epoch Some(0) stops the chain -> 0. Surfaces in the force_confirm refusal. RED.
89
+ #[test]
90
+ #[serial_test::serial(env)]
91
+ fn d4_precheck_epoch_zero_is_falsy_falls_through_to_receiver() {
92
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
93
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
94
+ let ws = p2_temp_ws("d4_epoch_falsy");
95
+ let mut state = serde_json::json!({
96
+ "session_name": "team-agent-x",
97
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":0,"claimed_at":"t","claimed_via":"claim-leader"},
98
+ "leader_receiver": {"pane_id":"%1","owner_epoch":5,"leader_session_uuid":"U"},
99
+ });
100
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
101
+ &PaneId::new("%2"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%1","%2"])).unwrap();
102
+ assert_eq!(r.status, LeaseStatus::Refused, "precondition: live owner %1 + no confirm -> force_confirm");
103
+ assert_eq!(r.owner_epoch, Some(OwnerEpoch(5)),
104
+ "golden _lease_epoch: owner_epoch=0 is falsy -> receiver_epoch=5; Rust stops at Some(0)");
105
+ }
106
+
107
+ // D5 [BLOCK] — success path emits leader_receiver.rebind_applied + owner_epoch_advanced (golden
108
+ // :712-729), NOT the incident-arm leader_receiver.claim_applied (:791). RED.
109
+ #[test]
110
+ #[serial_test::serial(env)]
111
+ fn d5_success_emits_rebind_applied_and_owner_epoch_advanced_not_claim_applied() {
112
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
113
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
114
+ let ws = p2_temp_ws("d5_events");
115
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
116
+ let _ = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
117
+ &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
118
+ let names = event_names(&ws);
119
+ assert!(names.iter().any(|n| n == "leader_receiver.rebind_applied"),
120
+ "golden success emits leader_receiver.rebind_applied (:712); got {names:?}");
121
+ assert!(names.iter().any(|n| n == "owner_epoch_advanced"),
122
+ "golden success emits owner_epoch_advanced (:721); got {names:?}");
123
+ assert!(!names.iter().any(|n| n == "leader_receiver.claim_applied"),
124
+ "claim_applied is the INCIDENT-arm event, NEVER on the no-incident path; got {names:?}");
125
+ }
126
+
127
+ // D6 [WARN] — EVERY lease refusal writes a leader_receiver audit event (golden _emit_lease_refusal).
128
+ // not_in_tmux_pane ∈ _LEASE_REBIND_REQUIRED_REASONS -> "leader_receiver.rebind_required" (:481).
129
+ // Rust refused() takes no event_log -> writes nothing. RED.
130
+ #[test]
131
+ #[serial_test::serial(env)]
132
+ fn d6_refusal_emits_leader_receiver_audit_event() {
133
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
134
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
135
+ let ws = p2_temp_ws("d6_refusal_event");
136
+ let mut state = serde_json::json!({});
137
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
138
+ &PaneId::new(""), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&[])).unwrap();
139
+ assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane), "precondition: empty caller -> not_in_tmux_pane");
140
+ let names = event_names(&ws);
141
+ assert!(names.iter().any(|n| n == "leader_receiver.rebind_required"),
142
+ "golden writes a leader_receiver.rebind_required audit on the not_in_tmux_pane refusal; Rust writes none. got {names:?}");
143
+ }
144
+
145
+ // D8 [WARN] — provider PRESERVED from the prior receiver on re-claim (golden
146
+ // _receiver_from_claim_target provider=previous.provider or 'codex'; new_owner=new_recv.provider or
147
+ // owner.provider or 'codex'). A dead-owner recover of a CLAUDE leader keeps provider='claude'. Rust
148
+ // make_receiver/make_owner hardcode Provider::Codex. RED.
149
+ #[test]
150
+ #[serial_test::serial(env)]
151
+ fn d8_provider_preserved_from_prior_receiver_on_reclaim() {
152
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
153
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
154
+ let ws = p2_temp_ws("d8_provider");
155
+ let mut state = serde_json::json!({
156
+ "session_name": "team-agent-x",
157
+ "team_owner": {"pane_id":"%1","provider":"claude","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
158
+ "leader_receiver": {"pane_id":"%1","provider":"claude","owner_epoch":3,"leader_session_uuid":"U"},
159
+ });
160
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
161
+ &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
162
+ assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: dead-owner %1 recover");
163
+ assert_eq!(state.pointer("/team_owner/provider").and_then(|v| v.as_str()), Some("claude"),
164
+ "golden preserves the prior provider (claude) on re-claim; Rust hardcodes codex");
165
+ assert_eq!(state.pointer("/leader_receiver/provider").and_then(|v| v.as_str()), Some("claude"),
166
+ "the new receiver also preserves the prior provider (golden previous.provider)");
167
+ }
168
+
169
+ // TOCTOU CAS [BLOCK, flagged] — golden :654-671 re-reads owner_epoch INSIDE the lock and refuses
170
+ // owner_epoch_advanced on a concurrent bump (no double-bind). Rust has NO locked re-read; the race
171
+ // silently double-binds (LeaseReason::OwnerEpochAdvanced is unreachable). Simulated: on-disk
172
+ // state.json (the locked-re-read source) carries epoch 5 while the passed precheck state is epoch 3.
173
+ // RED: today Rust ignores disk and claims. Porter: re-read select_runtime_state under the lock and
174
+ // refuse owner_epoch_advanced when locked_epoch != precheck_epoch.
175
+ #[test]
176
+ #[serial_test::serial(env)]
177
+ fn toctou_locked_reread_refuses_owner_epoch_advanced_on_concurrent_bump() {
178
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
179
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
180
+ let ws = p2_temp_ws("toctou_cas");
181
+ // disk (the locked-re-read source): a concurrent claimer bumped owner_epoch to 5.
182
+ crate::state::persist::save_runtime_state(&ws, &serde_json::json!({
183
+ "session_name": "team-agent-x",
184
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":5,"claimed_at":"t","claimed_via":"claim-leader"},
185
+ })).unwrap();
186
+ // precheck state passed to the claim: epoch 3, vacant (no owner pane) -> no force_confirm gate.
187
+ let mut state = serde_json::json!({"session_name":"team-agent-x","leader_receiver":{"owner_epoch":3}});
188
+ let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
189
+ &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
190
+ assert_eq!(r.status, LeaseStatus::Refused,
191
+ "golden TOCTOU CAS: locked epoch 5 != precheck 3 -> refused; Rust skips the re-read and claims. got {r:?}");
192
+ assert_eq!(r.reason, Some(LeaseReason::OwnerEpochAdvanced),
193
+ "the refusal reason is owner_epoch_advanced (:666)");
194
+ }
195
+
196
+ // attach_leader_to_state [BLOCK, flagged] — golden non-first-time path writes leader_receiver ONLY:
197
+ // NO team_owner write, NO epoch advance (after the strict-uuid gate). Rust stub writes BOTH and
198
+ // advances epoch (precheck+1), ignoring source/require_current. RED (locked before the autobind/launch
199
+ // wiring lands, so it does not silently overwrite the owner + bump epoch on every attach).
200
+ #[test]
201
+ #[serial_test::serial(env)]
202
+ fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
203
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
204
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
205
+ let ws = p2_temp_ws("attach_nonfirst");
206
+ let mut state = serde_json::json!({
207
+ "session_name": "team-agent-x",
208
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
209
+ });
210
+ let event_log = crate::event_log::EventLog::new(&ws);
211
+ let _ = attach_leader_to_state(&ws, &mut state, Some(&PaneId::new("%2")),
212
+ crate::provider::Provider::Codex, &event_log, LeaseSource::Manual, false);
213
+ assert_eq!(state.pointer("/team_owner/pane_id").and_then(|v| v.as_str()), Some("%1"),
214
+ "non-first-time attach must NOT overwrite team_owner.pane_id (golden writes leader_receiver only); Rust rebinds it to %2");
215
+ assert_eq!(state.pointer("/team_owner/owner_epoch").and_then(|v| v.as_u64()), Some(3),
216
+ "non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4");
217
+ }
218
+
219
+ // C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
220
+ // :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
221
+ // TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
222
+ // the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
223
+ // Rust reads attached_at only.)
224
+ #[test]
225
+ #[serial_test::serial(env)]
226
+ fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
227
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
228
+ let _e = EnvGuard::apply(&[
229
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
230
+ ("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
231
+ ("TMUX_PANE", None),
232
+ ]);
233
+ let ws = p2_temp_ws("c3_pane_env");
234
+ let v = leader_identity(&ws, None).unwrap();
235
+ assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
236
+ "golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
237
+ }
@@ -0,0 +1,206 @@
1
+ use super::*;
2
+
3
+ // =====================================================================
4
+ // 12. leader_identity_context — override / state uuid / derive 三源(unimplemented → RED)
5
+ // =====================================================================
6
+
7
+ // 无 override env、无 state record → derive(machine, ws_abspath, user, team)。
8
+ // 现 unimplemented → 调用即 RED;锁住返回 LeaderIdentity 且 source==Derived。
9
+ #[test]
10
+ #[serial_test::serial(env)]
11
+ fn leader_identity_context_derives_when_no_override_no_state() {
12
+ let ws = std::env::temp_dir().join(format!("ta_rs_lic_{}", std::process::id()));
13
+ std::fs::create_dir_all(&ws).unwrap();
14
+ // 空 state(无 team_owner/leader_receiver uuid)。
15
+ let state = serde_json::json!({});
16
+ let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
17
+ // 无 override → source 为 Derived(leader plan 侧;__init__.py:206)。
18
+ assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
19
+ // uuid 是 32 hex(derive 形)。
20
+ assert_eq!(id.leader_session_uuid.as_str().len(), 32);
21
+ }
22
+
23
+ // leader_identity:CLI 直出 dict(__init__.py:355-369)。unimplemented → RED。
24
+ // 强化:uuid_prefix 必须 == leader_identity_context 派生 uuid 的前 12 hex(绑到真值,
25
+ // 而非任意 12 字符);整个 dict 的 machine_fingerprint/os_user/team_id/source 必须与 context 一致。
26
+ #[test]
27
+ #[serial_test::serial(env)]
28
+ fn leader_identity_dict_ties_prefix_and_fields_to_derived_context() {
29
+ let ws = std::env::temp_dir().join(format!("ta_rs_lid_{}", std::process::id()));
30
+ std::fs::create_dir_all(&ws).unwrap();
31
+ // 空 state → context 走 derive(无 override/无 state uuid)。
32
+ let ctx = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
33
+ let expected_prefix = &ctx.leader_session_uuid.as_str()[..12];
34
+ let v = leader_identity(&ws, None).unwrap();
35
+ assert_eq!(v["ok"], serde_json::json!(true));
36
+ // uuid_prefix 绑到派生真值的前 12 hex(错的 12 字符串会被抓)。
37
+ assert_eq!(v["uuid_prefix"].as_str().unwrap(), expected_prefix);
38
+ // 其余身份字段与 context 字节一致。
39
+ assert_eq!(v["machine_fingerprint"].as_str().unwrap(), ctx.machine_fingerprint);
40
+ assert_eq!(v["os_user"].as_str().unwrap(), ctx.os_user);
41
+ assert_eq!(v["team_id"].as_str().unwrap(), ctx.team_id.as_str());
42
+ // source == 派生侧 "derived"(无 override → 不是 "override"/"env")。
43
+ assert_eq!(v["source"], serde_json::json!("derived"));
44
+ assert_eq!(ctx.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
45
+ // CLI 直出形态:current_pane_id / last_seen_at 在无 env/无 receiver 时为 null。
46
+ assert_eq!(v["last_seen_at"], serde_json::Value::Null);
47
+ }
48
+
49
+ // =====================================================================
50
+ // 13. leader_start_plan(unimplemented → RED):钉 mode 选择 + leader_env 导出键。
51
+ // =====================================================================
52
+
53
+ // leader_start_plan(__init__.py:82-145)。强化:钉具体 mode + plan 内容,而非 provider 回声。
54
+ // 确定性环境:在 TMUX 内 → exec_provider;不在 TMUX(且 tmux 可用)→ new_tmux_session,
55
+ // session_name==leader_session_name(Fake,ws),leader_env 携带 5 个 TEAM_AGENT_* 导出键。
56
+ // 注:`detached` 在 leader_start_plan 返回值里恒为 false(__init__.py:174 "detached": False);
57
+ // 非 tty 的 `-d` 插入发生在 start_leader 调用者层(:74-78),不在本 plan 边界 → 不在此断言 detached。
58
+ // unimplemented → RED。
59
+ #[test]
60
+ #[serial_test::serial(env)]
61
+ fn leader_start_plan_pins_mode_and_leader_env() {
62
+ let ws = std::env::temp_dir().join(format!("ta_rs_lsp_{}", std::process::id()));
63
+ std::fs::create_dir_all(&ws).unwrap();
64
+ let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None).unwrap();
65
+ assert_eq!(plan.provider, Provider::Fake);
66
+ if std::env::var_os("TMUX").is_some() {
67
+ // 已在 tmux 内 → exec in-place。
68
+ assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
69
+ } else {
70
+ // 不在 tmux → 新建 tmux session(测试环境假定 tmux 可用;否则 Err(Start))。
71
+ assert_eq!(plan.mode, LeaderStartMode::NewTmuxSession);
72
+ // session_name 由派生公式确定。
73
+ assert_eq!(plan.session_name.as_ref(), Some(&leader_session_name(Provider::Fake, &ws)));
74
+ // plan 边界 detached 恒 false(`-d` 插入在 start_leader 层,非此处)。
75
+ assert!(!plan.detached, "leader_start_plan 返回值 detached 恒 false");
76
+ // leader_env 携带 5 个 TEAM_AGENT_* 导出键(_leader_provider_env)。
77
+ for key in [
78
+ "TEAM_AGENT_LEADER_PROVIDER",
79
+ "TEAM_AGENT_LEADER_SESSION_UUID",
80
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
81
+ "TEAM_AGENT_WORKSPACE",
82
+ "TEAM_AGENT_TEAM_ID",
83
+ ] {
84
+ assert!(plan.leader_env.contains_key(key), "leader_env 缺导出键 {key}");
85
+ }
86
+ assert_eq!(
87
+ plan.leader_env.get("TEAM_AGENT_LEADER_PROVIDER").map(String::as_str),
88
+ Some("fake")
89
+ );
90
+ }
91
+ }
92
+
93
+ // ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
94
+ // Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
95
+ // Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
96
+ // (leader/__init__.py:_leader_identity_context / _identity_* / _detect_dual_state_divergence).
97
+
98
+ #[test]
99
+ #[serial_test::serial(env)]
100
+ fn p2_leader_state_uuid_source_is_derived_not_env() {
101
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
102
+ let _e = EnvGuard::apply(&[
103
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
104
+ ("TEAM_AGENT_LEADER_SESSION_UUID", None),
105
+ ]);
106
+ let ws = p2_temp_ws("src");
107
+ let state = serde_json::json!({"team_owner": {"leader_session_uuid": "STATEUUID123"}});
108
+ let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
109
+ assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Derived);
110
+ assert_eq!(id.leader_session_uuid.as_str(), "STATEUUID123");
111
+ }
112
+
113
+ // P1 — operator override env var is TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE (the
114
+ // _OVERRIDE suffix), per leader/__init__.py:197 — NOT the injected child-env var.
115
+ #[test]
116
+ #[serial_test::serial(env)]
117
+ fn p2_leader_override_reads_override_suffixed_env_var() {
118
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
119
+ let _e = EnvGuard::apply(&[
120
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", Some("OVERRIDE_X")),
121
+ ("TEAM_AGENT_LEADER_SESSION_UUID", None),
122
+ ]);
123
+ let ws = p2_temp_ws("ovr");
124
+ let id = leader_identity_context(&ws, None, None).unwrap();
125
+ assert_eq!(id.leader_session_uuid_source, LeaderSessionUuidSource::Override);
126
+ assert_eq!(id.leader_session_uuid.as_str(), "OVERRIDE_X");
127
+ }
128
+
129
+ // P1 — derived inputs read state: machine_fingerprint = state team_owner record first
130
+ // (_identity_machine_fingerprint); team_id = team_state_key(state) from session_name
131
+ // (default 'current', not a hardcoded 'default').
132
+ #[test]
133
+ #[serial_test::serial(env)]
134
+ fn p2_leader_derived_inputs_read_state_record() {
135
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
136
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_MACHINE_FINGERPRINT", None)]);
137
+ let ws = p2_temp_ws("der");
138
+ let state = serde_json::json!({
139
+ "team_owner": {"machine_fingerprint": "RECORDED-FP-FROM-STATE"},
140
+ "session_name": "team-agent-myteam"
141
+ });
142
+ let id = leader_identity_context(&ws, None, Some(&state)).unwrap();
143
+ assert_eq!(id.machine_fingerprint, "RECORDED-FP-FROM-STATE", "state record fp beats env/hostname");
144
+ assert_eq!(id.team_id.as_str(), "team-agent-myteam", "team_id from state.session_name");
145
+ }
146
+
147
+ // P1 — os_user fallback chain = USER or USERNAME or '' (_identity_os_user), NOT
148
+ // USER or LOGNAME or 'unknown'. (USERNAME is the 2nd choice; empty-string fallback.)
149
+ #[test]
150
+ #[serial_test::serial(env)]
151
+ fn p2_leader_os_user_honors_username_then_empty() {
152
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
153
+ let _e = EnvGuard::apply(&[
154
+ ("USER", None),
155
+ ("LOGNAME", None),
156
+ ("USERNAME", Some("winuser")),
157
+ ]);
158
+ let ws = p2_temp_ws("usr");
159
+ let id = leader_identity_context(&ws, None, None).unwrap();
160
+ assert_eq!(id.os_user, "winuser", "USERNAME is the second choice (not LOGNAME)");
161
+ }
162
+
163
+ // P1 — detect_dual_state_divergence must catch an owner leader_session_uuid split even
164
+ // when panes + epoch are identical (leader/__init__.py:574).
165
+ #[test]
166
+ #[serial_test::serial(env)]
167
+ fn p2_leader_detect_divergence_catches_owner_uuid_split() {
168
+ let ws = p2_temp_ws("div");
169
+ let snap_dir = crate::model::paths::runtime_dir(&ws).join("teams").join("sess1");
170
+ std::fs::create_dir_all(&snap_dir).unwrap();
171
+ let snap = serde_json::json!({
172
+ "session_name":"sess1",
173
+ "team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_B","owner_epoch":5},
174
+ "leader_receiver":{"pane_id":"%1"}
175
+ });
176
+ std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
177
+ let state = serde_json::json!({
178
+ "session_name":"sess1",
179
+ "team_owner":{"pane_id":"%1","leader_session_uuid":"UUID_A","owner_epoch":5},
180
+ "leader_receiver":{"pane_id":"%1"}
181
+ });
182
+ let div = detect_dual_state_divergence(&ws, &state).unwrap();
183
+ assert!(div.is_some(), "owner uuid split (A vs B) with matching panes/epoch must be detected");
184
+ }
185
+
186
+ // ═══════════════════════════════════════════════════════════════════════
187
+ // 14. WAVE-2 Lane B CONTRACT PASS — CLI-handler-facing byte-parity for the
188
+ // three verbs (claim-leader / takeover / identity) + their core lease
189
+ // machinery (_claim_lease_no_incident outcomes / _lease_refused shapes).
190
+ //
191
+ // GOLDEN (re-probed @ team-agent-public, leader/__init__.py +
192
+ // runtime.py:721/791). Each test labels RED|LOCK honestly:
193
+ // RED = drives an unimplemented!() body (claim_lease_no_incident /
194
+ // attach_leader_to_state) → panics today = correct RED-first.
195
+ // LOCK = drives an already-implemented stub/path → green today; pins
196
+ // the golden so a future port cannot regress it.
197
+ // Deferred to later adversarial rounds (Lane-A style): the ambiguous-
198
+ // incident claim arm (no_caller_pane / caller_not_candidate / dry_run /
199
+ // lost_race) which needs a seeded event-log incident + the broadcast
200
+ // requeue cross-lane; the strict-uuid attach refusal string (needs a
201
+ // live pane resolver). Those are #[ignore]/NOTE seams below.
202
+ // ═══════════════════════════════════════════════════════════════════════
203
+
204
+ // ── 14a. _claim_lease_no_incident OUTCOMES (golden __init__.py:598) ──────
205
+ //
206
+ // claim_lease_no_incident is unimplemented!() → every test here PANICS today