@team-agent/installer 0.3.13 → 0.3.14

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.
@@ -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
  }
@@ -228,17 +255,14 @@ fn resolve_backend_recorded_used_when_no_request() {
228
255
  fn probe_never_errors_platform_degradation_is_typed_outcome() {
229
256
  // C13:能力性降级绝不走 LifecycleError —— 它是 DisplayStatus::Blocked + reason。
230
257
  let ws = temp_ws();
231
- let probe = probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
258
+ let probe =
259
+ probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
232
260
  // 若 blocked,reason 必属 AdaptiveBlockReason 封闭集且与 status 一致。
233
261
  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
- ),
262
+ DisplayStatus::Blocked => {
263
+ assert!(probe.reason.is_some(), "blocked 必带 reason(C16 封闭集)")
264
+ }
265
+ DisplayStatus::Opened => assert!(probe.reason.is_none(), "opened 不应带 block reason"),
242
266
  DisplayStatus::Stopped => {}
243
267
  }
244
268
  }
@@ -294,7 +318,11 @@ fn adaptive_block_reason_serde_names_match_python_and_set_is_exactly_six() {
294
318
  .collect();
295
319
  names.sort();
296
320
  names.dedup();
297
- assert_eq!(names.len(), 6, "ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏");
321
+ assert_eq!(
322
+ names.len(),
323
+ 6,
324
+ "ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏"
325
+ );
298
326
  }
299
327
 
300
328
  #[test]
@@ -332,8 +360,14 @@ fn adaptive_blocked_out_of_set_reason_bottoms_to_aggregator_rebuild_failed() {
332
360
  #[test]
333
361
  fn start_mode_serde_names_match_python_start_mode_strings() {
334
362
  // 低价值 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\"");
363
+ assert_eq!(
364
+ serde_json::to_string(&StartMode::Resumed).unwrap(),
365
+ "\"resumed\""
366
+ );
367
+ assert_eq!(
368
+ serde_json::to_string(&StartMode::Fresh).unwrap(),
369
+ "\"fresh\""
370
+ );
337
371
  assert_eq!(
338
372
  serde_json::to_string(&StartMode::FreshAfterMissingRollout).unwrap(),
339
373
  "\"fresh_after_missing_rollout\""
@@ -368,7 +402,13 @@ fn decide_start_mode_codex_missing_rollout_with_allow_fresh_is_fresh_after_missi
368
402
  );
369
403
  // rollout 路径存在但文件已不在,同样命中。
370
404
  assert_eq!(
371
- decide_start_mode("codex", Some(&sid("s1")), Some(&rp("/gone.jsonl")), false, true),
405
+ decide_start_mode(
406
+ "codex",
407
+ Some(&sid("s1")),
408
+ Some(&rp("/gone.jsonl")),
409
+ false,
410
+ true
411
+ ),
372
412
  StartMode::FreshAfterMissingRollout
373
413
  );
374
414
  }
@@ -385,7 +425,13 @@ fn decide_start_mode_codex_missing_rollout_without_allow_fresh_refuses() {
385
425
  #[test]
386
426
  fn decide_start_mode_codex_rollout_present_is_resumed_regardless_of_fresh() {
387
427
  assert_eq!(
388
- decide_start_mode("codex", Some(&sid("s1")), Some(&rp("/r.jsonl")), true, false),
428
+ decide_start_mode(
429
+ "codex",
430
+ Some(&sid("s1")),
431
+ Some(&rp("/r.jsonl")),
432
+ true,
433
+ false
434
+ ),
389
435
  StartMode::Resumed
390
436
  );
391
437
  assert_eq!(
@@ -431,15 +477,37 @@ fn decide_start_mode_checks_backing_for_all_resumable_providers() {
431
477
  );
432
478
  }
433
479
 
480
+ #[test]
481
+ fn decide_start_mode_refuses_session_id_for_nonresumable_providers() {
482
+ for provider in ["fake", "gemini_cli"] {
483
+ assert_eq!(
484
+ decide_start_mode(provider, Some(&sid("s1")), None, true, false),
485
+ StartMode::Noop,
486
+ "{provider} has a persisted session id but caps.resume=false, so it must not resume"
487
+ );
488
+ assert_eq!(
489
+ decide_start_mode(provider, Some(&sid("s1")), None, true, true),
490
+ StartMode::FreshAfterMissingRollout,
491
+ "{provider} may only fresh-start a persisted session id under explicit allow_fresh"
492
+ );
493
+ }
494
+ }
495
+
434
496
  #[test]
435
497
  fn resume_decision_serde_names_match_python() {
436
498
  // 低价值 wire-format 守卫:_emit_resume_decisions: "resume"|"fresh_start"|"refuse"。
437
- assert_eq!(serde_json::to_string(&ResumeDecision::Resume).unwrap(), "\"resume\"");
499
+ assert_eq!(
500
+ serde_json::to_string(&ResumeDecision::Resume).unwrap(),
501
+ "\"resume\""
502
+ );
438
503
  assert_eq!(
439
504
  serde_json::to_string(&ResumeDecision::FreshStart).unwrap(),
440
505
  "\"fresh_start\""
441
506
  );
442
- assert_eq!(serde_json::to_string(&ResumeDecision::Refuse).unwrap(), "\"refuse\"");
507
+ assert_eq!(
508
+ serde_json::to_string(&ResumeDecision::Refuse).unwrap(),
509
+ "\"refuse\""
510
+ );
443
511
  }
444
512
 
445
513
  // ───────────────────────────────────────────────────────────────────────
@@ -472,7 +540,11 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
472
540
  .expect("纯验证不应 Err(资源型失败才走 LifecycleError)");
473
541
  assert!(plan.corrupt_entries.is_empty(), "valid ISO 不应判 corrupt");
474
542
  // 恰一条决策(每非 paused worker 一条),且为 Refuse。
475
- assert_eq!(plan.decisions.len(), 1, "每非 paused worker 恰一条 resume_decision");
543
+ assert_eq!(
544
+ plan.decisions.len(),
545
+ 1,
546
+ "每非 paused worker 恰一条 resume_decision"
547
+ );
476
548
  assert_eq!(plan.decisions[0].agent_id, aid("w1"));
477
549
  assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
478
550
  // unresumable 收口该 worker,reason 精确。
@@ -481,6 +553,30 @@ fn classify_restart_plan_interacted_unresumable_no_allow_fresh_yields_refuse() {
481
553
  assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
482
554
  }
483
555
 
556
+ #[test]
557
+ fn classify_restart_plan_fake_stale_session_refuses_without_allow_fresh() {
558
+ let state = json!({
559
+ "session_name": "team-a",
560
+ "agents": {
561
+ "w1": {
562
+ "provider": "fake",
563
+ "first_send_at": "2026-05-27T10:00:00+00:00",
564
+ "session_id": "stale-fake-session"
565
+ }
566
+ }
567
+ });
568
+ let plan = classify_restart_plan(&state, false).expect("pure validation should not Err");
569
+ assert_eq!(plan.decisions.len(), 1);
570
+ assert_eq!(plan.decisions[0].decision, ResumeDecision::Refuse);
571
+ assert_eq!(plan.decisions[0].restart_mode, StartMode::Noop);
572
+ assert_eq!(plan.unresumable.len(), 1);
573
+ assert_eq!(plan.unresumable[0].reason, "session_unresumable");
574
+ assert_eq!(
575
+ plan.unresumable[0].session_id.as_ref().map(|s| s.as_str()),
576
+ Some("stale-fake-session")
577
+ );
578
+ }
579
+
484
580
  #[test]
485
581
  fn classify_restart_plan_interacted_unresumable_with_allow_fresh_yields_fresh_start_not_refuse() {
486
582
  // 同一 worker,allow_fresh=true → FreshStart(可丢 context),unresumable 为空(无 atomic refusal)。
@@ -517,7 +613,11 @@ fn classify_restart_plan_never_interacted_null_session_refuses_not_fresh() {
517
613
  ResumeDecision::Refuse,
518
614
  "null-session 自启动 worker 默认必须 Refuse,不许静默 fresh"
519
615
  );
520
- assert_eq!(plan.unresumable.len(), 1, "Refuse 必入 unresumable(诚实出口)");
616
+ assert_eq!(
617
+ plan.unresumable.len(),
618
+ 1,
619
+ "Refuse 必入 unresumable(诚实出口)"
620
+ );
521
621
  assert_eq!(plan.unresumable[0].reason, "no_persisted_session_id");
522
622
  }
523
623
 
@@ -756,10 +856,13 @@ fn restart_candidates_empty_workspace_is_empty_list() {
756
856
  fn save_snapshot_writes_atomic_state_json_under_teams_dir() {
757
857
  let ws = temp_ws();
758
858
  let state = json!({"session_name": "team-alpha", "agents": {}});
759
- let path = save_team_runtime_snapshot(&ws, &state)
760
- .expect("bug-084:写路径返 Result,正常态须 Ok");
859
+ let path =
860
+ save_team_runtime_snapshot(&ws, &state).expect("bug-084:写路径返 Result,正常态须 Ok");
761
861
  // 末段必为 state.json,且落在 runtime/teams/<safe session> 下。
762
- assert_eq!(path.file_name().and_then(|s| s.to_str()), Some("state.json"));
862
+ assert_eq!(
863
+ path.file_name().and_then(|s| s.to_str()),
864
+ Some("state.json")
865
+ );
763
866
  let s = path.to_string_lossy();
764
867
  assert!(
765
868
  s.contains("teams") && s.contains("team-alpha"),
@@ -884,7 +987,9 @@ fn leader_pane_env_invalid_format_writes_warning_event() {
884
987
  validate_active_leader_pane_env_with_workspace(&transport, Some(&ws))
885
988
  .expect("invalid format is warning-only");
886
989
 
887
- let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
990
+ let events = crate::event_log::EventLog::new(&ws)
991
+ .tail(0)
992
+ .expect("events");
888
993
  let event = events
889
994
  .iter()
890
995
  .find(|event| {
@@ -947,8 +1052,8 @@ fn leader_pane_env_cross_socket_live_wins() {
947
1052
  #[test]
948
1053
  fn leader_pane_env_cross_socket_dead_rejects_without_live() {
949
1054
  let pane = crate::transport::PaneId::new("%404");
950
- let absent = crate::transport::test_support::OfflineTransport::new()
951
- .with_pane_presence("%404", false);
1055
+ let absent =
1056
+ crate::transport::test_support::OfflineTransport::new().with_pane_presence("%404", false);
952
1057
  let dead = crate::transport::test_support::OfflineTransport::new()
953
1058
  .with_liveness("%404", PaneLiveness::Dead);
954
1059
 
@@ -963,8 +1068,8 @@ fn leader_pane_env_cross_socket_dead_rejects_without_live() {
963
1068
  #[test]
964
1069
  fn leader_pane_env_cross_socket_absent_when_reachable_servers_confirm_missing() {
965
1070
  let pane = crate::transport::PaneId::new("%9999");
966
- let absent = crate::transport::test_support::OfflineTransport::new()
967
- .with_pane_presence("%9999", false);
1071
+ let absent =
1072
+ crate::transport::test_support::OfflineTransport::new().with_pane_presence("%9999", false);
968
1073
  let unknown = crate::transport::test_support::OfflineTransport::new()
969
1074
  .with_default_liveness(PaneLiveness::Unknown);
970
1075
 
@@ -1008,13 +1113,17 @@ fn mcp_auto_approval_env_marks_leader_bypass_namespace_only() {
1008
1113
 
1009
1114
  apply_mcp_auto_approval_env(&mut env, &safety);
1010
1115
 
1011
- assert_eq!(env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str), Some("1"));
1116
+ assert_eq!(
1117
+ env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
1118
+ Some("1")
1119
+ );
1012
1120
  assert_eq!(
1013
1121
  env.get("TEAM_AGENT_MCP_AUTO_APPROVE").map(String::as_str),
1014
1122
  Some("team_orchestrator")
1015
1123
  );
1016
1124
  assert_eq!(
1017
- env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE").map(String::as_str),
1125
+ env.get("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE")
1126
+ .map(String::as_str),
1018
1127
  Some("leader_bypass")
1019
1128
  );
1020
1129
  assert_eq!(
@@ -1030,7 +1139,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
1030
1139
  "TEAM_AGENT_MCP_AUTO_APPROVE".to_string(),
1031
1140
  "team_orchestrator".to_string(),
1032
1141
  ),
1033
- ("TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(), "leader_bypass".to_string()),
1142
+ (
1143
+ "TEAM_AGENT_MCP_AUTO_APPROVE_SOURCE".to_string(),
1144
+ "leader_bypass".to_string(),
1145
+ ),
1034
1146
  ]);
1035
1147
  let safety = DangerousApproval {
1036
1148
  enabled: false,
@@ -1045,7 +1157,10 @@ fn mcp_auto_approval_env_clears_when_leader_is_restricted() {
1045
1157
 
1046
1158
  apply_mcp_auto_approval_env(&mut env, &safety);
1047
1159
 
1048
- assert_eq!(env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str), Some("0"));
1160
+ assert_eq!(
1161
+ env.get("TEAM_AGENT_LEADER_BYPASS").map(String::as_str),
1162
+ Some("0")
1163
+ );
1049
1164
  assert!(
1050
1165
  !env.contains_key("TEAM_AGENT_MCP_AUTO_APPROVE"),
1051
1166
  "restricted leader must not leave MCP auto-approval env behind: {env:?}"
@@ -1113,7 +1228,12 @@ fn open_worker_displays_blocked_probe_yields_typed_blocked_not_error() {
1113
1228
  // 每个 worker 的 display 在 blocked 平台上应是 Blocked 变体(若有 worker)。
1114
1229
  for (id, d) in rep.displays.iter() {
1115
1230
  assert!(
1116
- matches!(d, WorkerDisplay::Blocked { reason: AdaptiveBlockReason::NotImplementedThisPlatform }),
1231
+ matches!(
1232
+ d,
1233
+ WorkerDisplay::Blocked {
1234
+ reason: AdaptiveBlockReason::NotImplementedThisPlatform
1235
+ }
1236
+ ),
1117
1237
  "worker {id} 在 windows 平台应 Blocked(not_implemented):{d:?}"
1118
1238
  );
1119
1239
  }
@@ -1130,7 +1250,11 @@ fn close_team_display_empty_workspace_closes_nothing_not_error() {
1130
1250
  let ws = temp_ws();
1131
1251
  let rep = close_team_display_backends(&ws, &sess("team-a"))
1132
1252
  .expect("C9:无 recorded backend 应 Ok(空报告),不是 Err");
1133
- assert!(rep.closed.is_empty(), "无 recorded backend 不应关任何东西:{:?}", rep.closed);
1253
+ assert!(
1254
+ rep.closed.is_empty(),
1255
+ "无 recorded backend 不应关任何东西:{:?}",
1256
+ rep.closed
1257
+ );
1134
1258
  assert!(
1135
1259
  rep.orphans_cleaned.is_empty(),
1136
1260
  "空 workspace 无 orphan:{:?}",
@@ -1175,8 +1299,14 @@ fn fork_agent_on_unowned_workspace_does_not_silently_fork() {
1175
1299
  #[test]
1176
1300
  fn add_agent_event_name_constants_match_python_lifecycle_strings() {
1177
1301
  // 锁死发射事件名(顺序被 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");
1302
+ assert_eq!(
1303
+ event_names::ADD_STEP_COMPLETED,
1304
+ "lifecycle.add_step_completed"
1305
+ );
1306
+ assert_eq!(
1307
+ event_names::ADD_STEP_ROLLED_BACK,
1308
+ "lifecycle.add_step_rolled_back"
1309
+ );
1180
1310
  assert_eq!(event_names::ADD_FAILED, "lifecycle.add_failed");
1181
1311
  }
1182
1312
 
@@ -1237,7 +1367,10 @@ fn halt_plan_unknown_id_is_not_found_error() {
1237
1367
  // 未持久化的 plan → "plan not found"(typed/Err),不幂等成 Halted。
1238
1368
  match halt_plan(&ws, &pid, "user_requested") {
1239
1369
  Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
1240
- assert!(msg.contains("not found") || msg.contains("ghost-plan"), "got: {msg}");
1370
+ assert!(
1371
+ msg.contains("not found") || msg.contains("ghost-plan"),
1372
+ "got: {msg}"
1373
+ );
1241
1374
  }
1242
1375
  Ok(PlanProgress::Halted { .. }) => {
1243
1376
  panic!("未找到的 plan 不得返回 Halted(应 not-found)")
@@ -1253,7 +1386,10 @@ fn plan_status_unknown_id_is_not_found() {
1253
1386
  // 读未持久化 plan → not-found error(Rust 入口签名 Result<PlanState,_>)。
1254
1387
  match plan_status(&ws, &pid) {
1255
1388
  Err(LifecycleError::InvalidPlan(msg)) | Err(LifecycleError::TeamSelect(msg)) => {
1256
- assert!(msg.contains("not found") || msg.contains("ghost-plan"), "got: {msg}");
1389
+ assert!(
1390
+ msg.contains("not found") || msg.contains("ghost-plan"),
1391
+ "got: {msg}"
1392
+ );
1257
1393
  }
1258
1394
  Ok(st) => panic!("未持久化 plan 不得返回 PlanState:{st:?}"),
1259
1395
  other => panic!("意外结果:{other:?}"),