@team-agent/installer 0.5.92 → 0.5.97
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +381 -157
- package/crates/team-agent/src/cli/diagnose.rs +40 -10
- package/crates/team-agent/src/cli/emit.rs +6 -4
- package/crates/team-agent/src/cli/mod.rs +483 -470
- package/crates/team-agent/src/cli/models.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/tests/base.rs +20 -10
- package/crates/team-agent/src/cli/tests/divergence.rs +12 -21
- package/crates/team-agent/src/cli/tests/main_preserved.rs +6 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +20 -11
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +197 -30
- package/crates/team-agent/src/cli/triage.rs +31 -31
- package/crates/team-agent/src/cli/types.rs +3 -1
- package/crates/team-agent/src/coordinator/health.rs +92 -30
- package/crates/team-agent/src/coordinator/tick.rs +62 -8
- package/crates/team-agent/src/coordinator/types.rs +2 -0
- package/crates/team-agent/src/kill_audit.rs +41 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +48 -14
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +4 -8
- package/crates/team-agent/src/lifecycle/launch.rs +17 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +10 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +10 -7
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +91 -4
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +128 -11
- package/crates/team-agent/src/platform/process.rs +4 -45
- package/crates/team-agent/src/tmux_backend.rs +113 -3
- package/crates/team-agent/src/transport/test_support.rs +30 -0
- package/crates/team-agent/src/transport.rs +41 -0
- package/package.json +4 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! E12 (P0) · `sessions_to_kill` 纯决策单测(kill 决策下沉)。
|
|
2
2
|
//!
|
|
3
3
|
//! spare = state 锚 session(anchor_sessions) ∪ `team-agent-leader-` 命名前缀(并集,锚优先)。
|
|
4
|
-
//!
|
|
4
|
+
//! Cleanup is always per-session and ownership-gated; a socket is never sufficient authority for kill-server.
|
|
5
5
|
//! 集成面由 tests/b5_leader_terminal_kill_red.rs 的真 tmux 契约覆盖,此处锁纯决策 + 4 反向 case。
|
|
6
6
|
|
|
7
7
|
use crate::cli::lifecycle_port::{sessions_to_kill, KillDecision};
|
|
@@ -23,12 +23,58 @@ fn anchors(raw: &[&str]) -> BTreeSet<String> {
|
|
|
23
23
|
raw.iter().map(|s| s.to_string()).collect()
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
// RC-4(独享 socket):仅目标 session(无 spare)→ 整 server 拆。
|
|
27
26
|
#[test]
|
|
28
|
-
fn
|
|
27
|
+
fn leader_anchors_are_endpoint_scoped_and_unknown_anchors_stay_protected() {
|
|
28
|
+
use crate::cli::lifecycle_port::collect_state_leader_anchor_pane_ids as collect;
|
|
29
|
+
let state = json!({
|
|
30
|
+
"team_owner": {"pane_id": "%2"},
|
|
31
|
+
"leader_receiver": {"pane_id": "%2", "tmux_socket": "/tmp/s4-host.sock"},
|
|
32
|
+
"teams": {
|
|
33
|
+
"local": {
|
|
34
|
+
"team_owner": {"pane_id": "%3", "tmux_socket": "/tmp/s4-worker.sock"},
|
|
35
|
+
"leader_receiver": {"pane_id": "%3"}
|
|
36
|
+
},
|
|
37
|
+
"legacy": {"team_owner": {"pane_id": "%4"}}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
assert_eq!(collect(&state, Some("/tmp/s4-worker.sock")), anchors(&["%3", "%4"]));
|
|
41
|
+
assert_eq!(collect(&state, Some("/tmp/s4-host.sock")), anchors(&["%2", "%4"]));
|
|
42
|
+
assert_eq!(collect(&state, None), anchors(&["%2", "%3", "%4"]));
|
|
43
|
+
|
|
44
|
+
// An unrelated receiver's socket is not evidence about the owner's pane.
|
|
45
|
+
let different_panes = json!({
|
|
46
|
+
"team_owner": {"pane_id": "%5"},
|
|
47
|
+
"leader_receiver": {"pane_id": "%2", "tmux_socket": "/tmp/s4-host.sock"}
|
|
48
|
+
});
|
|
49
|
+
assert_eq!(collect(&different_panes, Some("/tmp/s4-worker.sock")), anchors(&["%5"]));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
#[cfg(unix)]
|
|
54
|
+
fn leader_anchor_endpoint_aliases_preserve_same_server_protection() {
|
|
55
|
+
use crate::cli::lifecycle_port::collect_state_leader_anchor_pane_ids as collect;
|
|
56
|
+
let ws = tmp_shutdown_workspace("anchor-endpoint-alias");
|
|
57
|
+
let socket = ws.join("server.sock");
|
|
58
|
+
let alias = ws.join("alias.sock");
|
|
59
|
+
std::fs::write(&socket, b"fixture").unwrap();
|
|
60
|
+
std::os::unix::fs::symlink(&socket, &alias).unwrap();
|
|
61
|
+
let state = json!({"leader_receiver": {"pane_id": "%2", "tmux_socket": alias}});
|
|
62
|
+
assert_eq!(collect(&state, socket.to_str()), anchors(&["%2"]));
|
|
63
|
+
let short_name = format!("s4-anchor-{}", std::process::id());
|
|
64
|
+
let full_path = crate::tmux_backend::socket_path_for_name(&short_name).unwrap();
|
|
65
|
+
let state = json!({"leader_receiver": {"pane_id": "%2", "tmux_socket": short_name}});
|
|
66
|
+
assert_eq!(collect(&state, full_path.to_str()), anchors(&["%2"]));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// RC-4: even an apparently exclusive socket uses per-session cleanup.
|
|
70
|
+
#[test]
|
|
71
|
+
fn rc4_exclusive_socket_is_fail_closed() {
|
|
29
72
|
assert_eq!(
|
|
30
73
|
sessions_to_kill(&names(&["team-x", "team-y"]), &BTreeSet::new()),
|
|
31
|
-
KillDecision::
|
|
74
|
+
KillDecision::KillIndividually {
|
|
75
|
+
to_kill: names(&["team-x", "team-y"]),
|
|
76
|
+
spared: vec![]
|
|
77
|
+
}
|
|
32
78
|
);
|
|
33
79
|
// 空 session 集 → 逐 kill(no-op),不整 server 拆(没东西可拆)。
|
|
34
80
|
assert_eq!(
|
|
@@ -87,10 +133,13 @@ fn rc2_no_anchor_falls_back_to_naming() {
|
|
|
87
133
|
&BTreeSet::new(),
|
|
88
134
|
);
|
|
89
135
|
assert!(matches!(with_leader, KillDecision::KillIndividually { .. }));
|
|
90
|
-
// 无锚 +
|
|
136
|
+
// 无锚 + 无前缀仍只能逐 session 返回,实际 shutdown 会因 marker 缺失拒绝杀灭。
|
|
91
137
|
assert_eq!(
|
|
92
138
|
sessions_to_kill(&names(&["team-x"]), &BTreeSet::new()),
|
|
93
|
-
KillDecision::
|
|
139
|
+
KillDecision::KillIndividually {
|
|
140
|
+
to_kill: names(&["team-x"]),
|
|
141
|
+
spared: vec![]
|
|
142
|
+
}
|
|
94
143
|
);
|
|
95
144
|
}
|
|
96
145
|
|
|
@@ -169,6 +218,8 @@ fn owned_empty_endpoint_cleanup_removes_socket_file_before_reporting_ok() {
|
|
|
169
218
|
&ws,
|
|
170
219
|
&json!({
|
|
171
220
|
"session_name": "team-owned-clean",
|
|
221
|
+
"team_key": "team-owned-clean",
|
|
222
|
+
"generation": "team-owned-clean",
|
|
172
223
|
"tmux_endpoint": socket.to_string_lossy(),
|
|
173
224
|
"tmux_socket": socket.to_string_lossy(),
|
|
174
225
|
"tmux_socket_source": "workspace",
|
|
@@ -177,7 +228,12 @@ fn owned_empty_endpoint_cleanup_removes_socket_file_before_reporting_ok() {
|
|
|
177
228
|
}),
|
|
178
229
|
)
|
|
179
230
|
.unwrap();
|
|
180
|
-
let transport = CleanShutdownTransport::new()
|
|
231
|
+
let transport = CleanShutdownTransport::new().with_owned_session(
|
|
232
|
+
&ws,
|
|
233
|
+
"team-owned-clean",
|
|
234
|
+
"team-owned-clean",
|
|
235
|
+
"team-owned-clean",
|
|
236
|
+
);
|
|
181
237
|
|
|
182
238
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
183
239
|
.expect("shutdown should complete");
|
|
@@ -186,12 +242,12 @@ fn owned_empty_endpoint_cleanup_removes_socket_file_before_reporting_ok() {
|
|
|
186
242
|
assert_eq!(out["status"], json!("ok"));
|
|
187
243
|
assert_eq!(out["residuals"]["owned_files"], json!([]));
|
|
188
244
|
assert!(
|
|
189
|
-
|
|
190
|
-
"
|
|
245
|
+
socket.exists(),
|
|
246
|
+
"fail-closed endpoint cleanup must preserve the socket file"
|
|
191
247
|
);
|
|
192
248
|
assert!(
|
|
193
|
-
transport.kill_server_called(),
|
|
194
|
-
"
|
|
249
|
+
!transport.kill_server_called(),
|
|
250
|
+
"endpoint ownership must not authorize server teardown"
|
|
195
251
|
);
|
|
196
252
|
}
|
|
197
253
|
|
|
@@ -235,6 +291,8 @@ fn owned_file_residual_makes_shutdown_failed_not_partial() {
|
|
|
235
291
|
&ws,
|
|
236
292
|
&json!({
|
|
237
293
|
"session_name": "team-owned-residual",
|
|
294
|
+
"team_key": "team-owned-residual",
|
|
295
|
+
"generation": "team-owned-residual",
|
|
238
296
|
"tmux_endpoint": socket_dir.to_string_lossy(),
|
|
239
297
|
"tmux_socket": socket_dir.to_string_lossy(),
|
|
240
298
|
"tmux_socket_source": "workspace",
|
|
@@ -243,18 +301,22 @@ fn owned_file_residual_makes_shutdown_failed_not_partial() {
|
|
|
243
301
|
}),
|
|
244
302
|
)
|
|
245
303
|
.unwrap();
|
|
246
|
-
let transport = CleanShutdownTransport::new()
|
|
304
|
+
let transport = CleanShutdownTransport::new().with_owned_session(
|
|
305
|
+
&ws,
|
|
306
|
+
"team-owned-residual",
|
|
307
|
+
"team-owned-residual",
|
|
308
|
+
"team-owned-residual",
|
|
309
|
+
);
|
|
247
310
|
|
|
248
311
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
249
312
|
.expect("shutdown should complete");
|
|
250
313
|
|
|
251
|
-
assert_eq!(out["ok"], json!(
|
|
252
|
-
assert_eq!(out["status"], json!("
|
|
314
|
+
assert_eq!(out["ok"], json!(true));
|
|
315
|
+
assert_eq!(out["status"], json!("ok"));
|
|
253
316
|
assert_eq!(out["phase"], json!(null));
|
|
254
|
-
assert_eq!(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
317
|
+
assert_eq!(out["residuals"]["owned_files"], json!([]));
|
|
318
|
+
assert!(socket_dir.exists(), "fail-closed cleanup preserves residual files");
|
|
319
|
+
assert!(!transport.kill_server_called());
|
|
258
320
|
}
|
|
259
321
|
|
|
260
322
|
#[test]
|
|
@@ -340,6 +402,7 @@ fn scoped_clean_shutdown_persists_team_shutdown_status_after_disk_roundtrip() {
|
|
|
340
402
|
"schema_version": 1,
|
|
341
403
|
"active_team_key": "current",
|
|
342
404
|
"team_key": "current",
|
|
405
|
+
"generation": "current",
|
|
343
406
|
"session_name": "team-current",
|
|
344
407
|
"team_dir": ws.display().to_string(),
|
|
345
408
|
"workspace": ws.display().to_string(),
|
|
@@ -354,6 +417,7 @@ fn scoped_clean_shutdown_persists_team_shutdown_status_after_disk_roundtrip() {
|
|
|
354
417
|
"teams": {
|
|
355
418
|
"current": {
|
|
356
419
|
"team_key": "current",
|
|
420
|
+
"generation": "current",
|
|
357
421
|
"session_name": "team-current",
|
|
358
422
|
"team_dir": ws.display().to_string(),
|
|
359
423
|
"workspace": ws.display().to_string(),
|
|
@@ -375,7 +439,12 @@ fn scoped_clean_shutdown_persists_team_shutdown_status_after_disk_roundtrip() {
|
|
|
375
439
|
&ws,
|
|
376
440
|
true,
|
|
377
441
|
Some("current"),
|
|
378
|
-
&CleanShutdownTransport::new()
|
|
442
|
+
&CleanShutdownTransport::new().with_owned_session(
|
|
443
|
+
&ws,
|
|
444
|
+
"team-current",
|
|
445
|
+
"current",
|
|
446
|
+
"current",
|
|
447
|
+
),
|
|
379
448
|
)
|
|
380
449
|
.expect("shutdown should complete");
|
|
381
450
|
|
|
@@ -414,6 +483,7 @@ fn scoped_degraded_shutdown_does_not_persist_team_shutdown_status() {
|
|
|
414
483
|
"schema_version": 1,
|
|
415
484
|
"active_team_key": "current",
|
|
416
485
|
"team_key": "current",
|
|
486
|
+
"generation": "current",
|
|
417
487
|
"session_name": "team-current",
|
|
418
488
|
"agents": {
|
|
419
489
|
"adminweb": {
|
|
@@ -426,6 +496,7 @@ fn scoped_degraded_shutdown_does_not_persist_team_shutdown_status() {
|
|
|
426
496
|
"teams": {
|
|
427
497
|
"current": {
|
|
428
498
|
"team_key": "current",
|
|
499
|
+
"generation": "current",
|
|
429
500
|
"session_name": "team-current",
|
|
430
501
|
"agents": {
|
|
431
502
|
"adminweb": {
|
|
@@ -445,7 +516,9 @@ fn scoped_degraded_shutdown_does_not_persist_team_shutdown_status() {
|
|
|
445
516
|
&ws,
|
|
446
517
|
true,
|
|
447
518
|
Some("current"),
|
|
448
|
-
&CleanShutdownTransport::new()
|
|
519
|
+
&CleanShutdownTransport::new()
|
|
520
|
+
.with_owned_session(&ws, "team-current", "current", "current")
|
|
521
|
+
.with_probe_timeout("ps_table"),
|
|
449
522
|
)
|
|
450
523
|
.expect("shutdown should complete");
|
|
451
524
|
|
|
@@ -519,6 +592,8 @@ fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
|
|
|
519
592
|
&ws,
|
|
520
593
|
&json!({
|
|
521
594
|
"session_name": "team-owned-loop",
|
|
595
|
+
"team_key": "team-owned-loop",
|
|
596
|
+
"generation": "team-owned-loop",
|
|
522
597
|
"tmux_endpoint": socket.to_string_lossy(),
|
|
523
598
|
"tmux_socket": socket.to_string_lossy(),
|
|
524
599
|
"tmux_socket_source": "workspace",
|
|
@@ -535,7 +610,12 @@ fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
|
|
|
535
610
|
&ws,
|
|
536
611
|
true,
|
|
537
612
|
None,
|
|
538
|
-
&CleanShutdownTransport::new()
|
|
613
|
+
&CleanShutdownTransport::new().with_owned_session(
|
|
614
|
+
&ws,
|
|
615
|
+
"team-owned-loop",
|
|
616
|
+
"team-owned-loop",
|
|
617
|
+
"team-owned-loop",
|
|
618
|
+
),
|
|
539
619
|
);
|
|
540
620
|
let out = match &result {
|
|
541
621
|
Ok(out) if !shutdown_result_is_degraded(out) => out,
|
|
@@ -573,8 +653,9 @@ fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
|
|
|
573
653
|
}
|
|
574
654
|
let ending = sockets.iter().filter(|path| path.exists()).count();
|
|
575
655
|
assert_eq!(
|
|
576
|
-
ending,
|
|
577
|
-
|
|
656
|
+
ending,
|
|
657
|
+
starting + sockets.len(),
|
|
658
|
+
"fail-closed endpoint cleanup must not remove unverified socket files"
|
|
578
659
|
);
|
|
579
660
|
#[cfg(unix)]
|
|
580
661
|
if expected_probe_failure {
|
|
@@ -793,6 +874,7 @@ struct CleanShutdownTransport {
|
|
|
793
874
|
probe_timeout_kind: Option<&'static str>,
|
|
794
875
|
targets_persist_after_kill: bool,
|
|
795
876
|
kill_session_error: Option<String>,
|
|
877
|
+
owner: Option<crate::transport::SessionOwner>,
|
|
796
878
|
// E49 (0.3.24 P0, shutdown kills leader CLI): record per-pane / per-window /
|
|
797
879
|
// per-session kill targets so RED contracts can assert (a) the leader pane is
|
|
798
880
|
// never killed and (b) worker panes ARE killed via the new per-pane path.
|
|
@@ -810,6 +892,7 @@ impl CleanShutdownTransport {
|
|
|
810
892
|
probe_timeout_kind: None,
|
|
811
893
|
targets_persist_after_kill: false,
|
|
812
894
|
kill_session_error: None,
|
|
895
|
+
owner: None,
|
|
813
896
|
killed_panes: Mutex::new(Vec::new()),
|
|
814
897
|
killed_window_targets: Mutex::new(Vec::new()),
|
|
815
898
|
killed_sessions: Mutex::new(Vec::new()),
|
|
@@ -821,6 +904,46 @@ impl CleanShutdownTransport {
|
|
|
821
904
|
self
|
|
822
905
|
}
|
|
823
906
|
|
|
907
|
+
fn with_owned_session(
|
|
908
|
+
mut self,
|
|
909
|
+
workspace: &Path,
|
|
910
|
+
session: &str,
|
|
911
|
+
team: &str,
|
|
912
|
+
generation: &str,
|
|
913
|
+
) -> Self {
|
|
914
|
+
self.owner = Some(crate::transport::SessionOwner {
|
|
915
|
+
workspace: workspace
|
|
916
|
+
.canonicalize()
|
|
917
|
+
.unwrap_or_else(|_| workspace.to_path_buf())
|
|
918
|
+
.to_string_lossy()
|
|
919
|
+
.into_owned(),
|
|
920
|
+
team: team.to_string(),
|
|
921
|
+
generation: generation.to_string(),
|
|
922
|
+
});
|
|
923
|
+
if self.targets.is_empty() {
|
|
924
|
+
self.targets.push(PaneInfo {
|
|
925
|
+
pane_id: PaneId::new("%owned"),
|
|
926
|
+
session: SessionName::new(session),
|
|
927
|
+
window_index: Some(0),
|
|
928
|
+
window_name: Some(WindowName::new("owned")),
|
|
929
|
+
pane_index: Some(0),
|
|
930
|
+
tty: None,
|
|
931
|
+
current_command: Some("codex".to_string()),
|
|
932
|
+
current_path: Some(workspace.to_path_buf()),
|
|
933
|
+
active: true,
|
|
934
|
+
pane_pid: None,
|
|
935
|
+
leader_env: BTreeMap::new(),
|
|
936
|
+
});
|
|
937
|
+
} else {
|
|
938
|
+
for target in &mut self.targets {
|
|
939
|
+
if target.current_path.is_none() {
|
|
940
|
+
target.current_path = Some(workspace.to_path_buf());
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
self
|
|
945
|
+
}
|
|
946
|
+
|
|
824
947
|
fn kill_server_called(&self) -> bool {
|
|
825
948
|
*self.kill_server_called.lock().unwrap()
|
|
826
949
|
}
|
|
@@ -931,6 +1054,13 @@ impl Transport for CleanShutdownTransport {
|
|
|
931
1054
|
Ok(*self.session_present.lock().unwrap())
|
|
932
1055
|
}
|
|
933
1056
|
|
|
1057
|
+
fn session_owner(
|
|
1058
|
+
&self,
|
|
1059
|
+
_session: &SessionName,
|
|
1060
|
+
) -> Result<Option<crate::transport::SessionOwner>, TransportError> {
|
|
1061
|
+
Ok(self.owner.clone())
|
|
1062
|
+
}
|
|
1063
|
+
|
|
934
1064
|
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
935
1065
|
Ok(Vec::new())
|
|
936
1066
|
}
|
|
@@ -1001,6 +1131,8 @@ fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
|
|
|
1001
1131
|
&ws,
|
|
1002
1132
|
&json!({
|
|
1003
1133
|
"session_name": "team-lsof-cwd-timeout",
|
|
1134
|
+
"team_key": "team-lsof-cwd-timeout",
|
|
1135
|
+
"generation": "team-lsof-cwd-timeout",
|
|
1004
1136
|
"is_external_leader": true,
|
|
1005
1137
|
"agents": {
|
|
1006
1138
|
"fake_impl": {
|
|
@@ -1017,7 +1149,14 @@ fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
|
|
|
1017
1149
|
&ws,
|
|
1018
1150
|
true,
|
|
1019
1151
|
None,
|
|
1020
|
-
&CleanShutdownTransport::new()
|
|
1152
|
+
&CleanShutdownTransport::new()
|
|
1153
|
+
.with_owned_session(
|
|
1154
|
+
&ws,
|
|
1155
|
+
"team-lsof-cwd-timeout",
|
|
1156
|
+
"team-lsof-cwd-timeout",
|
|
1157
|
+
"team-lsof-cwd-timeout",
|
|
1158
|
+
)
|
|
1159
|
+
.with_probe_timeout("lsof_cwd"),
|
|
1021
1160
|
)
|
|
1022
1161
|
.expect("shutdown should complete");
|
|
1023
1162
|
|
|
@@ -1051,6 +1190,8 @@ fn ps_table_timeout_still_degrades_shutdown_truth() {
|
|
|
1051
1190
|
&ws,
|
|
1052
1191
|
&json!({
|
|
1053
1192
|
"session_name": "team-ps-table-timeout",
|
|
1193
|
+
"team_key": "team-ps-table-timeout",
|
|
1194
|
+
"generation": "team-ps-table-timeout",
|
|
1054
1195
|
"agents": {
|
|
1055
1196
|
"fake_impl": {
|
|
1056
1197
|
"status": "running",
|
|
@@ -1066,7 +1207,14 @@ fn ps_table_timeout_still_degrades_shutdown_truth() {
|
|
|
1066
1207
|
&ws,
|
|
1067
1208
|
true,
|
|
1068
1209
|
None,
|
|
1069
|
-
&CleanShutdownTransport::new()
|
|
1210
|
+
&CleanShutdownTransport::new()
|
|
1211
|
+
.with_owned_session(
|
|
1212
|
+
&ws,
|
|
1213
|
+
"team-ps-table-timeout",
|
|
1214
|
+
"team-ps-table-timeout",
|
|
1215
|
+
"team-ps-table-timeout",
|
|
1216
|
+
)
|
|
1217
|
+
.with_probe_timeout("ps_table"),
|
|
1070
1218
|
)
|
|
1071
1219
|
.expect("shutdown should complete");
|
|
1072
1220
|
|
|
@@ -1169,6 +1317,9 @@ fn leader_env_tmux_socket_never_kills_server_even_when_sessions_look_exclusive()
|
|
|
1169
1317
|
&ws,
|
|
1170
1318
|
&json!({
|
|
1171
1319
|
"is_external_leader": true,
|
|
1320
|
+
"team_key": "team-layout",
|
|
1321
|
+
"generation": "team-layout",
|
|
1322
|
+
"session_name": "team-layout",
|
|
1172
1323
|
"tmux_socket_source": "leader_env",
|
|
1173
1324
|
"agents": {
|
|
1174
1325
|
"fake_impl": {
|
|
@@ -1193,6 +1344,7 @@ fn leader_env_tmux_socket_never_kills_server_even_when_sessions_look_exclusive()
|
|
|
1193
1344
|
pane_pid: None,
|
|
1194
1345
|
leader_env: BTreeMap::new(),
|
|
1195
1346
|
}]);
|
|
1347
|
+
let transport = transport.with_owned_session(&ws, "team-layout", "team-layout", "team-layout");
|
|
1196
1348
|
|
|
1197
1349
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
1198
1350
|
.expect("shutdown should complete");
|
|
@@ -1239,6 +1391,8 @@ fn e49_managed_leader_shutdown_spares_leader_session_and_kills_workers_per_pane(
|
|
|
1239
1391
|
&ws,
|
|
1240
1392
|
&json!({
|
|
1241
1393
|
"session_name": "team-current",
|
|
1394
|
+
"team_key": "current",
|
|
1395
|
+
"generation": "current",
|
|
1242
1396
|
"is_external_leader": false,
|
|
1243
1397
|
"leader_receiver": {"pane_id": "%leader"},
|
|
1244
1398
|
"team_owner": {"pane_id": "%leader"},
|
|
@@ -1294,7 +1448,8 @@ fn e49_managed_leader_shutdown_spares_leader_session_and_kills_workers_per_pane(
|
|
|
1294
1448
|
leader_env: BTreeMap::new(),
|
|
1295
1449
|
},
|
|
1296
1450
|
])
|
|
1297
|
-
.with_targets_persist_after_kill()
|
|
1451
|
+
.with_targets_persist_after_kill()
|
|
1452
|
+
.with_owned_session(&ws, "team-current", "current", "current");
|
|
1298
1453
|
|
|
1299
1454
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
1300
1455
|
.expect("shutdown should complete");
|
|
@@ -1381,6 +1536,8 @@ fn e49_external_leader_shutdown_still_kills_team_session_unconditionally() {
|
|
|
1381
1536
|
&ws,
|
|
1382
1537
|
&json!({
|
|
1383
1538
|
"session_name": "team-external",
|
|
1539
|
+
"team_key": "team-external",
|
|
1540
|
+
"generation": "team-external",
|
|
1384
1541
|
"is_external_leader": true,
|
|
1385
1542
|
"leader_receiver": {"pane_id": "%leaderelsewhere"},
|
|
1386
1543
|
"agents": {
|
|
@@ -1420,7 +1577,8 @@ fn e49_external_leader_shutdown_still_kills_team_session_unconditionally() {
|
|
|
1420
1577
|
leader_env: BTreeMap::new(),
|
|
1421
1578
|
},
|
|
1422
1579
|
])
|
|
1423
|
-
.with_targets_persist_after_kill()
|
|
1580
|
+
.with_targets_persist_after_kill()
|
|
1581
|
+
.with_owned_session(&ws, "team-external", "team-external", "team-external");
|
|
1424
1582
|
|
|
1425
1583
|
let event_log = crate::event_log::EventLog::new(&ws);
|
|
1426
1584
|
let event_start = event_log.tail(0).expect("events before shutdown").len();
|
|
@@ -1455,6 +1613,8 @@ fn e49_external_leader_non_clean_diagnostic_is_bounded_and_still_kills() {
|
|
|
1455
1613
|
&ws,
|
|
1456
1614
|
&json!({
|
|
1457
1615
|
"session_name": "team-external",
|
|
1616
|
+
"team_key": "team-external",
|
|
1617
|
+
"generation": "team-external",
|
|
1458
1618
|
"is_external_leader": true,
|
|
1459
1619
|
"leader_receiver": {"pane_id": "%leaderelsewhere"},
|
|
1460
1620
|
"agents": {
|
|
@@ -1477,7 +1637,8 @@ fn e49_external_leader_non_clean_diagnostic_is_bounded_and_still_kills() {
|
|
|
1477
1637
|
pane_pid: None,
|
|
1478
1638
|
leader_env: BTreeMap::new(),
|
|
1479
1639
|
}])
|
|
1480
|
-
.with_kill_session_error("deterministic E49 kill-session failure")
|
|
1640
|
+
.with_kill_session_error("deterministic E49 kill-session failure")
|
|
1641
|
+
.with_owned_session(&ws, "team-external", "team-external", "team-external");
|
|
1481
1642
|
|
|
1482
1643
|
let event_log = crate::event_log::EventLog::new(&ws);
|
|
1483
1644
|
let event_start = event_log.tail(0).expect("events before shutdown").len();
|
|
@@ -1537,6 +1698,8 @@ fn e49_managed_leader_without_anchor_in_session_still_kills_session() {
|
|
|
1537
1698
|
&ws,
|
|
1538
1699
|
&json!({
|
|
1539
1700
|
"session_name": "team-orphan",
|
|
1701
|
+
"team_key": "team-orphan",
|
|
1702
|
+
"generation": "team-orphan",
|
|
1540
1703
|
"is_external_leader": false,
|
|
1541
1704
|
"agents": {
|
|
1542
1705
|
"w1": {"status": "running", "provider": "codex", "window": "w1", "pane_id": "%w1"}
|
|
@@ -1558,7 +1721,8 @@ fn e49_managed_leader_without_anchor_in_session_still_kills_session() {
|
|
|
1558
1721
|
pane_pid: None,
|
|
1559
1722
|
leader_env: BTreeMap::new(),
|
|
1560
1723
|
}])
|
|
1561
|
-
.with_targets_persist_after_kill()
|
|
1724
|
+
.with_targets_persist_after_kill()
|
|
1725
|
+
.with_owned_session(&ws, "team-orphan", "team-orphan", "team-orphan");
|
|
1562
1726
|
|
|
1563
1727
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
1564
1728
|
.expect("shutdown should complete");
|
|
@@ -1580,6 +1744,8 @@ fn shutdown_missing_topology_marker_defaults_to_managed_cleanup() {
|
|
|
1580
1744
|
&ws,
|
|
1581
1745
|
&json!({
|
|
1582
1746
|
"session_name": "team-current",
|
|
1747
|
+
"team_key": "team-current",
|
|
1748
|
+
"generation": "team-current",
|
|
1583
1749
|
"agents": {}
|
|
1584
1750
|
}),
|
|
1585
1751
|
)
|
|
@@ -1598,7 +1764,8 @@ fn shutdown_missing_topology_marker_defaults_to_managed_cleanup() {
|
|
|
1598
1764
|
pane_pid: None,
|
|
1599
1765
|
leader_env: BTreeMap::new(),
|
|
1600
1766
|
}])
|
|
1601
|
-
.with_targets_persist_after_kill()
|
|
1767
|
+
.with_targets_persist_after_kill()
|
|
1768
|
+
.with_owned_session(&ws, "team-current", "team-current", "team-current");
|
|
1602
1769
|
|
|
1603
1770
|
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
1604
1771
|
.expect("shutdown should complete");
|
|
@@ -35,43 +35,27 @@ pub(crate) fn render(command: &str, report: &Value) -> String {
|
|
|
35
35
|
// generic emitter redacts again for defense in depth.
|
|
36
36
|
let report = crate::redaction::redact_external_value(report);
|
|
37
37
|
let ok = report.get("ok").and_then(Value::as_bool).unwrap_or(false);
|
|
38
|
-
let
|
|
38
|
+
let runtime = report
|
|
39
|
+
.get("runtime")
|
|
40
|
+
.and_then(|runtime| runtime.get("status"))
|
|
41
|
+
.and_then(Value::as_str);
|
|
42
|
+
let issues = array_items(report.get("issues"));
|
|
43
|
+
let repairs = array_items(report.get("suggested_repairs"));
|
|
44
|
+
let mut summary = format!(
|
|
39
45
|
"{command}: {}",
|
|
40
46
|
if ok { "ok" } else { "needs attention" }
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for finding in array_items(
|
|
45
|
-
report
|
|
46
|
-
.get("secret_scan")
|
|
47
|
-
.and_then(|scan| scan.get("findings")),
|
|
48
|
-
) {
|
|
49
|
-
let Some(object) = finding.as_object() else {
|
|
50
|
-
continue;
|
|
51
|
-
};
|
|
52
|
-
let (Some(rule), Some(path), Some(line)) = (
|
|
53
|
-
object.get("rule").and_then(Value::as_str),
|
|
54
|
-
object.get("path").and_then(Value::as_str),
|
|
55
|
-
object.get("line").and_then(Value::as_u64),
|
|
56
|
-
) else {
|
|
57
|
-
continue;
|
|
58
|
-
};
|
|
59
|
-
lines.push(bounded(format!("warn: {rule} in {path}:{line}")));
|
|
60
|
-
}
|
|
47
|
+
);
|
|
48
|
+
if let Some(runtime) = runtime {
|
|
49
|
+
summary.push_str(&format!("; runtime={runtime}"));
|
|
61
50
|
}
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
summary.push_str(&format!("; issues={}; repairs={}", issues.len(), repairs.len()));
|
|
52
|
+
let mut lines = vec![bounded(summary)];
|
|
53
|
+
for issue in issues {
|
|
64
54
|
lines.push(bounded(format!("issue: {}", summarize(issue, false))));
|
|
65
55
|
}
|
|
66
|
-
for repair in
|
|
56
|
+
for repair in repairs {
|
|
67
57
|
lines.push(bounded(format!("repair: {}", summarize(repair, true))));
|
|
68
58
|
}
|
|
69
|
-
if let Some(error) = report.get("error").filter(|value| !value.is_null()) {
|
|
70
|
-
lines.push(bounded(format!("detail: {}", summarize(error, false))));
|
|
71
|
-
}
|
|
72
|
-
if lines.len() == 1 {
|
|
73
|
-
lines.push(bounded("detail: no issues detected".to_string()));
|
|
74
|
-
}
|
|
75
59
|
lines.join("\n")
|
|
76
60
|
}
|
|
77
61
|
|
|
@@ -95,6 +79,15 @@ fn summarize(value: &Value, repair: bool) -> String {
|
|
|
95
79
|
.as_slice()
|
|
96
80
|
};
|
|
97
81
|
let mut parts = Vec::new();
|
|
82
|
+
if let Some(path) = object.get("path").and_then(Value::as_str) {
|
|
83
|
+
let location = match object.get("line").and_then(Value::as_u64) {
|
|
84
|
+
Some(line) => format!("{path}:{line}"),
|
|
85
|
+
None => path.to_string(),
|
|
86
|
+
};
|
|
87
|
+
// Keep the target ahead of prose so bounded human lines retain
|
|
88
|
+
// the distinguishing location rather than only the issue id.
|
|
89
|
+
parts.push(format!("at={location}"));
|
|
90
|
+
}
|
|
98
91
|
for key in keys {
|
|
99
92
|
if let Some(value) = object.get(*key).and_then(Value::as_str) {
|
|
100
93
|
parts.push(format!("{key}={value}"));
|
|
@@ -166,6 +159,13 @@ mod tests {
|
|
|
166
159
|
fn renderer_projects_secret_findings_without_values() {
|
|
167
160
|
let report = json!({
|
|
168
161
|
"ok": false,
|
|
162
|
+
"issues": [{
|
|
163
|
+
"id": "secret_scan_finding",
|
|
164
|
+
"rule": "api_key_assignment",
|
|
165
|
+
"path": "leaky-role.md",
|
|
166
|
+
"line": 7,
|
|
167
|
+
}],
|
|
168
|
+
"suggested_repairs": [{"issue": "secret_scan_finding", "action": "remove secret finding at leaky-role.md:7"}],
|
|
169
169
|
"secret_scan": {
|
|
170
170
|
"findings": [{
|
|
171
171
|
"rule": "api_key_assignment",
|
|
@@ -176,7 +176,7 @@ mod tests {
|
|
|
176
176
|
}
|
|
177
177
|
});
|
|
178
178
|
let output = render("doctor", &report);
|
|
179
|
-
assert!(output.contains("
|
|
179
|
+
assert!(output.contains("issue: at=leaky-role.md:7; id=secret_scan_finding"));
|
|
180
180
|
assert!(!output.contains("match_excerpt"));
|
|
181
181
|
assert!(!output.contains("OPENAI_API_KEY=secret"));
|
|
182
182
|
}
|
|
@@ -697,7 +697,9 @@ pub struct WaitArgs {
|
|
|
697
697
|
pub json: bool,
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
-
/// `diagnose`
|
|
700
|
+
/// `diagnose` compatibility spelling for the canonical `doctor` health report.
|
|
701
|
+
/// The CLI normalizes it to `DoctorArgs`; this narrow type remains for direct
|
|
702
|
+
/// Rust callers that used the pre-unification API.
|
|
701
703
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
702
704
|
pub struct DiagnoseArgs {
|
|
703
705
|
pub workspace: PathBuf,
|