@team-agent/installer 0.5.61 → 0.5.62

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 (250) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -2
  4. package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
  5. package/crates/team-agent/src/cli/diagnose.rs +1 -0
  6. package/crates/team-agent/src/cli/emit.rs +41 -7
  7. package/crates/team-agent/src/cli/helpers.rs +1 -0
  8. package/crates/team-agent/src/cli/leader.rs +6 -1
  9. package/crates/team-agent/src/cli/leaders.rs +2 -0
  10. package/crates/team-agent/src/cli/mod.rs +33 -3
  11. package/crates/team-agent/src/cli/named_address.rs +8 -1
  12. package/crates/team-agent/src/cli/profile.rs +2 -1
  13. package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
  14. package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
  15. package/crates/team-agent/src/cli/send/persist.rs +1 -0
  16. package/crates/team-agent/src/cli/send/presentation.rs +1 -0
  17. package/crates/team-agent/src/cli/send/resolve.rs +206 -1
  18. package/crates/team-agent/src/cli/send.rs +2 -1
  19. package/crates/team-agent/src/cli/spec.rs +1 -0
  20. package/crates/team-agent/src/cli/status.rs +2 -1
  21. package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
  22. package/crates/team-agent/src/cli/status_port/format.rs +1 -0
  23. package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
  25. package/crates/team-agent/src/cli/types.rs +3 -2
  26. package/crates/team-agent/src/communication_mode/mod.rs +1 -0
  27. package/crates/team-agent/src/compiler.rs +1 -0
  28. package/crates/team-agent/src/conpty/backend.rs +1 -0
  29. package/crates/team-agent/src/conpty/mod.rs +1 -0
  30. package/crates/team-agent/src/coordinator/backoff.rs +5 -8
  31. package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
  32. package/crates/team-agent/src/coordinator/health.rs +1 -0
  33. package/crates/team-agent/src/coordinator/mod.rs +2 -2
  34. package/crates/team-agent/src/coordinator/orphan.rs +4 -116
  35. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  36. package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
  37. package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
  38. package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
  39. package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
  40. package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
  41. package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
  42. package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
  43. package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
  44. package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
  45. package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
  46. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
  47. package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
  49. package/crates/team-agent/src/coordinator/tick.rs +24 -28
  50. package/crates/team-agent/src/coordinator/types.rs +3 -176
  51. package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
  52. package/crates/team-agent/src/db/message_store.rs +2 -1
  53. package/crates/team-agent/src/db/migration.rs +5 -4
  54. package/crates/team-agent/src/db/mod.rs +1 -0
  55. package/crates/team-agent/src/db/schema.rs +1 -0
  56. package/crates/team-agent/src/diagnose/comms.rs +2 -1
  57. package/crates/team-agent/src/diagnose/mod.rs +1 -0
  58. package/crates/team-agent/src/diagnose/orphans.rs +1 -0
  59. package/crates/team-agent/src/layout/manager.rs +3 -7
  60. package/crates/team-agent/src/layout/mod.rs +2 -2
  61. package/crates/team-agent/src/layout/overlay.rs +6 -1
  62. package/crates/team-agent/src/layout/placement.rs +1 -0
  63. package/crates/team-agent/src/layout/recovery.rs +3 -0
  64. package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
  65. package/crates/team-agent/src/layout/sessions.rs +8 -1
  66. package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
  67. package/crates/team-agent/src/layout/worker_env.rs +3 -0
  68. package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
  69. package/crates/team-agent/src/leader/helpers.rs +1 -0
  70. package/crates/team-agent/src/leader/incident.rs +1 -0
  71. package/crates/team-agent/src/leader/lease.rs +27 -22
  72. package/crates/team-agent/src/leader/mod.rs +2 -3
  73. package/crates/team-agent/src/leader/owner_bind.rs +13 -3
  74. package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
  75. package/crates/team-agent/src/leader/rediscover.rs +4 -17
  76. package/crates/team-agent/src/leader/registry.rs +3 -15
  77. package/crates/team-agent/src/leader/start.rs +373 -125
  78. package/crates/team-agent/src/leader/takeover.rs +10 -232
  79. package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
  80. package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
  81. package/crates/team-agent/src/leader/tests/identity.rs +7 -2
  82. package/crates/team-agent/src/leader/tests/idle.rs +0 -193
  83. package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
  84. package/crates/team-agent/src/leader/tests/mod.rs +4 -41
  85. package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
  86. package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
  87. package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
  88. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
  89. package/crates/team-agent/src/leader/types.rs +1 -28
  90. package/crates/team-agent/src/lib.rs +3 -0
  91. package/crates/team-agent/src/lifecycle/display.rs +7 -1
  92. package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
  93. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
  94. package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
  95. package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
  96. package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
  97. package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
  98. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
  99. package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
  100. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
  101. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
  102. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
  103. package/crates/team-agent/src/lifecycle/launch.rs +11 -12
  104. package/crates/team-agent/src/lifecycle/mod.rs +2 -1
  105. package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
  106. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
  107. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
  108. package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
  109. package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
  110. package/crates/team-agent/src/lifecycle/restart.rs +13 -11
  111. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
  112. package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
  113. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
  114. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
  115. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
  116. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
  117. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
  118. package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
  119. package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
  120. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
  121. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
  122. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
  123. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
  124. package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
  125. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
  126. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
  127. package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
  128. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
  129. package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
  130. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
  131. package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
  132. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
  133. package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
  134. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
  135. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
  136. package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
  137. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
  138. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
  139. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
  140. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
  141. package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
  142. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
  143. package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
  144. package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
  145. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
  146. package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
  147. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
  148. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
  149. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
  150. package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
  151. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
  152. package/crates/team-agent/src/lifecycle/tests.rs +59 -0
  153. package/crates/team-agent/src/lifecycle/types.rs +0 -9
  154. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  155. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
  156. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
  157. package/crates/team-agent/src/mcp_server/mod.rs +1 -0
  158. package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
  159. package/crates/team-agent/src/mcp_server/tools.rs +1 -0
  160. package/crates/team-agent/src/mcp_server/types.rs +1 -18
  161. package/crates/team-agent/src/mcp_server/wire.rs +5 -5
  162. package/crates/team-agent/src/messaging/activity.rs +1 -0
  163. package/crates/team-agent/src/messaging/address.rs +1 -0
  164. package/crates/team-agent/src/messaging/delivery.rs +4 -3
  165. package/crates/team-agent/src/messaging/helpers.rs +2 -1
  166. package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
  167. package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
  168. package/crates/team-agent/src/messaging/mod.rs +10 -14
  169. package/crates/team-agent/src/messaging/peers.rs +2 -0
  170. package/crates/team-agent/src/messaging/persist.rs +3 -1
  171. package/crates/team-agent/src/messaging/presentation.rs +2 -1
  172. package/crates/team-agent/src/messaging/results.rs +1 -0
  173. package/crates/team-agent/src/messaging/scheduler.rs +1 -0
  174. package/crates/team-agent/src/messaging/selftest.rs +1 -1
  175. package/crates/team-agent/src/messaging/send.rs +41 -8
  176. package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
  177. package/crates/team-agent/src/messaging/types.rs +2 -2
  178. package/crates/team-agent/src/messaging/watchers.rs +19 -13
  179. package/crates/team-agent/src/model/enums.rs +1 -0
  180. package/crates/team-agent/src/model/errors.rs +1 -0
  181. package/crates/team-agent/src/model/ids.rs +1 -0
  182. package/crates/team-agent/src/model/mod.rs +1 -0
  183. package/crates/team-agent/src/model/name_similarity.rs +1 -0
  184. package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
  185. package/crates/team-agent/src/model/paths.rs +1 -0
  186. package/crates/team-agent/src/model/permissions.rs +3 -2
  187. package/crates/team-agent/src/model/routing.rs +1 -0
  188. package/crates/team-agent/src/model/spec.rs +1 -0
  189. package/crates/team-agent/src/model/task_graph.rs +1 -0
  190. package/crates/team-agent/src/model/yaml/tests.rs +1 -0
  191. package/crates/team-agent/src/model/yaml.rs +1 -0
  192. package/crates/team-agent/src/packaging/install.rs +9 -147
  193. package/crates/team-agent/src/packaging/migrate.rs +2 -0
  194. package/crates/team-agent/src/packaging/mod.rs +1 -0
  195. package/crates/team-agent/src/packaging/repair.rs +2 -0
  196. package/crates/team-agent/src/packaging/tests.rs +29 -218
  197. package/crates/team-agent/src/packaging/types.rs +8 -15
  198. package/crates/team-agent/src/platform/argv.rs +4 -0
  199. package/crates/team-agent/src/platform/errors.rs +10 -1
  200. package/crates/team-agent/src/platform/file_lock.rs +4 -157
  201. package/crates/team-agent/src/platform/mod.rs +0 -61
  202. package/crates/team-agent/src/platform/process.rs +1 -0
  203. package/crates/team-agent/src/provider/adapter.rs +1 -0
  204. package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
  205. package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
  206. package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
  207. package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
  208. package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
  209. package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
  210. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  211. package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
  212. package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
  213. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
  214. package/crates/team-agent/src/provider/classify.rs +1 -0
  215. package/crates/team-agent/src/provider/faults.rs +1 -26
  216. package/crates/team-agent/src/provider/helpers.rs +1 -0
  217. package/crates/team-agent/src/provider/mod.rs +1 -1
  218. package/crates/team-agent/src/provider/session/capture.rs +1 -4
  219. package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
  220. package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
  221. package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
  222. package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
  223. package/crates/team-agent/src/provider/session/mod.rs +2 -4
  224. package/crates/team-agent/src/provider/session/resume.rs +2 -209
  225. package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
  226. package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
  227. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  228. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  229. package/crates/team-agent/src/provider/session_scan.rs +1 -42
  230. package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
  231. package/crates/team-agent/src/provider/types.rs +1 -0
  232. package/crates/team-agent/src/provider/wire.rs +1 -0
  233. package/crates/team-agent/src/state/identity.rs +16 -5
  234. package/crates/team-agent/src/state/identity_keys.rs +1 -0
  235. package/crates/team-agent/src/state/mod.rs +2 -0
  236. package/crates/team-agent/src/state/owner_gate.rs +5 -0
  237. package/crates/team-agent/src/state/ownership.rs +10 -4
  238. package/crates/team-agent/src/state/paths.rs +12 -0
  239. package/crates/team-agent/src/state/persist.rs +9 -3
  240. package/crates/team-agent/src/state/projection.rs +15 -2
  241. package/crates/team-agent/src/state/repository/intent.rs +1 -0
  242. package/crates/team-agent/src/state/repository/tests.rs +1 -0
  243. package/crates/team-agent/src/state/repository.rs +7 -0
  244. package/crates/team-agent/src/state/selector.rs +1 -0
  245. package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
  246. package/crates/team-agent/src/tmux_backend.rs +63 -0
  247. package/crates/team-agent/src/transport.rs +15 -0
  248. package/package.json +4 -4
  249. package/crates/team-agent/src/leader/inject.rs +0 -33
  250. package/crates/team-agent/src/provider/command.rs +0 -80
@@ -0,0 +1,849 @@
1
+ //! #264 behavioral diff regression contracts (D1-D9).
2
+ //!
3
+ //! Source of truth: Python runtime 0.2.11 (`~/.team-agent/runtime/0.2.11/src/team_agent/`).
4
+ //! Every asserted value below was grounded by executing the Python truth source
5
+ //! (CodexAdapter.build_command / compile_system_prompt / providers.shell_command) —
6
+ //! see `.team/artifacts/264-behavioral-diff-report.md` for the per-item file:line evidence.
7
+ //!
8
+ //! D1: codex developer_instructions triple escape (\\ then " then \n) — codex.py:120-121
9
+ //! D2: codex MCP `-c mcp_servers.<name>.tool_timeout_sec=600.0` — codex.py:129
10
+ //! D3: codex `--profile <CODEX_PROFILE>` + `-c <codex_config>` items — codex.py:105-107,117-118 / provider_env.py:62-79
11
+ //! D4: worker shell env TEAM_AGENT_ID=<worker self> (overrides parent) — providers.py:131
12
+ //! D5: fresh launch spawn cwd == workspace (tmux cwd + state.spawn_cwd) — providers.py:145 / launch/core.py:253
13
+ //! D6: compiled system prompt first section is identity — prompt.py:39
14
+ //! D7: runtime.fast → codex fast-mode toggle "/fast" after spawn — launch/core.py:235-237
15
+ //! D8: launch session_name fallback = team-{name}, not a constant — launch/core.py:56
16
+ //! D9: profile env_unset truly unsets in the worker shell line — providers.py:142-145 / provider_env.py:82-90
17
+
18
+ #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
19
+
20
+ #[path = "../../../tests/support/hermetic.rs"]
21
+ mod hermetic_guard;
22
+ #[allow(dead_code)]
23
+ fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
24
+
25
+ use std::collections::BTreeMap;
26
+ use std::path::{Path, PathBuf};
27
+ use std::sync::atomic::{AtomicU64, Ordering};
28
+ use std::sync::Mutex;
29
+
30
+ use serial_test::serial;
31
+ use team_agent::lifecycle::{launch_with_transport, quick_start_with_transport_in_workspace};
32
+ use team_agent::state::persist::load_runtime_state;
33
+ use team_agent::tmux_backend::{CommandOutput, CommandRunner, TmuxBackend};
34
+ use team_agent::transport::{
35
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
36
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
37
+ SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
38
+ WindowName,
39
+ };
40
+
41
+ const ANCESTRY_KEY: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
42
+ const NEUTRAL_ANCESTRY: &str = "[\"/bin/zsh\"]";
43
+
44
+ /// D1+D2+D3: one codex argv golden locks the three codex command-construction gaps.
45
+ ///
46
+ /// Python golden (executed 0.2.11 CodexAdapter.build_command with the same profile values):
47
+ /// `codex --no-alt-screen --disable shell_snapshot --disable apps --profile relay-fast
48
+ /// --sandbox read-only --ask-for-approval on-request --model gpt-5.5
49
+ /// -c model_provider="relay" -c model_providers.relay.base_url="https://relay.example/v1"
50
+ /// -c model_providers.relay.env_key="TEAM_AGENT_PROVIDER_API_KEY"
51
+ /// -c model_providers.relay.wire_api="chat" -c model_providers.relay.name="Relay"
52
+ /// -c developer_instructions="...\n...ESCAPE-CHECK back\\slash \"dq\" end..."
53
+ /// -c mcp_servers.team_orchestrator.command="..." ... -c mcp_servers.team_orchestrator.tool_timeout_sec=600.0`
54
+ #[test]
55
+ #[serial(env)]
56
+ fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
57
+ let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
58
+ let ws = tmp_dir("d123-codex-argv");
59
+ let team_dir = write_team(
60
+ &ws,
61
+ &TeamFixture {
62
+ name: "d123team",
63
+ provider: "codex",
64
+ fast: false,
65
+ agents: &[AgentDoc {
66
+ id: "worker_a",
67
+ role: "Codex Diff Worker",
68
+ provider: "codex",
69
+ auth_mode: "compatible_api",
70
+ model: Some("gpt-5.5"),
71
+ profile: Some("relay"),
72
+ tools: &["mcp_team"],
73
+ body: "ESCAPE-CHECK back\\slash \"dq\" end",
74
+ }],
75
+ },
76
+ );
77
+ write_relay_profile(&team_dir);
78
+ seed_healthy_coordinator(&ws);
79
+ let transport = RecordingTransport::new();
80
+
81
+ quick_start_with_transport_in_workspace(
82
+ &ws,
83
+ &team_dir,
84
+ None,
85
+ true,
86
+ Some("d123team"),
87
+ &transport,
88
+ )
89
+ .expect("quick-start should reach codex command construction");
90
+ let spawn = transport.single_spawn();
91
+ let argv = &spawn.argv;
92
+ let mut failures = Vec::new();
93
+
94
+ // D1: triple escape, Python order \\ -> \" -> \n (codex.py:120-121).
95
+ let dev = argv
96
+ .iter()
97
+ .find(|arg| arg.starts_with("developer_instructions=\""));
98
+ match dev {
99
+ None => failures
100
+ .push("D1: no `-c developer_instructions=\"...\"` value in codex argv".to_string()),
101
+ Some(dev) => {
102
+ if dev.contains('\n') {
103
+ failures.push(format!(
104
+ "D1: developer_instructions value contains RAW newlines; Python escapes them as literal \\n (codex.py:120). head={:?}",
105
+ dev.chars().take(120).collect::<String>()
106
+ ));
107
+ }
108
+ // Raw marker `ESCAPE-CHECK back\slash "dq" end` must arrive as `back\\slash \"dq\" end`.
109
+ let escaped_marker = "ESCAPE-CHECK back\\\\slash \\\"dq\\\" end";
110
+ if !dev.contains(escaped_marker) {
111
+ failures.push(format!(
112
+ "D1: developer_instructions must contain the Python-escaped marker {escaped_marker:?} (backslash doubled before quote/newline escaping)"
113
+ ));
114
+ }
115
+ }
116
+ }
117
+
118
+ // D2: codex.py:129 — every MCP server gets tool_timeout_sec=600.0.
119
+ let timeout_item = "mcp_servers.team_orchestrator.tool_timeout_sec=600.0";
120
+ if !argv.iter().any(|arg| arg == timeout_item) {
121
+ failures.push(format!("D2: codex argv missing `-c {timeout_item}`"));
122
+ }
123
+
124
+ // D3: profile command overrides (provider_env.py:62-79 + codex.py:105-107,117-118).
125
+ if flag_value(argv, "--profile").as_deref() != Some("relay-fast") {
126
+ failures.push(format!(
127
+ "D3: codex argv must carry `--profile relay-fast` from profile CODEX_PROFILE; got {:?}",
128
+ flag_value(argv, "--profile")
129
+ ));
130
+ }
131
+ for config in [
132
+ "model_provider=\"relay\"",
133
+ "model_providers.relay.base_url=\"https://relay.example/v1\"",
134
+ "model_providers.relay.env_key=\"TEAM_AGENT_PROVIDER_API_KEY\"",
135
+ "model_providers.relay.wire_api=\"chat\"",
136
+ "model_providers.relay.name=\"Relay\"",
137
+ ] {
138
+ if !argv.iter().any(|arg| arg == config) {
139
+ failures.push(format!(
140
+ "D3: codex argv missing `-c {config}` (profile codex_config)"
141
+ ));
142
+ }
143
+ }
144
+ // Python golden order: --profile before --sandbox; codex_config -c before developer_instructions.
145
+ if let (Some(profile_idx), Some(sandbox_idx)) = (
146
+ argv.iter().position(|arg| arg == "--profile"),
147
+ argv.iter().position(|arg| arg == "--sandbox"),
148
+ ) {
149
+ if profile_idx > sandbox_idx {
150
+ failures
151
+ .push("D3: `--profile` must precede `--sandbox` (codex.py:105-113)".to_string());
152
+ }
153
+ }
154
+ if let (Some(config_idx), Some(dev_idx)) = (
155
+ argv.iter()
156
+ .position(|arg| arg == "model_provider=\"relay\""),
157
+ argv.iter()
158
+ .position(|arg| arg.starts_with("developer_instructions=")),
159
+ ) {
160
+ if config_idx > dev_idx {
161
+ failures.push(
162
+ "D3: profile codex_config `-c` items must precede developer_instructions (codex.py:117-121)"
163
+ .to_string(),
164
+ );
165
+ }
166
+ }
167
+
168
+ assert!(
169
+ failures.is_empty(),
170
+ "D1/D2/D3 codex argv contract failed:\n{}\nargv={argv:?}",
171
+ failures.join("\n")
172
+ );
173
+ }
174
+
175
+ /// D4: providers.py:131 — the worker shell env must export TEAM_AGENT_ID=<the worker itself>,
176
+ /// overriding any TEAM_AGENT_ID inherited from the launching process (e.g. an add-agent issued
177
+ /// from another worker's MCP server, whose environ carries the CALLER's TEAM_AGENT_ID).
178
+ #[test]
179
+ #[serial(env)]
180
+ fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
181
+ let _guard = EnvGuard::set(&[
182
+ (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
183
+ ("TEAM_AGENT_ID", "stale-mcp-caller"),
184
+ ]);
185
+ let ws = tmp_dir("d4-spawn-env");
186
+ let team_dir = write_team(&ws, &simple_codex_team("d4team", "worker_a", "Env Worker"));
187
+ seed_healthy_coordinator(&ws);
188
+ let transport = RecordingTransport::new();
189
+
190
+ quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d4team"), &transport)
191
+ .expect("quick-start should spawn the worker");
192
+ let spawn = transport.single_spawn();
193
+ let mut failures = Vec::new();
194
+ match spawn.env.get("TEAM_AGENT_ID").map(String::as_str) {
195
+ Some("worker_a") => {}
196
+ other => failures.push(format!(
197
+ "D4: worker spawn env must set TEAM_AGENT_ID=worker_a (Python providers.py:131), overriding the inherited caller value; got {other:?}"
198
+ )),
199
+ }
200
+ match spawn.env.get("TEAM_AGENT_AGENT_ID").map(String::as_str) {
201
+ Some("worker_a") => {}
202
+ other => failures.push(format!(
203
+ "D4: TEAM_AGENT_AGENT_ID=worker_a must stay alongside TEAM_AGENT_ID (#229 compat); got {other:?}"
204
+ )),
205
+ }
206
+ assert!(
207
+ failures.is_empty(),
208
+ "D4 worker shell env contract failed:\n{}\nenv_keys={:?}",
209
+ failures.join("\n"),
210
+ spawn.env.keys().collect::<Vec<_>>()
211
+ );
212
+ }
213
+
214
+ /// D5: providers.py:145 + launch/core.py:253 — fresh launch runs the worker with cwd=WORKSPACE
215
+ /// (`cd <workspace> && ... exec`), and persists state.agents[*].spawn_cwd = workspace.
216
+ /// RS fork/add (launch.rs:2304) and restart (restart/common.rs:125-133) already use workspace;
217
+ /// fresh launch passing team_dir is the odd one out.
218
+ #[test]
219
+ #[serial(env)]
220
+ fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
221
+ let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
222
+ let ws = tmp_dir("d5-spawn-cwd");
223
+ let team_dir = write_team(&ws, &simple_codex_team("d5team", "worker_a", "Cwd Worker"));
224
+ seed_healthy_coordinator(&ws);
225
+ let transport = RecordingTransport::new();
226
+
227
+ quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d5team"), &transport)
228
+ .expect("quick-start should spawn the worker");
229
+ let spawn = transport.single_spawn();
230
+ let mut failures = Vec::new();
231
+ if spawn.cwd != ws {
232
+ failures.push(format!(
233
+ "D5: transport spawn cwd must be the workspace {} (Python providers.py:145 `cd <workspace>`); got {}",
234
+ ws.display(),
235
+ spawn.cwd.display()
236
+ ));
237
+ }
238
+ let state = load_runtime_state(&ws).expect("load runtime state after quick-start");
239
+ let spawn_cwd = state
240
+ .pointer("/agents/worker_a/spawn_cwd")
241
+ .and_then(serde_json::Value::as_str)
242
+ .map(str::to_string);
243
+ if spawn_cwd.as_deref() != Some(&*ws.to_string_lossy()) {
244
+ failures.push(format!(
245
+ "D5: persisted agents.worker_a.spawn_cwd must equal the workspace {:?} (Python launch/core.py:253); got {spawn_cwd:?}",
246
+ ws.to_string_lossy()
247
+ ));
248
+ }
249
+ assert!(
250
+ failures.is_empty(),
251
+ "D5 fresh-launch spawn cwd contract failed:\n{}",
252
+ failures.join("\n")
253
+ );
254
+ }
255
+
256
+ /// D6: prompt.py:39 — chunks = [identity, TEAMMATE_SYSTEM_PROMPT, ...]: the compiled system
257
+ /// prompt must START with the worker identity line (live Python worker argv confirms this),
258
+ /// with the shared runtime contract section second.
259
+ #[test]
260
+ #[serial(env)]
261
+ fn d6_compiled_system_prompt_first_section_is_identity() {
262
+ let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
263
+ let ws = tmp_dir("d6-prompt-order");
264
+ let team_dir = write_team(
265
+ &ws,
266
+ &TeamFixture {
267
+ name: "d6team",
268
+ provider: "claude",
269
+ fast: false,
270
+ agents: &[AgentDoc {
271
+ id: "worker_a",
272
+ role: "Prompt Order Worker",
273
+ provider: "claude",
274
+ auth_mode: "subscription",
275
+ model: Some("fake"),
276
+ profile: None,
277
+ tools: &["mcp_team"],
278
+ body: "ROLE BODY SENTINEL for prompt ordering.",
279
+ }],
280
+ },
281
+ );
282
+ seed_healthy_coordinator(&ws);
283
+ let transport = RecordingTransport::new();
284
+
285
+ quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d6team"), &transport)
286
+ .expect("quick-start should spawn the claude worker");
287
+ let spawn = transport.single_spawn();
288
+ let prompt = flag_value(&spawn.argv, "--append-system-prompt")
289
+ .expect("claude argv must carry --append-system-prompt");
290
+
291
+ let identity = "You are Team Agent worker `worker_a` with role `Prompt Order Worker`. \
292
+ When asked about your role or identity, answer with this Team Agent worker identity first, \
293
+ not only the generic provider product identity.";
294
+ assert!(
295
+ prompt.starts_with(identity),
296
+ "D6: compiled system prompt must START with the identity section (Python prompt.py:39 \
297
+ chunks=[identity, TEAMMATE_SYSTEM_PROMPT, ...]; live ps confirms identity-first); got head={:?}",
298
+ prompt.chars().take(220).collect::<String>()
299
+ );
300
+ let contract_idx = prompt
301
+ .find("# Team Agent Teammate Runtime Contract")
302
+ .expect("prompt must contain the shared runtime contract section");
303
+ assert!(
304
+ contract_idx >= identity.len(),
305
+ "D6: the runtime contract section must come AFTER the identity section; contract_idx={contract_idx}"
306
+ );
307
+ }
308
+
309
+ /// D7: launch/core.py:235-237 — runtime.fast + provider==codex must toggle codex fast mode
310
+ /// after spawn by sending `/fast` + Enter to the worker pane (tmux_prompt.py:124-129).
311
+ #[test]
312
+ #[serial(env)]
313
+ fn d7_runtime_fast_sends_codex_fast_mode_toggle() {
314
+ let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
315
+ let ws = tmp_dir("d7-fast-mode");
316
+ let mut fixture = simple_codex_team("d7team", "worker_a", "Fast Worker");
317
+ fixture.fast = true;
318
+ let team_dir = write_team(&ws, &fixture);
319
+ seed_healthy_coordinator(&ws);
320
+ let transport = RecordingTransport::new();
321
+
322
+ quick_start_with_transport_in_workspace(&ws, &team_dir, None, true, Some("d7team"), &transport)
323
+ .expect("quick-start should spawn the codex worker");
324
+ transport.single_spawn();
325
+ let sent = transport.sent_text();
326
+ assert!(
327
+ sent.iter().any(|text| text.contains("/fast")),
328
+ "D7: TEAM.md `fast: true` (compiled to runtime.fast) must send the `/fast` toggle to the \
329
+ codex worker after spawn (Python launch/core.py:235-237 + tmux_prompt.py:124-129); recorded \
330
+ keystrokes/injections={sent:?}"
331
+ );
332
+ }
333
+
334
+ /// D8: launch/core.py:56 — when the spec carries no runtime.session_name, the session falls
335
+ /// back to `team-{team.name}`, never a fixed constant.
336
+ #[test]
337
+ #[serial(env)]
338
+ fn d8_launch_session_name_fallback_derives_from_team_name() {
339
+ let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
340
+ let ws = tmp_dir("d8-session-name");
341
+ let team_dir = write_team(
342
+ &ws,
343
+ &simple_codex_team("d8team", "worker_a", "Session Worker"),
344
+ );
345
+ let spec = team_agent::compiler::compile_team(&team_dir).expect("compile fixture team");
346
+ let spec_text = team_agent::model::yaml::dumps(&spec);
347
+ let stripped: String = spec_text
348
+ .lines()
349
+ .filter(|line| !line.trim_start().starts_with("session_name:"))
350
+ .map(|line| format!("{line}\n"))
351
+ .collect();
352
+ assert!(
353
+ stripped.len() < spec_text.len(),
354
+ "fixture must actually remove the compiled session_name line"
355
+ );
356
+ let spec_path = team_dir.join("team.spec.yaml");
357
+ std::fs::write(&spec_path, stripped).unwrap();
358
+ let transport = RecordingTransport::new();
359
+
360
+ launch_with_transport(&spec_path, false, true, true, &transport)
361
+ .expect("launch should spawn the worker without an explicit session_name");
362
+ let spawn = transport.single_spawn();
363
+ assert_eq!(
364
+ spawn.session, "team-d8team",
365
+ "D8: session_name fallback must derive `team-{{team.name}}` (Python launch/core.py:56), \
366
+ not a constant; argv={:?}",
367
+ spawn.argv
368
+ );
369
+ }
370
+
371
+ /// D9: provider_env.py:82-90 + providers.py:142-145 — profile env_unset keys (codex
372
+ /// compatible_api → OPENAI_API_KEY / OPENAI_BASE_URL) must be REALLY unset in the worker
373
+ /// shell line (Python sources the env file whose first lines are `unset <KEY>`), because the
374
+ /// tmux server environment can still carry stale values that plain map-removal cannot clear.
375
+ #[test]
376
+ #[serial(env)]
377
+ fn d9_profile_env_unset_reaches_worker_shell_line() {
378
+ let _guard = EnvGuard::set(&[
379
+ (ANCESTRY_KEY, NEUTRAL_ANCESTRY),
380
+ ("OPENAI_API_KEY", "stale-parent-key"),
381
+ ]);
382
+ let ws = tmp_dir("d9-env-unset");
383
+ let team_dir = write_team(
384
+ &ws,
385
+ &TeamFixture {
386
+ name: "d9team",
387
+ provider: "codex",
388
+ fast: false,
389
+ agents: &[AgentDoc {
390
+ id: "worker_a",
391
+ role: "Unset Worker",
392
+ provider: "codex",
393
+ auth_mode: "compatible_api",
394
+ model: Some("gpt-5.5"),
395
+ profile: Some("relay-nokey"),
396
+ tools: &["mcp_team"],
397
+ body: "Unset worker.",
398
+ }],
399
+ },
400
+ );
401
+ // No API_KEY on purpose: OPENAI_API_KEY lands in env_unset (provider_env.py:51-52) and is
402
+ // never re-exported, so the stale tmux-server/parent value leaks unless the shell really
403
+ // unsets it. (With API_KEY present the overlay re-export would mask the missing unset.)
404
+ let profiles = team_dir.join("profiles");
405
+ std::fs::create_dir_all(&profiles).unwrap();
406
+ std::fs::write(
407
+ profiles.join("relay-nokey.env"),
408
+ "AUTH_MODE=compatible_api\nBASE_URL=https://relay.example/v1\n",
409
+ )
410
+ .unwrap();
411
+ let spec = team_agent::compiler::compile_team(&team_dir).expect("compile fixture team");
412
+ let spec_path = team_dir.join("team.spec.yaml");
413
+ std::fs::write(&spec_path, team_agent::model::yaml::dumps(&spec)).unwrap();
414
+
415
+ let runner = RecordingRunner::new();
416
+ let argv_log = runner.argv_log();
417
+ let backend = TmuxBackend::with_runner(Box::new(runner));
418
+ // The spawn may be marked dead by the canned runner after the shell line is built;
419
+ // the contract only needs the recorded `sh -lc` line, so the report itself is not asserted.
420
+ let _ = launch_with_transport(&spec_path, false, true, true, &backend);
421
+
422
+ let recorded = argv_log.lock().unwrap();
423
+ let shell_line = recorded
424
+ .iter()
425
+ .flatten()
426
+ .find(|arg| arg.starts_with("cd ") && arg.contains(" exec "))
427
+ .cloned()
428
+ .unwrap_or_else(|| {
429
+ panic!("no worker `cd … exec …` shell line recorded; tmux calls={recorded:?}")
430
+ });
431
+
432
+ let env_file = ws.join(".team/runtime/provider-env/worker_a.env");
433
+ let mut failures = Vec::new();
434
+ let env_file_text = std::fs::read_to_string(&env_file).unwrap_or_default();
435
+ if !env_file_text.contains("unset OPENAI_API_KEY") {
436
+ failures.push(format!(
437
+ "D9: provider env file must contain `unset OPENAI_API_KEY` (Python provider_env.py:86); path={} content={env_file_text:?}",
438
+ env_file.display()
439
+ ));
440
+ }
441
+ let unsets_inline = shell_line.contains("unset OPENAI_API_KEY");
442
+ let sources_env_file = shell_line.contains(&*env_file.to_string_lossy());
443
+ if !(unsets_inline || sources_env_file) {
444
+ failures.push(format!(
445
+ "D9: worker shell line must really unset OPENAI_API_KEY — either inline `unset OPENAI_API_KEY` \
446
+ or by sourcing the env file `{}` before exec (Python providers.py:142-145); shell_line={shell_line:?}",
447
+ env_file.display()
448
+ ));
449
+ }
450
+ assert!(
451
+ failures.is_empty(),
452
+ "D9 env_unset shell contract failed:\n{}",
453
+ failures.join("\n")
454
+ );
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // fixtures
459
+ // ---------------------------------------------------------------------------
460
+
461
+ struct TeamFixture<'a> {
462
+ name: &'a str,
463
+ provider: &'a str,
464
+ fast: bool,
465
+ agents: &'a [AgentDoc<'a>],
466
+ }
467
+
468
+ struct AgentDoc<'a> {
469
+ id: &'a str,
470
+ role: &'a str,
471
+ provider: &'a str,
472
+ auth_mode: &'a str,
473
+ model: Option<&'a str>,
474
+ profile: Option<&'a str>,
475
+ tools: &'a [&'a str],
476
+ body: &'a str,
477
+ }
478
+
479
+ fn simple_codex_team(
480
+ name: &'static str,
481
+ agent: &'static str,
482
+ role: &'static str,
483
+ ) -> TeamFixture<'static> {
484
+ let agents: &'static [AgentDoc<'static>] = Box::leak(Box::new([AgentDoc {
485
+ id: agent,
486
+ role,
487
+ provider: "codex",
488
+ auth_mode: "subscription",
489
+ model: Some("gpt-5.5"),
490
+ profile: None,
491
+ tools: &["mcp_team"],
492
+ body: Box::leak(format!("{role}.").into_boxed_str()),
493
+ }]));
494
+ TeamFixture {
495
+ name,
496
+ provider: "codex",
497
+ fast: false,
498
+ agents,
499
+ }
500
+ }
501
+
502
+ fn write_team(workspace: &Path, fixture: &TeamFixture<'_>) -> PathBuf {
503
+ let team = workspace.join(fixture.name);
504
+ std::fs::create_dir_all(team.join("agents")).unwrap();
505
+ let fast = if fixture.fast { "fast: true\n" } else { "" };
506
+ std::fs::write(
507
+ team.join("TEAM.md"),
508
+ format!(
509
+ "---\nname: {}\nobjective: 264 behavioral diff fixture.\nprovider: {}\n{fast}---\n\nTeam fixture.\n",
510
+ fixture.name, fixture.provider
511
+ ),
512
+ )
513
+ .unwrap();
514
+ for agent in fixture.agents {
515
+ let tools = agent
516
+ .tools
517
+ .iter()
518
+ .map(|tool| format!(" - {tool}\n"))
519
+ .collect::<String>();
520
+ let model = agent
521
+ .model
522
+ .map(|model| format!("model: {model}\n"))
523
+ .unwrap_or_default();
524
+ let profile = agent
525
+ .profile
526
+ .map(|profile| format!("profile: {profile}\n"))
527
+ .unwrap_or_default();
528
+ std::fs::write(
529
+ team.join("agents").join(format!("{}.md", agent.id)),
530
+ format!(
531
+ "---\nname: {}\nrole: {}\nprovider: {}\n{model}auth_mode: {}\n{profile}tools:\n{tools}---\n\n{}\n",
532
+ agent.id, agent.role, agent.provider, agent.auth_mode, agent.body
533
+ ),
534
+ )
535
+ .unwrap();
536
+ }
537
+ team
538
+ }
539
+
540
+ /// Profile values mirror the Python golden run: provider_env.py:62-79 turns these into
541
+ /// codex_profile=relay-fast plus the five `-c` codex_config items asserted in D3.
542
+ fn write_relay_profile(team_dir: &Path) {
543
+ let profiles = team_dir.join("profiles");
544
+ std::fs::create_dir_all(&profiles).unwrap();
545
+ std::fs::write(
546
+ profiles.join("relay.env"),
547
+ "AUTH_MODE=compatible_api\n\
548
+ BASE_URL=https://relay.example/v1\n\
549
+ API_KEY=sk-relay-264\n\
550
+ MODEL_PROVIDER=relay\n\
551
+ WIRE_API=chat\n\
552
+ PROVIDER_NAME=Relay\n\
553
+ CODEX_PROFILE=relay-fast\n",
554
+ )
555
+ .unwrap();
556
+ }
557
+
558
+ fn seed_healthy_coordinator(workspace: &Path) {
559
+ let workspace = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
560
+ let pid = team_agent::coordinator::Pid::new(std::process::id());
561
+ std::fs::create_dir_all(
562
+ team_agent::coordinator::coordinator_pid_path(&workspace)
563
+ .parent()
564
+ .unwrap(),
565
+ )
566
+ .unwrap();
567
+ team_agent::coordinator::write_coordinator_metadata(
568
+ &workspace,
569
+ pid,
570
+ team_agent::coordinator::MetadataSource::Boot,
571
+ )
572
+ .expect("write coordinator metadata");
573
+ std::fs::write(
574
+ team_agent::coordinator::coordinator_pid_path(&workspace),
575
+ pid.to_string(),
576
+ )
577
+ .expect("write coordinator pid");
578
+ }
579
+
580
+ fn tmp_dir(tag: &str) -> PathBuf {
581
+ static N: AtomicU64 = AtomicU64::new(0);
582
+ let dir = std::env::temp_dir().join(format!(
583
+ "ta-rs-264-{tag}-{}-{}",
584
+ std::process::id(),
585
+ N.fetch_add(1, Ordering::Relaxed)
586
+ ));
587
+ let _ = std::fs::remove_dir_all(&dir);
588
+ std::fs::create_dir_all(&dir).unwrap();
589
+ std::fs::canonicalize(dir).unwrap()
590
+ }
591
+
592
+ fn flag_value(argv: &[String], flag: &str) -> Option<String> {
593
+ argv.windows(2)
594
+ .find_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
595
+ }
596
+
597
+ struct EnvGuard {
598
+ previous: Vec<(&'static str, Option<String>)>,
599
+ }
600
+
601
+ impl EnvGuard {
602
+ fn set(values: &[(&'static str, &'static str)]) -> Self {
603
+ let previous = values
604
+ .iter()
605
+ .map(|(key, _)| (*key, std::env::var(key).ok()))
606
+ .collect::<Vec<_>>();
607
+ for (key, value) in values {
608
+ std::env::set_var(key, value);
609
+ }
610
+ Self { previous }
611
+ }
612
+ }
613
+
614
+ impl Drop for EnvGuard {
615
+ fn drop(&mut self) {
616
+ for (key, value) in self.previous.drain(..).rev() {
617
+ match value {
618
+ Some(value) => std::env::set_var(key, value),
619
+ None => std::env::remove_var(key),
620
+ }
621
+ }
622
+ }
623
+ }
624
+
625
+ // ---------------------------------------------------------------------------
626
+ // recording transport (argv + env + cwd + session + keystrokes)
627
+ // ---------------------------------------------------------------------------
628
+
629
+ #[derive(Debug, Clone)]
630
+ struct RecordedSpawn {
631
+ argv: Vec<String>,
632
+ env: BTreeMap<String, String>,
633
+ cwd: PathBuf,
634
+ session: String,
635
+ }
636
+
637
+ #[derive(Default)]
638
+ struct RecordingTransport {
639
+ spawns: Mutex<Vec<RecordedSpawn>>,
640
+ sent_text: Mutex<Vec<String>>,
641
+ session_present: Mutex<bool>,
642
+ }
643
+
644
+ impl RecordingTransport {
645
+ fn new() -> Self {
646
+ Self::default()
647
+ }
648
+
649
+ fn single_spawn(&self) -> RecordedSpawn {
650
+ let spawns = self.spawns.lock().unwrap();
651
+ assert_eq!(
652
+ spawns.len(),
653
+ 1,
654
+ "fixture should record exactly one worker spawn; spawns={spawns:?}"
655
+ );
656
+ spawns[0].clone()
657
+ }
658
+
659
+ fn sent_text(&self) -> Vec<String> {
660
+ self.sent_text.lock().unwrap().clone()
661
+ }
662
+
663
+ fn record_spawn(
664
+ &self,
665
+ session: &SessionName,
666
+ window: &WindowName,
667
+ argv: &[String],
668
+ cwd: &Path,
669
+ env: &BTreeMap<String, String>,
670
+ ) -> SpawnResult {
671
+ let mut spawns = self.spawns.lock().unwrap();
672
+ spawns.push(RecordedSpawn {
673
+ argv: argv.to_vec(),
674
+ env: env.clone(),
675
+ cwd: cwd.to_path_buf(),
676
+ session: session.as_str().to_string(),
677
+ });
678
+ *self.session_present.lock().unwrap() = true;
679
+ SpawnResult {
680
+ pane_id: PaneId::new(format!("%{}", spawns.len())),
681
+ session: session.clone(),
682
+ window: window.clone(),
683
+ child_pid: Some(20_000 + spawns.len() as u32),
684
+ }
685
+ }
686
+ }
687
+
688
+ impl Transport for RecordingTransport {
689
+ fn kind(&self) -> BackendKind {
690
+ BackendKind::Tmux
691
+ }
692
+
693
+ fn spawn_first(
694
+ &self,
695
+ session: &SessionName,
696
+ window: &WindowName,
697
+ argv: &[String],
698
+ cwd: &Path,
699
+ env: &BTreeMap<String, String>,
700
+ ) -> Result<SpawnResult, TransportError> {
701
+ Ok(self.record_spawn(session, window, argv, cwd, env))
702
+ }
703
+
704
+ fn spawn_into(
705
+ &self,
706
+ session: &SessionName,
707
+ window: &WindowName,
708
+ argv: &[String],
709
+ cwd: &Path,
710
+ env: &BTreeMap<String, String>,
711
+ ) -> Result<SpawnResult, TransportError> {
712
+ Ok(self.record_spawn(session, window, argv, cwd, env))
713
+ }
714
+
715
+ fn inject(
716
+ &self,
717
+ _target: &Target,
718
+ payload: &InjectPayload,
719
+ _submit: Key,
720
+ _bracketed: bool,
721
+ ) -> Result<InjectReport, TransportError> {
722
+ if let Some(text) = payload.text() {
723
+ self.sent_text.lock().unwrap().push(text.to_string());
724
+ }
725
+ Ok(InjectReport {
726
+ stage_reached: InjectStage::Submit,
727
+ inject_verification: InjectVerification::CaptureContainsToken,
728
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
729
+ turn_verification: TurnVerification::NotYetObserved,
730
+ attempts: 1,
731
+ submit_diagnostics: None,
732
+ })
733
+ }
734
+
735
+ fn send_keys(&self, _target: &Target, keys: &[Key]) -> Result<(), TransportError> {
736
+ let text: String = keys
737
+ .iter()
738
+ .map(|key| match key {
739
+ Key::Char(c) => *c,
740
+ Key::Enter => '\n',
741
+ _ => '\u{FFFD}',
742
+ })
743
+ .collect();
744
+ self.sent_text.lock().unwrap().push(text);
745
+ Ok(())
746
+ }
747
+
748
+ fn capture(
749
+ &self,
750
+ _target: &Target,
751
+ range: CaptureRange,
752
+ ) -> Result<CapturedText, TransportError> {
753
+ Ok(CapturedText {
754
+ text: "Claude Code\n> \nOpenAI Codex\ncodex>".to_string(),
755
+ range,
756
+ })
757
+ }
758
+
759
+ fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
760
+ match field {
761
+ PaneField::PaneWidth => Ok(Some("120".to_string())),
762
+ _ => Ok(None),
763
+ }
764
+ }
765
+
766
+ fn liveness(
767
+ &self,
768
+ _pane: &PaneId,
769
+ ) -> Result<team_agent::transport::PaneLiveness, TransportError> {
770
+ Ok(team_agent::transport::PaneLiveness::Live)
771
+ }
772
+
773
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
774
+ Ok(Vec::new())
775
+ }
776
+
777
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
778
+ Ok(*self.session_present.lock().unwrap())
779
+ }
780
+
781
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
782
+ Ok(Vec::new())
783
+ }
784
+
785
+ fn set_session_env(
786
+ &self,
787
+ _session: &SessionName,
788
+ _key: &str,
789
+ _value: &str,
790
+ ) -> Result<SetEnvOutcome, TransportError> {
791
+ Ok(SetEnvOutcome::Applied)
792
+ }
793
+
794
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
795
+ *self.session_present.lock().unwrap() = false;
796
+ Ok(())
797
+ }
798
+
799
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
800
+ Ok(())
801
+ }
802
+
803
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
804
+ Ok(AttachOutcome::Attached)
805
+ }
806
+ }
807
+
808
+ // ---------------------------------------------------------------------------
809
+ // recording tmux runner (D9: observes the real TmuxBackend shell line)
810
+ // ---------------------------------------------------------------------------
811
+
812
+ type ArgvLog = std::sync::Arc<Mutex<Vec<Vec<String>>>>;
813
+
814
+ struct RecordingRunner {
815
+ argv_log: ArgvLog,
816
+ }
817
+
818
+ impl RecordingRunner {
819
+ fn new() -> Self {
820
+ Self {
821
+ argv_log: std::sync::Arc::new(Mutex::new(Vec::new())),
822
+ }
823
+ }
824
+
825
+ fn argv_log(&self) -> ArgvLog {
826
+ std::sync::Arc::clone(&self.argv_log)
827
+ }
828
+ }
829
+
830
+ impl CommandRunner for RecordingRunner {
831
+ fn run(&self, argv: &[String]) -> Result<CommandOutput, std::io::Error> {
832
+ self.argv_log.lock().unwrap().push(argv.to_vec());
833
+ let subcommand = argv
834
+ .iter()
835
+ .find(|arg| !arg.starts_with('-') && *arg != "tmux");
836
+ let (success, stdout) = match subcommand.map(String::as_str) {
837
+ Some("has-session") => (false, String::new()),
838
+ Some("display-message") => (true, "%1\n".to_string()),
839
+ Some("capture-pane") => (true, "OpenAI Codex\ncodex>\n".to_string()),
840
+ _ => (true, String::new()),
841
+ };
842
+ Ok(CommandOutput {
843
+ success,
844
+ code: Some(if success { 0 } else { 1 }),
845
+ stdout,
846
+ stderr: String::new(),
847
+ })
848
+ }
849
+ }