@team-agent/installer 0.2.11 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (326) hide show
  1. package/Cargo.lock +744 -0
  2. package/Cargo.toml +34 -0
  3. package/crates/team-agent/Cargo.toml +33 -0
  4. package/crates/team-agent/src/cli/adapters.rs +1343 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +554 -0
  6. package/crates/team-agent/src/cli/emit.rs +1077 -0
  7. package/crates/team-agent/src/cli/helpers.rs +88 -0
  8. package/crates/team-agent/src/cli/leader.rs +216 -0
  9. package/crates/team-agent/src/cli/mod.rs +1141 -0
  10. package/crates/team-agent/src/cli/profile.rs +306 -0
  11. package/crates/team-agent/src/cli/send.rs +215 -0
  12. package/crates/team-agent/src/cli/status.rs +179 -0
  13. package/crates/team-agent/src/cli/status_port.rs +502 -0
  14. package/crates/team-agent/src/cli/tests/base.rs +616 -0
  15. package/crates/team-agent/src/cli/tests/compile.rs +96 -0
  16. package/crates/team-agent/src/cli/tests/divergence.rs +509 -0
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +333 -0
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +395 -0
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +675 -0
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +390 -0
  21. package/crates/team-agent/src/cli/tests/mod.rs +97 -0
  22. package/crates/team-agent/src/cli/tests/peer_allow.rs +137 -0
  23. package/crates/team-agent/src/cli/tests/repair_state_byte_lock.rs +302 -0
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +305 -0
  25. package/crates/team-agent/src/cli/tests/status_send.rs +385 -0
  26. package/crates/team-agent/src/cli/tests/verb_profile.rs +182 -0
  27. package/crates/team-agent/src/cli/tests/verb_settle.rs +236 -0
  28. package/crates/team-agent/src/cli/tests/verb_validate.rs +184 -0
  29. package/crates/team-agent/src/cli/types.rs +605 -0
  30. package/crates/team-agent/src/compiler/tests.rs +701 -0
  31. package/crates/team-agent/src/compiler.rs +489 -0
  32. package/crates/team-agent/src/coordinator/backoff.rs +153 -0
  33. package/crates/team-agent/src/coordinator/health.rs +436 -0
  34. package/crates/team-agent/src/coordinator/mod.rs +80 -0
  35. package/crates/team-agent/src/coordinator/orphan.rs +179 -0
  36. package/crates/team-agent/src/coordinator/tests/abnormal.rs +255 -0
  37. package/crates/team-agent/src/coordinator/tests/basics.rs +262 -0
  38. package/crates/team-agent/src/coordinator/tests/daemon.rs +323 -0
  39. package/crates/team-agent/src/coordinator/tests/health_sync.rs +263 -0
  40. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +136 -0
  41. package/crates/team-agent/src/coordinator/tests/mod.rs +310 -0
  42. package/crates/team-agent/src/coordinator/tests/spine.rs +261 -0
  43. package/crates/team-agent/src/coordinator/tests/takeover.rs +227 -0
  44. package/crates/team-agent/src/coordinator/tests/tick_core.rs +256 -0
  45. package/crates/team-agent/src/coordinator/tests/watch.rs +167 -0
  46. package/crates/team-agent/src/coordinator/tick.rs +2032 -0
  47. package/crates/team-agent/src/coordinator/types.rs +584 -0
  48. package/crates/team-agent/src/db/migration.rs +716 -0
  49. package/crates/team-agent/src/db/mod.rs +23 -0
  50. package/crates/team-agent/src/db/schema.rs +378 -0
  51. package/crates/team-agent/src/event_log.rs +375 -0
  52. package/crates/team-agent/src/fake_worker.rs +253 -0
  53. package/crates/team-agent/src/leader/helpers.rs +190 -0
  54. package/crates/team-agent/src/leader/inject.rs +33 -0
  55. package/crates/team-agent/src/leader/lease.rs +1063 -0
  56. package/crates/team-agent/src/leader/mod.rs +99 -0
  57. package/crates/team-agent/src/leader/owner_bind.rs +292 -0
  58. package/crates/team-agent/src/leader/rediscover/tests.rs +525 -0
  59. package/crates/team-agent/src/leader/rediscover.rs +1099 -0
  60. package/crates/team-agent/src/leader/start.rs +273 -0
  61. package/crates/team-agent/src/leader/takeover.rs +235 -0
  62. package/crates/team-agent/src/leader/tests/basics.rs +183 -0
  63. package/crates/team-agent/src/leader/tests/byte_findings.rs +234 -0
  64. package/crates/team-agent/src/leader/tests/identity.rs +206 -0
  65. package/crates/team-agent/src/leader/tests/idle.rs +271 -0
  66. package/crates/team-agent/src/leader/tests/lease_api.rs +225 -0
  67. package/crates/team-agent/src/leader/tests/lease_claim.rs +253 -0
  68. package/crates/team-agent/src/leader/tests/mod.rs +125 -0
  69. package/crates/team-agent/src/leader/tests/rediscover.rs +351 -0
  70. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +204 -0
  71. package/crates/team-agent/src/leader/types.rs +487 -0
  72. package/crates/team-agent/src/lib.rs +85 -0
  73. package/crates/team-agent/src/lifecycle/display.rs +228 -0
  74. package/crates/team-agent/src/lifecycle/helpers.rs +112 -0
  75. package/crates/team-agent/src/lifecycle/launch/plan.rs +227 -0
  76. package/crates/team-agent/src/lifecycle/launch.rs +1833 -0
  77. package/crates/team-agent/src/lifecycle/mod.rs +62 -0
  78. package/crates/team-agent/src/lifecycle/restart/agent.rs +533 -0
  79. package/crates/team-agent/src/lifecycle/restart/common.rs +517 -0
  80. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +41 -0
  81. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +268 -0
  82. package/crates/team-agent/src/lifecycle/restart/remove.rs +780 -0
  83. package/crates/team-agent/src/lifecycle/restart/selection.rs +208 -0
  84. package/crates/team-agent/src/lifecycle/restart/team_state.rs +242 -0
  85. package/crates/team-agent/src/lifecycle/restart.rs +76 -0
  86. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +455 -0
  87. package/crates/team-agent/src/lifecycle/tests/core.rs +989 -0
  88. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +583 -0
  89. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +933 -0
  90. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +265 -0
  91. package/crates/team-agent/src/lifecycle/tests.rs +27 -0
  92. package/crates/team-agent/src/lifecycle/types.rs +685 -0
  93. package/crates/team-agent/src/main.rs +41 -0
  94. package/crates/team-agent/src/mcp_server/helpers.rs +228 -0
  95. package/crates/team-agent/src/mcp_server/mod.rs +183 -0
  96. package/crates/team-agent/src/mcp_server/normalize.rs +312 -0
  97. package/crates/team-agent/src/mcp_server/tests/golden.rs +283 -0
  98. package/crates/team-agent/src/mcp_server/tests/normalize.rs +244 -0
  99. package/crates/team-agent/src/mcp_server/tests/scoped.rs +189 -0
  100. package/crates/team-agent/src/mcp_server/tests/send.rs +222 -0
  101. package/crates/team-agent/src/mcp_server/tests/tools.rs +158 -0
  102. package/crates/team-agent/src/mcp_server/tests/wire.rs +159 -0
  103. package/crates/team-agent/src/mcp_server/tests.rs +38 -0
  104. package/crates/team-agent/src/mcp_server/tools.rs +603 -0
  105. package/crates/team-agent/src/mcp_server/types.rs +421 -0
  106. package/crates/team-agent/src/mcp_server/wire.rs +388 -0
  107. package/crates/team-agent/src/message_store.rs +767 -0
  108. package/crates/team-agent/src/messaging/activity.rs +433 -0
  109. package/crates/team-agent/src/messaging/delivery.rs +542 -0
  110. package/crates/team-agent/src/messaging/helpers.rs +209 -0
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +340 -0
  112. package/crates/team-agent/src/messaging/mod.rs +147 -0
  113. package/crates/team-agent/src/messaging/peers.rs +32 -0
  114. package/crates/team-agent/src/messaging/results.rs +537 -0
  115. package/crates/team-agent/src/messaging/scheduler.rs +344 -0
  116. package/crates/team-agent/src/messaging/selftest.rs +100 -0
  117. package/crates/team-agent/src/messaging/send.rs +582 -0
  118. package/crates/team-agent/src/messaging/tests/basic.rs +357 -0
  119. package/crates/team-agent/src/messaging/tests/main_preserved.rs +122 -0
  120. package/crates/team-agent/src/messaging/tests/mod.rs +293 -0
  121. package/crates/team-agent/src/messaging/tests/runtime.rs +1422 -0
  122. package/crates/team-agent/src/messaging/tests/spine.rs +437 -0
  123. package/crates/team-agent/src/messaging/trust.rs +192 -0
  124. package/crates/team-agent/src/messaging/types.rs +355 -0
  125. package/crates/team-agent/src/messaging/watchers.rs +591 -0
  126. package/crates/team-agent/src/model/enums.rs +311 -0
  127. package/crates/team-agent/src/model/errors.rs +17 -0
  128. package/crates/team-agent/src/model/ids.rs +155 -0
  129. package/crates/team-agent/src/model/mod.rs +22 -0
  130. package/crates/team-agent/src/model/paths.rs +228 -0
  131. package/crates/team-agent/src/model/permissions.rs +567 -0
  132. package/crates/team-agent/src/model/routing.rs +340 -0
  133. package/crates/team-agent/src/model/spec.rs +680 -0
  134. package/crates/team-agent/src/model/task_graph.rs +380 -0
  135. package/crates/team-agent/src/model/testdata/fuzz.golden.yaml +43 -0
  136. package/crates/team-agent/src/model/testdata/fuzz.yaml +43 -0
  137. package/crates/team-agent/src/model/testdata/spec_invalid_a.yaml +207 -0
  138. package/crates/team-agent/src/model/testdata/team.spec.golden.yaml +206 -0
  139. package/crates/team-agent/src/model/testdata/team.spec.yaml +206 -0
  140. package/crates/team-agent/src/model/yaml/tests.rs +288 -0
  141. package/crates/team-agent/src/model/yaml.rs +800 -0
  142. package/crates/team-agent/src/packaging/install.rs +305 -0
  143. package/crates/team-agent/src/packaging/migrate.rs +30 -0
  144. package/crates/team-agent/src/packaging/mod.rs +82 -0
  145. package/crates/team-agent/src/packaging/repair.rs +24 -0
  146. package/crates/team-agent/src/packaging/tests.rs +829 -0
  147. package/crates/team-agent/src/packaging/types.rs +369 -0
  148. package/crates/team-agent/src/provider/adapter.rs +801 -0
  149. package/crates/team-agent/src/provider/approvals/mod.rs +2 -0
  150. package/crates/team-agent/src/provider/approvals/parsing.rs +452 -0
  151. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +163 -0
  152. package/crates/team-agent/src/provider/classify.rs +456 -0
  153. package/crates/team-agent/src/provider/faults.rs +136 -0
  154. package/crates/team-agent/src/provider/helpers.rs +41 -0
  155. package/crates/team-agent/src/provider/mod.rs +53 -0
  156. package/crates/team-agent/src/provider/startup_prompt.rs +423 -0
  157. package/crates/team-agent/src/provider/tests/adapter.rs +239 -0
  158. package/crates/team-agent/src/provider/tests/classify.rs +240 -0
  159. package/crates/team-agent/src/provider/tests/faults.rs +120 -0
  160. package/crates/team-agent/src/provider/tests/idle.rs +208 -0
  161. package/crates/team-agent/src/provider/tests/wire.rs +213 -0
  162. package/crates/team-agent/src/provider/tests.rs +31 -0
  163. package/crates/team-agent/src/provider/types.rs +424 -0
  164. package/crates/team-agent/src/state/identity.rs +656 -0
  165. package/crates/team-agent/src/state/mod.rs +58 -0
  166. package/crates/team-agent/src/state/owner_gate.rs +423 -0
  167. package/crates/team-agent/src/state/persist.rs +712 -0
  168. package/crates/team-agent/src/state/projection.rs +657 -0
  169. package/crates/team-agent/src/state/selector.rs +105 -0
  170. package/crates/team-agent/src/state/testdata/state-rich.canonical.json +133 -0
  171. package/crates/team-agent/src/tmux_backend/tests.rs +586 -0
  172. package/crates/team-agent/src/tmux_backend.rs +758 -0
  173. package/crates/team-agent/src/transport/test_support.rs +252 -0
  174. package/crates/team-agent/src/transport/tests/behavior.rs +327 -0
  175. package/crates/team-agent/src/transport/tests/mod.rs +199 -0
  176. package/crates/team-agent/src/transport/tests/wire.rs +527 -0
  177. package/crates/team-agent/src/transport.rs +774 -0
  178. package/npm/install.mjs +90 -106
  179. package/package.json +15 -13
  180. package/crates/team-agent-core/Cargo.toml +0 -12
  181. package/crates/team-agent-core/src/lib.rs +0 -332
  182. package/crates/team-agent-core/src/main.rs +0 -152
  183. package/pyproject.toml +0 -18
  184. package/scripts/install.py +0 -88
  185. package/scripts/run_regression_tests.py +0 -83
  186. package/src/team_agent/__init__.py +0 -3
  187. package/src/team_agent/__main__.py +0 -5
  188. package/src/team_agent/_legacy_pane_discovery.py +0 -186
  189. package/src/team_agent/abnormal_track.py +0 -253
  190. package/src/team_agent/approvals/__init__.py +0 -65
  191. package/src/team_agent/approvals/constants.py +0 -6
  192. package/src/team_agent/approvals/parsing.py +0 -176
  193. package/src/team_agent/approvals/runtime_prompts.py +0 -171
  194. package/src/team_agent/approvals/status.py +0 -176
  195. package/src/team_agent/cli/__init__.py +0 -137
  196. package/src/team_agent/cli/commands.py +0 -481
  197. package/src/team_agent/cli/e2e.py +0 -202
  198. package/src/team_agent/cli/helpers.py +0 -226
  199. package/src/team_agent/cli/parser.py +0 -540
  200. package/src/team_agent/compiler.py +0 -334
  201. package/src/team_agent/coordinator/__init__.py +0 -53
  202. package/src/team_agent/coordinator/__main__.py +0 -119
  203. package/src/team_agent/coordinator/lifecycle.py +0 -411
  204. package/src/team_agent/coordinator/metadata.py +0 -61
  205. package/src/team_agent/coordinator/paths.py +0 -17
  206. package/src/team_agent/diagnose/__init__.py +0 -48
  207. package/src/team_agent/diagnose/checks.py +0 -101
  208. package/src/team_agent/diagnose/comms.py +0 -213
  209. package/src/team_agent/diagnose/health.py +0 -241
  210. package/src/team_agent/diagnose/orphan_cleanup.py +0 -364
  211. package/src/team_agent/diagnose/preflight.py +0 -194
  212. package/src/team_agent/diagnose/quick_start.py +0 -324
  213. package/src/team_agent/display/__init__.py +0 -92
  214. package/src/team_agent/display/adaptive.py +0 -511
  215. package/src/team_agent/display/backend.py +0 -46
  216. package/src/team_agent/display/close.py +0 -154
  217. package/src/team_agent/display/ghostty.py +0 -77
  218. package/src/team_agent/display/rebuild.py +0 -102
  219. package/src/team_agent/display/tiling.py +0 -156
  220. package/src/team_agent/display/worker_window.py +0 -114
  221. package/src/team_agent/display/workspace.py +0 -382
  222. package/src/team_agent/errors.py +0 -10
  223. package/src/team_agent/events.py +0 -84
  224. package/src/team_agent/fake_worker.py +0 -80
  225. package/src/team_agent/idle_predicate.py +0 -218
  226. package/src/team_agent/idle_takeover.py +0 -59
  227. package/src/team_agent/idle_takeover_wiring.py +0 -114
  228. package/src/team_agent/launch/__init__.py +0 -41
  229. package/src/team_agent/launch/bootstrap.py +0 -85
  230. package/src/team_agent/launch/config.py +0 -106
  231. package/src/team_agent/launch/core.py +0 -301
  232. package/src/team_agent/launch/requirements.py +0 -57
  233. package/src/team_agent/leader/__init__.py +0 -926
  234. package/src/team_agent/leader_binding.py +0 -183
  235. package/src/team_agent/lifecycle/__init__.py +0 -5
  236. package/src/team_agent/lifecycle/agents.py +0 -278
  237. package/src/team_agent/lifecycle/operations.py +0 -411
  238. package/src/team_agent/lifecycle/paste_buffer_hygiene.py +0 -39
  239. package/src/team_agent/lifecycle/start.py +0 -363
  240. package/src/team_agent/mcp_server/__init__.py +0 -42
  241. package/src/team_agent/mcp_server/__main__.py +0 -7
  242. package/src/team_agent/mcp_server/contracts.py +0 -148
  243. package/src/team_agent/mcp_server/normalize.py +0 -257
  244. package/src/team_agent/mcp_server/server.py +0 -150
  245. package/src/team_agent/mcp_server/tools.py +0 -352
  246. package/src/team_agent/message_store/__init__.py +0 -23
  247. package/src/team_agent/message_store/agent_health.py +0 -113
  248. package/src/team_agent/message_store/core.py +0 -497
  249. package/src/team_agent/message_store/leader_notification_log.py +0 -198
  250. package/src/team_agent/message_store/result_watchers.py +0 -251
  251. package/src/team_agent/message_store/schema.py +0 -308
  252. package/src/team_agent/message_store/schema_migration.py +0 -448
  253. package/src/team_agent/messaging/__init__.py +0 -1
  254. package/src/team_agent/messaging/activity_detector.py +0 -262
  255. package/src/team_agent/messaging/delivery.py +0 -504
  256. package/src/team_agent/messaging/deps.py +0 -247
  257. package/src/team_agent/messaging/idle_alerts.py +0 -423
  258. package/src/team_agent/messaging/internal_delivery.py +0 -46
  259. package/src/team_agent/messaging/leader.py +0 -497
  260. package/src/team_agent/messaging/leader_api_errors.py +0 -216
  261. package/src/team_agent/messaging/leader_panes.py +0 -673
  262. package/src/team_agent/messaging/owner_bypass.py +0 -29
  263. package/src/team_agent/messaging/result_delivery.py +0 -539
  264. package/src/team_agent/messaging/results.py +0 -447
  265. package/src/team_agent/messaging/scheduler.py +0 -450
  266. package/src/team_agent/messaging/send.py +0 -532
  267. package/src/team_agent/messaging/session_drift.py +0 -94
  268. package/src/team_agent/messaging/tmux_io.py +0 -506
  269. package/src/team_agent/messaging/tmux_prompt.py +0 -338
  270. package/src/team_agent/messaging/trust_auto_answer.py +0 -52
  271. package/src/team_agent/orchestrator/__init__.py +0 -376
  272. package/src/team_agent/orchestrator/plan.py +0 -122
  273. package/src/team_agent/orchestrator/state.py +0 -128
  274. package/src/team_agent/paths.py +0 -45
  275. package/src/team_agent/permissions.py +0 -123
  276. package/src/team_agent/profiles/__init__.py +0 -82
  277. package/src/team_agent/profiles/constants.py +0 -19
  278. package/src/team_agent/profiles/core.py +0 -407
  279. package/src/team_agent/profiles/helpers.py +0 -69
  280. package/src/team_agent/profiles/provider_env.py +0 -188
  281. package/src/team_agent/profiles/smoke.py +0 -201
  282. package/src/team_agent/provider_cli/__init__.py +0 -43
  283. package/src/team_agent/provider_cli/adapter.py +0 -172
  284. package/src/team_agent/provider_cli/base.py +0 -48
  285. package/src/team_agent/provider_cli/claude.py +0 -503
  286. package/src/team_agent/provider_cli/codex.py +0 -336
  287. package/src/team_agent/provider_cli/copilot.py +0 -8
  288. package/src/team_agent/provider_cli/fake.py +0 -39
  289. package/src/team_agent/provider_cli/gemini.py +0 -95
  290. package/src/team_agent/provider_cli/opencode.py +0 -8
  291. package/src/team_agent/provider_cli/prompt.py +0 -62
  292. package/src/team_agent/provider_cli/registry.py +0 -18
  293. package/src/team_agent/provider_cli/unsupported.py +0 -32
  294. package/src/team_agent/provider_state/README.md +0 -78
  295. package/src/team_agent/provider_state/__init__.py +0 -91
  296. package/src/team_agent/provider_state/claude.py +0 -86
  297. package/src/team_agent/provider_state/codex.py +0 -84
  298. package/src/team_agent/provider_state/common.py +0 -207
  299. package/src/team_agent/provider_state/registry.py +0 -118
  300. package/src/team_agent/providers.py +0 -163
  301. package/src/team_agent/quality_gates.py +0 -104
  302. package/src/team_agent/restart/__init__.py +0 -34
  303. package/src/team_agent/restart/orchestration.py +0 -554
  304. package/src/team_agent/restart/selection.py +0 -89
  305. package/src/team_agent/restart/snapshot.py +0 -70
  306. package/src/team_agent/routing.py +0 -84
  307. package/src/team_agent/runtime.py +0 -1243
  308. package/src/team_agent/rust_core.py +0 -327
  309. package/src/team_agent/sessions/__init__.py +0 -25
  310. package/src/team_agent/sessions/capture.py +0 -144
  311. package/src/team_agent/sessions/inventory.py +0 -44
  312. package/src/team_agent/sessions/resume.py +0 -135
  313. package/src/team_agent/simple_yaml.py +0 -236
  314. package/src/team_agent/spec.py +0 -370
  315. package/src/team_agent/state.py +0 -693
  316. package/src/team_agent/status/__init__.py +0 -63
  317. package/src/team_agent/status/approvals.py +0 -52
  318. package/src/team_agent/status/compact.py +0 -158
  319. package/src/team_agent/status/constants.py +0 -18
  320. package/src/team_agent/status/inbox.py +0 -58
  321. package/src/team_agent/status/peek.py +0 -117
  322. package/src/team_agent/status/queries.py +0 -199
  323. package/src/team_agent/task_graph.py +0 -80
  324. package/src/team_agent/terminal.py +0 -57
  325. package/src/team_agent/wake.py +0 -58
  326. package/src/team_agent/watch/__init__.py +0 -145
@@ -0,0 +1,586 @@
1
+ //! TMUX-BACKEND RED — every `Transport` method is `unimplemented!()` today, so these PANIC (RED)
2
+ //! until the porter wires the bodies + `RealCommandRunner`. The OS edge is mocked by
3
+ //! `MockCommandRunner` (records each argv; returns canned `CommandOutput`/io::Error you stage).
4
+ //! Each test asserts (1) the recorded argv == the golden-locked `transport::tmux_*_argv` builder
5
+ //! (or the golden command form for builder-less ops) and (2) the parsed typed return. Golden:
6
+ //! runtime.py (has-session/spawn/kill), leader/__init__.py:335 (set-environment), state.py:341
7
+ //! (_tmux_pane_liveness three-state, §bug-085 unknown != dead), transport.rs argv-builders.
8
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
9
+
10
+ use std::collections::{BTreeMap, VecDeque};
11
+ use std::path::Path;
12
+ use std::sync::{Arc, Mutex};
13
+
14
+ use super::{CommandOutput, CommandRunner, RealCommandRunner, TmuxBackend};
15
+ use crate::model::enums::PaneLiveness;
16
+ use crate::transport::{
17
+ normalize_capture, tmux_capture_argv, tmux_query_argv, tmux_send_keys_argv, tmux_spawn_argv,
18
+ AttachOutcome, CaptureRange, InjectPayload, InjectStage, InjectVerification, Key, PaneField,
19
+ PaneId, SessionName, SetEnvOutcome, SubmitVerification, Target, Transport, TransportError,
20
+ TurnVerification, WindowName,
21
+ };
22
+
23
+ type RecordedArgv = Arc<Mutex<Vec<Vec<String>>>>;
24
+ type RecordedStdin = Arc<Mutex<Vec<String>>>;
25
+
26
+ /// A staged runner response: a canned `CommandOutput`, or an io::Error (kind) for the error path.
27
+ #[derive(Clone)]
28
+ enum MockResp {
29
+ Out(CommandOutput),
30
+ Io(std::io::ErrorKind),
31
+ }
32
+
33
+ /// Records every argv it is asked to run; replays staged responses (then a default).
34
+ struct MockCommandRunner {
35
+ recorded: RecordedArgv,
36
+ stdin_recorded: RecordedStdin,
37
+ queue: Mutex<VecDeque<MockResp>>,
38
+ default: MockResp,
39
+ }
40
+
41
+ impl CommandRunner for MockCommandRunner {
42
+ fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
43
+ self.recorded.lock().unwrap().push(argv.to_vec());
44
+ let resp = self.queue.lock().unwrap().pop_front().unwrap_or_else(|| self.default.clone());
45
+ match resp {
46
+ MockResp::Out(o) => Ok(o),
47
+ MockResp::Io(kind) => Err(std::io::Error::new(kind, "mock runner io error")),
48
+ }
49
+ }
50
+
51
+ fn run_with_stdin(
52
+ &self,
53
+ argv: &[String],
54
+ stdin: &str,
55
+ ) -> Result<CommandOutput, std::io::Error> {
56
+ self.stdin_recorded.lock().unwrap().push(stdin.to_string());
57
+ self.run(argv)
58
+ }
59
+ }
60
+
61
+ fn ok(stdout: &str) -> CommandOutput {
62
+ CommandOutput { success: true, code: Some(0), stdout: stdout.to_string(), stderr: String::new() }
63
+ }
64
+ fn fail(code: i32, stderr: &str) -> CommandOutput {
65
+ CommandOutput { success: false, code: Some(code), stdout: String::new(), stderr: stderr.to_string() }
66
+ }
67
+
68
+ /// Build a backend over a mock runner: `default` answers every un-queued call; `queued` is drained
69
+ /// first. Returns the backend + the shared recorded-argv handle (read AFTER the call).
70
+ fn backend_with(default: MockResp, queued: Vec<MockResp>) -> (TmuxBackend, RecordedArgv) {
71
+ let recorded = Arc::new(Mutex::new(Vec::new()));
72
+ let stdin_recorded = Arc::new(Mutex::new(Vec::new()));
73
+ let runner = MockCommandRunner {
74
+ recorded: Arc::clone(&recorded),
75
+ stdin_recorded,
76
+ queue: Mutex::new(queued.into_iter().collect()),
77
+ default,
78
+ };
79
+ (TmuxBackend::with_runner(Box::new(runner)), recorded)
80
+ }
81
+
82
+ fn backend_with_stdin(
83
+ default: MockResp,
84
+ queued: Vec<MockResp>,
85
+ ) -> (TmuxBackend, RecordedArgv, RecordedStdin) {
86
+ let recorded = Arc::new(Mutex::new(Vec::new()));
87
+ let stdin_recorded = Arc::new(Mutex::new(Vec::new()));
88
+ let runner = MockCommandRunner {
89
+ recorded: Arc::clone(&recorded),
90
+ stdin_recorded: Arc::clone(&stdin_recorded),
91
+ queue: Mutex::new(queued.into_iter().collect()),
92
+ default,
93
+ };
94
+ (TmuxBackend::with_runner(Box::new(runner)), recorded, stdin_recorded)
95
+ }
96
+
97
+ fn svec(items: &[&str]) -> Vec<String> {
98
+ items.iter().map(|s| (*s).to_string()).collect()
99
+ }
100
+
101
+ // ── 1. has_session: exit 0 -> true, exit 1 -> false; argv = `tmux has-session -t <s>` ──────────
102
+ #[test]
103
+ fn has_session_argv_and_exit_code_maps_to_bool() {
104
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
105
+ assert!(be.has_session(&SessionName::new("sess")).expect("has_session"), "exit 0 -> true");
106
+ assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "has-session", "-t", "sess"]));
107
+
108
+ let (be, rec) = backend_with(MockResp::Out(fail(1, "can't find session: sess")), vec![]);
109
+ assert!(!be.has_session(&SessionName::new("sess")).expect("has_session"), "exit 1 -> false");
110
+ assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "has-session", "-t", "sess"]));
111
+ }
112
+
113
+ // ── 2. spawn_first / spawn_into frame via tmux_spawn_argv; canned output parses pane id ────────
114
+ #[test]
115
+ fn spawn_first_frames_via_new_session_builder_and_parses_pane_id() {
116
+ let (be, rec) = backend_with(MockResp::Out(ok("%3")), vec![]);
117
+ let s = SessionName::new("teamsess");
118
+ let w = WindowName::new("w1");
119
+ let env = BTreeMap::from([("TEAM_AGENT_ID".to_string(), "w1".to_string())]);
120
+ let result = be
121
+ .spawn_first(&s, &w, &svec(&["provider-bin", "--flag"]), Path::new("/work/dir"), &env)
122
+ .expect("spawn_first");
123
+ let argv = rec.lock().unwrap()[0].clone();
124
+ let cmd = argv.last().expect("the sh -lc command string").clone();
125
+ assert_eq!(
126
+ argv,
127
+ tmux_spawn_argv(&s, &w, &cmd, true),
128
+ "spawn_first must frame via tmux_spawn_argv (new-session -d -s <s> -n <w> sh -lc <cmd>)"
129
+ );
130
+ assert!(cmd.contains("provider-bin"), "the provider argv must be in the sh -lc command; got {cmd}");
131
+ assert_eq!(result.pane_id.as_str(), "%3", "SpawnResult.pane_id must parse from the tmux output");
132
+ }
133
+
134
+ #[test]
135
+ fn spawn_into_frames_via_new_window_builder() {
136
+ let (be, rec) = backend_with(MockResp::Out(ok("%4")), vec![]);
137
+ let s = SessionName::new("teamsess");
138
+ let w = WindowName::new("w2");
139
+ let result = be
140
+ .spawn_into(&s, &w, &svec(&["provider-bin"]), Path::new("/work/dir"), &BTreeMap::new())
141
+ .expect("spawn_into");
142
+ let argv = rec.lock().unwrap()[0].clone();
143
+ let cmd = argv.last().expect("the sh -lc command string").clone();
144
+ assert_eq!(
145
+ argv,
146
+ tmux_spawn_argv(&s, &w, &cmd, false),
147
+ "spawn_into must frame via tmux_spawn_argv first=false (new-window -t <s> -n <w> sh -lc <cmd>)"
148
+ );
149
+ assert_eq!(result.pane_id.as_str(), "%4");
150
+ }
151
+
152
+ // ── 3. set_session_env: argv = `tmux set-environment -t <s> <k> <v>`; success -> Applied ───────
153
+ #[test]
154
+ fn set_session_env_argv_and_applied_outcome() {
155
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
156
+ let outcome = be.set_session_env(&SessionName::new("sess"), "KEY", "VAL").expect("set env");
157
+ assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "set-environment", "-t", "sess", "KEY", "VAL"]));
158
+ assert_eq!(outcome, SetEnvOutcome::Applied, "tmux set-environment success -> SetEnvOutcome::Applied");
159
+ }
160
+
161
+ // ── 4. capture: argv = tmux_capture_argv; canned scrollback -> normalize_capture -> CapturedText ─
162
+ #[test]
163
+ fn capture_argv_and_normalizes_scrollback() {
164
+ let scroll = "line one \nbusy\u{a0}marker \n \n";
165
+ let (be, rec) = backend_with(MockResp::Out(ok(scroll)), vec![]);
166
+ let pane = PaneId::new("%7");
167
+ let captured = be
168
+ .capture(&Target::Pane(pane.clone()), CaptureRange::Tail(40))
169
+ .expect("capture");
170
+ assert_eq!(rec.lock().unwrap()[0], tmux_capture_argv(&pane, CaptureRange::Tail(40)));
171
+ assert_eq!(captured.text, normalize_capture(scroll), "capture output must be normalize_capture'd");
172
+ assert_eq!(captured.range, CaptureRange::Tail(40));
173
+ }
174
+
175
+ // ── 5a. send_keys: argv = tmux_send_keys_argv ──────────────────────────────────────────────────
176
+ #[test]
177
+ fn send_keys_argv_matches_builder() {
178
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
179
+ let pane = PaneId::new("%7");
180
+ be.send_keys(&Target::Pane(pane.clone()), &[Key::Enter]).expect("send_keys");
181
+ assert_eq!(rec.lock().unwrap()[0], tmux_send_keys_argv(&pane, &[Key::Enter]));
182
+ }
183
+
184
+ // ── 5b. inject (text): set/load-buffer(text) -> paste-buffer -p -> submit send-keys; report Submit ─
185
+ #[test]
186
+ fn inject_text_runs_buffer_paste_submit_sequence_and_reports_submit() {
187
+ let (be, rec) = backend_with(MockResp::Out(ok("hello")), vec![]);
188
+ let pane = PaneId::new("%7");
189
+ let report = be
190
+ .inject(&Target::Pane(pane.clone()), &InjectPayload::Text("hello".to_string()), Key::Enter, true)
191
+ .expect("inject");
192
+ let calls = rec.lock().unwrap().clone();
193
+ let is = |a: &[String], sub: &str| a.get(1).map(String::as_str) == Some(sub);
194
+ assert!(
195
+ calls.iter().any(|a| (is(a, "set-buffer") || is(a, "load-buffer")) && a.iter().any(|x| x.contains("hello"))),
196
+ "inject must stage the text into a tmux buffer (set-buffer/load-buffer); got {calls:?}"
197
+ );
198
+ assert!(
199
+ calls.iter().any(|a| is(a, "paste-buffer") && a.contains(&"-p".to_string()) && a.contains(&"%7".to_string())),
200
+ "inject must bracketed-paste (-p) the buffer to the pane; got {calls:?}"
201
+ );
202
+ assert!(
203
+ calls.iter().any(|a| is(a, "send-keys") && a.contains(&"Enter".to_string())),
204
+ "inject must send the submit key (Enter) last; got {calls:?}"
205
+ );
206
+ assert_eq!(report.stage_reached, InjectStage::Submit, "a fully-applied inject reaches the Submit stage");
207
+ assert_eq!(report.inject_verification, InjectVerification::NoToken);
208
+ assert_eq!(
209
+ report.submit_verification,
210
+ SubmitVerification::EnterSentWithoutPlaceholderCheck
211
+ );
212
+ assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
213
+ }
214
+
215
+ #[test]
216
+ fn inject_large_text_load_buffer_writes_stdin_and_token_report() {
217
+ let (be, rec, stdin_rec) = backend_with_stdin(MockResp::Out(ok("")), vec![]);
218
+ let text = format!("{}{}", "x".repeat(16 * 1024), " [team-agent-token:abc]");
219
+ let report = be
220
+ .inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text.clone()), Key::Down, true)
221
+ .expect("inject large text");
222
+
223
+ assert_eq!(report.inject_verification, InjectVerification::CaptureContainsToken);
224
+ assert_eq!(
225
+ report.submit_verification,
226
+ SubmitVerification::KeySentAfterVisibleToken { key: Key::Down }
227
+ );
228
+ let calls = rec.lock().unwrap().clone();
229
+ assert_eq!(calls[0], svec(&["tmux", "load-buffer", "-b", "team-agent-send-abc", "-"]));
230
+ assert_eq!(stdin_rec.lock().unwrap()[0], text);
231
+ }
232
+
233
+ #[test]
234
+ fn send_keys_cancel_mode_queries_mode_and_dispatches_cancel_argv() {
235
+ let (be, rec) = backend_with(
236
+ MockResp::Out(ok("")),
237
+ vec![MockResp::Out(ok("tree-mode\n")), MockResp::Out(ok(""))],
238
+ );
239
+ be.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::CancelMode])
240
+ .expect("cancel mode");
241
+
242
+ let calls = rec.lock().unwrap().clone();
243
+ assert_eq!(
244
+ calls[0],
245
+ svec(&["tmux", "display-message", "-p", "-t", "%7", "#{pane_mode}"])
246
+ );
247
+ assert_eq!(calls[1], svec(&["tmux", "send-keys", "-t", "%7", "q"]));
248
+ }
249
+
250
+ #[test]
251
+ fn cancel_mode_numeric_zero_is_input_ready_and_does_not_send_cancel() {
252
+ // Golden /tmp/transport_golden_probe.py:
253
+ // `_normalize_pane_mode("0") == ""`; `_prepare_tmux_pane_for_input` returns
254
+ // pane_input_ready and does NOT call `_pane_mode_cancel`.
255
+ // RED: pane_mode_from_raw("0") maps to Unknown, so Rust sends `-X cancel`.
256
+ let (be, rec) = backend_with(
257
+ MockResp::Out(ok("")),
258
+ vec![MockResp::Out(ok("0\n"))],
259
+ );
260
+ be.send_keys(&Target::Pane(PaneId::new("%7")), &[Key::CancelMode])
261
+ .expect("cancel mode input-ready no-op");
262
+
263
+ let calls = rec.lock().unwrap().clone();
264
+ assert_eq!(
265
+ calls,
266
+ vec![svec(&["tmux", "display-message", "-p", "-t", "%7", "#{pane_mode}"])],
267
+ "pane_mode='0' is Python input-ready; CancelMode must stop after the mode query, got {calls:?}"
268
+ );
269
+ }
270
+
271
+ #[test]
272
+ fn inject_text_uses_message_id_scoped_buffer_from_token() {
273
+ // Golden delivery.py:109-114 passes buffer_name = `team-agent-send-{message_id}` into
274
+ // `_tmux_inject_text`; tmux_io.py then uses that exact name for set/load, paste, delete.
275
+ // This prevents interleaved sends from sharing a stale global tmux buffer.
276
+ // RED: Rust currently hard-codes `team-agent-buf`.
277
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
278
+ let text = "Team Agent message from leader:\n\nhello\n\n[team-agent-token:msg_abc123]".to_string();
279
+ be.inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text), Key::Enter, true)
280
+ .expect("inject");
281
+
282
+ let calls = rec.lock().unwrap().clone();
283
+ let buffer_args: Vec<String> = calls
284
+ .iter()
285
+ .filter(|argv| matches!(argv.get(1).map(String::as_str), Some("set-buffer" | "load-buffer" | "paste-buffer" | "delete-buffer")))
286
+ .filter_map(|argv| argv.iter().position(|arg| arg == "-b").and_then(|i| argv.get(i + 1)).cloned())
287
+ .collect();
288
+ assert_eq!(
289
+ buffer_args,
290
+ vec![
291
+ "team-agent-send-msg_abc123".to_string(),
292
+ "team-agent-send-msg_abc123".to_string(),
293
+ "team-agent-send-msg_abc123".to_string(),
294
+ ],
295
+ "every tmux buffer operation must use the message-id-scoped golden buffer name; calls={calls:?}"
296
+ );
297
+ }
298
+
299
+ // ── 6. liveness three-state (§bug-085): exit 0 -> Live; "can't find …" -> Dead; else -> Unknown ─
300
+ #[test]
301
+ fn liveness_is_three_state_unknown_is_not_dead() {
302
+ let (be, rec) = backend_with(MockResp::Out(ok("%7")), vec![]);
303
+ assert_eq!(be.liveness(&PaneId::new("%7")).expect("liveness"), PaneLiveness::Live);
304
+ let argv0 = rec.lock().unwrap()[0].clone();
305
+ assert!(
306
+ argv0.contains(&"display-message".to_string())
307
+ && argv0.iter().any(|x| x.contains("#{pane_id}"))
308
+ && argv0.contains(&"%7".to_string()),
309
+ "liveness must probe the pane via display-message #{{pane_id}}; got {argv0:?}"
310
+ );
311
+
312
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "can't find pane %7")), vec![]);
313
+ assert_eq!(
314
+ be.liveness(&PaneId::new("%7")).expect("liveness"),
315
+ PaneLiveness::Dead,
316
+ "a 'can't find pane' failure -> Dead"
317
+ );
318
+
319
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "error connecting to server: No such file or directory")), vec![]);
320
+ assert_eq!(
321
+ be.liveness(&PaneId::new("%7")).expect("liveness"),
322
+ PaneLiveness::Unknown,
323
+ "a NON-'can't find' failure is UNKNOWN, not DEAD (§bug-085 three-state)"
324
+ );
325
+ }
326
+
327
+ // ── CP-1: per-team socket — for_workspace injects `-L ta-<hash>` at the run chokepoint; new() does NOT ─
328
+ #[test]
329
+ fn for_workspace_backend_injects_per_team_socket_but_default_backend_does_not() {
330
+ use super::socket_name_for_workspace;
331
+ let ws = Path::new("/tmp/ta-cp1-socket-test-ws");
332
+ let socket = socket_name_for_workspace(ws);
333
+ assert!(
334
+ socket.starts_with("ta-") && socket.len() == 15,
335
+ "socket name must be short + deterministic `ta-<12 hex>`; got {socket:?}"
336
+ );
337
+ // deterministic: the SAME workspace path always derives the SAME socket (CLI == daemon == ops).
338
+ assert_eq!(socket, socket_name_for_workspace(ws), "socket derivation must be deterministic");
339
+
340
+ // workspace-bound backend: every executed `tmux` argv gets `-L <socket>` after the leading token.
341
+ let recorded = Arc::new(Mutex::new(Vec::new()));
342
+ let runner = MockCommandRunner {
343
+ recorded: Arc::clone(&recorded),
344
+ stdin_recorded: Arc::new(Mutex::new(Vec::new())),
345
+ queue: Mutex::new(VecDeque::new()),
346
+ default: MockResp::Out(ok("")),
347
+ };
348
+ let be = TmuxBackend::with_runner_for_workspace(Box::new(runner), ws);
349
+ be.has_session(&SessionName::new("sess")).expect("has_session");
350
+ let argv = recorded.lock().unwrap()[0].clone();
351
+ assert_eq!(
352
+ argv,
353
+ svec(&["tmux", "-L", &socket, "has-session", "-t", "sess"]),
354
+ "for_workspace backend must inject `-L <socket>` right after `tmux`; got {argv:?}"
355
+ );
356
+
357
+ // default backend (new()/with_runner): NO `-L` — argv stays the golden-locked builder form.
358
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
359
+ be.has_session(&SessionName::new("sess")).expect("has_session");
360
+ assert_eq!(
361
+ rec.lock().unwrap()[0],
362
+ svec(&["tmux", "has-session", "-t", "sess"]),
363
+ "the default-socket backend must NOT inject `-L` (existing tests + non-team callers unaffected)"
364
+ );
365
+ }
366
+
367
+ // ── 7. kill_session / kill_window: golden argv; success -> Ok(()) ───────────────────────────────
368
+ #[test]
369
+ fn kill_session_and_kill_window_argv() {
370
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
371
+ be.kill_session(&SessionName::new("sess")).expect("kill_session");
372
+ assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "kill-session", "-t", "sess"]));
373
+
374
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
375
+ be.kill_window(&Target::Pane(PaneId::new("%7"))).expect("kill_window");
376
+ assert_eq!(rec.lock().unwrap()[0], svec(&["tmux", "kill-window", "-t", "%7"]));
377
+ }
378
+
379
+ // ── 8. ERROR MAPPING: non-zero tmux exit -> TransportError::Subprocess; runner io::Error -> Err ──
380
+ #[test]
381
+ fn error_paths_map_to_transport_error_not_panic() {
382
+ // tmux cli non-zero exit (the Subprocess variant's documented purpose).
383
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "no server running on /tmp/tmux-x/default")), vec![]);
384
+ let err = be.kill_session(&SessionName::new("sess")).expect_err("kill_session must error on non-zero exit");
385
+ assert!(
386
+ matches!(err, TransportError::Subprocess { code: Some(1), .. }),
387
+ "a non-zero tmux exit must map to TransportError::Subprocess{{code,stderr}}; got {err:?}"
388
+ );
389
+
390
+ // a runner io::Error (e.g. tmux not on PATH) must surface as a TransportError, never a panic.
391
+ let (be, _r) = backend_with(MockResp::Io(std::io::ErrorKind::NotFound), vec![]);
392
+ let err = be
393
+ .capture(&Target::Pane(PaneId::new("%7")), CaptureRange::Full)
394
+ .expect_err("capture must surface the runner io error");
395
+ assert!(
396
+ matches!(err, TransportError::Capture { .. } | TransportError::Io(_)),
397
+ "a runner io error must map to a TransportError (not panic); got {err:?}"
398
+ );
399
+ }
400
+
401
+ // ── 9. RealCommandRunner GOLDEN 5s TIMEOUT (rt-host-b transient-session race) ────────────────────
402
+ // GOLDEN: terminal.py:12-13 `run_cmd(args, timeout=timeout, check=False)`; runtime.py:1010-1014
403
+ // `_tmux_session_exists` runs `tmux has-session -t <s>` with timeout=5. A has-session that outlives
404
+ // 5s raises `subprocess.TimeoutExpired`, which the coordinator daemon CATCHES
405
+ // (coordinator/__main__.py:60-90 `except Exception`) and treats as a TOLERATED transient
406
+ // (exponential backoff + retry next tick) — it is NEVER read as a definitive "session gone".
407
+ // The 5s subprocess timeout is golden's ONLY tolerance for a slow/hung probe.
408
+ //
409
+ // RUST GAP (THE BUG): `RealCommandRunner::run` (tmux_backend.rs:52) calls
410
+ // `std::process::Command::output()` with NO timeout, so a slow/hung tmux blocks indefinitely.
411
+ // On the (slow) mac mini this is the ~17% single-round-trip flake: a transient slow has-session
412
+ // tears down a healthy team. This is rt-host-b's deterministic 5/5 anchor — `run` on a HUNG
413
+ // command must abandon at the golden 5s and surface `Err(TimedOut)`, NOT block on the full
414
+ // subprocess.
415
+ //
416
+ // RED today: there is no timeout, so `run(["sleep","30"])` blocks ~30s and the `< 6s` bound fails.
417
+ // #[ignore] real-machine: this is the only test here that spawns a real subprocess.
418
+ // PORTER SEAM: add a 5s timeout inside `RealCommandRunner::run` (spawn child + wait-with-timeout
419
+ // via a thread/channel + kill the child on expiry), returning `Err(io::Error, kind TimedOut)` —
420
+ // NO new crate dependency. Keep the existing `CommandRunner::run(&[String]) -> Result<…, io::Error>`
421
+ // signature (the timeout is internal; do not add a parameter).
422
+ #[test]
423
+ #[ignore = "real-machine: spawns a real sleeping subprocess; asserts RealCommandRunner enforces \
424
+ the golden 5s timeout (terminal.py run_cmd timeout / runtime.py:1013 \
425
+ _tmux_session_exists timeout=5)"]
426
+ fn real_command_runner_enforces_golden_5s_timeout_on_hang() {
427
+ use std::time::{Duration, Instant};
428
+ let runner = RealCommandRunner;
429
+ let started = Instant::now();
430
+ let result = runner.run(&svec(&["sleep", "30"]));
431
+ let elapsed = started.elapsed();
432
+ assert!(
433
+ elapsed < Duration::from_secs(6),
434
+ "RealCommandRunner::run must abandon a hung command at the golden 5s timeout, not block on \
435
+ the full subprocess (terminal.py run_cmd timeout / runtime.py:1013 timeout=5); blocked {elapsed:?}"
436
+ );
437
+ let err = result.expect_err(
438
+ "a command outliving the 5s timeout must surface as Err (subprocess.TimeoutExpired analog) so \
439
+ the daemon backoff path tolerates it, instead of yielding a bogus has-session bool",
440
+ );
441
+ assert_eq!(
442
+ err.kind(),
443
+ std::io::ErrorKind::TimedOut,
444
+ "the timeout must be io::ErrorKind::TimedOut (golden: TimeoutExpired -> daemon except -> backoff/retry)"
445
+ );
446
+ }
447
+
448
+ // ── 10. query (TRANSPORT TRIO) — single-field display-message; nonzero -> None ──────────────────
449
+ // Golden _legacy_pane_discovery.py:35-39 _tmux_pane_info: `tmux display-message -p -t <target> -F
450
+ // <fmt>` (returncode != 0 -> None), single-field reads at state.py:346 (#{pane_id}) / delivery.py:34
451
+ // (#{pane_width}). The argv is exactly `transport::tmux_query_argv(pane, field)` (the golden-locked
452
+ // builder). RED today: `query` is unimplemented!() -> PANIC. Porter: pane_from_target(target) ->
453
+ // tmux_query_argv -> run; success => Some(stdout.trim()); nonzero => None (never Err).
454
+ #[test]
455
+ fn query_single_field_argv_and_nonzero_maps_to_none() {
456
+ // PaneId field: argv == the golden builder; present value parsed (trimmed) into Some.
457
+ let (be, rec) = backend_with(MockResp::Out(ok("%7\n")), vec![]);
458
+ let got = be.query(&Target::Pane(PaneId::new("%7")), PaneField::PaneId).expect("query ok");
459
+ assert_eq!(
460
+ rec.lock().unwrap()[0],
461
+ tmux_query_argv(&PaneId::new("%7"), PaneField::PaneId),
462
+ "query must build the golden single-field `display-message -p -t <t> -F #{{pane_id}}` argv"
463
+ );
464
+ assert_eq!(got, Some("%7".to_string()), "a present field value is parsed (stripped) into Some");
465
+
466
+ // PaneWidth uses -F too; lock argv + the parsed numeric-as-string field.
467
+ let (be, rec) = backend_with(MockResp::Out(ok("180\n")), vec![]);
468
+ let got = be.query(&Target::Pane(PaneId::new("%7")), PaneField::PaneWidth).expect("query ok");
469
+ assert_eq!(rec.lock().unwrap()[0], tmux_query_argv(&PaneId::new("%7"), PaneField::PaneWidth));
470
+ assert_eq!(got, Some("180".to_string()));
471
+
472
+ // nonzero exit (pane gone) -> None, NOT an Err (golden _tmux_pane_info: returncode != 0 -> None).
473
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "can't find pane %7")), vec![]);
474
+ assert_eq!(
475
+ be.query(&Target::Pane(PaneId::new("%7")), PaneField::PaneId).expect("query ok on nonzero"),
476
+ None,
477
+ "a nonzero / pane-gone query must map to None (not Err)"
478
+ );
479
+ }
480
+
481
+ // ── 11. list_targets (TRANSPORT TRIO) — `list-panes -a -F TMUX_PANE_FORMAT` + per-line parse ────
482
+ // Golden _legacy_pane_discovery.py:29-33 _tmux_list_panes: `tmux list-panes -a -F <TMUX_PANE_FORMAT>`
483
+ // (returncode != 0 -> []), parse each tab line via _parse_tmux_pane_info. TMUX_PANE_FORMAT
484
+ // (runtime.py:456-460) is the byte-exact 11-field tab string locked below. RED today: list_targets is
485
+ // unimplemented!() -> PANIC. Porter: build the argv, split each stdout line on '\t', map the fields
486
+ // into PaneInfo (pane_active=="1" -> active). leader_env / pane_pid are the reverse-env real-machine
487
+ // bit (no field in TMUX_PANE_FORMAT) — out of this canned parse; the structured fields are locked here.
488
+ #[test]
489
+ fn list_targets_argv_and_parses_tmux_pane_format() {
490
+ const FMT: &str = "#{pane_id}\t#{session_name}\t#{window_index}\t#{window_name}\t#{pane_index}\t#{pane_tty}\t#{pane_current_command}\t#{pane_active}\t#{pane_current_path}\t#{session_attached}\t#{pane_in_mode}";
491
+ let stdout = "%7\tteam-x\t0\twin0\t0\t/dev/ttys003\tcodex\t1\t/Users/me/work\t1\t0\n\
492
+ %8\tteam-x\t1\twin1\t0\t/dev/ttys004\tnode\t0\t/Users/me/other\t0\t0\n";
493
+ let (be, rec) = backend_with(MockResp::Out(ok(stdout)), vec![]);
494
+ let panes = be.list_targets().expect("list_targets ok");
495
+ assert_eq!(
496
+ rec.lock().unwrap()[0],
497
+ svec(&["tmux", "list-panes", "-a", "-F", FMT]),
498
+ "list_targets must run `tmux list-panes -a -F <TMUX_PANE_FORMAT>` (golden _legacy_pane_discovery.py:29)"
499
+ );
500
+ assert_eq!(panes.len(), 2, "one PaneInfo per output line");
501
+ let p = &panes[0];
502
+ assert_eq!(p.pane_id.as_str(), "%7", "field[0] -> pane_id");
503
+ assert_eq!(p.session.as_str(), "team-x", "field[1] -> session_name");
504
+ assert_eq!(p.window_index, Some(0), "field[2] -> window_index (parsed u32)");
505
+ assert_eq!(p.window_name.as_ref().map(|w| w.as_str().to_string()), Some("win0".to_string()), "field[3] -> window_name");
506
+ assert_eq!(p.pane_index, Some(0), "field[4] -> pane_index (parsed u32)");
507
+ assert_eq!(p.tty.as_deref(), Some("/dev/ttys003"), "field[5] -> pane_tty");
508
+ assert_eq!(p.current_command.as_deref(), Some("codex"), "field[6] -> pane_current_command");
509
+ assert!(p.active, "field[7] pane_active='1' -> active=true");
510
+ assert_eq!(
511
+ p.current_path.as_ref().map(|x| x.to_string_lossy().to_string()),
512
+ Some("/Users/me/work".to_string()),
513
+ "field[8] -> pane_current_path"
514
+ );
515
+ assert!(!panes[1].active, "field[7] pane_active='0' -> active=false");
516
+
517
+ // nonzero exit -> empty vec (golden returncode != 0 -> []).
518
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "no server running on /tmp/tmux-x/default")), vec![]);
519
+ assert!(
520
+ be.list_targets().expect("list_targets ok on nonzero").is_empty(),
521
+ "a nonzero list-panes must map to an EMPTY Vec (not Err)"
522
+ );
523
+ }
524
+
525
+ // ── 12. attach_session (TRANSPORT TRIO) — `tmux attach-session -t <s>` -> Attached ──────────────
526
+ // Golden tmux attach is `tmux attach-session -t <session>`; a successful attach -> AttachOutcome::
527
+ // Attached. RED today: attach_session is unimplemented!() -> PANIC. The in-process lock asserts the
528
+ // argv + outcome via the recording runner; the REAL attach is interactive (takes over the terminal)
529
+ // — that is the real-machine boundary, not unit-testable.
530
+ #[test]
531
+ fn attach_session_argv_and_attached_outcome() {
532
+ let (be, rec) = backend_with(MockResp::Out(ok("")), vec![]);
533
+ let outcome = be.attach_session(&SessionName::new("sess")).expect("attach ok");
534
+ assert_eq!(
535
+ rec.lock().unwrap()[0],
536
+ svec(&["tmux", "attach-session", "-t", "sess"]),
537
+ "attach_session must run `tmux attach-session -t <session>`"
538
+ );
539
+ assert_eq!(outcome, AttachOutcome::Attached, "a successful tmux attach -> AttachOutcome::Attached");
540
+ }
541
+
542
+ // ── 13. TARGET-SCAN WIRING (a): list_targets is the LIVE pane-discovery primitive ───────────────
543
+ // WAVE-2 Lane C. `list_targets` (the `tmux list-panes -a` scan, locked argv/parse in test #11) has
544
+ // ZERO production callers today — it is dead code. Golden wires pane discovery on top of it: status
545
+ // (_capture_missing_sessions / _tmux_session_exists, queries.py:46,52) and doctor (coordinator_health)
546
+ // consume the live scan. The in-process wiring obligation is exercised at the status level by
547
+ // cli::tests::status_tmux_session_present_uses_live_tmux_probe_not_is_some (RED). This #[ignore]
548
+ // real-machine seam locks that a LIVE `list_targets` actually enumerates the running panes, proving
549
+ // the primitive is usable by the status/doctor discovery the porter must wire.
550
+ #[test]
551
+ #[ignore = "real-machine: needs a live tmux server+session; asserts list_targets() (the dangling \
552
+ pane-discovery primitive, zero production callers) enumerates live panes so status/doctor \
553
+ discovery can consume it (golden _legacy_pane_discovery list-panes -a)"]
554
+ fn list_targets_is_live_pane_discovery_primitive_for_status_doctor() {
555
+ let be = TmuxBackend::with_runner(Box::new(RealCommandRunner));
556
+ let panes = be.list_targets().expect("live list_targets must not error");
557
+ assert!(
558
+ !panes.is_empty(),
559
+ "a live `tmux list-panes -a` must surface the running panes; status/doctor pane discovery \
560
+ is wired on top of this scan (currently dead code — zero production callers)"
561
+ );
562
+ }
563
+
564
+ // ── 14. TARGET-SCAN WIRING (b): R1 — caller_target.uuid is FIRST leader_session_uuid precedence ──
565
+ // WAVE-2 Lane C / wave2-laneB-rereview PROBE-D. When the caller-target scan lands, golden
566
+ // claim_lease_no_incident threads `_target_leader_session_uuid(caller_target)` as the FIRST
567
+ // leader_session_uuid precedence (leader/__init__.py:679-684): caller_target.uuid BEFORE
568
+ // owner.uuid / receiver.uuid / derived. A DIFFERENT live pane reclaiming a DEAD owner must persist
569
+ // the CALLER's uuid, not the dead owner's (PROBE-D: PY "NEWUUID" / RUST persists "OLD"). The
570
+ // caller-target uuid is read from the caller pane's INJECTED TEAM_AGENT_LEADER_SESSION_UUID via a
571
+ // per-pane env query (NOT a TMUX_PANE_FORMAT field), so the live scan is the dependency this seam
572
+ // marks. SCOPE NOTE: the decisive IN-PROCESS claim-path R1 RED belongs in leader/tests.rs, which is
573
+ // outside this task's (cli + tmux_backend) editor scope — flagged to the leader for the
574
+ // leader-contracts agent to graduate R1 to its own claim-path RED.
575
+ #[test]
576
+ #[ignore = "real-machine + SCOPE: R1 (PROBE-D) caller_target.uuid is FIRST leader_session_uuid \
577
+ precedence (leader/__init__.py:679-684); the in-process claim-path assertion lives in \
578
+ leader/tests.rs (out of cli+tmux_backend scope) — this seam marks the live caller-target \
579
+ env-scan dependency"]
580
+ fn r1_caller_target_uuid_is_first_leader_session_uuid_precedence_seam() {
581
+ // The caller-target scan (reading the caller pane's injected TEAM_AGENT_LEADER_SESSION_UUID)
582
+ // is the live precursor to R1's uuid precedence. The full uuid-persistence assertion is the
583
+ // leader claim path's obligation (see report). Here we only confirm the scan is reachable.
584
+ let be = TmuxBackend::with_runner(Box::new(RealCommandRunner));
585
+ let _panes = be.list_targets().expect("live list_targets (caller-target scan precursor)");
586
+ }