@team-agent/installer 0.2.10 → 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 -83
  203. package/src/team_agent/coordinator/lifecycle.py +0 -363
  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 -200
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -111
  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 -254
  255. package/src/team_agent/messaging/delivery.py +0 -473
  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 -457
  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 -86
  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 -1239
  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 -143
  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 -602
  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,351 @@
1
+ //! R12 leader rediscover ambiguity-incident — core cluster (R12-0 two-tier + R12-1/2/3).
2
+ //! Offline byte-lock via rediscover_leader_receiver_from_targets_with_owner_identity (injected
3
+ //! &[PaneInfo], no real tmux). golden: messaging/leader_panes.py:101-187.
4
+ use super::*;
5
+
6
+ fn r12_ws(tag: &str) -> std::path::PathBuf {
7
+ let ws = std::env::temp_dir().join(format!(
8
+ "ta-r12-{}-{}-{}",
9
+ tag,
10
+ std::process::id(),
11
+ std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
12
+ ));
13
+ std::fs::create_dir_all(&ws).unwrap();
14
+ ws
15
+ }
16
+
17
+ fn usable_target(pane: &str, cmd: &str) -> crate::transport::PaneInfo {
18
+ crate::transport::PaneInfo {
19
+ pane_id: crate::transport::PaneId::new(pane),
20
+ session: crate::transport::SessionName::new("s"),
21
+ window_index: Some(0),
22
+ window_name: None,
23
+ pane_index: Some(0),
24
+ tty: None,
25
+ current_command: Some(cmd.to_string()),
26
+ current_path: None,
27
+ active: true,
28
+ pane_pid: None,
29
+ leader_env: std::collections::BTreeMap::new(),
30
+ }
31
+ }
32
+
33
+ fn r12_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
34
+ crate::event_log::EventLog::new(ws).tail(0).unwrap_or_default()
35
+ }
36
+ fn find_ev<'a>(evs: &'a [serde_json::Value], name: &str) -> Option<&'a serde_json::Value> {
37
+ evs.iter().rev().find(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
38
+ }
39
+ fn r12_keys(e: &serde_json::Value) -> std::collections::BTreeSet<String> {
40
+ e.as_object()
41
+ .map(|o| o.keys().filter(|k| *k != "ts" && *k != "event").cloned().collect())
42
+ .unwrap_or_default()
43
+ }
44
+
45
+ // usable target that matches an owner_identity via leader_session_uuid (drives the OWNER-ambiguous
46
+ // path D3: golden _rediscover_leader_receiver:133-145, owner present + >1 owner_candidates → broadcast).
47
+ fn owner_matching_target(pane: &str, cmd: &str, uuid: &str) -> crate::transport::PaneInfo {
48
+ let mut t = usable_target(pane, cmd);
49
+ t.leader_env
50
+ .insert("TEAM_AGENT_LEADER_SESSION_UUID".to_string(), uuid.to_string());
51
+ t
52
+ }
53
+ fn count_ev(evs: &[serde_json::Value], name: &str) -> usize {
54
+ evs.iter().filter(|e| e.get("event").and_then(|v| v.as_str()) == Some(name)).count()
55
+ }
56
+
57
+ // R12-0 + R12-3: NO owner_identity + 2 command-usable targets -> golden two-tier AMBIGUOUS (command-usable
58
+ // candidates, no owner filter), return key "candidates". Rust unified filter (command ∩ matches_owner_identity)
59
+ // with an empty identity matches nothing -> 0 candidates -> "missing" => loses the golden no-owner path.
60
+ #[test]
61
+ fn r12_no_owner_two_usable_targets_is_ambiguous_not_missing() {
62
+ let ws = r12_ws("noownerambig");
63
+ let log = crate::event_log::EventLog::new(&ws);
64
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
65
+ let targets = vec![usable_target("%a", "codex"), usable_target("%b", "codex")];
66
+ let out = crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
67
+ &ws, &mut state, &targets, &log, &serde_json::json!({}), None,
68
+ )
69
+ .unwrap();
70
+ assert_eq!(
71
+ out.get("status").and_then(|v| v.as_str()),
72
+ Some("ambiguous"),
73
+ "R12-0: no owner_identity + 2 command-usable -> golden two-tier AMBIGUOUS (command-usable, no owner \
74
+ filter); Rust unified filter loses the no-owner path -> missing. out={out:?}"
75
+ );
76
+ assert!(
77
+ out.get("candidates").is_some() && out.get("owner_candidates").is_none(),
78
+ "R12-3: ambiguous return key is 'candidates' (golden), not 'owner_candidates'; out={out:?}"
79
+ );
80
+ }
81
+
82
+ // R12-0/R12-1/R12-2: NO owner_identity + 0 command-usable -> golden no-owner MISSING (D7): rediscover_missing
83
+ // = {provider, old_target} LEAN; rebind_required = {..., rediscovery_status:"missing"} (no recovery_action/
84
+ // owner_identity). Rust attaches owner_identity + candidate_count + recovery_action.
85
+ #[test]
86
+ fn r12_no_owner_zero_usable_missing_is_lean_payload() {
87
+ let ws = r12_ws("noownermiss");
88
+ let log = crate::event_log::EventLog::new(&ws);
89
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
90
+ let targets: Vec<crate::transport::PaneInfo> = vec![];
91
+ let out = crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
92
+ &ws, &mut state, &targets, &log, &serde_json::json!({}), None,
93
+ )
94
+ .unwrap();
95
+ assert_eq!(out.get("status").and_then(|v| v.as_str()), Some("missing"));
96
+ let evs = r12_events(&ws);
97
+ let miss = find_ev(&evs, "leader_receiver.rediscover_missing").expect("rediscover_missing");
98
+ let mk = r12_keys(miss);
99
+ assert!(
100
+ !mk.contains("owner_identity") && !mk.contains("candidate_count"),
101
+ "R12-2: no-owner D7 rediscover_missing must be lean {{provider, old_target}} (golden leader_panes.py:185), \
102
+ not carry owner_identity/candidate_count; got {mk:?}"
103
+ );
104
+ let rebind = find_ev(&evs, "leader_receiver.rebind_required").expect("rebind_required");
105
+ let rk = r12_keys(rebind);
106
+ assert!(
107
+ !rk.contains("recovery_action") && !rk.contains("owner_identity"),
108
+ "R12-1: no-owner D7 rebind_required (golden leader_panes.py:186) has rediscovery_status only, NO \
109
+ recovery_action/owner_identity; got {rk:?}"
110
+ );
111
+ assert_eq!(rebind.get("rediscovery_status").and_then(|v| v.as_str()), Some("missing"));
112
+ }
113
+
114
+ // R12-1/R12-2: owner_identity PRESENT + non-matching + 2 command-usable -> golden owner-MISSING (D4):
115
+ // rediscover_missing candidate_count == len(command-usable)=2 + owner_identity; rebind_required carries
116
+ // recovery_action (golden string) + owner_identity + uuid_prefix, NO rediscovery_status. Rust: 0 candidates
117
+ // (post owner filter) -> candidate_count 0 + unified rebind (recovery_action="run team-agent attach-leader...").
118
+ #[test]
119
+ fn r12_owner_missing_candidate_count_is_command_usable_len_and_rebind_shape() {
120
+ let ws = r12_ws("ownermiss");
121
+ let log = crate::event_log::EventLog::new(&ws);
122
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
123
+ let owner = serde_json::json!({
124
+ "pane_id": "%owner", "leader_session_uuid": "U-abcdef01",
125
+ "machine_fingerprint": "F", "provider": "codex", "team_id": "team-a"
126
+ });
127
+ let targets = vec![usable_target("%a", "codex"), usable_target("%b", "codex")];
128
+ let out = crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
129
+ &ws, &mut state, &targets, &log, &owner, None,
130
+ )
131
+ .unwrap();
132
+ assert_eq!(out.get("status").and_then(|v| v.as_str()), Some("missing"));
133
+ let evs = r12_events(&ws);
134
+ let miss = find_ev(&evs, "leader_receiver.rediscover_missing").expect("rediscover_missing");
135
+ assert_eq!(
136
+ miss.get("candidate_count").and_then(|v| v.as_u64()),
137
+ Some(2),
138
+ "R12-2: owner-missing D4 candidate_count == len(command-usable)=2 (golden leader_panes.py:161, \
139
+ candidate_count is PRE owner-filter), not 0; miss={miss:?}"
140
+ );
141
+ let rebind = find_ev(&evs, "leader_receiver.rebind_required").expect("rebind_required");
142
+ assert_eq!(
143
+ rebind.get("recovery_action").and_then(|v| v.as_str()),
144
+ Some("open the owning leader pane or run team-agent claim-leader --confirm from a matching pane"),
145
+ "R12-1: owner-missing D4 rebind recovery_action == golden string (leader_panes.py:171)"
146
+ );
147
+ assert!(
148
+ rebind.get("rediscovery_status").is_none(),
149
+ "R12-1: owner-missing D4 rebind has NO rediscovery_status (golden D4); rebind={rebind:?}"
150
+ );
151
+ }
152
+
153
+ // R12-5: owner present + 2 owner-matching usable -> OWNER-ambiguous (D3) broadcasts the
154
+ // leader_receiver.ambiguous_candidates event. golden payload (probe-captured, sort_keys):
155
+ // {candidates:[sorted pane_ids], debounce_bucket, incident_id, old_pane_id, provider,
156
+ // reason:"force_confirm_required", team_id, uuid_prefix}.
157
+ // Rust emit_ambiguous_candidates (rediscover.rs:650) emits key "pane_ids" (not "candidates") and
158
+ // embeds owner_identity + invalidation_reason + queued[] (golden has none of these in this event;
159
+ // the per-candidate queue is R12-6's separate ambiguous_candidate_queued events).
160
+ #[test]
161
+ fn r12_5_ambiguous_candidates_event_is_full_golden_candidates_key() {
162
+ let ws = r12_ws("ambigbroadcast");
163
+ let log = crate::event_log::EventLog::new(&ws);
164
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
165
+ let owner = serde_json::json!({
166
+ "pane_id": "%owner", "leader_session_uuid": "U-abcdef01",
167
+ "machine_fingerprint": "F", "provider": "codex", "team_id": "team-a"
168
+ });
169
+ let targets = vec![
170
+ owner_matching_target("%b", "codex", "U-abcdef01"),
171
+ owner_matching_target("%a", "codex", "U-abcdef01"),
172
+ ];
173
+ let out = crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
174
+ &ws, &mut state, &targets, &log, &owner, None,
175
+ )
176
+ .unwrap();
177
+ assert_eq!(out.get("status").and_then(|v| v.as_str()), Some("ambiguous"));
178
+ let evs = r12_events(&ws);
179
+ let bc = find_ev(&evs, "leader_receiver.ambiguous_candidates")
180
+ .expect("OWNER-ambiguous must broadcast leader_receiver.ambiguous_candidates");
181
+ let k = r12_keys(bc);
182
+ assert!(
183
+ k.contains("candidates") && !k.contains("pane_ids"),
184
+ "R12-5: broadcast key is 'candidates' (golden _broadcast_ambiguous_candidates:270), not 'pane_ids'; got {k:?}"
185
+ );
186
+ assert!(
187
+ !k.contains("queued") && !k.contains("owner_identity") && !k.contains("invalidation_reason"),
188
+ "R12-5: golden ambiguous_candidates carries NO queued[]/owner_identity/invalidation_reason (per-candidate \
189
+ queue is the separate ambiguous_candidate_queued event, R12-6); got {k:?}"
190
+ );
191
+ assert_eq!(
192
+ bc.get("candidates").cloned(),
193
+ Some(serde_json::json!(["%a", "%b"])),
194
+ "R12-5: candidates == sorted pane_ids (golden candidate_ids = sorted); bc={bc:?}"
195
+ );
196
+ assert_eq!(bc.get("reason").and_then(|v| v.as_str()), Some("force_confirm_required"));
197
+ }
198
+
199
+ // R12-5 dedup: golden _broadcast_ambiguous_candidates:263 skips re-broadcast when a prior
200
+ // ambiguous_candidates with the same incident_id is in event_log.tail(200) (returns deduped:true).
201
+ // Rust incident_id is deterministic (✦ R12-4), so a repeat drive yields the SAME incident_id; Rust
202
+ // hardcodes deduped:false (rediscover.rs:245/255) and re-emits every time -> RED (2 events, golden 1).
203
+ #[test]
204
+ fn r12_5_ambiguous_candidates_dedups_on_repeat_incident() {
205
+ let ws = r12_ws("ambigdedup");
206
+ let log = crate::event_log::EventLog::new(&ws);
207
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
208
+ let owner = serde_json::json!({
209
+ "pane_id": "%owner", "leader_session_uuid": "U-abcdef01",
210
+ "machine_fingerprint": "F", "provider": "codex", "team_id": "team-a"
211
+ });
212
+ let targets = vec![
213
+ owner_matching_target("%a", "codex", "U-abcdef01"),
214
+ owner_matching_target("%b", "codex", "U-abcdef01"),
215
+ ];
216
+ for _ in 0..2 {
217
+ crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
218
+ &ws, &mut state, &targets, &log, &owner, None,
219
+ )
220
+ .unwrap();
221
+ }
222
+ let evs = r12_events(&ws);
223
+ assert_eq!(
224
+ count_ev(&evs, "leader_receiver.ambiguous_candidates"),
225
+ 1,
226
+ "R12-5: repeat drive with the same incident_id must dedup the broadcast (golden tail(200) skip, \
227
+ leader_panes.py:263); Rust re-emits every time. ambiguous_candidates count={}",
228
+ count_ev(&evs, "leader_receiver.ambiguous_candidates")
229
+ );
230
+ }
231
+
232
+ // R12-6: golden _broadcast_ambiguous_candidates:279-294 loops candidates and emits a SEPARATE
233
+ // leader_receiver.ambiguous_candidate_queued event per candidate (probe-captured keys
234
+ // {error, event, incident_id, ok, pane_id}), tagged with the broadcast's incident_id. Rust embeds a
235
+ // queued:[{pane_id,...}] array INSIDE the single ambiguous_candidates event (rediscover.rs:663) and
236
+ // emits ZERO per-candidate events. NOTE (surfaced): the ok/error VALUES come from the tmux inject
237
+ // (real-tmux); the offline from_targets seam can assert structure (separate events, incident_id,
238
+ // pane_id) but not the inject result — the porter/leader decide the offline ok/error semantics.
239
+ #[test]
240
+ fn r12_6_per_candidate_emits_separate_ambiguous_candidate_queued_events() {
241
+ let ws = r12_ws("percand");
242
+ let log = crate::event_log::EventLog::new(&ws);
243
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
244
+ let owner = serde_json::json!({
245
+ "pane_id": "%owner", "leader_session_uuid": "U-abcdef01",
246
+ "machine_fingerprint": "F", "provider": "codex", "team_id": "team-a"
247
+ });
248
+ let targets = vec![
249
+ owner_matching_target("%a", "codex", "U-abcdef01"),
250
+ owner_matching_target("%b", "codex", "U-abcdef01"),
251
+ ];
252
+ crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
253
+ &ws, &mut state, &targets, &log, &owner, None,
254
+ )
255
+ .unwrap();
256
+ let evs = r12_events(&ws);
257
+ let n = count_ev(&evs, "leader_receiver.ambiguous_candidate_queued");
258
+ assert_eq!(
259
+ n, 2,
260
+ "R12-6: each candidate must emit a SEPARATE leader_receiver.ambiguous_candidate_queued event \
261
+ (golden leader_panes.py:288); Rust embeds queued[] in the single ambiguous_candidates event and \
262
+ emits none. count={n}"
263
+ );
264
+ let qpanes: std::collections::BTreeSet<String> = evs
265
+ .iter()
266
+ .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("leader_receiver.ambiguous_candidate_queued"))
267
+ .filter_map(|e| e.get("pane_id").and_then(|v| v.as_str()).map(str::to_string))
268
+ .collect();
269
+ assert_eq!(
270
+ qpanes,
271
+ ["%a".to_string(), "%b".to_string()].into_iter().collect(),
272
+ "R12-6: per-candidate queued events cover each candidate pane_id; got {qpanes:?}"
273
+ );
274
+ let bc = find_ev(&evs, "leader_receiver.ambiguous_candidates").expect("broadcast");
275
+ let iid = bc.get("incident_id").cloned();
276
+ let q = find_ev(&evs, "leader_receiver.ambiguous_candidate_queued").expect("queued");
277
+ assert_eq!(
278
+ q.get("incident_id").cloned(),
279
+ iid,
280
+ "R12-6: each ambiguous_candidate_queued carries the broadcast incident_id (golden :290); q={q:?}"
281
+ );
282
+ }
283
+
284
+ // helper: pull the candidate-raw array from the ambiguous return regardless of the R12-3 key rename.
285
+ fn r12_return_candidates(out: &serde_json::Value) -> &Vec<serde_json::Value> {
286
+ out.get("candidates")
287
+ .or_else(|| out.get("owner_candidates"))
288
+ .and_then(|v| v.as_array())
289
+ .expect("ambiguous return must carry a candidate array")
290
+ }
291
+
292
+ // R12-7: golden returns the candidate RAW as the passthrough tmux target dict (probe-captured keys
293
+ // {leader_session_uuid, pane_current_command, pane_current_path, pane_id}). Rust reconstructs via
294
+ // pane_info_value (rediscover.rs:568) with "current_command"/"current_path". Assert golden spelling.
295
+ #[test]
296
+ fn r12_7_candidate_raw_uses_golden_pane_current_spelling() {
297
+ let ws = r12_ws("rawkeys");
298
+ let log = crate::event_log::EventLog::new(&ws);
299
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old", "provider": "codex"}});
300
+ let owner = serde_json::json!({
301
+ "pane_id": "%owner", "leader_session_uuid": "U-abcdef01",
302
+ "machine_fingerprint": "F", "provider": "codex", "team_id": "team-a"
303
+ });
304
+ let targets = vec![
305
+ owner_matching_target("%a", "codex", "U-abcdef01"),
306
+ owner_matching_target("%b", "codex", "U-abcdef01"),
307
+ ];
308
+ let out = crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
309
+ &ws, &mut state, &targets, &log, &owner, None,
310
+ )
311
+ .unwrap();
312
+ let c0 = &r12_return_candidates(&out)[0];
313
+ let k = r12_keys(c0);
314
+ assert!(
315
+ k.contains("pane_current_command") && k.contains("pane_current_path"),
316
+ "R12-7: candidate raw uses golden tmux spelling pane_current_command/pane_current_path (passthrough); \
317
+ got {k:?}"
318
+ );
319
+ assert!(
320
+ !k.contains("current_command") && !k.contains("current_path"),
321
+ "R12-7: Rust pane_info_value spelling current_command/current_path must not leak; got {k:?}"
322
+ );
323
+ }
324
+
325
+ // R12-7: golden provider = str(receiver.provider or "codex") (leader_panes.py:108) — DEFAULTS to "codex"
326
+ // when absent, applied to every emitted event's provider field. Rust uses identity.provider (null when
327
+ // absent). Drive with no provider anywhere -> golden event provider "codex", Rust null.
328
+ #[test]
329
+ fn r12_7_event_provider_defaults_to_codex_when_absent() {
330
+ let ws = r12_ws("providerdefault");
331
+ let log = crate::event_log::EventLog::new(&ws);
332
+ let mut state = serde_json::json!({"leader_receiver": {"pane_id": "%old"}});
333
+ let owner = serde_json::json!({"leader_session_uuid": "U-abcdef01", "team_id": "team-a"});
334
+ let targets = vec![
335
+ owner_matching_target("%a", "codex", "U-abcdef01"),
336
+ owner_matching_target("%b", "codex", "U-abcdef01"),
337
+ ];
338
+ let out = crate::leader::rediscover_leader_receiver_from_targets_with_owner_identity(
339
+ &ws, &mut state, &targets, &log, &owner, None,
340
+ )
341
+ .unwrap();
342
+ assert_eq!(out.get("status").and_then(|v| v.as_str()), Some("ambiguous"));
343
+ let evs = r12_events(&ws);
344
+ let amb = find_ev(&evs, "leader_receiver.rediscover_ambiguous").expect("rediscover_ambiguous");
345
+ assert_eq!(
346
+ amb.get("provider").and_then(|v| v.as_str()),
347
+ Some("codex"),
348
+ "R12-7: event provider defaults to \"codex\" when absent (golden leader_panes.py:108 \
349
+ receiver.provider or 'codex'); Rust emits identity.provider=null. amb={amb:?}"
350
+ );
351
+ }
@@ -0,0 +1,204 @@
1
+ use super::*;
2
+
3
+ // =====================================================================
4
+ // 4. wake 层纯函数(unimplemented → RED)。golden:probe_leader.py 5 分支 + boundary。
5
+ // =====================================================================
6
+
7
+ #[test]
8
+ fn should_reread_no_file_when_current_mtime_none() {
9
+ // current_mtime is None → {reread:false, no_file}(wake.py:31-32)。
10
+ let d = should_reread(None, None, None, 100.0, 60.0);
11
+ assert!(!d.reread);
12
+ assert_eq!(d.reason, RereadReason::NoFile);
13
+ }
14
+
15
+ #[test]
16
+ fn should_reread_never_classified() {
17
+ // last_classified_mtime is None → {reread:true, never_classified}。
18
+ let d = should_reread(None, Some(10.0), None, 100.0, 60.0);
19
+ assert!(d.reread);
20
+ assert_eq!(d.reason, RereadReason::NeverClassified);
21
+ }
22
+
23
+ #[test]
24
+ fn should_reread_file_changed() {
25
+ // current != last_classified → {reread:true, file_changed}。
26
+ let d = should_reread(Some(5.0), Some(20.0), Some(10.0), 100.0, 60.0);
27
+ assert!(d.reread);
28
+ assert_eq!(d.reason, RereadReason::FileChanged);
29
+ }
30
+
31
+ #[test]
32
+ fn should_reread_quiescent_already_classified_at_and_past_debounce() {
33
+ // current==last_classified 且 silent_for >= debounce → quiescent(不再重读)。
34
+ // silent_for = max(0, now-current_mtime) = 100-10 = 90 >= 60。
35
+ let d = should_reread(Some(10.0), Some(10.0), Some(10.0), 100.0, 60.0);
36
+ assert!(!d.reread);
37
+ assert_eq!(d.reason, RereadReason::QuiescentAlreadyClassified);
38
+ // 边界:silent_for 恰 == debounce(now-current=60)→ 仍 quiescent(>= 比较)。
39
+ let b = should_reread(Some(40.0), Some(40.0), Some(40.0), 100.0, 60.0);
40
+ assert!(!b.reread);
41
+ assert_eq!(b.reason, RereadReason::QuiescentAlreadyClassified);
42
+ }
43
+
44
+ #[test]
45
+ fn should_reread_unchanged_within_debounce() {
46
+ // current==last_classified 且 silent_for < debounce → unchanged。
47
+ // now-current = 100-95 = 5 < 60。注:last_mtime 在 Python body 中未被使用。
48
+ let d = should_reread(Some(10.0), Some(95.0), Some(95.0), 100.0, 60.0);
49
+ assert!(!d.reread);
50
+ assert_eq!(d.reason, RereadReason::Unchanged);
51
+ }
52
+
53
+ #[test]
54
+ fn on_file_changed_adds_node_sorted_and_records_mtime() {
55
+ // golden probe_leader.py:add b → pending ["b"];add a → sorted ["a","b"]。
56
+ let s0 = on_file_changed(None, "b", 1.0);
57
+ assert_eq!(s0.pending, vec!["b".to_string()]);
58
+ assert_eq!(s0.mtimes.get("b"), Some(&1.0));
59
+ let s1 = on_file_changed(Some(&s0), "a", 2.0);
60
+ assert_eq!(s1.pending, vec!["a".to_string(), "b".to_string()], "pending 必须 sorted");
61
+ assert_eq!(s1.mtimes.get("a"), Some(&2.0));
62
+ // 重复 add b:pending 仍是 set(不重复),mtime 被更新到 3.0。
63
+ let s2 = on_file_changed(Some(&s1), "b", 3.0);
64
+ assert_eq!(s2.pending, vec!["a".to_string(), "b".to_string()]);
65
+ assert_eq!(s2.mtimes.get("b"), Some(&3.0));
66
+ }
67
+
68
+ #[test]
69
+ fn take_pending_drains_sorted_and_clears_pending_but_keeps_mtimes() {
70
+ // golden probe_leader.py:drain → (["a","b"], state{pending:[], mtimes 保留})。
71
+ let mut st = on_file_changed(None, "b", 3.0);
72
+ st = on_file_changed(Some(&st), "a", 2.0);
73
+ let (drained, after) = take_pending(Some(&st));
74
+ assert_eq!(drained, vec!["a".to_string(), "b".to_string()]);
75
+ assert!(after.pending.is_empty());
76
+ // mtimes 不被 drain 清空。
77
+ assert_eq!(after.mtimes.get("a"), Some(&2.0));
78
+ assert_eq!(after.mtimes.get("b"), Some(&3.0));
79
+ // 再 drain → 空。
80
+ let (drained2, _after2) = take_pending(Some(&after));
81
+ assert!(drained2.is_empty());
82
+ // drain None → ([], default state)。
83
+ let (none_drained, none_state) = take_pending(None);
84
+ assert!(none_drained.is_empty());
85
+ assert!(none_state.pending.is_empty());
86
+ }
87
+
88
+ // =====================================================================
89
+ // 5. leader_session_name — sha1 派生 + 文件夹消毒(unimplemented → RED)
90
+ // =====================================================================
91
+
92
+ // 公式:team-agent-leader-<provider>-<sanitized folder[:48]>-<sha1(resolve(ws))[:8]>。
93
+ // 用真实 temp 目录,sha1/sanitize 在测试内复算后断言函数输出与之一致(probe 已验证公式)。
94
+ #[test]
95
+ fn leader_session_name_formula_and_sanitization() {
96
+ // 公式 = team-agent-leader-<provider>-<sanitized folder>-<sha1(resolve(ws))[:8]>。
97
+ // sha1 复算需 sha1 crate(本测试不引);改为断言格式不变量(provider/消毒/8-hex 后缀),
98
+ // 字节级 sha1 由 golden probe_leader.py 已验证公式正确。
99
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn_{}", std::process::id()));
100
+ let weird = base.join("My Proj!@#name");
101
+ std::fs::create_dir_all(&weird).unwrap();
102
+ let got = leader_session_name(Provider::Codex, &weird);
103
+ // 前缀 + provider + 消毒后的 folder(非字母数字_.- → '_')。
104
+ let s = got.as_str();
105
+ assert!(s.starts_with("team-agent-leader-codex-My_Proj___name-"), "got {s}");
106
+ // sha1[:8] 后缀:8 个 hex。
107
+ let suffix = s.rsplit('-').next().unwrap();
108
+ assert_eq!(suffix.len(), 8, "sha1 前缀须 8 hex,got {suffix}");
109
+ assert!(suffix.chars().all(|c| c.is_ascii_hexdigit()));
110
+ }
111
+
112
+ // folder 名消毒成空 → 回退 "workspace"(probe_leader.py allsym 用例)。
113
+ #[test]
114
+ fn leader_session_name_empty_sanitized_folder_falls_back_to_workspace() {
115
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn2_{}", std::process::id()));
116
+ // 全符号目录名 → 消毒后 strip('._-') 为空 → "workspace"。
117
+ let allsym = base.join("@@@");
118
+ std::fs::create_dir_all(&allsym).unwrap();
119
+ let got = leader_session_name(Provider::Codex, &allsym);
120
+ assert!(
121
+ got.as_str().contains("-workspace-"),
122
+ "全符号 folder 应回退 'workspace',got {}",
123
+ got.as_str()
124
+ );
125
+ }
126
+
127
+ // claude_code provider 出现在 session 名里(probe:team-agent-leader-claude_code-...)。
128
+ #[test]
129
+ fn leader_session_name_uses_claude_code_provider_string() {
130
+ let base = std::env::temp_dir().join(format!("ta_rs_lsn3_{}", std::process::id()));
131
+ let dir = base.join("proj");
132
+ std::fs::create_dir_all(&dir).unwrap();
133
+ let got = leader_session_name(Provider::ClaudeCode, &dir);
134
+ assert!(got.as_str().starts_with("team-agent-leader-claude_code-proj-"), "got {}", got.as_str());
135
+ }
136
+
137
+ // =====================================================================
138
+ // 6. Family A 正源 owner 绑定 — bind_owner_from_caller_pane(unimplemented → RED)
139
+ // =====================================================================
140
+
141
+ // $TMUX_PANE 缺 → refuse + reason=caller_pane_missing(leader_binding.py:79-95)。
142
+ // 此处只能在 $TMUX_PANE 缺失环境下断言(测试进程通常无 TMUX_PANE)。
143
+ #[test]
144
+ fn bind_owner_refuses_when_caller_pane_missing() {
145
+ // 防御:确保本测试看到的环境无 TMUX_PANE(若 CI 在 tmux 内跑,跳过断言形态)。
146
+ if std::env::var_os("TMUX_PANE").is_some() {
147
+ // 在 tmux 内:正源存在,不该走 refuse 分支;此用例只验缺失分支,直接返回。
148
+ return;
149
+ }
150
+ let ws = std::env::temp_dir().join(format!("ta_rs_bind_{}", std::process::id()));
151
+ std::fs::create_dir_all(&ws).unwrap();
152
+ let team = TeamKey::new("default");
153
+ let res = bind_owner_from_caller_pane(&ws, &team, None).unwrap();
154
+ assert!(!res.ok);
155
+ assert_eq!(res.reason, Some(LeaseReason::CallerPaneMissing));
156
+ // caller_pane_id 为空(probe:""),hint 为 _HINT_RUN_FROM_LEADER_PANE。
157
+ assert_eq!(res.caller_pane_id, PaneId::new(""));
158
+ assert_eq!(res.caller_current_command, "");
159
+ assert_eq!(
160
+ res.hint.as_deref(),
161
+ Some("run team-agent from inside your leader pane (the tmux pane you want to own this team).")
162
+ );
163
+ assert_eq!(res.team_id, team);
164
+ }
165
+
166
+ // owner.bind_refused 事件名字节锁(LeaderEvent::name unimplemented → RED;与 #5 重叠但锁 binding 路径)。
167
+ #[test]
168
+ fn owner_bind_refused_event_name_is_owner_bind_refused() {
169
+ assert_eq!(LeaderEvent::OwnerBindRefused.name(), "owner.bind_refused");
170
+ }
171
+
172
+ // emit_owner_bound_event:成功绑定 hook(owner.bound_from_caller_pane;leader_binding.py:162-183)。
173
+ // 强化(no-full-uuid-leak 命门):事件只写 derived_uuid_prefix == derived[:12](12 hex),
174
+ // old uuid 为 None → old_uuid_prefix == ""(空串,非缺省);全 32 hex uuid 绝不出现在任何字段。
175
+ // unimplemented → RED。
176
+ #[test]
177
+ fn emit_owner_bound_event_logs_prefix_only_never_full_uuid() {
178
+ let ws = std::env::temp_dir().join(format!("ta_rs_emit_{}", std::process::id()));
179
+ std::fs::create_dir_all(&ws).unwrap();
180
+ let caller = PaneId::new("%7");
181
+ let derived = uuid("fp", "/ws", "u", "default");
182
+ let full = derived.as_str().to_string();
183
+ assert_eq!(full.len(), 32, "derive 产 32 hex");
184
+ let prefix12 = full[..12].to_string();
185
+ emit_owner_bound_event(&ws, &caller, "codex", &derived, &TeamKey::new("default"), None).unwrap();
186
+ // 读回审计事件:恰一条 owner.bound_from_caller_pane。
187
+ let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
188
+ let ev = events
189
+ .iter()
190
+ .find(|e| e["event"] == serde_json::json!("owner.bound_from_caller_pane"))
191
+ .expect("必写 owner.bound_from_caller_pane");
192
+ assert_eq!(ev["caller_pane_id"], serde_json::json!("%7"));
193
+ assert_eq!(ev["caller_current_command"], serde_json::json!("codex"));
194
+ assert_eq!(ev["team_id"], serde_json::json!("default"));
195
+ // derived_uuid_prefix == derived[:12](只前缀,12 hex)。
196
+ assert_eq!(ev["derived_uuid_prefix"], serde_json::json!(prefix12));
197
+ // old uuid=None → old_uuid_prefix == ""(空串,非 null/缺省;golden probe 已验)。
198
+ assert_eq!(ev["old_uuid_prefix"], serde_json::json!(""));
199
+ // no-full-uuid-leak:整条事件序列化文本里绝不出现完整 32-hex uuid。
200
+ let raw = serde_json::to_string(ev).unwrap();
201
+ assert!(!raw.contains(&full), "审计事件绝不泄露完整 leader_session_uuid");
202
+ // 审计事件名字节锁。
203
+ assert_eq!(LeaderEvent::OwnerBoundFromCallerPane.name(), "owner.bound_from_caller_pane");
204
+ }