@team-agent/installer 0.5.41 → 0.5.43

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 (152) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +27 -7
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +47 -52
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/coordinator/tick.rs +19 -1
  51. package/crates/team-agent/src/db/message_store.rs +138 -30
  52. package/crates/team-agent/src/db/migration.rs +249 -61
  53. package/crates/team-agent/src/db/schema.rs +303 -82
  54. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  55. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  56. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  57. package/crates/team-agent/src/event_log.rs +70 -16
  58. package/crates/team-agent/src/layout/manager.rs +15 -4
  59. package/crates/team-agent/src/layout/mod.rs +4 -4
  60. package/crates/team-agent/src/layout/overlay.rs +10 -3
  61. package/crates/team-agent/src/layout/placement.rs +5 -1
  62. package/crates/team-agent/src/layout/recovery.rs +4 -2
  63. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  64. package/crates/team-agent/src/layout/sessions.rs +17 -9
  65. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  66. package/crates/team-agent/src/layout/worker_env.rs +52 -19
  67. package/crates/team-agent/src/leader/helpers.rs +7 -1
  68. package/crates/team-agent/src/leader/lease.rs +195 -82
  69. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/start.rs +75 -54
  73. package/crates/team-agent/src/leader/takeover.rs +46 -11
  74. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  75. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  76. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  77. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  78. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  79. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  80. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  81. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  82. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  83. package/crates/team-agent/src/lib.rs +4 -4
  84. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  85. package/crates/team-agent/src/lifecycle/launch.rs +6 -1
  86. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  87. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  88. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  89. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  90. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  91. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  92. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  93. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  94. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  95. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  96. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  97. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  98. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  99. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  100. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  101. package/crates/team-agent/src/main.rs +4 -4
  102. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  103. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  104. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  105. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  106. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  107. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  108. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  109. package/crates/team-agent/src/messaging/mod.rs +2 -3
  110. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  111. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  112. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  113. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  114. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  115. package/crates/team-agent/src/messaging/trust.rs +25 -2
  116. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  117. package/crates/team-agent/src/model/enums.rs +65 -16
  118. package/crates/team-agent/src/model/ids.rs +12 -3
  119. package/crates/team-agent/src/model/paths.rs +28 -7
  120. package/crates/team-agent/src/model/permissions.rs +176 -33
  121. package/crates/team-agent/src/model/routing.rs +66 -20
  122. package/crates/team-agent/src/model/spec.rs +365 -69
  123. package/crates/team-agent/src/model/task_graph.rs +36 -9
  124. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  125. package/crates/team-agent/src/model/yaml.rs +7 -6
  126. package/crates/team-agent/src/packaging/install.rs +23 -9
  127. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  128. package/crates/team-agent/src/packaging/mod.rs +9 -1
  129. package/crates/team-agent/src/packaging/repair.rs +13 -6
  130. package/crates/team-agent/src/packaging/tests.rs +63 -16
  131. package/crates/team-agent/src/packaging/types.rs +22 -7
  132. package/crates/team-agent/src/platform/argv.rs +4 -1
  133. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  134. package/crates/team-agent/src/platform/process.rs +21 -22
  135. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  136. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  137. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  138. package/crates/team-agent/src/provider/classify.rs +127 -42
  139. package/crates/team-agent/src/provider/faults.rs +17 -5
  140. package/crates/team-agent/src/provider/helpers.rs +6 -5
  141. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  142. package/crates/team-agent/src/state/repository.rs +27 -14
  143. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  144. package/crates/team-agent/src/tmux_backend.rs +80 -44
  145. package/crates/team-agent/src/topology.rs +27 -20
  146. package/crates/team-agent/src/transport/test_support.rs +20 -23
  147. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  148. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  149. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  150. package/crates/team-agent/src/transport.rs +12 -17
  151. package/crates/team-agent/src/transport_factory.rs +29 -14
  152. package/package.json +4 -4
@@ -1,246 +1,388 @@
1
1
  use super::*;
2
2
 
3
- #[test]
4
- #[serial_test::serial(env)]
5
- fn d1_claim_team_owner_includes_os_user() {
6
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
7
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
8
- let ws = p2_temp_ws("d1_owner_keys");
9
- let mut state = serde_json::json!({"session_name": "team-agent-x"});
10
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
11
- &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
12
- assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
13
- // Stage 3d (identity-boundary unified plan, architect direction
14
- // 2026-06-23): canonical owner now at teams.<team_key>.team_owner.
15
- let team_key = crate::state::projection::team_state_key(&state);
16
- let owner = state["teams"][&team_key]["team_owner"]
17
- .as_object()
18
- .expect("Stage 3d: team_owner at canonical teams.<team_key>");
19
- assert!(owner.contains_key("os_user"),
20
- "golden claim-path team_owner carries os_user even when empty; keys={:?}", owner.keys().collect::<Vec<_>>());
21
- let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
22
- assert_eq!(keys, vec!["pane_id","provider","machine_fingerprint","leader_session_uuid","owner_epoch","claimed_at","claimed_via","os_user"],
23
- "golden new_owner includes os_user");
24
- }
3
+ #[test]
4
+ #[serial_test::serial(env)]
5
+ fn d1_claim_team_owner_includes_os_user() {
6
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
7
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
8
+ let ws = p2_temp_ws("d1_owner_keys");
9
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
10
+ let r = claim_lease_no_incident(
11
+ &ws,
12
+ &mut state,
13
+ None,
14
+ &TeamKey::new("current"),
15
+ &PaneId::new("%5"),
16
+ false,
17
+ &crate::event_log::EventLog::new(&ws),
18
+ &seeded_liveness(&["%5"]),
19
+ )
20
+ .unwrap();
21
+ assert!(
22
+ r.ok && r.status == LeaseStatus::Claimed,
23
+ "precondition: vacant acquire"
24
+ );
25
+ // Stage 3d (identity-boundary unified plan, architect direction
26
+ // 2026-06-23): canonical owner now at teams.<team_key>.team_owner.
27
+ let team_key = crate::state::projection::team_state_key(&state);
28
+ let owner = state["teams"][&team_key]["team_owner"]
29
+ .as_object()
30
+ .expect("Stage 3d: team_owner at canonical teams.<team_key>");
31
+ assert!(
32
+ owner.contains_key("os_user"),
33
+ "golden claim-path team_owner carries os_user even when empty; keys={:?}",
34
+ owner.keys().collect::<Vec<_>>()
35
+ );
36
+ let keys: Vec<&str> = owner.keys().map(String::as_str).collect();
37
+ assert_eq!(
38
+ keys,
39
+ vec![
40
+ "pane_id",
41
+ "provider",
42
+ "machine_fingerprint",
43
+ "leader_session_uuid",
44
+ "owner_epoch",
45
+ "claimed_at",
46
+ "claimed_via",
47
+ "os_user"
48
+ ],
49
+ "golden new_owner includes os_user"
50
+ );
51
+ }
25
52
 
26
- // D2 [BLOCK] — claim-path leader_receiver is golden's 15 keys in golden order; NO
27
- // fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
28
- // Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
29
- // (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
30
- // deferred real-tmux seam, see d2_receiver_populated_from_caller_target_seam; the KEY-SET + ORDER
31
- // locked here are unchanged by that scan.)
32
- #[test]
33
- #[serial_test::serial(env)]
34
- fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
35
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
36
- let _e = EnvGuard::apply(&[
37
- ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
38
- ("TMUX", Some("/tmp/tmux-501/default,123,0")),
39
- ]);
40
- let ws = p2_temp_ws("d2_recv_keys");
41
- let mut state = serde_json::json!({"session_name": "team-agent-x"});
42
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
43
- &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
44
- assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: vacant acquire");
45
- // Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
46
- let team_key = crate::state::projection::team_state_key(&state);
47
- let recv = state["teams"][&team_key]["leader_receiver"]
48
- .as_object()
49
- .expect("Stage 3d: leader_receiver at canonical teams.<team_key>");
50
- for extra in ["fingerprint", "requested_provider", "warning"] {
51
- assert!(!recv.contains_key(extra),
53
+ // D2 [BLOCK] — claim-path leader_receiver is golden's 15 keys in golden order; NO
54
+ // fingerprint/requested_provider/warning. Golden _receiver_from_claim_target (__init__.py:861-877).
55
+ // Rust LeaderReceiver serializes all 17 (no skip_serializing_if) -> 3 always-null extras leak. RED.
56
+ // (The POPULATED tmux values session_name/window_*/pane_* come from the caller-target scan — a
57
+ // deferred real-tmux seam, see d2_receiver_populated_from_caller_target_seam; the KEY-SET + ORDER
58
+ // locked here are unchanged by that scan.)
59
+ #[test]
60
+ #[serial_test::serial(env)]
61
+ fn d2_claim_leader_receiver_is_fifteen_golden_keys_in_order_no_extras() {
62
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
63
+ let _e = EnvGuard::apply(&[
64
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
65
+ ("TMUX", Some("/tmp/tmux-501/default,123,0")),
66
+ ]);
67
+ let ws = p2_temp_ws("d2_recv_keys");
68
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
69
+ let r = claim_lease_no_incident(
70
+ &ws,
71
+ &mut state,
72
+ None,
73
+ &TeamKey::new("current"),
74
+ &PaneId::new("%5"),
75
+ false,
76
+ &crate::event_log::EventLog::new(&ws),
77
+ &seeded_liveness(&["%5"]),
78
+ )
79
+ .unwrap();
80
+ assert!(
81
+ r.ok && r.status == LeaseStatus::Claimed,
82
+ "precondition: vacant acquire"
83
+ );
84
+ // Stage 3d: canonical receiver at teams.<team_key>.leader_receiver.
85
+ let team_key = crate::state::projection::team_state_key(&state);
86
+ let recv = state["teams"][&team_key]["leader_receiver"]
87
+ .as_object()
88
+ .expect("Stage 3d: leader_receiver at canonical teams.<team_key>");
89
+ for extra in ["fingerprint", "requested_provider", "warning"] {
90
+ assert!(!recv.contains_key(extra),
52
91
  "golden leader_receiver has NO '{extra}' key (Rust must add skip_serializing_if); keys={:?}", recv.keys().collect::<Vec<_>>());
53
- }
54
- let keys: Vec<&str> = recv.keys().map(String::as_str).collect();
55
- assert_eq!(keys, vec![
92
+ }
93
+ let keys: Vec<&str> = recv.keys().map(String::as_str).collect();
94
+ assert_eq!(keys, vec![
56
95
  "mode","status","provider","pane_id","session_name","window_index","window_name",
57
96
  "pane_index","pane_tty","pane_current_command","tmux_socket","leader_session_uuid",
58
97
  "owner_epoch","attached_at","discovery",
59
98
  ], "golden _receiver_from_claim_target 15-key set + ORDER (__init__.py:861-877 + BUG-4 socket-qualified receiver)");
60
- }
99
+ }
61
100
 
62
- // D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
63
- // pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
64
- // target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
65
- #[test]
66
- #[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
101
+ // D2 seam — the caller-target SCAN that fills session_name/window_index/window_name/pane_index/
102
+ // pane_tty/pane_current_command from core_list_targets (golden _receiver_from_claim_target reads
103
+ // target[...]). Rust make_receiver leaves them None (no scan). Real-tmux: needs core_list_targets.
104
+ #[test]
105
+ #[ignore = "real-tmux seam: leader_receiver session_name/window_*/pane_* are populated from the \
67
106
  caller target via core_list_targets (golden _receiver_from_claim_target); Rust has no \
68
107
  scan (values null). Porter wires the target scan; this asserts the populated values."]
69
- fn d2_receiver_populated_from_caller_target_seam() {
70
- // Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
71
- // tmux target list (core_list_targets) — out of in-process scope.
72
- }
108
+ fn d2_receiver_populated_from_caller_target_seam() {
109
+ // Golden contract: new_receiver.session_name == caller_target.session_name, etc. Needs a live
110
+ // tmux target list (core_list_targets) — out of in-process scope.
111
+ }
73
112
 
74
- // D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
75
- // (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
76
- // (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.
77
- #[test]
78
- #[serial_test::serial(env)]
79
- fn d3_bound_pane_receiver_first_reclaim_from_receiver_pane_is_already_bound() {
80
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
81
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
82
- let ws = p2_temp_ws("d3_receiver_first");
83
- let mut state = serde_json::json!({
84
- "session_name": "team-agent-x",
85
- "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
86
- "leader_receiver": {"pane_id":"%9","owner_epoch":3,"leader_session_uuid":"U"},
87
- });
88
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
89
- &PaneId::new("%9"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%9","%1"])).unwrap();
90
- assert!(r.ok, "a re-claim from the bound RECEIVER pane (%9) is idempotent ok=true; got {r:?}");
91
- assert_eq!(r.status, LeaseStatus::AlreadyBound,
113
+ // D3 [BLOCK] — bound_pane is RECEIVER-first (golden :624 receiver.pane_id OR owner.pane_id). PROBE-B
114
+ // (receiver=%9, owner=%1, caller=%9): golden ok=True already_bound. Rust derives OWNER-first
115
+ // (owner %1 != caller %9) -> owner live + !confirm -> refused force_confirm_required. RED.
116
+ #[test]
117
+ #[serial_test::serial(env)]
118
+ fn d3_bound_pane_receiver_first_reclaim_from_receiver_pane_is_already_bound() {
119
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
120
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
121
+ let ws = p2_temp_ws("d3_receiver_first");
122
+ let mut state = serde_json::json!({
123
+ "session_name": "team-agent-x",
124
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
125
+ "leader_receiver": {"pane_id":"%9","owner_epoch":3,"leader_session_uuid":"U"},
126
+ });
127
+ let r = claim_lease_no_incident(
128
+ &ws,
129
+ &mut state,
130
+ None,
131
+ &TeamKey::new("current"),
132
+ &PaneId::new("%9"),
133
+ false,
134
+ &crate::event_log::EventLog::new(&ws),
135
+ &seeded_liveness(&["%9", "%1"]),
136
+ )
137
+ .unwrap();
138
+ assert!(
139
+ r.ok,
140
+ "a re-claim from the bound RECEIVER pane (%9) is idempotent ok=true; got {r:?}"
141
+ );
142
+ assert_eq!(r.status, LeaseStatus::AlreadyBound,
92
143
  "golden :624 receiver-first: bound_pane=receiver(%9)==caller(%9) -> already_bound, NOT force_confirm");
93
- }
144
+ }
94
145
 
95
- // D4 [WARN] — precheck_epoch uses Python truthiness: owner_epoch=0 is FALSY -> falls through to
96
- // receiver_epoch=5 (golden _lease_epoch :400 int(owner.epoch or recv.epoch or 0)). Rust
97
- // current_owner_epoch Some(0) stops the chain -> 0. Surfaces in the force_confirm refusal. RED.
98
- #[test]
99
- #[serial_test::serial(env)]
100
- fn d4_precheck_epoch_zero_is_falsy_falls_through_to_receiver() {
101
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
102
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
103
- let ws = p2_temp_ws("d4_epoch_falsy");
104
- let mut state = serde_json::json!({
105
- "session_name": "team-agent-x",
106
- "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":0,"claimed_at":"t","claimed_via":"claim-leader"},
107
- "leader_receiver": {"pane_id":"%1","owner_epoch":5,"leader_session_uuid":"U"},
108
- });
109
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
110
- &PaneId::new("%2"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%1","%2"])).unwrap();
111
- assert_eq!(r.status, LeaseStatus::Refused, "precondition: live owner %1 + no confirm -> force_confirm");
112
- assert_eq!(r.owner_epoch, Some(OwnerEpoch(5)),
113
- "golden _lease_epoch: owner_epoch=0 is falsy -> receiver_epoch=5; Rust stops at Some(0)");
114
- }
146
+ // D4 [WARN] — precheck_epoch uses Python truthiness: owner_epoch=0 is FALSY -> falls through to
147
+ // receiver_epoch=5 (golden _lease_epoch :400 int(owner.epoch or recv.epoch or 0)). Rust
148
+ // current_owner_epoch Some(0) stops the chain -> 0. Surfaces in the force_confirm refusal. RED.
149
+ #[test]
150
+ #[serial_test::serial(env)]
151
+ fn d4_precheck_epoch_zero_is_falsy_falls_through_to_receiver() {
152
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
153
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
154
+ let ws = p2_temp_ws("d4_epoch_falsy");
155
+ let mut state = serde_json::json!({
156
+ "session_name": "team-agent-x",
157
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":0,"claimed_at":"t","claimed_via":"claim-leader"},
158
+ "leader_receiver": {"pane_id":"%1","owner_epoch":5,"leader_session_uuid":"U"},
159
+ });
160
+ let r = claim_lease_no_incident(
161
+ &ws,
162
+ &mut state,
163
+ None,
164
+ &TeamKey::new("current"),
165
+ &PaneId::new("%2"),
166
+ false,
167
+ &crate::event_log::EventLog::new(&ws),
168
+ &seeded_liveness(&["%1", "%2"]),
169
+ )
170
+ .unwrap();
171
+ assert_eq!(
172
+ r.status,
173
+ LeaseStatus::Refused,
174
+ "precondition: live owner %1 + no confirm -> force_confirm"
175
+ );
176
+ assert_eq!(
177
+ r.owner_epoch,
178
+ Some(OwnerEpoch(5)),
179
+ "golden _lease_epoch: owner_epoch=0 is falsy -> receiver_epoch=5; Rust stops at Some(0)"
180
+ );
181
+ }
115
182
 
116
- // D5 [BLOCK] — success path emits leader_receiver.rebind_applied + owner_epoch_advanced (golden
117
- // :712-729), NOT the incident-arm leader_receiver.claim_applied (:791). RED.
118
- #[test]
119
- #[serial_test::serial(env)]
120
- fn d5_success_emits_rebind_applied_and_owner_epoch_advanced_not_claim_applied() {
121
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
122
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
123
- let ws = p2_temp_ws("d5_events");
124
- let mut state = serde_json::json!({"session_name": "team-agent-x"});
125
- let _ = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
126
- &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
127
- let names = event_names(&ws);
128
- assert!(names.iter().any(|n| n == "leader_receiver.rebind_applied"),
129
- "golden success emits leader_receiver.rebind_applied (:712); got {names:?}");
130
- assert!(names.iter().any(|n| n == "owner_epoch_advanced"),
131
- "golden success emits owner_epoch_advanced (:721); got {names:?}");
132
- assert!(!names.iter().any(|n| n == "leader_receiver.claim_applied"),
133
- "claim_applied is the INCIDENT-arm event, NEVER on the no-incident path; got {names:?}");
134
- }
183
+ // D5 [BLOCK] — success path emits leader_receiver.rebind_applied + owner_epoch_advanced (golden
184
+ // :712-729), NOT the incident-arm leader_receiver.claim_applied (:791). RED.
185
+ #[test]
186
+ #[serial_test::serial(env)]
187
+ fn d5_success_emits_rebind_applied_and_owner_epoch_advanced_not_claim_applied() {
188
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
189
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
190
+ let ws = p2_temp_ws("d5_events");
191
+ let mut state = serde_json::json!({"session_name": "team-agent-x"});
192
+ let _ = claim_lease_no_incident(
193
+ &ws,
194
+ &mut state,
195
+ None,
196
+ &TeamKey::new("current"),
197
+ &PaneId::new("%5"),
198
+ false,
199
+ &crate::event_log::EventLog::new(&ws),
200
+ &seeded_liveness(&["%5"]),
201
+ )
202
+ .unwrap();
203
+ let names = event_names(&ws);
204
+ assert!(
205
+ names.iter().any(|n| n == "leader_receiver.rebind_applied"),
206
+ "golden success emits leader_receiver.rebind_applied (:712); got {names:?}"
207
+ );
208
+ assert!(
209
+ names.iter().any(|n| n == "owner_epoch_advanced"),
210
+ "golden success emits owner_epoch_advanced (:721); got {names:?}"
211
+ );
212
+ assert!(
213
+ !names.iter().any(|n| n == "leader_receiver.claim_applied"),
214
+ "claim_applied is the INCIDENT-arm event, NEVER on the no-incident path; got {names:?}"
215
+ );
216
+ }
135
217
 
136
- // D6 [WARN] — EVERY lease refusal writes a leader_receiver audit event (golden _emit_lease_refusal).
137
- // not_in_tmux_pane ∈ _LEASE_REBIND_REQUIRED_REASONS -> "leader_receiver.rebind_required" (:481).
138
- // Rust refused() takes no event_log -> writes nothing. RED.
139
- #[test]
140
- #[serial_test::serial(env)]
141
- fn d6_refusal_emits_leader_receiver_audit_event() {
142
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
143
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
144
- let ws = p2_temp_ws("d6_refusal_event");
145
- let mut state = serde_json::json!({});
146
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
147
- &PaneId::new(""), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&[])).unwrap();
148
- assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane), "precondition: empty caller -> not_in_tmux_pane");
149
- let names = event_names(&ws);
150
- assert!(names.iter().any(|n| n == "leader_receiver.rebind_required"),
218
+ // D6 [WARN] — EVERY lease refusal writes a leader_receiver audit event (golden _emit_lease_refusal).
219
+ // not_in_tmux_pane ∈ _LEASE_REBIND_REQUIRED_REASONS -> "leader_receiver.rebind_required" (:481).
220
+ // Rust refused() takes no event_log -> writes nothing. RED.
221
+ #[test]
222
+ #[serial_test::serial(env)]
223
+ fn d6_refusal_emits_leader_receiver_audit_event() {
224
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
225
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
226
+ let ws = p2_temp_ws("d6_refusal_event");
227
+ let mut state = serde_json::json!({});
228
+ let r = claim_lease_no_incident(
229
+ &ws,
230
+ &mut state,
231
+ None,
232
+ &TeamKey::new("current"),
233
+ &PaneId::new(""),
234
+ false,
235
+ &crate::event_log::EventLog::new(&ws),
236
+ &seeded_liveness(&[]),
237
+ )
238
+ .unwrap();
239
+ assert_eq!(
240
+ r.reason,
241
+ Some(LeaseReason::NotInTmuxPane),
242
+ "precondition: empty caller -> not_in_tmux_pane"
243
+ );
244
+ let names = event_names(&ws);
245
+ assert!(names.iter().any(|n| n == "leader_receiver.rebind_required"),
151
246
  "golden writes a leader_receiver.rebind_required audit on the not_in_tmux_pane refusal; Rust writes none. got {names:?}");
152
- }
247
+ }
153
248
 
154
- // D8 [WARN] — provider PRESERVED from the prior receiver on re-claim (golden
155
- // _receiver_from_claim_target provider=previous.provider or 'codex'; new_owner=new_recv.provider or
156
- // owner.provider or 'codex'). A dead-owner recover of a CLAUDE leader keeps provider='claude'. Rust
157
- // make_receiver/make_owner hardcode Provider::Codex. RED.
158
- #[test]
159
- #[serial_test::serial(env)]
160
- fn d8_provider_preserved_from_prior_receiver_on_reclaim() {
161
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
162
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
163
- let ws = p2_temp_ws("d8_provider");
164
- let mut state = serde_json::json!({
165
- "session_name": "team-agent-x",
166
- "team_owner": {"pane_id":"%1","provider":"claude","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
167
- "leader_receiver": {"pane_id":"%1","provider":"claude","owner_epoch":3,"leader_session_uuid":"U"},
168
- });
169
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
170
- &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
171
- assert!(r.ok && r.status == LeaseStatus::Claimed, "precondition: dead-owner %1 recover");
172
- assert_eq!(state.pointer("/team_owner/provider").and_then(|v| v.as_str()), Some("claude"),
173
- "golden preserves the prior provider (claude) on re-claim; Rust hardcodes codex");
174
- assert_eq!(state.pointer("/leader_receiver/provider").and_then(|v| v.as_str()), Some("claude"),
175
- "the new receiver also preserves the prior provider (golden previous.provider)");
176
- }
249
+ // D8 [WARN] — provider PRESERVED from the prior receiver on re-claim (golden
250
+ // _receiver_from_claim_target provider=previous.provider or 'codex'; new_owner=new_recv.provider or
251
+ // owner.provider or 'codex'). A dead-owner recover of a CLAUDE leader keeps provider='claude'. Rust
252
+ // make_receiver/make_owner hardcode Provider::Codex. RED.
253
+ #[test]
254
+ #[serial_test::serial(env)]
255
+ fn d8_provider_preserved_from_prior_receiver_on_reclaim() {
256
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
257
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
258
+ let ws = p2_temp_ws("d8_provider");
259
+ let mut state = serde_json::json!({
260
+ "session_name": "team-agent-x",
261
+ "team_owner": {"pane_id":"%1","provider":"claude","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
262
+ "leader_receiver": {"pane_id":"%1","provider":"claude","owner_epoch":3,"leader_session_uuid":"U"},
263
+ });
264
+ let r = claim_lease_no_incident(
265
+ &ws,
266
+ &mut state,
267
+ None,
268
+ &TeamKey::new("current"),
269
+ &PaneId::new("%5"),
270
+ false,
271
+ &crate::event_log::EventLog::new(&ws),
272
+ &seeded_liveness(&["%5"]),
273
+ )
274
+ .unwrap();
275
+ assert!(
276
+ r.ok && r.status == LeaseStatus::Claimed,
277
+ "precondition: dead-owner %1 recover"
278
+ );
279
+ assert_eq!(
280
+ state
281
+ .pointer("/team_owner/provider")
282
+ .and_then(|v| v.as_str()),
283
+ Some("claude"),
284
+ "golden preserves the prior provider (claude) on re-claim; Rust hardcodes codex"
285
+ );
286
+ assert_eq!(
287
+ state
288
+ .pointer("/leader_receiver/provider")
289
+ .and_then(|v| v.as_str()),
290
+ Some("claude"),
291
+ "the new receiver also preserves the prior provider (golden previous.provider)"
292
+ );
293
+ }
177
294
 
178
- // TOCTOU CAS [BLOCK, flagged] — golden :654-671 re-reads owner_epoch INSIDE the lock and refuses
179
- // owner_epoch_advanced on a concurrent bump (no double-bind). Rust has NO locked re-read; the race
180
- // silently double-binds (LeaseReason::OwnerEpochAdvanced is unreachable). Simulated: on-disk
181
- // state.json (the locked-re-read source) carries epoch 5 while the passed precheck state is epoch 3.
182
- // RED: today Rust ignores disk and claims. Porter: re-read select_runtime_state under the lock and
183
- // refuse owner_epoch_advanced when locked_epoch != precheck_epoch.
184
- #[test]
185
- #[serial_test::serial(env)]
186
- fn toctou_locked_reread_refuses_owner_epoch_advanced_on_concurrent_bump() {
187
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
188
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
189
- let ws = p2_temp_ws("toctou_cas");
190
- // disk (the locked-re-read source): a concurrent claimer bumped owner_epoch to 5.
191
- crate::state::persist::save_runtime_state(&ws, &serde_json::json!({
295
+ // TOCTOU CAS [BLOCK, flagged] — golden :654-671 re-reads owner_epoch INSIDE the lock and refuses
296
+ // owner_epoch_advanced on a concurrent bump (no double-bind). Rust has NO locked re-read; the race
297
+ // silently double-binds (LeaseReason::OwnerEpochAdvanced is unreachable). Simulated: on-disk
298
+ // state.json (the locked-re-read source) carries epoch 5 while the passed precheck state is epoch 3.
299
+ // RED: today Rust ignores disk and claims. Porter: re-read select_runtime_state under the lock and
300
+ // refuse owner_epoch_advanced when locked_epoch != precheck_epoch.
301
+ #[test]
302
+ #[serial_test::serial(env)]
303
+ fn toctou_locked_reread_refuses_owner_epoch_advanced_on_concurrent_bump() {
304
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
305
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
306
+ let ws = p2_temp_ws("toctou_cas");
307
+ // disk (the locked-re-read source): a concurrent claimer bumped owner_epoch to 5.
308
+ crate::state::persist::save_runtime_state(&ws, &serde_json::json!({
192
309
  "session_name": "team-agent-x",
193
310
  "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":5,"claimed_at":"t","claimed_via":"claim-leader"},
194
311
  })).unwrap();
195
- // precheck state passed to the claim: epoch 3, vacant (no owner pane) -> no force_confirm gate.
196
- let mut state = serde_json::json!({"session_name":"team-agent-x","leader_receiver":{"owner_epoch":3}});
197
- let r = claim_lease_no_incident(&ws, &mut state, None, &TeamKey::new("current"),
198
- &PaneId::new("%5"), false, &crate::event_log::EventLog::new(&ws), &seeded_liveness(&["%5"])).unwrap();
199
- assert_eq!(r.status, LeaseStatus::Refused,
312
+ // precheck state passed to the claim: epoch 3, vacant (no owner pane) -> no force_confirm gate.
313
+ let mut state =
314
+ serde_json::json!({"session_name":"team-agent-x","leader_receiver":{"owner_epoch":3}});
315
+ let r = claim_lease_no_incident(
316
+ &ws,
317
+ &mut state,
318
+ None,
319
+ &TeamKey::new("current"),
320
+ &PaneId::new("%5"),
321
+ false,
322
+ &crate::event_log::EventLog::new(&ws),
323
+ &seeded_liveness(&["%5"]),
324
+ )
325
+ .unwrap();
326
+ assert_eq!(r.status, LeaseStatus::Refused,
200
327
  "golden TOCTOU CAS: locked epoch 5 != precheck 3 -> refused; Rust skips the re-read and claims. got {r:?}");
201
- assert_eq!(r.reason, Some(LeaseReason::OwnerEpochAdvanced),
202
- "the refusal reason is owner_epoch_advanced (:666)");
203
- }
328
+ assert_eq!(
329
+ r.reason,
330
+ Some(LeaseReason::OwnerEpochAdvanced),
331
+ "the refusal reason is owner_epoch_advanced (:666)"
332
+ );
333
+ }
204
334
 
205
- // attach_leader_to_state [BLOCK, flagged] — golden non-first-time path writes leader_receiver ONLY:
206
- // NO team_owner write, NO epoch advance (after the strict-uuid gate). Rust stub writes BOTH and
207
- // advances epoch (precheck+1), ignoring source/require_current. RED (locked before the autobind/launch
208
- // wiring lands, so it does not silently overwrite the owner + bump epoch on every attach).
209
- #[test]
210
- #[serial_test::serial(env)]
211
- fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
212
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
213
- let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
214
- let ws = p2_temp_ws("attach_nonfirst");
215
- let mut state = serde_json::json!({
216
- "session_name": "team-agent-x",
217
- "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
218
- });
219
- let event_log = crate::event_log::EventLog::new(&ws);
220
- let _ = attach_leader_to_state(&ws, &mut state, Some(&PaneId::new("%2")),
221
- crate::provider::Provider::Codex, &event_log, LeaseSource::Manual, false);
222
- assert_eq!(state.pointer("/team_owner/pane_id").and_then(|v| v.as_str()), Some("%1"),
335
+ // attach_leader_to_state [BLOCK, flagged] — golden non-first-time path writes leader_receiver ONLY:
336
+ // NO team_owner write, NO epoch advance (after the strict-uuid gate). Rust stub writes BOTH and
337
+ // advances epoch (precheck+1), ignoring source/require_current. RED (locked before the autobind/launch
338
+ // wiring lands, so it does not silently overwrite the owner + bump epoch on every attach).
339
+ #[test]
340
+ #[serial_test::serial(env)]
341
+ fn attach_leader_to_state_nonfirst_does_not_overwrite_owner_or_advance_epoch() {
342
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
343
+ let _e = EnvGuard::apply(&[("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None)]);
344
+ let ws = p2_temp_ws("attach_nonfirst");
345
+ let mut state = serde_json::json!({
346
+ "session_name": "team-agent-x",
347
+ "team_owner": {"pane_id":"%1","provider":"codex","machine_fingerprint":"fp","leader_session_uuid":"U","owner_epoch":3,"claimed_at":"t","claimed_via":"claim-leader"},
348
+ });
349
+ let event_log = crate::event_log::EventLog::new(&ws);
350
+ let _ = attach_leader_to_state(
351
+ &ws,
352
+ &mut state,
353
+ Some(&PaneId::new("%2")),
354
+ crate::provider::Provider::Codex,
355
+ &event_log,
356
+ LeaseSource::Manual,
357
+ false,
358
+ );
359
+ assert_eq!(state.pointer("/team_owner/pane_id").and_then(|v| v.as_str()), Some("%1"),
223
360
  "non-first-time attach must NOT overwrite team_owner.pane_id (golden writes leader_receiver only); Rust rebinds it to %2");
224
- assert_eq!(state.pointer("/team_owner/owner_epoch").and_then(|v| v.as_u64()), Some(3),
225
- "non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4");
226
- }
361
+ assert_eq!(
362
+ state
363
+ .pointer("/team_owner/owner_epoch")
364
+ .and_then(|v| v.as_u64()),
365
+ Some(3),
366
+ "non-first-time attach must NOT advance owner_epoch (golden keeps 3); Rust bumps to 4"
367
+ );
368
+ }
227
369
 
228
- // C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
229
- // :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
230
- // TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
231
- // the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
232
- // Rust reads attached_at only.)
233
- #[test]
234
- #[serial_test::serial(env)]
235
- fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
236
- let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
237
- let _e = EnvGuard::apply(&[
238
- ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
239
- ("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
240
- ("TMUX_PANE", None),
241
- ]);
242
- let ws = p2_temp_ws("c3_pane_env");
243
- let v = leader_identity(&ws, None).unwrap();
244
- assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
370
+ // C3 [WARN] — leader_identity.current_pane_id honors TEAM_AGENT_LEADER_PANE_ID priority (golden
371
+ // :366 = env(TEAM_AGENT_LEADER_PANE_ID) or env(TMUX_PANE) or None). Rust owner_bind.rs:32 reads
372
+ // TMUX_PANE only. RED: with TEAM_AGENT_LEADER_PANE_ID set + TMUX_PANE unset, current_pane_id must be
373
+ // the LEADER_PANE_ID. (Sibling gap, noted: last_seen_at = receiver.attached_at OR receiver.last_seen_at;
374
+ // Rust reads attached_at only.)
375
+ #[test]
376
+ #[serial_test::serial(env)]
377
+ fn c3_leader_identity_current_pane_honors_leader_pane_id_env() {
378
+ let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
379
+ let _e = EnvGuard::apply(&[
380
+ ("TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE", None),
381
+ ("TEAM_AGENT_LEADER_PANE_ID", Some("%foo")),
382
+ ("TMUX_PANE", None),
383
+ ]);
384
+ let ws = p2_temp_ws("c3_pane_env");
385
+ let v = leader_identity(&ws, None).unwrap();
386
+ assert_eq!(v["current_pane_id"], serde_json::json!("%foo"),
245
387
  "golden :366 current_pane_id = TEAM_AGENT_LEADER_PANE_ID or TMUX_PANE or None; Rust reads TMUX_PANE only");
246
- }
388
+ }