@team-agent/installer 0.3.13 → 0.3.15

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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/emit.rs +92 -11
  5. package/crates/team-agent/src/cli/mod.rs +228 -85
  6. package/crates/team-agent/src/cli/send.rs +252 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
  9. package/crates/team-agent/src/cli/types.rs +30 -1
  10. package/crates/team-agent/src/compiler/tests.rs +2 -2
  11. package/crates/team-agent/src/compiler.rs +1 -1
  12. package/crates/team-agent/src/fake_worker.rs +32 -145
  13. package/crates/team-agent/src/leader/start.rs +279 -4
  14. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  15. package/crates/team-agent/src/lifecycle/launch.rs +692 -92
  16. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  17. package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
  18. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
  19. package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
  20. package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
  21. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
  22. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  23. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  24. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  25. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  26. package/crates/team-agent/src/messaging/mod.rs +6 -3
  27. package/crates/team-agent/src/messaging/results.rs +240 -158
  28. package/crates/team-agent/src/messaging/send.rs +3 -2
  29. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  30. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  32. package/crates/team-agent/src/os_probe.rs +11 -0
  33. package/crates/team-agent/src/state/persist.rs +6 -0
  34. package/crates/team-agent/src/tmux_backend.rs +90 -0
  35. package/crates/team-agent/src/transport/test_support.rs +46 -1
  36. package/crates/team-agent/src/transport.rs +31 -0
  37. package/package.json +4 -4
  38. package/skills/team-agent/references/recovery-runbook.md +277 -0
@@ -12,34 +12,58 @@ use crate::transport::PaneLiveness;
12
12
  #[test]
13
13
  fn classify_first_send_at_none_is_absent_not_corrupt() {
14
14
  // None / 缺失 = 从未交互,可丢弃 fresh —— 不能误判成 corrupt。
15
- assert_eq!(classify_first_send_at(&json!(null)), FirstSendAtState::Absent);
15
+ assert_eq!(
16
+ classify_first_send_at(&json!(null)),
17
+ FirstSendAtState::Absent
18
+ );
16
19
  }
17
20
 
18
21
  #[test]
19
22
  fn classify_first_send_at_empty_string_is_corrupt_not_absent() {
20
23
  // 陷阱核心:Python truthiness 会把 "" 当 falsey/absent;契约要求 corrupt。
21
- assert_eq!(classify_first_send_at(&json!("")), FirstSendAtState::Corrupt);
24
+ assert_eq!(
25
+ classify_first_send_at(&json!("")),
26
+ FirstSendAtState::Corrupt
27
+ );
22
28
  }
23
29
 
24
30
  #[test]
25
31
  fn classify_first_send_at_zero_and_false_are_corrupt() {
26
32
  // 0 / False 非 str → corrupt(绝不靠 bool/int truthiness 当 absent)。
27
33
  assert_eq!(classify_first_send_at(&json!(0)), FirstSendAtState::Corrupt);
28
- assert_eq!(classify_first_send_at(&json!(false)), FirstSendAtState::Corrupt);
29
- assert_eq!(classify_first_send_at(&json!(123)), FirstSendAtState::Corrupt);
34
+ assert_eq!(
35
+ classify_first_send_at(&json!(false)),
36
+ FirstSendAtState::Corrupt
37
+ );
38
+ assert_eq!(
39
+ classify_first_send_at(&json!(123)),
40
+ FirstSendAtState::Corrupt
41
+ );
30
42
  }
31
43
 
32
44
  #[test]
33
45
  fn classify_first_send_at_literal_null_string_is_corrupt() {
34
46
  // 字面量字符串 "null"(非 ISO)→ corrupt,而 JSON null → absent(上面已测)。
35
- assert_eq!(classify_first_send_at(&json!("null")), FirstSendAtState::Corrupt);
36
- assert_eq!(classify_first_send_at(&json!("not-a-date")), FirstSendAtState::Corrupt);
47
+ assert_eq!(
48
+ classify_first_send_at(&json!("null")),
49
+ FirstSendAtState::Corrupt
50
+ );
51
+ assert_eq!(
52
+ classify_first_send_at(&json!("not-a-date")),
53
+ FirstSendAtState::Corrupt
54
+ );
37
55
  }
38
56
 
39
57
  #[test]
40
58
  fn classify_first_send_at_non_string_containers_are_corrupt() {
41
- assert_eq!(classify_first_send_at(&json!([])), FirstSendAtState::Corrupt);
42
- assert_eq!(classify_first_send_at(&json!({})), FirstSendAtState::Corrupt);
59
+ assert_eq!(
60
+ classify_first_send_at(&json!([])),
61
+ FirstSendAtState::Corrupt
62
+ );
63
+ assert_eq!(
64
+ classify_first_send_at(&json!({})),
65
+ FirstSendAtState::Corrupt
66
+ );
43
67
  }
44
68
 
45
69
  #[test]
@@ -178,7 +202,10 @@ fn plan_condition_rejects_out_of_grammar() {
178
202
  "report_result.s == bar",
179
203
  ] {
180
204
  assert!(
181
- matches!(PlanCondition::parse(bad), Err(LifecycleError::InvalidPlan(_))),
205
+ matches!(
206
+ PlanCondition::parse(bad),
207
+ Err(LifecycleError::InvalidPlan(_))
208
+ ),
182
209
  "expected InvalidPlan for {bad:?}"
183
210
  );
184
211
  }
@@ -186,14 +213,21 @@ fn plan_condition_rejects_out_of_grammar() {
186
213
 
187
214
  // ───────────────────────────────────────────────────────────────────────
188
215
  // resolve_display_backend — display/backend.py
189
- // 默认 none;非默认非静默(non_default=true 触发 display.backend_resolved)。
216
+ // 默认 adaptive;显式 none 是逃生口。
190
217
  // ───────────────────────────────────────────────────────────────────────
191
218
 
192
219
  #[test]
193
- fn resolve_backend_defaults_to_none_when_none_requested() {
220
+ fn resolve_backend_defaults_to_adaptive_when_none_requested() {
194
221
  let r = resolve_display_backend(None, None);
222
+ assert_eq!(r.backend, DisplayBackend::Adaptive);
223
+ assert!(!r.non_default, "默认 adaptive 不应标记 non_default");
224
+ }
225
+
226
+ #[test]
227
+ fn resolve_backend_explicit_none_is_escape_hatch() {
228
+ let r = resolve_display_backend(Some(DisplayBackend::None), None);
195
229
  assert_eq!(r.backend, DisplayBackend::None);
196
- assert!(!r.non_default, "默认 none 不应标记 non_default");
230
+ assert!(r.non_default, "显式 none 是偏离默认 adaptive 的逃生口");
197
231
  }
198
232
 
199
233
  #[test]
@@ -228,17 +262,14 @@ fn resolve_backend_recorded_used_when_no_request() {
228
262
  fn probe_never_errors_platform_degradation_is_typed_outcome() {
229
263
  // C13:能力性降级绝不走 LifecycleError —— 它是 DisplayStatus::Blocked + reason。
230
264
  let ws = temp_ws();
231
- let probe = probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
265
+ let probe =
266
+ probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
232
267
  // 若 blocked,reason 必属 AdaptiveBlockReason 封闭集且与 status 一致。
233
268
  match probe.adaptive_status {
234
- DisplayStatus::Blocked => assert!(
235
- probe.reason.is_some(),
236
- "blocked 必带 reason(C16 封闭集)"
237
- ),
238
- DisplayStatus::Opened => assert!(
239
- probe.reason.is_none(),
240
- "opened 不应带 block reason"
241
- ),
269
+ DisplayStatus::Blocked => {
270
+ assert!(probe.reason.is_some(), "blocked 必带 reason(C16 封闭集)")
271
+ }
272
+ DisplayStatus::Opened => assert!(probe.reason.is_none(), "opened 不应带 block reason"),
242
273
  DisplayStatus::Stopped => {}
243
274
  }
244
275
  }
@@ -294,7 +325,11 @@ fn adaptive_block_reason_serde_names_match_python_and_set_is_exactly_six() {
294
325
  .collect();
295
326
  names.sort();
296
327
  names.dedup();
297
- assert_eq!(names.len(), 6, "ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏");
328
+ assert_eq!(
329
+ names.len(),
330
+ 6,
331
+ "ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏"
332
+ );
298
333
  }
299
334
 
300
335
  #[test]
@@ -332,8 +367,14 @@ fn adaptive_blocked_out_of_set_reason_bottoms_to_aggregator_rebuild_failed() {
332
367
  #[test]
333
368
  fn start_mode_serde_names_match_python_start_mode_strings() {
334
369
  // 低价值 wire-format 守卫:start_mode ∈ {"resumed","fresh","fresh_after_missing_rollout","noop"}。
335
- assert_eq!(serde_json::to_string(&StartMode::Resumed).unwrap(), "\"resumed\"");
336
- assert_eq!(serde_json::to_string(&StartMode::Fresh).unwrap(), "\"fresh\"");
370
+ assert_eq!(
371
+ serde_json::to_string(&StartMode::Resumed).unwrap(),
372
+ "\"resumed\""
373
+ );
374
+ assert_eq!(
375
+ serde_json::to_string(&StartMode::Fresh).unwrap(),
376
+ "\"fresh\""
377
+ );
337
378
  assert_eq!(
338
379
  serde_json::to_string(&StartMode::FreshAfterMissingRollout).unwrap(),
339
380
  "\"fresh_after_missing_rollout\""
@@ -368,7 +409,13 @@ fn decide_start_mode_codex_missing_rollout_with_allow_fresh_is_fresh_after_missi
368
409
  );
369
410
  // rollout 路径存在但文件已不在,同样命中。
370
411
  assert_eq!(
371
- decide_start_mode("codex", Some(&sid("s1")), Some(&rp("/gone.jsonl")), false, true),
412
+ decide_start_mode(
413
+ "codex",
414
+ Some(&sid("s1")),
415
+ Some(&rp("/gone.jsonl")),
416
+ false,
417
+ true
418
+ ),
372
419
  StartMode::FreshAfterMissingRollout
373
420
  );
374
421
  }
@@ -385,7 +432,13 @@ fn decide_start_mode_codex_missing_rollout_without_allow_fresh_refuses() {
385
432
  #[test]
386
433
  fn decide_start_mode_codex_rollout_present_is_resumed_regardless_of_fresh() {
387
434
  assert_eq!(
388
- decide_start_mode("codex", Some(&sid("s1")), Some(&rp("/r.jsonl")), true, false),
435
+ decide_start_mode(
436
+ "codex",
437
+ Some(&sid("s1")),
438
+ Some(&rp("/r.jsonl")),
439
+ true,
440
+ false
441
+ ),
389
442
  StartMode::Resumed
390
443
  );
391
444
  assert_eq!(
@@ -431,15 +484,37 @@ fn decide_start_mode_checks_backing_for_all_resumable_providers() {
431
484
  );
432
485
  }
433
486
 
487
+ #[test]
488
+ fn decide_start_mode_refuses_session_id_for_nonresumable_providers() {
489
+ for provider in ["fake", "gemini_cli"] {
490
+ assert_eq!(
491
+ decide_start_mode(provider, Some(&sid("s1")), None, true, false),
492
+ StartMode::Noop,
493
+ "{provider} has a persisted session id but caps.resume=false, so it must not resume"
494
+ );
495
+ assert_eq!(
496
+ decide_start_mode(provider, Some(&sid("s1")), None, true, true),
497
+ StartMode::FreshAfterMissingRollout,
498
+ "{provider} may only fresh-start a persisted session id under explicit allow_fresh"
499
+ );
500
+ }
501
+ }
502
+
434
503
  #[test]
435
504
  fn resume_decision_serde_names_match_python() {
436
505
  // 低价值 wire-format 守卫:_emit_resume_decisions: "resume"|"fresh_start"|"refuse"。
437
- assert_eq!(serde_json::to_string(&ResumeDecision::Resume).unwrap(), "\"resume\"");
506
+ assert_eq!(
507
+ serde_json::to_string(&ResumeDecision::Resume).unwrap(),
508
+ "\"resume\""
509
+ );
438
510
  assert_eq!(
439
511
  serde_json::to_string(&ResumeDecision::FreshStart).unwrap(),
440
512
  "\"fresh_start\""
441
513
  );
442
- assert_eq!(serde_json::to_string(&ResumeDecision::Refuse).unwrap(), "\"refuse\"");
514
+ assert_eq!(
515
+ serde_json::to_string(&ResumeDecision::Refuse).unwrap(),
516
+ "\"refuse\""
517
+ );
443
518
  }
444
519
 
445
520
  // ───────────────────────────────────────────────────────────────────────
@@ -472,7 +547,11 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
472
547
  .expect("纯验证不应 Err(资源型失败才走 LifecycleError)");
473
548
  assert!(plan.corrupt_entries.is_empty(), "valid ISO 不应判 corrupt");
474
549
  // 恰一条决策(每非 paused worker 一条),且为 Refuse。
475
- assert_eq!(plan.decisions.len(), 1, "每非 paused worker 恰一条 resume_decision");
550
+ assert_eq!(
551
+ plan.decisions.len(),
552
+ 1,
553
+ "每非 paused worker 恰一条 resume_decision"
554
+ );
476
555
  assert_eq!(plan.decisions[0].agent_id, aid("w1"));
477
556
  assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
478
557
  // unresumable 收口该 worker,reason 精确。
@@ -481,6 +560,30 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
481
560
  assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
482
561
  }
483
562
 
563
+ #[test]
564
+ fn classify_restart_plan_fake_stale_session_refuses_without_allow_fresh() {
565
+ let state = json!({
566
+ "session_name": "team-a",
567
+ "agents": {
568
+ "w1": {
569
+ "provider": "fake",
570
+ "first_send_at": "2026-05-27T10:00:00+00:00",
571
+ "session_id": "stale-fake-session"
572
+ }
573
+ }
574
+ });
575
+ let plan = classify_restart_plan(&state, false).expect("pure validation should not Err");
576
+ assert_eq!(plan.decisions.len(), 1);
577
+ assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
578
+ assert_eq!(plan.decisions[0].restart_mode, StartMode::Noop);
579
+ assert_eq!(plan.unresumable.len(), 1);
580
+ assert_eq!(plan.unresumable[0].reason, "session_unresumable");
581
+ assert_eq!(
582
+ plan.unresumable[0].session_id.as_ref().map(|s| s.as_str()),
583
+ Some("stale-fake-session")
584
+ );
585
+ }
586
+
484
587
  #[test]
485
588
  fn classify_restart_plan_interacted_unresumable_with_allow_fresh_yields_fresh_start_not_refuse() {
486
589
  // 同一 worker,allow_fresh=true → FreshStart(可丢 context),unresumable 为空(无 atomic refusal)。
@@ -517,7 +620,11 @@ fn classify_restart_plan_never_interacted_null_session_refuses_not_fresh() {
517
620
  ResumeDecision::Refuse,
518
621
  "null-session 自启动 worker 默认必须 Refuse,不许静默 fresh"
519
622
  );
520
- assert_eq!(plan.unresumable.len(), 1, "Refuse 必入 unresumable(诚实出口)");
623
+ assert_eq!(
624
+ plan.unresumable.len(),
625
+ 1,
626
+ "Refuse 必入 unresumable(诚实出口)"
627
+ );
521
628
  assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
522
629
  }
523
630
 
@@ -632,8 +739,10 @@ fn remove_agent_without_from_spec_is_refused_confirm() {
632
739
  Ok(RemoveAgentOutcome::RefusedFromSpecConfirm { agent_id }) => {
633
740
  assert_eq!(agent_id, aid("w1"));
634
741
  }
635
- // 若先撞 owner / 缺 state 门也可接受(门在 confirm 之前)。
636
- Err(LifecycleError::OwnerRefused(_)) | Err(LifecycleError::TeamSelect(_)) => {}
742
+ // 若先撞 owner / 缺 state / runtime-missing 门也可接受(门在 confirm 之前)。
743
+ Err(LifecycleError::OwnerRefused(_))
744
+ | Err(LifecycleError::RequirementUnmet(_))
745
+ | Err(LifecycleError::TeamSelect(_)) => {}
637
746
  other => panic!("from_spec=false 应 RefusedFromSpecConfirm 或更上游门:{other:?}"),
638
747
  }
639
748
  }
@@ -756,10 +865,13 @@ fn restart_candidates_empty_workspace_is_empty_list() {
756
865
  fn save_snapshot_writes_atomic_state_json_under_teams_dir() {
757
866
  let ws = temp_ws();
758
867
  let state = json!({"session_name": "team-alpha", "agents": {}});
759
- let path = save_team_runtime_snapshot(&ws, &state)
760
- .expect("bug-084:写路径返 Result,正常态须 Ok");
868
+ let path =
869
+ save_team_runtime_snapshot(&ws, &state).expect("bug-084:写路径返 Result,正常态须 Ok");
761
870
  // 末段必为 state.json,且落在 runtime/teams/<safe session> 下。
762
- assert_eq!(path.file_name().and_then(|s| s.to_str()), Some("state.json"));
871
+ assert_eq!(
872
+ path.file_name().and_then(|s| s.to_str()),
873
+ Some("state.json")
874
+ );
763
875
  let s = path.to_string_lossy();
764
876
  assert!(
765
877
  s.contains("teams") && s.contains("team-alpha"),
@@ -884,7 +996,9 @@ fn leader_pane_env_invalid_format_writes_warning_event() {
884
996
  validate_active_leader_pane_env_with_workspace(&transport, Some(&ws))
885
997
  .expect("invalid format is warning-only");
886
998
 
887
- let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
999
+ let events = crate::event_log::EventLog::new(&ws)
1000
+ .tail(0)
1001
+ .expect("events");
888
1002
  let event = events
889
1003
  .iter()
890
1004
  .find(|event| {
@@ -947,8 +1061,8 @@ fn leader_pane_env_cross_socket_live_wins() {
947
1061
  #[test]
948
1062
  fn leader_pane_env_cross_socket_dead_rejects_without_live() {
949
1063
  let pane = crate::transport::PaneId::new("%404");
950
- let absent = crate::transport::test_support::OfflineTransport::new()
951
- .with_pane_presence("%404", false);
1064
+ let absent =
1065
+ crate::transport::test_support::OfflineTransport::new().with_pane_presence("%404", false);
952
1066
  let dead = crate::transport::test_support::OfflineTransport::new()
953
1067
  .with_liveness("%404", PaneLiveness::Dead);
954
1068
 
@@ -963,8 +1077,8 @@ fn leader_pane_env_cross_socket_dead_rejects_without_live() {
963
1077
  #[test]
964
1078
  fn leader_pane_env_cross_socket_absent_when_reachable_servers_confirm_missing() {
965
1079
  let pane = crate::transport::PaneId::new("%9999");
966
- let absent = crate::transport::test_support::OfflineTransport::new()
967
- .with_pane_presence("%9999", false);
1080
+ let absent =
1081
+ crate::transport::test_support::OfflineTransport::new().with_pane_presence("%9999", false);
968
1082
  let unknown = crate::transport::test_support::OfflineTransport::new()
969
1083
  .with_default_liveness(PaneLiveness::Unknown);
970
1084
 
@@ -1008,13 +1122,17 @@ fn mcp_auto_approval_env_marks_leader_bypass_namespace_only() {
1008
1122
 
1009
1123
  apply_mcp_auto_approval_env(&mut env, &safety);
1010
1124
 
1011
- assert_eq!(env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str), Some("1"));
1125
+ assert_eq!(
1126
+ env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
1127
+ Some("1")
1128
+ );
1012
1129
  assert_eq!(
1013
1130
  env.get("TEAM_AGENT_MCP_AUTO_APPROVE").map(String::as_str),
1014
1131
  Some("team_orchestrator")
1015
1132
  );
1016
1133
  assert_eq!(
1017
- env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE").map(String::as_str),
1134
+ env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE")
1135
+ .map(String::as_str),
1018
1136
  Some("leader_bypass")
1019
1137
  );
1020
1138
  assert_eq!(
@@ -1030,7 +1148,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
1030
1148
  "TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
1031
1149
  "team_orchestrator".to_string(),
1032
1150
  ),
1033
- ("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(), "leader_bypass".to_string()),
1151
+ (
1152
+ "TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
1153
+ "leader_bypass".to_string(),
1154
+ ),
1034
1155
  ]);
1035
1156
  let safety = DangerousApproval {
1036
1157
  enabled: false,
@@ -1045,7 +1166,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
1045
1166
 
1046
1167
  apply_mcp_auto_approval_env(&mut env, &safety);
1047
1168
 
1048
- assert_eq!(env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str), Some("0"));
1169
+ assert_eq!(
1170
+ env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
1171
+ Some("0")
1172
+ );
1049
1173
  assert!(
1050
1174
  !env.contains_key("TEAM_AGENT_MCP_AUTO_APPROVE"),
1051
1175
  "restricted leader must not leave MCP auto-approval env behind: {env:?}"
@@ -1113,7 +1237,12 @@ fn open_worker_displays_blocked_probe_yields_typed_blocked_not_error() {
1113
1237
  // 每个 worker 的 display 在 blocked 平台上应是 Blocked 变体(若有 worker)。
1114
1238
  for (id, d) in rep.displays.iter() {
1115
1239
  assert!(
1116
- matches!(d, WorkerDisplay::Blocked { reason: AdaptiveBlockReason::NotImplementedThisPlatform }),
1240
+ matches!(
1241
+ d,
1242
+ WorkerDisplay::Blocked {
1243
+ reason: AdaptiveBlockReason::NotImplementedThisPlatform
1244
+ }
1245
+ ),
1117
1246
  "worker {id} 在 windows 平台应 Blocked(not_implemented):{d:?}"
1118
1247
  );
1119
1248
  }
@@ -1130,7 +1259,11 @@ fn close_team_display_empty_workspace_closes_nothing_not_error() {
1130
1259
  let ws = temp_ws();
1131
1260
  let rep = close_team_display_backends(&ws, &sess("team-a"))
1132
1261
  .expect("C9:无 recorded backend 应 Ok(空报告),不是 Err");
1133
- assert!(rep.closed.is_empty(), "无 recorded backend 不应关任何东西:{:?}", rep.closed);
1262
+ assert!(
1263
+ rep.closed.is_empty(),
1264
+ "无 recorded backend 不应关任何东西:{:?}",
1265
+ rep.closed
1266
+ );
1134
1267
  assert!(
1135
1268
  rep.orphans_cleaned.is_empty(),
1136
1269
  "空 workspace 无 orphan:{:?}",
@@ -1175,8 +1308,14 @@ fn fork_agent_on_unowned_workspace_does_not_silently_fork() {
1175
1308
  #[test]
1176
1309
  fn add_agent_event_name_constants_match_python_lifecycle_strings() {
1177
1310
  // 锁死发射事件名(顺序被 porter 实现后的事件流锁死;此处锁名)。
1178
- assert_eq!(event_names::ADD_STEP_COMPLETED, "lifecycle.add_step_completed");
1179
- assert_eq!(event_names::ADD_STEP_ROLLED_BACK, "lifecycle.add_step_rolled_back");
1311
+ assert_eq!(
1312
+ event_names::ADD_STEP_COMPLETED,
1313
+ "lifecycle.add_step_completed"
1314
+ );
1315
+ assert_eq!(
1316
+ event_names::ADD_STEP_ROLLED_BACK,
1317
+ "lifecycle.add_step_rolled_back"
1318
+ );
1180
1319
  assert_eq!(event_names::ADD_FAILED, "lifecycle.add_failed");
1181
1320
  }
1182
1321
 
@@ -1237,7 +1376,10 @@ fn halt_plan_unknown_id_is_not_found_error() {
1237
1376
  // 未持久化的 plan → "plan not found"(typed/Err),不幂等成 Halted。
1238
1377
  match halt_plan(&ws, &pid, "user_requested") {
1239
1378
  Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
1240
- assert!(msg.contains("not found") || msg.contains("ghost-plan"), "got: {msg}");
1379
+ assert!(
1380
+ msg.contains("not found") || msg.contains("ghost-plan"),
1381
+ "got: {msg}"
1382
+ );
1241
1383
  }
1242
1384
  Ok(PlanProgress::Halted { .. }) => {
1243
1385
  panic!("未找到的 plan 不得返回 Halted(应 not-found)")
@@ -1253,7 +1395,10 @@ fn plan_status_unknown_id_is_not_found() {
1253
1395
  // 读未持久化 plan → not-found error(Rust 入口签名 Result<PlanState,_>)。
1254
1396
  match plan_status(&ws, &pid) {
1255
1397
  Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
1256
- assert!(msg.contains("not found") || msg.contains("ghost-plan"), "got: {msg}");
1398
+ assert!(
1399
+ msg.contains("not found") || msg.contains("ghost-plan"),
1400
+ "got: {msg}"
1401
+ );
1257
1402
  }
1258
1403
  Ok(st) => panic!("未持久化 plan 不得返回 PlanState:{st:?}"),
1259
1404
  other => panic!("意外结果:{other:?}"),