@team-agent/installer 0.5.42 → 0.5.44

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 (156) 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 +126 -16
  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 +51 -56
  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/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  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/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -1,292 +1,330 @@
1
1
  use super::*;
2
2
 
3
- // =====================================================================
4
- // 7. 五条 lease 路径签名 + 返回 LeaseResult 形态(unimplemented → RED)
5
- // =====================================================================
3
+ // =====================================================================
4
+ // 7. 五条 lease 路径签名 + 返回 LeaseResult 形态(unimplemented → RED)
5
+ // =====================================================================
6
6
 
7
- // attach_leader:手动 CLI attach(__init__.py:19-58 → attach_leader_to_state:276 →
8
- // _resolve_leader_pane)。在无 live tmux 的测试环境,指定一个不存在的 pane %1 →
9
- // _resolve_leader_pane raise RuntimeError("tmux pane not found: %1")(_legacy_pane_discovery.py:153),
10
- // 映射到 LeaderError::Validation。golden(probe_attach.py 已验:真跑即 raise)。
11
- // 强化:钉具体的 Err 形态 + 错误串含 pane id;并断言失败时绝不留下半绑定 state(无 team_owner)。
12
- // unimplemented → RED(unimplemented panic ≠ 期望的 Validation,且后续 is_err 断言不会被求值)。
13
- #[test]
14
- fn attach_leader_errors_when_pane_not_resolvable() {
15
- let ws = std::env::temp_dir().join(format!("ta_rs_attach_{}", std::process::id()));
16
- std::fs::create_dir_all(&ws).unwrap();
17
- let r = attach_leader(&ws, None, Some(&PaneId::new("%1")), Provider::Codex);
18
- // 不可解析 pane → Err(Validation),错误串提及 pane not found。
19
- match r {
20
- Err(LeaderError::Validation(msg)) => {
21
- assert!(msg.contains("%1"), "Validation 错误须含目标 pane id,got {msg}");
22
- assert!(msg.contains("not found"), "须是 pane-not-found 形态,got {msg}");
23
- }
24
- Err(other) => panic!("期望 Validation(pane not found),got {other:?}"),
25
- Ok(v) => panic!("无 live tmux pane 时不该成功 attach,got {v:?}"),
7
+ // attach_leader:手动 CLI attach(__init__.py:19-58 → attach_leader_to_state:276 →
8
+ // _resolve_leader_pane)。在无 live tmux 的测试环境,指定一个不存在的 pane %1 →
9
+ // _resolve_leader_pane raise RuntimeError("tmux pane not found: %1")(_legacy_pane_discovery.py:153),
10
+ // 映射到 LeaderError::Validation。golden(probe_attach.py 已验:真跑即 raise)。
11
+ // 强化:钉具体的 Err 形态 + 错误串含 pane id;并断言失败时绝不留下半绑定 state(无 team_owner)。
12
+ // unimplemented → RED(unimplemented panic ≠ 期望的 Validation,且后续 is_err 断言不会被求值)。
13
+ #[test]
14
+ fn attach_leader_errors_when_pane_not_resolvable() {
15
+ let ws = std::env::temp_dir().join(format!("ta_rs_attach_{}", std::process::id()));
16
+ std::fs::create_dir_all(&ws).unwrap();
17
+ let r = attach_leader(&ws, None, Some(&PaneId::new("%1")), Provider::Codex);
18
+ // 不可解析 pane → Err(Validation),错误串提及 pane not found。
19
+ match r {
20
+ Err(LeaderError::Validation(msg)) => {
21
+ assert!(
22
+ msg.contains("%1"),
23
+ "Validation 错误须含目标 pane id,got {msg}"
24
+ );
25
+ assert!(
26
+ msg.contains("not found"),
27
+ "须是 pane-not-found 形态,got {msg}"
28
+ );
26
29
  }
27
- // 失败不留半绑定:state.json team_owner。
28
- let st = crate::state::persist::load_runtime_state(&ws).unwrap();
29
- assert!(st.get("team_owner").is_none(), "resolve 失败不得落 team_owner");
30
+ Err(other) => panic!("期望 Validation(pane not found),got {other:?}"),
31
+ Ok(v) => panic!("无 live tmux pane 时不该成功 attach,got {v:?}"),
30
32
  }
33
+ // 失败不留半绑定:state.json 无 team_owner。
34
+ let st = crate::state::persist::load_runtime_state(&ws).unwrap();
35
+ assert!(
36
+ st.get("team_owner").is_none(),
37
+ "resolve 失败不得落 team_owner"
38
+ );
39
+ }
31
40
 
32
- // attach_leader 成功 post-state(需 live tmux pane + cross-lane _resolve_leader_pane):
33
- // vacant acquire → status=Claimed、reason=vacant_acquired、owner_epoch 0→1、owner/receiver 绑同 pane、
34
- // 且 workspace state.json 真被持久化。golden(_claim_lease_no_incident:81/102/139-143)。
35
- // real-machine-gated(无 live tmux 无法驱动 pane resolver)。
36
- #[test]
37
- #[ignore = "needs a live tmux pane + cross-lane _resolve_leader_pane (step 9/11)"]
38
- fn attach_leader_binds_pane_advances_epoch_and_persists() {
39
- let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
40
- std::fs::create_dir_all(&ws).unwrap();
41
- let pane = PaneId::new("%1");
42
- let r = attach_leader(&ws, None, Some(&pane), Provider::Codex).unwrap();
43
- assert!(r.ok);
44
- assert_eq!(r.status, LeaseStatus::Claimed);
45
- let owner = r.owner.as_ref().expect("attach 成功必带 owner");
46
- assert_eq!(owner.pane_id, pane);
47
- assert_eq!(owner.owner_epoch, OwnerEpoch(1), "vacant acquire 后 epoch=1");
48
- assert_eq!(owner.provider, Provider::Codex);
49
- let receiver = r.receiver.as_ref().expect("attach 成功必带 receiver");
50
- assert_eq!(receiver.pane_id, pane);
51
- assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
52
- let persisted = crate::state::persist::load_runtime_state(&ws).unwrap();
53
- assert_eq!(persisted["team_owner"]["pane_id"], serde_json::json!("%1"));
54
- assert_eq!(persisted["team_owner"]["owner_epoch"], serde_json::json!(1));
55
- }
41
+ // attach_leader 成功 post-state(需 live tmux pane + cross-lane _resolve_leader_pane):
42
+ // vacant acquire → status=Claimed、reason=vacant_acquired、owner_epoch 0→1、owner/receiver 绑同 pane、
43
+ // 且 workspace state.json 真被持久化。golden(_claim_lease_no_incident:81/102/139-143)。
44
+ // real-machine-gated(无 live tmux 无法驱动 pane resolver)。
45
+ #[test]
46
+ #[ignore = "needs a live tmux pane + cross-lane _resolve_leader_pane (step 9/11)"]
47
+ fn attach_leader_binds_pane_advances_epoch_and_persists() {
48
+ let ws = std::env::temp_dir().join(format!("ta_rs_attach_ok_{}", std::process::id()));
49
+ std::fs::create_dir_all(&ws).unwrap();
50
+ let pane = PaneId::new("%1");
51
+ let r = attach_leader(&ws, None, Some(&pane), Provider::Codex).unwrap();
52
+ assert!(r.ok);
53
+ assert_eq!(r.status, LeaseStatus::Claimed);
54
+ let owner = r.owner.as_ref().expect("attach 成功必带 owner");
55
+ assert_eq!(owner.pane_id, pane);
56
+ assert_eq!(
57
+ owner.owner_epoch,
58
+ OwnerEpoch(1),
59
+ "vacant acquire 后 epoch=1"
60
+ );
61
+ assert_eq!(owner.provider, Provider::Codex);
62
+ let receiver = r.receiver.as_ref().expect("attach 成功必带 receiver");
63
+ assert_eq!(receiver.pane_id, pane);
64
+ assert_eq!(r.reason, Some(LeaseReason::VacantAcquired));
65
+ let persisted = crate::state::persist::load_runtime_state(&ws).unwrap();
66
+ assert_eq!(persisted["team_owner"]["pane_id"], serde_json::json!("%1"));
67
+ assert_eq!(persisted["team_owner"]["owner_epoch"], serde_json::json!(1));
68
+ }
56
69
 
57
- // autobind:$TMUX_PANE 缺 → Ok(None)(__init__.py:885-887,锁前直接返回,不开锁)。
58
- // 强化:断言这是 lock-not-acquired 早退 —— 不写 state.json(无 receiver/owner 落盘),
59
- // 不发任何 leader_receiver.* 事件(锁前 return,连 EventLog 都不构造)。
60
- #[test]
61
- fn autobind_returns_none_when_tmux_pane_missing() {
62
- if std::env::var_os("TMUX_PANE").is_some() {
63
- return; // 在 tmux 内:走绑定路径,本用例只验缺失分支。
64
- }
65
- let ws = std::env::temp_dir().join(format!("ta_rs_auto_{}", std::process::id()));
66
- std::fs::create_dir_all(&ws).unwrap();
67
- let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
68
- assert!(r.is_none(), "$TMUX_PANE 缺 → autobind 返回 Ok(None)");
69
- // lock-not-acquired 早退:state.json 未被写(load 兜底成空 state,无 team_owner/receiver)。
70
- let st = crate::state::persist::load_runtime_state(&ws).unwrap();
71
- assert!(st.get("leader_receiver").is_none(), "skip 路径不应落 receiver");
72
- assert!(st.get("team_owner").is_none(), "skip 路径不应落 owner");
73
- // 早退发生在 EventLog 构造前 → 无任何事件文件。
74
- let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
75
- assert!(events.is_empty(), "skip 路径绝不写审计事件");
70
+ // autobind:$TMUX_PANE 缺 → Ok(None)(__init__.py:885-887,锁前直接返回,不开锁)。
71
+ // 强化:断言这是 lock-not-acquired 早退 —— 不写 state.json(无 receiver/owner 落盘),
72
+ // 不发任何 leader_receiver.* 事件(锁前 return,连 EventLog 都不构造)。
73
+ #[test]
74
+ fn autobind_returns_none_when_tmux_pane_missing() {
75
+ if std::env::var_os("TMUX_PANE").is_some() {
76
+ return; // 在 tmux 内:走绑定路径,本用例只验缺失分支。
76
77
  }
78
+ let ws = std::env::temp_dir().join(format!("ta_rs_auto_{}", std::process::id()));
79
+ std::fs::create_dir_all(&ws).unwrap();
80
+ let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
81
+ assert!(r.is_none(), "$TMUX_PANE 缺 → autobind 返回 Ok(None)");
82
+ // lock-not-acquired 早退:state.json 未被写(load 兜底成空 state,无 team_owner/receiver)。
83
+ let st = crate::state::persist::load_runtime_state(&ws).unwrap();
84
+ assert!(
85
+ st.get("leader_receiver").is_none(),
86
+ "skip 路径不应落 receiver"
87
+ );
88
+ assert!(st.get("team_owner").is_none(), "skip 路径不应落 owner");
89
+ // 早退发生在 EventLog 构造前 → 无任何事件文件。
90
+ let events = crate::event_log::EventLog::new(&ws).tail(50).unwrap();
91
+ assert!(events.is_empty(), "skip 路径绝不写审计事件");
92
+ }
77
93
 
78
- // autobind 成功支:$TMUX_PANE 命中 → 锁内 attach_leader_to_state → Ok(Some(receiver))。
79
- // receiver.pane_id == 注入 pane,discovery==EnvPane($TMUX_PANE 直接命中)。
80
- // env 变更进程全局且与并行 test race,且依赖跨 lane 的 live pane resolver,故 real-machine-gated。
81
- #[test]
82
- #[ignore = "needs live $TMUX_PANE + cross-lane _resolve_leader_pane; env mutation races parallel tests"]
83
- fn autobind_binds_env_pane_on_success() {
84
- let ws = std::env::temp_dir().join(format!("ta_rs_auto_ok_{}", std::process::id()));
85
- std::fs::create_dir_all(&ws).unwrap();
86
- // SAFETY: #[ignore]d,单独 `--ignored --test-threads=1` 跑,不与并行 test race。
87
- unsafe { std::env::set_var("TMUX_PANE", "%42") };
88
- let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
89
- unsafe { std::env::remove_var("TMUX_PANE") };
90
- let receiver = r.expect("$TMUX_PANE 命中 → Ok(Some(receiver))");
91
- assert_eq!(receiver.pane_id, PaneId::new("%42"));
92
- assert_eq!(receiver.discovery, Some(Discovery::EnvPane), "$TMUX_PANE 命中 → discovery=env_pane");
93
- }
94
+ // autobind 成功支:$TMUX_PANE 命中 → 锁内 attach_leader_to_state → Ok(Some(receiver))。
95
+ // receiver.pane_id == 注入 pane,discovery==EnvPane($TMUX_PANE 直接命中)。
96
+ // env 变更进程全局且与并行 test race,且依赖跨 lane 的 live pane resolver,故 real-machine-gated。
97
+ #[test]
98
+ #[ignore = "needs live $TMUX_PANE + cross-lane _resolve_leader_pane; env mutation races parallel tests"]
99
+ fn autobind_binds_env_pane_on_success() {
100
+ let ws = std::env::temp_dir().join(format!("ta_rs_auto_ok_{}", std::process::id()));
101
+ std::fs::create_dir_all(&ws).unwrap();
102
+ // SAFETY: #[ignore]d,单独 `--ignored --test-threads=1` 跑,不与并行 test race。
103
+ unsafe { std::env::set_var("TMUX_PANE", "%42") };
104
+ let r = autobind_leader_receiver_from_env(&ws, Provider::Codex, LeaseSource::Restart).unwrap();
105
+ unsafe { std::env::remove_var("TMUX_PANE") };
106
+ let receiver = r.expect("$TMUX_PANE 命中 → Ok(Some(receiver))");
107
+ assert_eq!(receiver.pane_id, PaneId::new("%42"));
108
+ assert_eq!(
109
+ receiver.discovery,
110
+ Some(Discovery::EnvPane),
111
+ "$TMUX_PANE 命中 → discovery=env_pane"
112
+ );
113
+ }
94
114
 
95
- // claim_leader:无 ambiguous incident → 走 claim_lease_no_incident 直接 acquire/CAS。
96
- // 现 unimplemented → RED;锁住返回 LeaseResult。
97
- #[test]
98
- fn claim_leader_returns_lease_result() {
99
- let ws = std::env::temp_dir().join(format!("ta_rs_claim_{}", std::process::id()));
100
- std::fs::create_dir_all(&ws).unwrap();
101
- let r = claim_leader(&ws, None, false).unwrap();
102
- // 无 caller pane(测试进程无 TMUX_PANE)→ refused not_in_tmux_pane(__init__.py:616-618)。
103
- if std::env::var_os("TMUX_PANE").is_none() {
104
- assert!(!r.ok);
105
- assert_eq!(r.status, LeaseStatus::Refused);
106
- assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
107
- assert!(r.action.is_some());
108
- }
115
+ // claim_leader:无 ambiguous incident → 走 claim_lease_no_incident 直接 acquire/CAS。
116
+ // 现 unimplemented → RED;锁住返回 LeaseResult。
117
+ #[test]
118
+ fn claim_leader_returns_lease_result() {
119
+ let ws = std::env::temp_dir().join(format!("ta_rs_claim_{}", std::process::id()));
120
+ std::fs::create_dir_all(&ws).unwrap();
121
+ let r = claim_leader(&ws, None, false).unwrap();
122
+ // 无 caller pane(测试进程无 TMUX_PANE)→ refused not_in_tmux_pane(__init__.py:616-618)。
123
+ if std::env::var_os("TMUX_PANE").is_none() {
124
+ assert!(!r.ok);
125
+ assert_eq!(r.status, LeaseStatus::Refused);
126
+ assert_eq!(r.reason, Some(LeaseReason::NotInTmuxPane));
127
+ assert!(r.action.is_some());
109
128
  }
129
+ }
110
130
 
111
- // write_lease_dual_state:同一锁内双写(C17,__init__.py:588-596);unimplemented → RED。
112
- // 强化:带 session_name 时必须落 BOTH —— workspace state.json + team/<session> snapshot,
113
- // 两份 team_owner.pane_id / owner_epoch 必须一致(永不分叉)。空 body Ok(()) 会被这里抓。
114
- #[test]
115
- // Foundation-0 F0-2 conversion of the C17 dual-write invariant test:
116
- // legacy per-session snapshot dual-write has been retired
117
- // (`.team/artifacts/foundation-0-slice-design.md` §§4-5). The lease
118
- // helper now persists ONLY the canonical root state; a legacy
119
- // snapshot is diagnostic-only and never re-appears as a side effect
120
- // of `write_lease_dual_state`. This test enforces exactly that:
121
- // canonical root gets the owner tuple, and the legacy per-session
122
- // path stays absent unless a diagnostic path explicitly writes it.
123
- fn write_lease_dual_state_persists_canonical_root_only_no_legacy_side_effect() {
124
- let ws = std::env::temp_dir().join(format!("ta_rs_dual_{}", std::process::id()));
125
- std::fs::create_dir_all(&ws).unwrap();
126
- let state = serde_json::json!({
127
- "session_name": "team-sess",
128
- "team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
129
- "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
130
- });
131
- write_lease_dual_state(&ws, &state).unwrap();
132
- // Canonical root state.json IS written; F0-2 keeps this as the
133
- // single product authority.
134
- let ws_path = crate::state::persist::runtime_state_path(&ws);
135
- let ws_state: serde_json::Value =
136
- serde_json::from_str(&std::fs::read_to_string(&ws_path).expect("workspace state.json 必须存在")).unwrap();
137
- assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%1"));
138
- assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(2));
139
- // Legacy per-session snapshot MUST NOT be a side effect of the
140
- // lease save path any more — this is the RED2 authority-write
141
- // guard shape at the unit-test layer.
142
- let legacy_snap = crate::lifecycle::helpers::team_snapshot_path(&ws, "team-sess");
143
- assert!(
144
- !legacy_snap.exists(),
145
- "F0-2: write_lease_dual_state must not write the legacy per-session snapshot; found {}",
146
- legacy_snap.display(),
147
- );
148
- }
131
+ // write_lease_dual_state:同一锁内双写(C17,__init__.py:588-596);unimplemented → RED。
132
+ // 强化:带 session_name 时必须落 BOTH —— workspace state.json + team/<session> snapshot,
133
+ // 两份 team_owner.pane_id / owner_epoch 必须一致(永不分叉)。空 body Ok(()) 会被这里抓。
134
+ #[test]
135
+ // Foundation-0 F0-2 conversion of the C17 dual-write invariant test:
136
+ // legacy per-session snapshot dual-write has been retired
137
+ // (`.team/artifacts/foundation-0-slice-design.md` §§4-5). The lease
138
+ // helper now persists ONLY the canonical root state; a legacy
139
+ // snapshot is diagnostic-only and never re-appears as a side effect
140
+ // of `write_lease_dual_state`. This test enforces exactly that:
141
+ // canonical root gets the owner tuple, and the legacy per-session
142
+ // path stays absent unless a diagnostic path explicitly writes it.
143
+ fn write_lease_dual_state_persists_canonical_root_only_no_legacy_side_effect() {
144
+ let ws = std::env::temp_dir().join(format!("ta_rs_dual_{}", std::process::id()));
145
+ std::fs::create_dir_all(&ws).unwrap();
146
+ let state = serde_json::json!({
147
+ "session_name": "team-sess",
148
+ "team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
149
+ "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
150
+ });
151
+ write_lease_dual_state(&ws, &state).unwrap();
152
+ // Canonical root state.json IS written; F0-2 keeps this as the
153
+ // single product authority.
154
+ let ws_path = crate::state::persist::runtime_state_path(&ws);
155
+ let ws_state: serde_json::Value = serde_json::from_str(
156
+ &std::fs::read_to_string(&ws_path).expect("workspace state.json 必须存在"),
157
+ )
158
+ .unwrap();
159
+ assert_eq!(ws_state["team_owner"]["pane_id"], serde_json::json!("%1"));
160
+ assert_eq!(ws_state["team_owner"]["owner_epoch"], serde_json::json!(2));
161
+ // Legacy per-session snapshot MUST NOT be a side effect of the
162
+ // lease save path any more — this is the RED2 authority-write
163
+ // guard shape at the unit-test layer.
164
+ let legacy_snap = crate::lifecycle::helpers::team_snapshot_path(&ws, "team-sess");
165
+ assert!(
166
+ !legacy_snap.exists(),
167
+ "F0-2: write_lease_dual_state must not write the legacy per-session snapshot; found {}",
168
+ legacy_snap.display(),
169
+ );
170
+ }
149
171
 
150
- #[test]
151
- fn lease_divergence_reads_restart_snapshot_for_special_session_names() {
152
- // Foundation-0 F0-2 conversion: `write_lease_dual_state` no longer
153
- // side-effects a legacy snapshot; the divergence detector is a
154
- // diagnostic reader that fires only when a diagnostic writer
155
- // (e.g. `lifecycle::save_team_runtime_snapshot`) has produced
156
- // the sidecar. Sequence exercised here: canonical root save →
157
- // diagnostic snapshot save with a different pane → divergence
158
- // reader observes the drift.
159
- let ws = std::env::temp_dir().join(format!("ta_rs_dual_special_{}", std::process::id()));
160
- std::fs::create_dir_all(&ws).unwrap();
161
- let state = serde_json::json!({
162
- "session_name": "team:proj-中文",
163
- "team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
164
- "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
165
- });
166
- write_lease_dual_state(&ws, &state).unwrap();
167
- let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:proj-中文");
168
- let raw_path = crate::model::paths::runtime_dir(&ws)
169
- .join("teams")
170
- .join("team:proj-中文")
171
- .join("state.json");
172
- // F0-2: canonical save must not create either the restart-safe
173
- // or the raw legacy snapshot as a side effect.
174
- assert!(
172
+ #[test]
173
+ fn lease_divergence_reads_restart_snapshot_for_special_session_names() {
174
+ // Foundation-0 F0-2 conversion: `write_lease_dual_state` no longer
175
+ // side-effects a legacy snapshot; the divergence detector is a
176
+ // diagnostic reader that fires only when a diagnostic writer
177
+ // (e.g. `lifecycle::save_team_runtime_snapshot`) has produced
178
+ // the sidecar. Sequence exercised here: canonical root save →
179
+ // diagnostic snapshot save with a different pane → divergence
180
+ // reader observes the drift.
181
+ let ws = std::env::temp_dir().join(format!("ta_rs_dual_special_{}", std::process::id()));
182
+ std::fs::create_dir_all(&ws).unwrap();
183
+ let state = serde_json::json!({
184
+ "session_name": "team:proj-中文",
185
+ "team_owner": {"pane_id": "%1", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
186
+ "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
187
+ });
188
+ write_lease_dual_state(&ws, &state).unwrap();
189
+ let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:proj-中文");
190
+ let raw_path = crate::model::paths::runtime_dir(&ws)
191
+ .join("teams")
192
+ .join("team:proj-中文")
193
+ .join("state.json");
194
+ // F0-2: canonical save must not create either the restart-safe
195
+ // or the raw legacy snapshot as a side effect.
196
+ assert!(
175
197
  !safe_path.exists(),
176
198
  "F0-2: canonical write_lease_dual_state must not create the restart-safe legacy snapshot: {}",
177
199
  safe_path.display()
178
200
  );
179
- assert!(
180
- !raw_path.exists(),
181
- "F0-2: canonical write_lease_dual_state must not create the raw legacy snapshot: {}",
182
- raw_path.display()
183
- );
201
+ assert!(
202
+ !raw_path.exists(),
203
+ "F0-2: canonical write_lease_dual_state must not create the raw legacy snapshot: {}",
204
+ raw_path.display()
205
+ );
184
206
 
185
- // Diagnostic write path with a divergent pane so we can exercise
186
- // the divergence reader below.
187
- let restart_state = serde_json::json!({
188
- "session_name": "team:proj-中文",
189
- "team_owner": {"pane_id": "%9", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
190
- "leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
191
- });
192
- let restart_path = crate::lifecycle::save_team_runtime_snapshot(&ws, &restart_state).unwrap();
193
- assert_eq!(restart_path, safe_path);
194
- assert!(
195
- restart_path.exists(),
196
- "diagnostic snapshot must be written to a real file: {}",
197
- restart_path.display()
198
- );
207
+ // Diagnostic write path with a divergent pane so we can exercise
208
+ // the divergence reader below.
209
+ let restart_state = serde_json::json!({
210
+ "session_name": "team:proj-中文",
211
+ "team_owner": {"pane_id": "%9", "owner_epoch": 2, "leader_session_uuid": "uuuu"},
212
+ "leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
213
+ });
214
+ let restart_path = crate::lifecycle::save_team_runtime_snapshot(&ws, &restart_state).unwrap();
215
+ assert_eq!(restart_path, safe_path);
216
+ assert!(
217
+ restart_path.exists(),
218
+ "diagnostic snapshot must be written to a real file: {}",
219
+ restart_path.display()
220
+ );
199
221
 
200
- // F0-2 RED1 diagnostic marker is present on the on-disk file.
201
- let snap_state: serde_json::Value =
202
- serde_json::from_str(&std::fs::read_to_string(&restart_path).unwrap()).unwrap();
203
- assert_eq!(snap_state["_not_authoritative"], serde_json::json!(true));
222
+ // F0-2 RED1 diagnostic marker is present on the on-disk file.
223
+ let snap_state: serde_json::Value =
224
+ serde_json::from_str(&std::fs::read_to_string(&restart_path).unwrap()).unwrap();
225
+ assert_eq!(snap_state["_not_authoritative"], serde_json::json!(true));
204
226
 
205
- let d = detect_dual_state_divergence(&ws, &state)
206
- .unwrap()
207
- .expect("divergence detector must read the diagnostic snapshot");
208
- assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
209
- assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
210
- assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
211
- assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
212
- }
227
+ let d = detect_dual_state_divergence(&ws, &state)
228
+ .unwrap()
229
+ .expect("divergence detector must read the diagnostic snapshot");
230
+ assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
231
+ assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
232
+ assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
233
+ assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
234
+ }
213
235
 
214
- #[test]
215
- fn detect_dual_state_divergence_reads_legacy_raw_snapshot_when_safe_absent() {
216
- let ws = std::env::temp_dir().join(format!("ta_rs_dual_legacy_{}", std::process::id()));
217
- std::fs::create_dir_all(&ws).unwrap();
218
- let state = serde_json::json!({
219
- "session_name": "team:legacy-中文",
220
- "team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
221
- "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
222
- });
223
- let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:legacy-中文");
224
- assert!(!safe_path.exists(), "test setup requires safe path absent");
225
- let raw_dir = crate::model::paths::runtime_dir(&ws)
226
- .join("teams")
227
- .join("team:legacy-中文");
228
- std::fs::create_dir_all(&raw_dir).unwrap();
229
- let snap = serde_json::json!({
230
- "session_name": "team:legacy-中文",
231
- "team_owner": {"pane_id": "%8", "leader_session_uuid": "uuuu", "owner_epoch": 2},
232
- "leader_receiver": {"pane_id": "%8", "owner_epoch": 2},
233
- });
234
- std::fs::write(raw_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
236
+ #[test]
237
+ fn detect_dual_state_divergence_reads_legacy_raw_snapshot_when_safe_absent() {
238
+ let ws = std::env::temp_dir().join(format!("ta_rs_dual_legacy_{}", std::process::id()));
239
+ std::fs::create_dir_all(&ws).unwrap();
240
+ let state = serde_json::json!({
241
+ "session_name": "team:legacy-中文",
242
+ "team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
243
+ "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
244
+ });
245
+ let safe_path = crate::lifecycle::helpers::team_snapshot_path(&ws, "team:legacy-中文");
246
+ assert!(!safe_path.exists(), "test setup requires safe path absent");
247
+ let raw_dir = crate::model::paths::runtime_dir(&ws)
248
+ .join("teams")
249
+ .join("team:legacy-中文");
250
+ std::fs::create_dir_all(&raw_dir).unwrap();
251
+ let snap = serde_json::json!({
252
+ "session_name": "team:legacy-中文",
253
+ "team_owner": {"pane_id": "%8", "leader_session_uuid": "uuuu", "owner_epoch": 2},
254
+ "leader_receiver": {"pane_id": "%8", "owner_epoch": 2},
255
+ });
256
+ std::fs::write(
257
+ raw_dir.join("state.json"),
258
+ serde_json::to_string(&snap).unwrap(),
259
+ )
260
+ .unwrap();
235
261
 
236
- let d = detect_dual_state_divergence(&ws, &state)
237
- .unwrap()
238
- .expect("legacy raw snapshot fallback must preserve old teams");
239
- assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
240
- assert_eq!(d["team_owner_pane"], serde_json::json!("%8"));
241
- assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
242
- assert_eq!(d["team_receiver_pane"], serde_json::json!("%8"));
243
- }
262
+ let d = detect_dual_state_divergence(&ws, &state)
263
+ .unwrap()
264
+ .expect("legacy raw snapshot fallback must preserve old teams");
265
+ assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
266
+ assert_eq!(d["team_owner_pane"], serde_json::json!("%8"));
267
+ assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
268
+ assert_eq!(d["team_receiver_pane"], serde_json::json!("%8"));
269
+ }
244
270
 
245
- // detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
246
- #[test]
247
- fn detect_dual_state_divergence_none_without_session_name() {
248
- let ws = std::env::temp_dir().join(format!("ta_rs_div_{}", std::process::id()));
249
- std::fs::create_dir_all(&ws).unwrap();
250
- let state = serde_json::json!({"team_owner": {"pane_id": "%1"}}); // 无 session_name。
251
- let d = detect_dual_state_divergence(&ws, &state).unwrap();
252
- assert!(d.is_none(), "无 session_name → 无 snapshot 可比 → None");
253
- }
271
+ // detect_dual_state_divergence:无 session_name → None(__init__.py:560-561);unimplemented → RED。
272
+ #[test]
273
+ fn detect_dual_state_divergence_none_without_session_name() {
274
+ let ws = std::env::temp_dir().join(format!("ta_rs_div_{}", std::process::id()));
275
+ std::fs::create_dir_all(&ws).unwrap();
276
+ let state = serde_json::json!({"team_owner": {"pane_id": "%1"}}); // 无 session_name。
277
+ let d = detect_dual_state_divergence(&ws, &state).unwrap();
278
+ assert!(d.is_none(), "无 session_name → 无 snapshot 可比 → None");
279
+ }
254
280
 
255
- // C18 核心:workspace state.json 与 team snapshot 在 owner pane 上分叉 → Some(具体分叉字段)。
256
- // golden(probe_leader_strengthen.py):workspace owner=%1 / snapshot owner=%9 →
257
- // {workspace_owner_pane:%1, team_owner_pane:%9, workspace_receiver_pane:%1, team_receiver_pane:%9}。
258
- // unimplemented → RED。
259
- #[test]
260
- fn detect_dual_state_divergence_reports_diverging_panes() {
261
- let ws = std::env::temp_dir().join(format!("ta_rs_divx_{}", std::process::id()));
262
- std::fs::create_dir_all(&ws).unwrap();
263
- let state = serde_json::json!({
264
- "session_name": "team-sess",
265
- "team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
266
- "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
267
- });
268
- // 写一份分叉 snapshot:owner/receiver pane 都是 %9(不同于 workspace 的 %1)。
269
- let snap_dir = crate::model::paths::runtime_dir(&ws).join("teams").join("team-sess");
270
- std::fs::create_dir_all(&snap_dir).unwrap();
271
- let snap = serde_json::json!({
272
- "session_name": "team-sess",
273
- "team_owner": {"pane_id": "%9", "leader_session_uuid": "uuuu", "owner_epoch": 2},
274
- "leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
275
- });
276
- std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&snap).unwrap()).unwrap();
277
- let d = detect_dual_state_divergence(&ws, &state).unwrap().expect("分叉 → Some(details)");
278
- assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
279
- assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
280
- assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
281
- assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
281
+ // C18 核心:workspace state.json 与 team snapshot 在 owner pane 上分叉 → Some(具体分叉字段)。
282
+ // golden(probe_leader_strengthen.py):workspace owner=%1 / snapshot owner=%9 →
283
+ // {workspace_owner_pane:%1, team_owner_pane:%9, workspace_receiver_pane:%1, team_receiver_pane:%9}。
284
+ // unimplemented → RED。
285
+ #[test]
286
+ fn detect_dual_state_divergence_reports_diverging_panes() {
287
+ let ws = std::env::temp_dir().join(format!("ta_rs_divx_{}", std::process::id()));
288
+ std::fs::create_dir_all(&ws).unwrap();
289
+ let state = serde_json::json!({
290
+ "session_name": "team-sess",
291
+ "team_owner": {"pane_id": "%1", "leader_session_uuid": "uuuu", "owner_epoch": 2},
292
+ "leader_receiver": {"pane_id": "%1", "owner_epoch": 2},
293
+ });
294
+ // 写一份分叉 snapshot:owner/receiver pane 都是 %9(不同于 workspace 的 %1)。
295
+ let snap_dir = crate::model::paths::runtime_dir(&ws)
296
+ .join("teams")
297
+ .join("team-sess");
298
+ std::fs::create_dir_all(&snap_dir).unwrap();
299
+ let snap = serde_json::json!({
300
+ "session_name": "team-sess",
301
+ "team_owner": {"pane_id": "%9", "leader_session_uuid": "uuuu", "owner_epoch": 2},
302
+ "leader_receiver": {"pane_id": "%9", "owner_epoch": 2},
303
+ });
304
+ std::fs::write(
305
+ snap_dir.join("state.json"),
306
+ serde_json::to_string(&snap).unwrap(),
307
+ )
308
+ .unwrap();
309
+ let d = detect_dual_state_divergence(&ws, &state)
310
+ .unwrap()
311
+ .expect("分叉 → Some(details)");
312
+ assert_eq!(d["workspace_owner_pane"], serde_json::json!("%1"));
313
+ assert_eq!(d["team_owner_pane"], serde_json::json!("%9"));
314
+ assert_eq!(d["workspace_receiver_pane"], serde_json::json!("%1"));
315
+ assert_eq!(d["team_receiver_pane"], serde_json::json!("%9"));
282
316
 
283
- // 匹配 snapshot(与 workspace 一致)→ None(无分叉)。
284
- std::fs::write(snap_dir.join("state.json"), serde_json::to_string(&state).unwrap()).unwrap();
285
- assert!(
286
- detect_dual_state_divergence(&ws, &state).unwrap().is_none(),
287
- "两份一致 → 无分叉 → None"
288
- );
289
- }
317
+ // 匹配 snapshot(与 workspace 一致)→ None(无分叉)。
318
+ std::fs::write(
319
+ snap_dir.join("state.json"),
320
+ serde_json::to_string(&state).unwrap(),
321
+ )
322
+ .unwrap();
323
+ assert!(
324
+ detect_dual_state_divergence(&ws, &state).unwrap().is_none(),
325
+ "两份一致 → 无分叉 → None"
326
+ );
327
+ }
290
328
 
291
329
  // R8 D4 (c-lite offline byte-lock): the leader_receiver.requeued_exhausted_watchers payload-build,
292
330
  // extracted from the real-tmux attach flow into a pure helper, must produce golden shape.
@@ -308,17 +346,34 @@ fn r8_requeued_exhausted_watchers_event_payload_golden_shape() {
308
346
  &crate::model::ids::TeamKey::new("team-a"),
309
347
  &notices,
310
348
  );
311
- let keys: std::collections::BTreeSet<&str> =
312
- payload.as_object().unwrap().keys().map(String::as_str).collect();
313
- let expected: std::collections::BTreeSet<&str> = ["watcher_ids", "count", "trigger"].into_iter().collect();
349
+ let keys: std::collections::BTreeSet<&str> = payload
350
+ .as_object()
351
+ .unwrap()
352
+ .keys()
353
+ .map(String::as_str)
354
+ .collect();
355
+ let expected: std::collections::BTreeSet<&str> =
356
+ ["watcher_ids", "count", "trigger"].into_iter().collect();
314
357
  assert_eq!(keys, expected,
315
358
  "D4: leader_receiver.requeued_exhausted_watchers payload must be golden {{watcher_ids, count, trigger}} \
316
359
  (leader/__init__.py:39-44), not the Rust {{pane_id, team_id, watcher_ids, requeued}}; got {keys:?}");
317
360
  assert_eq!(
318
- payload.get("watcher_ids").and_then(|v| v.as_array()).map(|a| a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>()),
319
- Some(vec!["w1"]), "watcher_ids must be the string list of requeued ids");
320
- assert_eq!(payload.get("count").and_then(|v| v.as_u64()), Some(1), "count == number of requeued watchers");
321
- assert_eq!(payload.get("trigger").and_then(|v| v.as_str()), Some("attach_leader"));
361
+ payload
362
+ .get("watcher_ids")
363
+ .and_then(|v| v.as_array())
364
+ .map(|a| a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>()),
365
+ Some(vec!["w1"]),
366
+ "watcher_ids must be the string list of requeued ids"
367
+ );
368
+ assert_eq!(
369
+ payload.get("count").and_then(|v| v.as_u64()),
370
+ Some(1),
371
+ "count == number of requeued watchers"
372
+ );
373
+ assert_eq!(
374
+ payload.get("trigger").and_then(|v| v.as_str()),
375
+ Some("attach_leader")
376
+ );
322
377
  }
323
378
 
324
379
  // 0.5.x Windows portability Batch 5: `FakeAppServer` uses UNIX domain
@@ -355,8 +410,14 @@ fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
355
410
  .expect("app-server attach should succeed");
356
411
 
357
412
  assert_eq!(out["ok"], serde_json::json!(true));
358
- assert_eq!(out["leader_receiver"]["transport_kind"], serde_json::json!("codex_app_server"));
359
- assert_eq!(out["leader_receiver"]["mode"], serde_json::json!("codex_app_server"));
413
+ assert_eq!(
414
+ out["leader_receiver"]["transport_kind"],
415
+ serde_json::json!("codex_app_server")
416
+ );
417
+ assert_eq!(
418
+ out["leader_receiver"]["mode"],
419
+ serde_json::json!("codex_app_server")
420
+ );
360
421
  assert_eq!(
361
422
  out["leader_receiver"]["app_server"]["thread_id"],
362
423
  serde_json::json!("thread-live")
@@ -376,7 +437,10 @@ fn app_server_attach_writes_transport_kind_tuple_and_advances_epoch() {
376
437
  serde_json::json!("thread-live")
377
438
  );
378
439
  assert_eq!(saved["leader_receiver"].get("pane_id"), None);
379
- assert_eq!(saved["team_owner"]["transport_kind"], serde_json::json!("codex_app_server"));
440
+ assert_eq!(
441
+ saved["team_owner"]["transport_kind"],
442
+ serde_json::json!("codex_app_server")
443
+ );
380
444
  assert_eq!(saved["owner_epoch"], serde_json::json!(1));
381
445
  }
382
446