@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,616 @@
1
+ use super::*;
2
+
3
+ // RED 契约 lane:argv→run 端到端、五行 summary 字节锁(Gap 18a)、
4
+ // classify_agent_bucket(unknown≠idle)、cli-error 信封字节锁、leader_launcher_args 解析、
5
+ // consume_leader_inbox_summary 游标+预算截断、send_target fanout 解析。
6
+ // Python golden 来源:cli/{parser,commands,helpers}.py @ v0.2.11(439bef8)。
7
+ // 所有期望值用 `PYTHONPATH=.../src python3` 实跑 Python 实现抓取的字节级 golden。
8
+ use super::*;
9
+ use serde_json::json;
10
+
11
+ // =========================================================================
12
+ // provider_args(helpers.py:190-193):values[0]=='--' ? values[1..] : values
13
+ // =========================================================================
14
+
15
+ #[test]
16
+ fn provider_args_strips_leading_dashdash() {
17
+ // golden: _provider_args(["--","-x"]) == ["-x"]
18
+ assert_eq!(provider_args(&["--".into(), "-x".into()]), vec!["-x".to_string()]);
19
+ }
20
+
21
+ #[test]
22
+ fn provider_args_keeps_when_no_leading_dashdash() {
23
+ // golden: _provider_args(["-x","-y"]) == ["-x","-y"]
24
+ assert_eq!(
25
+ provider_args(&["-x".into(), "-y".into()]),
26
+ vec!["-x".to_string(), "-y".to_string()]
27
+ );
28
+ }
29
+
30
+ #[test]
31
+ fn provider_args_empty_is_empty() {
32
+ // golden: _provider_args([]) == []
33
+ assert_eq!(provider_args(&[]), Vec::<String>::new());
34
+ }
35
+
36
+ #[test]
37
+ fn provider_args_lone_dashdash_yields_empty() {
38
+ // golden: _provider_args(["--"]) == [] (values[1:] of single-elem list)
39
+ assert_eq!(provider_args(&["--".into()]), Vec::<String>::new());
40
+ }
41
+
42
+ // =========================================================================
43
+ // leader_launcher_args(helpers.py:196-226):attach 旗标解析 + 缺值 Err
44
+ // =========================================================================
45
+
46
+ #[test]
47
+ fn leader_launcher_args_empty_all_default() {
48
+ // golden: {'provider_args': [], 'attach_existing': False, 'confirm_attach': False, 'attach_session': None}
49
+ let got = leader_launcher_args(&[]).expect("empty should parse");
50
+ assert_eq!(got, LeaderLauncherArgs::default());
51
+ assert!(got.provider_args.is_empty());
52
+ assert!(!got.attach_existing);
53
+ assert!(!got.confirm_attach);
54
+ assert_eq!(got.attach_session, None);
55
+ }
56
+
57
+ #[test]
58
+ fn leader_launcher_args_attach_and_confirm() {
59
+ // golden: ["--attach","--confirm"] -> attach_existing=True, confirm_attach=True
60
+ let got = leader_launcher_args(&["--attach".into(), "--confirm".into()]).unwrap();
61
+ assert!(got.attach_existing);
62
+ assert!(got.confirm_attach);
63
+ assert!(got.provider_args.is_empty());
64
+ assert_eq!(got.attach_session, None);
65
+ }
66
+
67
+ #[test]
68
+ fn leader_launcher_args_attach_existing_alias() {
69
+ // golden: ["--attach-existing"] -> attach_existing=True (alias of --attach)
70
+ let got = leader_launcher_args(&["--attach-existing".into()]).unwrap();
71
+ assert!(got.attach_existing);
72
+ assert!(!got.confirm_attach);
73
+ }
74
+
75
+ #[test]
76
+ fn leader_launcher_args_attach_session_spaced() {
77
+ // golden: ["--attach-session","mysess"] -> attach_session="mysess"
78
+ let got = leader_launcher_args(&["--attach-session".into(), "mysess".into()]).unwrap();
79
+ assert_eq!(got.attach_session, Some("mysess".to_string()));
80
+ assert!(!got.attach_existing);
81
+ }
82
+
83
+ #[test]
84
+ fn leader_launcher_args_attach_session_equals() {
85
+ // golden: ["--attach-session=mysess"] -> attach_session="mysess"
86
+ let got = leader_launcher_args(&["--attach-session=mysess".into()]).unwrap();
87
+ assert_eq!(got.attach_session, Some("mysess".to_string()));
88
+ }
89
+
90
+ #[test]
91
+ fn leader_launcher_args_dashdash_passthrough_includes_following_flags() {
92
+ // golden: ["--attach","--","-x","--confirm"] ->
93
+ // provider_args=["--","-x","--confirm"], attach_existing=True, confirm_attach=False
94
+ // (everything from `--` onward, INCLUDING the `--` token itself, is provider passthrough)
95
+ let got = leader_launcher_args(&[
96
+ "--attach".into(),
97
+ "--".into(),
98
+ "-x".into(),
99
+ "--confirm".into(),
100
+ ])
101
+ .unwrap();
102
+ assert!(got.attach_existing);
103
+ assert!(!got.confirm_attach, "--confirm AFTER -- must NOT set confirm_attach");
104
+ assert_eq!(
105
+ got.provider_args,
106
+ vec!["--".to_string(), "-x".to_string(), "--confirm".to_string()]
107
+ );
108
+ }
109
+
110
+ #[test]
111
+ fn leader_launcher_args_unknown_tokens_collect_as_provider_args() {
112
+ // golden: ["foo","--attach","bar"] -> provider_args=["foo","bar"], attach_existing=True
113
+ let got = leader_launcher_args(&["foo".into(), "--attach".into(), "bar".into()]).unwrap();
114
+ assert_eq!(got.provider_args, vec!["foo".to_string(), "bar".to_string()]);
115
+ assert!(got.attach_existing);
116
+ }
117
+
118
+ #[test]
119
+ fn leader_launcher_args_attach_session_missing_value_errors() {
120
+ // golden: ["--attach-session"] raises RuntimeError("--attach-session requires a tmux session name")
121
+ let err = leader_launcher_args(&["--attach-session".into()]).unwrap_err();
122
+ let msg = err.to_string();
123
+ assert!(
124
+ msg.contains("--attach-session requires a tmux session name"),
125
+ "expected exact missing-value message, got: {msg}"
126
+ );
127
+ }
128
+
129
+ // =========================================================================
130
+ // send_target(commands.py:181-184):--to split / target / None
131
+ // =========================================================================
132
+
133
+ #[test]
134
+ fn send_target_fanout_strips_and_filters_empty() {
135
+ // golden: _send_target(targets="a, b ,,c") == ["a","b","c"]
136
+ let got = send_target(Some("a, b ,,c"), None);
137
+ assert_eq!(
138
+ got,
139
+ MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()])
140
+ );
141
+ }
142
+
143
+ #[test]
144
+ fn send_target_single_target() {
145
+ // golden: _send_target(target="agent_x") == "agent_x"
146
+ assert_eq!(send_target(None, Some("agent_x")), MessageTarget::Single("agent_x".to_string()));
147
+ }
148
+
149
+ #[test]
150
+ fn send_target_broadcast_star() {
151
+ // skeleton contract: bare "*" target -> Broadcast (send.py interprets "*" as全队广播)
152
+ assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
153
+ }
154
+
155
+ #[test]
156
+ fn send_target_empty_targets_falls_through_to_target() {
157
+ // golden: targets="" is falsy in Python -> returns args.target ("fallback")
158
+ assert_eq!(send_target(Some(""), Some("fallback")), MessageTarget::Single("fallback".to_string()));
159
+ }
160
+
161
+ // =========================================================================
162
+ // classify_agent_bucket / agent_summary_counts(commands.py:309-330)
163
+ // bug-071/077/085 铁律:unknown ≠ idle,无匹配态显式落 Unknown
164
+ // =========================================================================
165
+
166
+ #[test]
167
+ fn classify_failed_takes_priority() {
168
+ // raw in {failed,error} OR hstatus in {failed,error} -> Failed
169
+ assert_eq!(classify_agent_bucket("failed", ""), SummaryBucket::Failed);
170
+ assert_eq!(classify_agent_bucket("error", ""), SummaryBucket::Failed);
171
+ assert_eq!(classify_agent_bucket("running", "error"), SummaryBucket::Failed);
172
+ }
173
+
174
+ #[test]
175
+ fn classify_stopped() {
176
+ // raw in {stopped,done} OR hstatus==done -> Stopped
177
+ assert_eq!(classify_agent_bucket("stopped", ""), SummaryBucket::Stopped);
178
+ assert_eq!(classify_agent_bucket("done", ""), SummaryBucket::Stopped);
179
+ assert_eq!(classify_agent_bucket("running", "done"), SummaryBucket::Stopped);
180
+ }
181
+
182
+ #[test]
183
+ fn classify_busy() {
184
+ // raw==busy OR hstatus in {running,working} -> Busy
185
+ assert_eq!(classify_agent_bucket("busy", ""), SummaryBucket::Busy);
186
+ assert_eq!(classify_agent_bucket("", "running"), SummaryBucket::Busy);
187
+ assert_eq!(classify_agent_bucket("", "working"), SummaryBucket::Busy);
188
+ }
189
+
190
+ #[test]
191
+ fn classify_hstatus_idle_beats_raw_running() {
192
+ // golden: raw=running, h=idle -> idle (hstatus==idle branch precedes raw==running branch)
193
+ assert_eq!(classify_agent_bucket("running", "idle"), SummaryBucket::Idle);
194
+ }
195
+
196
+ #[test]
197
+ fn classify_pure_running() {
198
+ // raw==running, no overriding hstatus -> Running
199
+ assert_eq!(classify_agent_bucket("running", ""), SummaryBucket::Running);
200
+ }
201
+
202
+ #[test]
203
+ fn classify_blocked_and_unmatched_are_unknown_never_idle() {
204
+ // bug-071/077/085: blocked/stuck/missing AND any unmatched value -> Unknown, NOT idle.
205
+ assert_eq!(classify_agent_bucket("blocked", ""), SummaryBucket::Unknown);
206
+ assert_eq!(classify_agent_bucket("stuck", ""), SummaryBucket::Unknown);
207
+ assert_eq!(classify_agent_bucket("", "missing"), SummaryBucket::Unknown);
208
+ assert_eq!(classify_agent_bucket("weird_value", ""), SummaryBucket::Unknown);
209
+ assert_eq!(classify_agent_bucket("", ""), SummaryBucket::Unknown);
210
+ }
211
+
212
+ #[test]
213
+ fn agent_summary_counts_mixed_golden() {
214
+ // golden (empty health): a1 running->Running; a2 busy->Busy; a3 failed->Failed;
215
+ // a4 stopped->Stopped; a5 blocked->Unknown; a6 ""->Unknown; a7 weird->Unknown.
216
+ // => running=1 busy=1 idle=0 stopped=1 failed=1 unknown=3
217
+ let agents = json!({
218
+ "a1": {"status": "running"},
219
+ "a2": {"status": "busy"},
220
+ "a3": {"status": "failed"},
221
+ "a4": {"status": "stopped"},
222
+ "a5": {"status": "blocked"},
223
+ "a6": {"status": ""},
224
+ "a7": {"status": "weird_value"},
225
+ });
226
+ let got = agent_summary_counts(&agents, &json!({}));
227
+ assert_eq!(
228
+ got,
229
+ SummaryCounts { running: 1, busy: 1, idle: 0, stopped: 1, failed: 1, unknown: 3 }
230
+ );
231
+ assert_eq!(got.total(), 7);
232
+ }
233
+
234
+ #[test]
235
+ fn agent_summary_counts_none_agent_is_unknown() {
236
+ // golden: {"x": None} -> unknown=1
237
+ let got = agent_summary_counts(&json!({"x": Value::Null}), &json!({}));
238
+ assert_eq!(got, SummaryCounts { unknown: 1, ..Default::default() });
239
+ }
240
+
241
+ #[test]
242
+ fn agent_summary_counts_uppercase_status_lowercased() {
243
+ // golden: {"x":{"status":"RUNNING"}} -> running=1 (str(...).lower())
244
+ let got = agent_summary_counts(&json!({"x": {"status": "RUNNING"}}), &json!({}));
245
+ assert_eq!(got, SummaryCounts { running: 1, ..Default::default() });
246
+ }
247
+
248
+ // =========================================================================
249
+ // interaction_counts(commands.py:292-306):interacted 非空且≠"never"
250
+ // =========================================================================
251
+
252
+ #[test]
253
+ fn interaction_counts_mixed_golden() {
254
+ // golden: a:"5m ago"->interacted; b:"never"->never; c:""->never; d:{}->never; e:None->never
255
+ // result (1, 4)
256
+ let agents = json!({
257
+ "a": {"interacted": "5m ago"},
258
+ "b": {"interacted": "never"},
259
+ "c": {"interacted": ""},
260
+ "d": {},
261
+ "e": Value::Null,
262
+ });
263
+ let got = interaction_counts(&agents);
264
+ assert_eq!(got, InteractionCounts { interacted: 1, never: 4 });
265
+ }
266
+
267
+ // =========================================================================
268
+ // format_status_summary(commands.py:263-289):五行 triage 字节锁(Gap 18a)
269
+ // =========================================================================
270
+
271
+ #[test]
272
+ fn format_status_summary_full_byte_lock() {
273
+ // golden:
274
+ // coordinator: running schema_ok=True tmux=True
275
+ // receiver: %3 cmd=codex
276
+ // agents: 2 — running=1 busy=1 idle=0 stopped=0 failed=0 unknown=0
277
+ // queued: 2 mailbox messages awaiting delivery
278
+ // latest result: a1 -> did the thing @ -
279
+ let data = json!({
280
+ "coordinator": {"status": "running", "schema_ok": true},
281
+ "leader_receiver": {"pane_id": "%3", "pane_current_command": "codex"},
282
+ "agents": {"a1": {"status": "running"}, "a2": {"status": "busy"}},
283
+ "agent_health": {},
284
+ "tmux_session_present": true,
285
+ "queued_messages": [1, 2],
286
+ "latest_results": [{"agent_id": "a1", "summary": "did the thing", "created_at": Value::Null}],
287
+ });
288
+ let got = format_status_summary(&data);
289
+ let expected = "coordinator: running schema_ok=true tmux=true\n\
290
+ receiver: %3 cmd=codex\n\
291
+ agents: 2 — running=1 busy=1 idle=0 stopped=0 failed=0 unknown=0\n\
292
+ queued: 2 mailbox messages awaiting delivery\n\
293
+ latest result: a1 -> did the thing @ -";
294
+ assert_eq!(got, expected);
295
+ }
296
+
297
+ #[test]
298
+ fn format_status_summary_empty_byte_lock() {
299
+ // golden empty data: stopped/false/false, dashes, 0 counts, none latest.
300
+ let got = format_status_summary(&json!({}));
301
+ let expected = "coordinator: stopped schema_ok=false tmux=false\n\
302
+ receiver: - cmd=-\n\
303
+ agents: 0 — running=0 busy=0 idle=0 stopped=0 failed=0 unknown=0\n\
304
+ queued: 0 mailbox messages awaiting delivery\n\
305
+ latest result: none";
306
+ assert_eq!(got, expected);
307
+ }
308
+
309
+ #[test]
310
+ fn format_status_summary_interacted_marker_appended() {
311
+ // golden: when interacted>0, agents line gets " (1 interacted, 1 never)" suffix.
312
+ let data = json!({
313
+ "coordinator": {},
314
+ "agents": {"a1": {"status": "running", "interacted": "3m"}, "a2": {"status": "idle"}},
315
+ "agent_health": {"a2": {"status": "idle"}},
316
+ });
317
+ let got = format_status_summary(&data);
318
+ let line2 = got.lines().nth(2).unwrap();
319
+ assert_eq!(
320
+ line2,
321
+ "agents: 2 — running=1 busy=0 idle=1 stopped=0 failed=0 unknown=0 (1 interacted, 1 never)"
322
+ );
323
+ }
324
+
325
+ #[test]
326
+ fn format_status_summary_no_interacted_marker_when_zero() {
327
+ // Gap 18a contract: interacted==0 -> line[2] stays byte-identical with NO marker suffix.
328
+ let data = json!({
329
+ "agents": {"a1": {"status": "running"}},
330
+ "agent_health": {},
331
+ });
332
+ let line2 = format_status_summary(&data).lines().nth(2).unwrap().to_string();
333
+ assert_eq!(line2, "agents: 1 — running=1 busy=0 idle=0 stopped=0 failed=0 unknown=0");
334
+ assert!(!line2.contains("interacted"), "no marker when interacted==0");
335
+ }
336
+
337
+ // =========================================================================
338
+ // emit(helpers.py:12-23):--json sort_keys+indent=2 | dict 逐键 | 非 dict
339
+ // =========================================================================
340
+
341
+ #[test]
342
+ fn emit_json_sorted_indented() {
343
+ // golden json.dumps(indent=2, sort_keys=True): keys sorted a,b,nested; nested list expanded.
344
+ let out = emit(&CmdOutput::Json(json!({"b": 2, "a": 1, "nested": {"x": [1, 2]}})), true)
345
+ .expect("json emit returns Some");
346
+ let expected = "{\n \"a\": 1,\n \"b\": 2,\n \"nested\": {\n \"x\": [\n 1,\n 2\n ]\n }\n}";
347
+ assert_eq!(out, expected);
348
+ }
349
+
350
+ #[test]
351
+ fn emit_dict_human_per_key() {
352
+ // golden human dict: scalar -> "key: value"; dict/list -> compact json value.
353
+ // KEY INSERTION ORDER preserved (NOT sorted) in non-json path.
354
+ let out = emit(
355
+ &CmdOutput::Json(json!({"key1": "val1", "nested": {"a": 1}, "lst": [1, 2]})),
356
+ false,
357
+ )
358
+ .expect("dict human emit returns Some");
359
+ let expected = "key1: val1\nnested: {\"a\": 1}\nlst: [1, 2]";
360
+ assert_eq!(out, expected);
361
+ }
362
+
363
+ #[test]
364
+ fn emit_human_non_dict_passthrough() {
365
+ // golden: non-dict (Human string) printed raw.
366
+ let out = emit(&CmdOutput::Human("just a string".into()), false)
367
+ .expect("human emit returns Some");
368
+ assert_eq!(out, "just a string");
369
+ }
370
+
371
+ #[test]
372
+ fn emit_none_output_produces_nothing() {
373
+ // passthrough/watch: CmdOutput::None never reaches emit -> None (no stdout line).
374
+ assert_eq!(emit(&CmdOutput::None, false), None);
375
+ assert_eq!(emit(&CmdOutput::None, true), None);
376
+ }
377
+
378
+ // =========================================================================
379
+ // CliError::to_payload(helpers.py:137-187):稳定信封 + tmux 冲突富化
380
+ // =========================================================================
381
+
382
+ #[test]
383
+ fn cli_error_payload_plain_runtime() {
384
+ // golden plain: ok=false, error=str(exc), action=generic, log=path, NO reason/session/next.
385
+ let err = CliError::Runtime("some other error".into());
386
+ let payload = err.to_payload(Path::new("/tmp/y.log"), "status");
387
+ assert!(!payload.ok);
388
+ assert_eq!(payload.error, "some other error");
389
+ assert_eq!(payload.action, "run `team-agent doctor` or inspect the log path shown here");
390
+ assert_eq!(payload.log, "/tmp/y.log");
391
+ assert_eq!(payload.reason, None);
392
+ assert_eq!(payload.session_name, None);
393
+ assert_eq!(payload.next_actions, None);
394
+ }
395
+
396
+ #[test]
397
+ fn cli_error_payload_tmux_conflict_quick_start_enrichment() {
398
+ // golden quick-start enrichment (exact bytes):
399
+ let err = CliError::Runtime("tmux session already exists: my-team. Startup aborted".into());
400
+ let payload = err.to_payload(Path::new("/tmp/cli-error-123.log"), "quick-start");
401
+ assert_eq!(payload.reason.as_deref(), Some("tmux_session_name_conflict"));
402
+ assert_eq!(payload.session_name.as_deref(), Some("my-team"));
403
+ assert_eq!(
404
+ payload.action,
405
+ "tmux session `my-team` already exists. It may be an active team. \
406
+ Do not terminate existing tmux sessions from quick-start; \
407
+ change `name:` in TEAM.md and run quick-start again."
408
+ );
409
+ assert_eq!(
410
+ payload.next_actions,
411
+ Some(vec!["Change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string()])
412
+ );
413
+ }
414
+
415
+ #[test]
416
+ fn cli_error_payload_tmux_conflict_non_quick_start_enrichment() {
417
+ // golden non-quick-start (command="restart") enrichment uses generic startup wording.
418
+ let err = CliError::Runtime("tmux session already exists: my-team. Startup aborted".into());
419
+ let payload = err.to_payload(Path::new("/tmp/x.log"), "restart");
420
+ assert_eq!(payload.session_name.as_deref(), Some("my-team"));
421
+ assert_eq!(
422
+ payload.action,
423
+ "tmux session `my-team` already exists. It may be an active team. \
424
+ Do not terminate existing tmux sessions from startup; \
425
+ use a different team name or runtime.session_name and start again."
426
+ );
427
+ assert_eq!(
428
+ payload.next_actions,
429
+ Some(vec!["Use a different team name or runtime.session_name before starting again.".to_string()])
430
+ );
431
+ }
432
+
433
+ #[test]
434
+ fn cli_error_payload_json_shape_serializes_optional_fields_skipped() {
435
+ // skip_serializing_if for reason/session_name/next_actions: plain payload omits them.
436
+ let err = CliError::Runtime("boom".into());
437
+ let payload = err.to_payload(Path::new("/tmp/z.log"), "status");
438
+ let v = serde_json::to_value(&payload).unwrap();
439
+ let obj = v.as_object().unwrap();
440
+ assert!(!obj.contains_key("reason"), "reason omitted on plain error");
441
+ assert!(!obj.contains_key("session_name"));
442
+ assert!(!obj.contains_key("next_actions"));
443
+ assert_eq!(obj.get("ok"), Some(&json!(false)));
444
+ }
445
+
446
+ #[test]
447
+ fn consume_inbox_missing_file_returns_none() {
448
+ // helpers.py:30-31: inbox_path absent -> None (no crash).
449
+ let ws = tmp_workspace();
450
+ assert_eq!(consume_leader_inbox_summary(&ws, 500), None);
451
+ let _ = std::fs::remove_dir_all(&ws);
452
+ }
453
+
454
+ #[test]
455
+ fn consume_inbox_single_entry_summary_and_cursor_advance() {
456
+ // golden _leader_inbox_summary single entry:
457
+ // "Leader inbox: 1 new fallback entry\n- Hello world message\nHint: team-agent inbox leader"
458
+ let ws = tmp_workspace();
459
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
460
+ std::fs::write(&inbox, "[x fallback]\nHello world message").unwrap();
461
+ let summary = consume_leader_inbox_summary(&ws, 500).expect("new entry -> Some");
462
+ assert_eq!(
463
+ summary,
464
+ "Leader inbox: 1 new fallback entry\n- Hello world message\nHint: team-agent inbox leader"
465
+ );
466
+ // cursor advanced: a second call with no new bytes -> None (offset==size).
467
+ assert_eq!(consume_leader_inbox_summary(&ws, 500), None);
468
+ let _ = std::fs::remove_dir_all(&ws);
469
+ }
470
+
471
+ #[test]
472
+ fn consume_inbox_two_entries_plural() {
473
+ // golden two-entry summary uses plural "entries".
474
+ let ws = tmp_workspace();
475
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
476
+ std::fs::write(&inbox, "[a fallback]\nFirst msg\n[b fallback]\nSecond msg").unwrap();
477
+ let summary = consume_leader_inbox_summary(&ws, 500).expect("Some");
478
+ assert_eq!(
479
+ summary,
480
+ "Leader inbox: 2 new fallback entries\n- First msg\n- Second msg\nHint: team-agent inbox leader"
481
+ );
482
+ let _ = std::fs::remove_dir_all(&ws);
483
+ }
484
+
485
+ #[test]
486
+ fn consume_inbox_budget_truncation_footer() {
487
+ // golden budget=200: header + 2 lines then truncation footer (exact bytes from Python).
488
+ let ws = tmp_workspace();
489
+ let inbox = ws.join(".team").join("runtime").join("leader-inbox.log");
490
+ let many: String = (0..20)
491
+ .map(|i| format!("[e{i} fallback]\nMessage number {i} with some text padding here"))
492
+ .collect::<Vec<_>>()
493
+ .join("\n");
494
+ std::fs::write(&inbox, &many).unwrap();
495
+ let summary = consume_leader_inbox_summary(&ws, 200).expect("Some");
496
+ let expected = "Leader inbox: 20 new fallback entries\n\
497
+ - Message number 0 with some text padding here\n\
498
+ - Message number 1 with some text padd ...\n\
499
+ Truncated: more fallback entries available; run team-agent inbox leader";
500
+ assert_eq!(summary, expected);
501
+ let _ = std::fs::remove_dir_all(&ws);
502
+ }
503
+
504
+ // =========================================================================
505
+ // CmdResult::from_json(parser.py:507-508):ok is False -> ExitCode::Error
506
+ // =========================================================================
507
+
508
+ #[test]
509
+ fn cmd_result_from_json_ok_true_exits_ok() {
510
+ let r = CmdResult::from_json(json!({"ok": true, "x": 1}), true);
511
+ assert_eq!(r.exit, ExitCode::Ok);
512
+ assert!(r.as_json);
513
+ assert_eq!(r.output, CmdOutput::Json(json!({"ok": true, "x": 1})));
514
+ }
515
+
516
+ #[test]
517
+ fn cmd_result_from_json_ok_false_exits_error() {
518
+ // parser.py:507: result.get("ok") is False -> SystemExit(1)
519
+ let r = CmdResult::from_json(json!({"ok": false, "error": "x"}), false);
520
+ assert_eq!(r.exit, ExitCode::Error);
521
+ assert!(!r.as_json);
522
+ }
523
+
524
+ #[test]
525
+ fn cmd_result_from_json_missing_ok_exits_ok() {
526
+ // result with NO "ok" key: `result.get("ok") is False` is False -> NOT an error (exit Ok).
527
+ // None-vs-missing: absence of ok != ok:false.
528
+ let r = CmdResult::from_json(json!({"summary": "fine"}), false);
529
+ assert_eq!(r.exit, ExitCode::Ok);
530
+ }
531
+
532
+ #[test]
533
+ fn exit_code_numeric() {
534
+ assert_eq!(ExitCode::Ok.code(), 0);
535
+ assert_eq!(ExitCode::Error.code(), 1);
536
+ }
537
+
538
+ // =========================================================================
539
+ // cmd_doctor 分派(commands.py:218-260):--fix 缺 gate -> Usage err
540
+ // =========================================================================
541
+
542
+ #[test]
543
+ fn cmd_doctor_fix_without_gate_is_usage_error() {
544
+ // commands.py:220-221: --fix and not gate -> TeamAgentError("--fix requires --gate")
545
+ let args = DoctorArgs {
546
+ spec: None,
547
+ workspace: PathBuf::from("."),
548
+ gate: None,
549
+ comms: false,
550
+ team: None,
551
+ fix: true,
552
+ fix_schema: false,
553
+ cleanup_orphans: false,
554
+ confirm: false,
555
+ json: false,
556
+ };
557
+ let err = cmd_doctor(&args).unwrap_err();
558
+ let msg = err.to_string();
559
+ assert!(
560
+ msg.contains("--fix requires --gate"),
561
+ "expected '--fix requires --gate', got: {msg}"
562
+ );
563
+ }
564
+
565
+ // =========================================================================
566
+ // cmd_status 三态互斥(commands.py:90-100)
567
+ // =========================================================================
568
+
569
+ #[test]
570
+ fn cmd_status_summary_with_json_is_mutually_exclusive() {
571
+ // commands.py:92-93: --summary and --json -> TeamAgentError(mutually exclusive)
572
+ let args = StatusArgs {
573
+ agent: None,
574
+ workspace: PathBuf::from("."),
575
+ detail: false,
576
+ summary: true,
577
+ json: true,
578
+ };
579
+ let err = cmd_status(&args).unwrap_err();
580
+ assert!(
581
+ err.to_string().contains("--summary and --json are mutually exclusive"),
582
+ "got: {err}"
583
+ );
584
+ }
585
+
586
+ #[test]
587
+ fn cmd_status_summary_with_agent_rejected() {
588
+ // commands.py:94-95: --summary + agent -> TeamAgentError(does not accept an agent argument)
589
+ let args = StatusArgs {
590
+ agent: Some("a1".into()),
591
+ workspace: PathBuf::from("."),
592
+ detail: false,
593
+ summary: true,
594
+ json: false,
595
+ };
596
+ let err = cmd_status(&args).unwrap_err();
597
+ assert!(
598
+ err.to_string().contains("status --summary does not accept an agent argument"),
599
+ "got: {err}"
600
+ );
601
+ }
602
+
603
+ // =========================================================================
604
+ // cmd_leader_passthrough(parser.py:515-522):-h/--help 早返回 CmdResult::none
605
+ // =========================================================================
606
+
607
+ #[test]
608
+ fn cmd_leader_passthrough_help_returns_none() {
609
+ // parser.py:516: provider_args in (["-h"],["--help"]) -> print usage, return (no emit).
610
+ let r = cmd_leader_passthrough("codex", &["-h".into()], Path::new(".")).unwrap();
611
+ assert_eq!(r.output, CmdOutput::None);
612
+ assert_eq!(r.exit, ExitCode::Ok);
613
+ let r2 = cmd_leader_passthrough("claude", &["--help".into()], Path::new(".")).unwrap();
614
+ assert_eq!(r2.output, CmdOutput::None);
615
+ }
616
+