@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,272 +1,303 @@
1
1
  use super::*;
2
2
 
3
- // =====================================================================
4
- // 8. idle-takeover 接线 — build_idle_nodes / leader_node(unimplemented → RED)
5
- // 命门:rollout_path=None → Unknown → never idle;leader path/provider 缺 → 省略;
6
- // MUST-NOT-13:经 TurnStateClassifier mock,断言零 provider client 直连。
7
- // =====================================================================
3
+ // =====================================================================
4
+ // 8. idle-takeover 接线 — build_idle_nodes / leader_node(unimplemented → RED)
5
+ // 命门:rollout_path=None → Unknown → never idle;leader path/provider 缺 → 省略;
6
+ // MUST-NOT-13:经 TurnStateClassifier mock,断言零 provider client 直连。
7
+ // =====================================================================
8
8
 
9
- // build_idle_nodes:一个 worker 有 rollout_path(可读)→ classifier.classify 被调一次;
10
- // 经注入分类器(零 provider client)。stopped/paused 跳过。
11
- #[test]
12
- fn build_idle_nodes_uses_injected_classifier_no_provider_client() {
13
- // 真实 session 文件,使 _read_session_tail 读到非空 → classifier 返回注入 state。
14
- let dir = std::env::temp_dir().join(format!("ta_rs_idle_{}", std::process::id()));
15
- std::fs::create_dir_all(&dir).unwrap();
16
- let log = dir.join("w1.jsonl");
17
- std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
18
- let state = serde_json::json!({
19
- "agents": {
20
- "w1": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "running"},
21
- "w_stopped": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "stopped"},
22
- }
23
- });
24
- let clf = CountingClassifier::new(TurnState::Idle);
25
- let nodes = build_idle_nodes(&state, &clf).unwrap();
26
- // stopped 被跳过(__init__ wiring:29)→ 仅 w1。
27
- assert_eq!(nodes.len(), 1);
28
- assert_eq!(nodes[0].node_id, "w1");
29
- assert_eq!(nodes[0].role, NodeRole::Worker);
30
- assert_eq!(nodes[0].state, TurnState::Idle);
31
- // MUST-NOT-13:分类只经注入 classifier(此 mock 计数==1),零 provider client 直连。
32
- assert_eq!(clf.calls.get(), 1, "每个 live node 恰调一次注入 classify");
33
- }
9
+ // build_idle_nodes:一个 worker 有 rollout_path(可读)→ classifier.classify 被调一次;
10
+ // 经注入分类器(零 provider client)。stopped/paused 跳过。
11
+ #[test]
12
+ fn build_idle_nodes_uses_injected_classifier_no_provider_client() {
13
+ // 真实 session 文件,使 _read_session_tail 读到非空 → classifier 返回注入 state。
14
+ let dir = std::env::temp_dir().join(format!("ta_rs_idle_{}", std::process::id()));
15
+ std::fs::create_dir_all(&dir).unwrap();
16
+ let log = dir.join("w1.jsonl");
17
+ std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
18
+ let state = serde_json::json!({
19
+ "agents": {
20
+ "w1": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "running"},
21
+ "w_stopped": {"provider": "codex", "rollout_path": log.to_string_lossy(), "status": "stopped"},
22
+ }
23
+ });
24
+ let clf = CountingClassifier::new(TurnState::Idle);
25
+ let nodes = build_idle_nodes(&state, &clf).unwrap();
26
+ // stopped 被跳过(__init__ wiring:29)→ 仅 w1。
27
+ assert_eq!(nodes.len(), 1);
28
+ assert_eq!(nodes[0].node_id, "w1");
29
+ assert_eq!(nodes[0].role, NodeRole::Worker);
30
+ assert_eq!(nodes[0].state, TurnState::Idle);
31
+ // MUST-NOT-13:分类只经注入 classifier(此 mock 计数==1),零 provider client 直连。
32
+ assert_eq!(clf.calls.get(), 1, "每个 live node 恰调一次注入 classify");
33
+ }
34
34
 
35
- // bug-085:rollout_path=None → 读到空串 → Unknown(不当 idle)。
36
- #[test]
37
- fn build_idle_nodes_none_rollout_path_yields_unknown_not_idle() {
38
- let state = serde_json::json!({
39
- "agents": {
40
- "w1": {"provider": "codex", "status": "running"} // 无 rollout_path。
41
- }
42
- });
43
- // 即使 mock 默认想返 Idle,空 session-log → classifier 返 Unknown(见 mock 逻辑)。
44
- let clf = CountingClassifier::new(TurnState::Idle);
45
- let nodes = build_idle_nodes(&state, &clf).unwrap();
46
- assert_eq!(nodes.len(), 1);
47
- assert_eq!(nodes[0].state, TurnState::Unknown, "None rollout_path → Unknown,绝不 idle");
48
- assert!(
49
- !nodes[0].state.is_idle_for_takeover(),
50
- "unknown idle:不得对 takeover 放行"
51
- );
52
- }
35
+ // bug-085:rollout_path=None → 读到空串 → Unknown(不当 idle)。
36
+ #[test]
37
+ fn build_idle_nodes_none_rollout_path_yields_unknown_not_idle() {
38
+ let state = serde_json::json!({
39
+ "agents": {
40
+ "w1": {"provider": "codex", "status": "running"} // 无 rollout_path。
41
+ }
42
+ });
43
+ // 即使 mock 默认想返 Idle,空 session-log → classifier 返 Unknown(见 mock 逻辑)。
44
+ let clf = CountingClassifier::new(TurnState::Idle);
45
+ let nodes = build_idle_nodes(&state, &clf).unwrap();
46
+ assert_eq!(nodes.len(), 1);
47
+ assert_eq!(
48
+ nodes[0].state,
49
+ TurnState::Unknown,
50
+ "None rollout_path Unknown,绝不 idle"
51
+ );
52
+ assert!(
53
+ !nodes[0].state.is_idle_for_takeover(),
54
+ "unknown ≠ idle:不得对 takeover 放行"
55
+ );
56
+ }
53
57
 
54
- // _leader_node:leader path 或 provider 缺 → None(省略而非猜 idle)。
55
- #[test]
56
- fn leader_node_omitted_when_path_or_provider_missing() {
57
- let clf = CountingClassifier::new(TurnState::Idle);
58
- // 既无 leader.rollout_path 也无 receiver.rollout_path → None。
59
- let state_no_path = serde_json::json!({
60
- "leader": {"id": "leader", "provider": "codex"},
61
- "leader_receiver": {"provider": "codex"}
62
- });
63
- assert!(leader_node(&state_no_path, &clf).unwrap().is_none(), "缺 path → 省略 leader 节点");
64
- // 有 path 但无 provider → None。
65
- let dir = std::env::temp_dir().join(format!("ta_rs_lnode_{}", std::process::id()));
66
- std::fs::create_dir_all(&dir).unwrap();
67
- let log = dir.join("leader.jsonl");
68
- std::fs::write(&log, b"{\"type\":\"turn_open\"}\n").unwrap();
69
- let state_no_provider = serde_json::json!({
70
- "leader": {"id": "leader", "rollout_path": log.to_string_lossy()}
71
- });
72
- assert!(
73
- leader_node(&state_no_provider, &clf).unwrap().is_none(),
74
- "缺 provider → 省略 leader 节点(不猜 idle)"
75
- );
76
- }
58
+ // _leader_node:leader path 或 provider 缺 → None(省略而非猜 idle)。
59
+ #[test]
60
+ fn leader_node_omitted_when_path_or_provider_missing() {
61
+ let clf = CountingClassifier::new(TurnState::Idle);
62
+ // 既无 leader.rollout_path 也无 receiver.rollout_path → None。
63
+ let state_no_path = serde_json::json!({
64
+ "leader": {"id": "leader", "provider": "codex"},
65
+ "leader_receiver": {"provider": "codex"}
66
+ });
67
+ assert!(
68
+ leader_node(&state_no_path, &clf).unwrap().is_none(),
69
+ "缺 path 省略 leader 节点"
70
+ );
71
+ // path 但无 provider → None。
72
+ let dir = std::env::temp_dir().join(format!("ta_rs_lnode_{}", std::process::id()));
73
+ std::fs::create_dir_all(&dir).unwrap();
74
+ let log = dir.join("leader.jsonl");
75
+ std::fs::write(&log, b"{\"type\":\"turn_open\"}\n").unwrap();
76
+ let state_no_provider = serde_json::json!({
77
+ "leader": {"id": "leader", "rollout_path": log.to_string_lossy()}
78
+ });
79
+ assert!(
80
+ leader_node(&state_no_provider, &clf).unwrap().is_none(),
81
+ "缺 provider → 省略 leader 节点(不猜 idle)"
82
+ );
83
+ }
77
84
 
78
- // _leader_node:path+provider 齐 → 经 classifier 产 role=leader 节点(C13)。
79
- #[test]
80
- fn leader_node_classified_via_injected_classifier() {
81
- let dir = std::env::temp_dir().join(format!("ta_rs_lnode2_{}", std::process::id()));
82
- std::fs::create_dir_all(&dir).unwrap();
83
- let log = dir.join("leader.jsonl");
84
- std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
85
- let state = serde_json::json!({
86
- "leader": {"id": "leader", "provider": "codex", "rollout_path": log.to_string_lossy()}
87
- });
88
- let clf = CountingClassifier::new(TurnState::Working);
89
- let node = leader_node(&state, &clf).unwrap().expect("path+provider 齐 → 有 leader 节点");
90
- assert_eq!(node.role, NodeRole::Leader);
91
- assert_eq!(node.node_id, "leader");
92
- assert_eq!(node.state, TurnState::Working);
93
- assert_eq!(clf.calls.get(), 1, "leader 分类经注入 classifier 一次,零 provider client");
94
- }
85
+ // _leader_node:path+provider 齐 → 经 classifier 产 role=leader 节点(C13)。
86
+ #[test]
87
+ fn leader_node_classified_via_injected_classifier() {
88
+ let dir = std::env::temp_dir().join(format!("ta_rs_lnode2_{}", std::process::id()));
89
+ std::fs::create_dir_all(&dir).unwrap();
90
+ let log = dir.join("leader.jsonl");
91
+ std::fs::write(&log, b"{\"type\":\"turn_complete\"}\n").unwrap();
92
+ let state = serde_json::json!({
93
+ "leader": {"id": "leader", "provider": "codex", "rollout_path": log.to_string_lossy()}
94
+ });
95
+ let clf = CountingClassifier::new(TurnState::Working);
96
+ let node = leader_node(&state, &clf)
97
+ .unwrap()
98
+ .expect("path+provider 齐 → 有 leader 节点");
99
+ assert_eq!(node.role, NodeRole::Leader);
100
+ assert_eq!(node.node_id, "leader");
101
+ assert_eq!(node.state, TurnState::Working);
102
+ assert_eq!(
103
+ clf.calls.get(),
104
+ 1,
105
+ "leader 分类经注入 classifier 一次,零 provider client"
106
+ );
107
+ }
95
108
 
96
- // =====================================================================
97
- // 9. classify_provider_turn_state 门面(unimplemented → RED)
98
- // unknown/abnormal 且有 event_sink → 写 idle_takeover.classify。
99
- // =====================================================================
109
+ // =====================================================================
110
+ // 9. classify_provider_turn_state 门面(unimplemented → RED)
111
+ // unknown/abnormal 且有 event_sink → 写 idle_takeover.classify。
112
+ // =====================================================================
100
113
 
101
- // 门面经注入 classifier 分类;空文本 → Unknown(本 mock),验证返回 TurnClassification。
102
- #[test]
103
- fn classify_provider_turn_state_returns_classification_via_injected_classifier() {
104
- let clf = CountingClassifier::new(TurnState::Idle);
105
- let c = classify_provider_turn_state(Provider::Codex, "{\"type\":\"turn_complete\"}", &clf, None).unwrap();
106
- assert_eq!(c.state, TurnState::Idle);
107
- assert_eq!(clf.calls.get(), 1);
108
- // 空文本 → Unknown(unknown ≠ idle 命门下游)
109
- let clf2 = CountingClassifier::new(TurnState::Idle);
110
- let c2 = classify_provider_turn_state(Provider::Codex, "", &clf2, None).unwrap();
111
- assert_eq!(c2.state, TurnState::Unknown);
112
- assert!(!c2.state.is_idle_for_takeover());
113
- }
114
+ // 门面经注入 classifier 分类;空文本 → Unknown(本 mock),验证返回 TurnClassification。
115
+ #[test]
116
+ fn classify_provider_turn_state_returns_classification_via_injected_classifier() {
117
+ let clf = CountingClassifier::new(TurnState::Idle);
118
+ let c =
119
+ classify_provider_turn_state(Provider::Codex, "{\"type\":\"turn_complete\"}", &clf, None)
120
+ .unwrap();
121
+ assert_eq!(c.state, TurnState::Idle);
122
+ assert_eq!(clf.calls.get(), 1);
123
+ // 空文本 Unknown(unknown idle 命门下游)
124
+ let clf2 = CountingClassifier::new(TurnState::Idle);
125
+ let c2 = classify_provider_turn_state(Provider::Codex, "", &clf2, None).unwrap();
126
+ assert_eq!(c2.state, TurnState::Unknown);
127
+ assert!(!c2.state.is_idle_for_takeover());
128
+ }
114
129
 
115
- // event_sink + unknown/abnormal → 写 idle_takeover.classify(事件名字节锁)。
116
- #[test]
117
- fn classify_event_name_is_idle_takeover_classify() {
118
- assert_eq!(LeaderEvent::IdleTakeoverClassify.name(), "idle_takeover.classify");
119
- }
130
+ // event_sink + unknown/abnormal → 写 idle_takeover.classify(事件名字节锁)。
131
+ #[test]
132
+ fn classify_event_name_is_idle_takeover_classify() {
133
+ assert_eq!(
134
+ LeaderEvent::IdleTakeoverClassify.name(),
135
+ "idle_takeover.classify"
136
+ );
137
+ }
120
138
 
121
- // =====================================================================
122
- // 10. push_idle_reminder(unimplemented → RED):!should_ping → no-op。
123
- // =====================================================================
139
+ // =====================================================================
140
+ // 10. push_idle_reminder(unimplemented → RED):!should_ping → no-op。
141
+ // =====================================================================
124
142
 
125
- #[test]
126
- fn push_idle_reminder_noop_when_should_not_ping() {
127
- let ws = std::env::temp_dir().join(format!("ta_rs_push_{}", std::process::id()));
128
- std::fs::create_dir_all(&ws).unwrap();
129
- let event_log = crate::event_log::EventLog::new(&ws);
130
- let state = serde_json::json!({"leader": {"id": "leader"}});
131
- let result = TakeoverReminderResult {
132
- should_ping: false,
133
- message: None,
134
- interrupted_nodes: vec![],
135
- reason: Some("not_armed_no_worker_turn".into()),
136
- };
137
- // should_ping=false → no-op,返回 Ok(())。现 unimplemented → RED。
138
- push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
139
- // 强化:no-op 必须真的什么都不做 —— 不写 idle_takeover.reminder 事件(EventLog 无该事件)。
140
- let events = event_log.tail(50).unwrap();
141
- assert!(
142
- !events.iter().any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
143
- "should_ping=false 时绝不写 reminder 事件"
144
- );
145
- }
143
+ #[test]
144
+ fn push_idle_reminder_noop_when_should_not_ping() {
145
+ let ws = std::env::temp_dir().join(format!("ta_rs_push_{}", std::process::id()));
146
+ std::fs::create_dir_all(&ws).unwrap();
147
+ let event_log = crate::event_log::EventLog::new(&ws);
148
+ let state = serde_json::json!({"leader": {"id": "leader"}});
149
+ let result = TakeoverReminderResult {
150
+ should_ping: false,
151
+ message: None,
152
+ interrupted_nodes: vec![],
153
+ reason: Some("not_armed_no_worker_turn".into()),
154
+ };
155
+ // should_ping=false → no-op,返回 Ok(())。现 unimplemented → RED。
156
+ push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
157
+ // 强化:no-op 必须真的什么都不做 —— 不写 idle_takeover.reminder 事件(EventLog 无该事件)。
158
+ let events = event_log.tail(50).unwrap();
159
+ assert!(
160
+ !events
161
+ .iter()
162
+ .any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
163
+ "should_ping=false 时绝不写 reminder 事件"
164
+ );
165
+ }
146
166
 
147
- // #236 nag_removal (N35) — push_idle_reminder is now a no-op shim.
148
- // [OLD] assertion: should_ping=true → writes idle_takeover.reminder event (with
149
- // interrupted/reason byte-locked golden payload).
150
- // [NEW] assertion: even when should_ping=true, push_idle_reminder writes NO event
151
- // and emits NO leader-bound message; ownership/handover happens only via explicit
152
- // `claim-leader` / `takeover` commands. The function signature is preserved so
153
- // existing callers (coordinator/tick.rs, lifecycle wiring) still resolve.
154
- #[test]
155
- fn push_idle_reminder_is_silent_no_op_under_n35_even_when_should_ping_true() {
156
- let ws = std::env::temp_dir().join(format!("ta_rs_push2_{}", std::process::id()));
157
- std::fs::create_dir_all(&ws).unwrap();
158
- let event_log = crate::event_log::EventLog::new(&ws);
159
- let state = serde_json::json!({"leader": {"id": "leader"}});
160
- let result = TakeoverReminderResult {
161
- should_ping: true,
162
- message: Some("neutral reminder body".into()),
163
- interrupted_nodes: vec!["w1".into()],
164
- reason: Some("armed_all_idle".into()),
165
- };
166
- push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
167
- let events = event_log.tail(50).unwrap();
168
- assert!(
169
- !events.iter().any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
170
- "#236 N35: push_idle_reminder must no longer emit the reminder nag event; got {events:?}"
171
- );
172
- }
167
+ // #236 nag_removal (N35) — push_idle_reminder is now a no-op shim.
168
+ // [OLD] assertion: should_ping=true → writes idle_takeover.reminder event (with
169
+ // interrupted/reason byte-locked golden payload).
170
+ // [NEW] assertion: even when should_ping=true, push_idle_reminder writes NO event
171
+ // and emits NO leader-bound message; ownership/handover happens only via explicit
172
+ // `claim-leader` / `takeover` commands. The function signature is preserved so
173
+ // existing callers (coordinator/tick.rs, lifecycle wiring) still resolve.
174
+ #[test]
175
+ fn push_idle_reminder_is_silent_no_op_under_n35_even_when_should_ping_true() {
176
+ let ws = std::env::temp_dir().join(format!("ta_rs_push2_{}", std::process::id()));
177
+ std::fs::create_dir_all(&ws).unwrap();
178
+ let event_log = crate::event_log::EventLog::new(&ws);
179
+ let state = serde_json::json!({"leader": {"id": "leader"}});
180
+ let result = TakeoverReminderResult {
181
+ should_ping: true,
182
+ message: Some("neutral reminder body".into()),
183
+ interrupted_nodes: vec!["w1".into()],
184
+ reason: Some("armed_all_idle".into()),
185
+ };
186
+ push_idle_reminder(&ws, &state, &event_log, &result).unwrap();
187
+ let events = event_log.tail(50).unwrap();
188
+ assert!(
189
+ !events
190
+ .iter()
191
+ .any(|e| e["event"] == serde_json::json!("idle_takeover.reminder")),
192
+ "#236 N35: push_idle_reminder must no longer emit the reminder nag event; got {events:?}"
193
+ );
194
+ }
173
195
 
174
- // idle_takeover.reminder / push_failed 事件名字节锁。
175
- #[test]
176
- fn idle_takeover_event_names_byte_locked() {
177
- assert_eq!(LeaderEvent::IdleTakeoverReminder.name(), "idle_takeover.reminder");
178
- assert_eq!(LeaderEvent::IdleTakeoverPushFailed.name(), "idle_takeover.push_failed");
179
- assert_eq!(LeaderEvent::IdleTakeoverPing.name(), "idle_takeover.ping");
180
- }
196
+ // idle_takeover.reminder / push_failed 事件名字节锁。
197
+ #[test]
198
+ fn idle_takeover_event_names_byte_locked() {
199
+ assert_eq!(
200
+ LeaderEvent::IdleTakeoverReminder.name(),
201
+ "idle_takeover.reminder"
202
+ );
203
+ assert_eq!(
204
+ LeaderEvent::IdleTakeoverPushFailed.name(),
205
+ "idle_takeover.push_failed"
206
+ );
207
+ assert_eq!(LeaderEvent::IdleTakeoverPing.name(), "idle_takeover.ping");
208
+ }
181
209
 
182
- // =====================================================================
183
- // 11. struct 构造 / 序列化形态 + key 插入序证据(纯数据,不依赖 body)
184
- // =====================================================================
210
+ // =====================================================================
211
+ // 11. struct 构造 / 序列化形态 + key 插入序证据(纯数据,不依赖 body)
212
+ // =====================================================================
185
213
 
186
- // LeaderReceiver:所有可选字段 Option(bug-085 半状态合法);序列化保字段名。
187
- #[test]
188
- fn leader_receiver_struct_serializes_with_python_field_names() {
189
- let recv = LeaderReceiver {
190
- mode: ReceiverMode::DirectTmux,
191
- status: ReceiverStatus::Attached,
192
- provider: Provider::ClaudeCode,
193
- pane_id: PaneId::new("%648"),
194
- session_name: Some(SessionName::new("S")),
195
- window_index: Some("1".into()),
196
- window_name: Some(WindowName::new("W")),
197
- pane_index: Some("2".into()),
198
- pane_tty: Some("/dev/ttys001".into()),
199
- pane_current_command: Some("claude".into()),
200
- tmux_socket: None,
201
- fingerprint: Some("fp".into()),
202
- leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
203
- owner_epoch: Some(OwnerEpoch(3)),
204
- attached_at: Some("2026-06-02T00:00:00+00:00".into()),
205
- discovery: Some(Discovery::ClaimLeader),
206
- requested_provider: None,
207
- warning: None,
208
- };
209
- let v = serde_json::to_value(&recv).unwrap();
210
- assert_eq!(v["mode"], serde_json::json!("direct_tmux"));
211
- assert_eq!(v["status"], serde_json::json!("attached"));
212
- assert_eq!(v["provider"], serde_json::json!("claude_code"));
213
- assert_eq!(v["pane_id"], serde_json::json!("%648"));
214
- assert_eq!(v["owner_epoch"], serde_json::json!(3));
215
- assert_eq!(v["discovery"], serde_json::json!("claim_leader"));
216
- // bug-085:None 字段序列化为 null(半状态合法,不崩)。
217
- assert_eq!(v["requested_provider"], serde_json::Value::Null);
218
- assert_eq!(v["warning"], serde_json::Value::Null);
219
- }
214
+ // LeaderReceiver:所有可选字段 Option(bug-085 半状态合法);序列化保字段名。
215
+ #[test]
216
+ fn leader_receiver_struct_serializes_with_python_field_names() {
217
+ let recv = LeaderReceiver {
218
+ mode: ReceiverMode::DirectTmux,
219
+ status: ReceiverStatus::Attached,
220
+ provider: Provider::ClaudeCode,
221
+ pane_id: PaneId::new("%648"),
222
+ session_name: Some(SessionName::new("S")),
223
+ window_index: Some("1".into()),
224
+ window_name: Some(WindowName::new("W")),
225
+ pane_index: Some("2".into()),
226
+ pane_tty: Some("/dev/ttys001".into()),
227
+ pane_current_command: Some("claude".into()),
228
+ tmux_socket: None,
229
+ fingerprint: Some("fp".into()),
230
+ leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
231
+ owner_epoch: Some(OwnerEpoch(3)),
232
+ attached_at: Some("2026-06-02T00:00:00+00:00".into()),
233
+ discovery: Some(Discovery::ClaimLeader),
234
+ requested_provider: None,
235
+ warning: None,
236
+ };
237
+ let v = serde_json::to_value(&recv).unwrap();
238
+ assert_eq!(v["mode"], serde_json::json!("direct_tmux"));
239
+ assert_eq!(v["status"], serde_json::json!("attached"));
240
+ assert_eq!(v["provider"], serde_json::json!("claude_code"));
241
+ assert_eq!(v["pane_id"], serde_json::json!("%648"));
242
+ assert_eq!(v["owner_epoch"], serde_json::json!(3));
243
+ assert_eq!(v["discovery"], serde_json::json!("claim_leader"));
244
+ // bug-085:None 字段序列化为 null(半状态合法,不崩)。
245
+ assert_eq!(v["requested_provider"], serde_json::Value::Null);
246
+ assert_eq!(v["warning"], serde_json::Value::Null);
247
+ }
220
248
 
221
- // TeamOwner:claimed_via kebab + owner_epoch int;os_user Option(Family A 才写)。
222
- #[test]
223
- fn team_owner_struct_serializes_with_python_shape() {
224
- let owner = TeamOwner {
225
- pane_id: PaneId::new("%9"),
226
- provider: Provider::Codex,
227
- machine_fingerprint: "fp".into(),
228
- leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
229
- owner_epoch: OwnerEpoch(1),
230
- claimed_at: "2026-06-02T00:00:00+00:00".into(),
231
- claimed_via: ClaimedVia::ClaimLeader,
232
- os_user: Some("alice".into()),
233
- };
234
- let v = serde_json::to_value(&owner).unwrap();
235
- assert_eq!(v["claimed_via"], serde_json::json!("claim-leader"));
236
- assert_eq!(v["owner_epoch"], serde_json::json!(1));
237
- assert_eq!(v["provider"], serde_json::json!("codex"));
238
- assert_eq!(v["os_user"], serde_json::json!("alice"));
239
- }
249
+ // TeamOwner:claimed_via kebab + owner_epoch int;os_user Option(Family A 才写)。
250
+ #[test]
251
+ fn team_owner_struct_serializes_with_python_shape() {
252
+ let owner = TeamOwner {
253
+ pane_id: PaneId::new("%9"),
254
+ provider: Provider::Codex,
255
+ machine_fingerprint: "fp".into(),
256
+ leader_session_uuid: Some(uuid("fp", "/ws", "u", "default")),
257
+ owner_epoch: OwnerEpoch(1),
258
+ claimed_at: "2026-06-02T00:00:00+00:00".into(),
259
+ claimed_via: ClaimedVia::ClaimLeader,
260
+ os_user: Some("alice".into()),
261
+ };
262
+ let v = serde_json::to_value(&owner).unwrap();
263
+ assert_eq!(v["claimed_via"], serde_json::json!("claim-leader"));
264
+ assert_eq!(v["owner_epoch"], serde_json::json!(1));
265
+ assert_eq!(v["provider"], serde_json::json!("codex"));
266
+ assert_eq!(v["os_user"], serde_json::json!("alice"));
267
+ }
240
268
 
241
- // LeaderIdentity:source 用 leader-plan 枚举值(Override→"override");team_id 透明串。
242
- #[test]
243
- fn leader_identity_struct_serializes_with_leader_plan_source() {
244
- let id = LeaderIdentity {
245
- leader_session_uuid: uuid("fp", "/ws", "u", "default"),
246
- leader_session_uuid_source: LeaderSessionUuidSource::Override,
247
- machine_fingerprint: "fp".into(),
248
- workspace_abspath: std::path::PathBuf::from("/ws"),
249
- os_user: "u".into(),
250
- team_id: TeamKey::new("default"),
251
- };
252
- let v = serde_json::to_value(&id).unwrap();
253
- assert_eq!(v["leader_session_uuid_source"], serde_json::json!("override"));
254
- assert_eq!(v["team_id"], serde_json::json!("default"));
255
- }
269
+ // LeaderIdentity:source 用 leader-plan 枚举值(Override→"override");team_id 透明串。
270
+ #[test]
271
+ fn leader_identity_struct_serializes_with_leader_plan_source() {
272
+ let id = LeaderIdentity {
273
+ leader_session_uuid: uuid("fp", "/ws", "u", "default"),
274
+ leader_session_uuid_source: LeaderSessionUuidSource::Override,
275
+ machine_fingerprint: "fp".into(),
276
+ workspace_abspath: std::path::PathBuf::from("/ws"),
277
+ os_user: "u".into(),
278
+ team_id: TeamKey::new("default"),
279
+ };
280
+ let v = serde_json::to_value(&id).unwrap();
281
+ assert_eq!(
282
+ v["leader_session_uuid_source"],
283
+ serde_json::json!("override")
284
+ );
285
+ assert_eq!(v["team_id"], serde_json::json!("default"));
286
+ }
256
287
 
257
- // IdleNode:bug-085 rollout_path Option;state 是 TurnState(穷尽,Unknown 非 idle)。
258
- #[test]
259
- fn idle_node_unknown_state_is_not_idle() {
260
- let n = IdleNode {
261
- node_id: "w1".into(),
262
- role: NodeRole::Worker,
263
- state: TurnState::Unknown,
264
- turn_id: None,
265
- annotations: vec![],
266
- provider: Some(Provider::Codex),
267
- auth_mode: None,
268
- rollout_path: None, // bug-085:None 合法 → 该 node Unknown。
269
- };
270
- assert!(!n.state.is_idle_for_takeover(), "Unknown 不当 idle");
271
- assert!(n.rollout_path.is_none());
272
- }
288
+ // IdleNode:bug-085 rollout_path Option;state 是 TurnState(穷尽,Unknown 非 idle)。
289
+ #[test]
290
+ fn idle_node_unknown_state_is_not_idle() {
291
+ let n = IdleNode {
292
+ node_id: "w1".into(),
293
+ role: NodeRole::Worker,
294
+ state: TurnState::Unknown,
295
+ turn_id: None,
296
+ annotations: vec![],
297
+ provider: Some(Provider::Codex),
298
+ auth_mode: None,
299
+ rollout_path: None, // bug-085:None 合法 → 该 node Unknown。
300
+ };
301
+ assert!(!n.state.is_idle_for_takeover(), "Unknown 不当 idle");
302
+ assert!(n.rollout_path.is_none());
303
+ }