@team-agent/installer 0.5.110 → 0.5.112
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 +231 -812
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +493 -451
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -169
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +10 -5
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +53 -109
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +26 -121
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +0 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -16,8 +16,16 @@ use serde_json::json;
|
|
|
16
16
|
fn provider_args_cases() {
|
|
17
17
|
// (old case-id, input, Python golden); values[1:] of a lone "--" is empty.
|
|
18
18
|
const CASES: &[(&str, &[&str], &[&str])] = &[
|
|
19
|
-
(
|
|
20
|
-
|
|
19
|
+
(
|
|
20
|
+
"provider_args_strips_leading_dashdash",
|
|
21
|
+
&["--", "-x"],
|
|
22
|
+
&["-x"],
|
|
23
|
+
),
|
|
24
|
+
(
|
|
25
|
+
"provider_args_keeps_when_no_leading_dashdash",
|
|
26
|
+
&["-x", "-y"],
|
|
27
|
+
&["-x", "-y"],
|
|
28
|
+
),
|
|
21
29
|
("provider_args_empty_is_empty", &[], &[]),
|
|
22
30
|
("provider_args_lone_dashdash_yields_empty", &["--"], &[]),
|
|
23
31
|
];
|
|
@@ -36,41 +44,73 @@ fn provider_args_cases() {
|
|
|
36
44
|
fn leader_launcher_args_success_cases() {
|
|
37
45
|
// (old case-id, argv, complete expected structure), including default false flags.
|
|
38
46
|
let cases: &[(&str, &[&str], LeaderLauncherArgs)] = &[
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
(
|
|
48
|
+
"leader_launcher_args_empty_all_default",
|
|
49
|
+
&[],
|
|
50
|
+
LeaderLauncherArgs::default(),
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
"leader_launcher_args_attach_and_confirm",
|
|
54
|
+
&["--attach", "--confirm"],
|
|
55
|
+
LeaderLauncherArgs {
|
|
56
|
+
attach_existing: true,
|
|
57
|
+
confirm_attach: true,
|
|
58
|
+
..Default::default()
|
|
59
|
+
},
|
|
60
|
+
),
|
|
45
61
|
// --attach-existing is an alias of --attach.
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
(
|
|
63
|
+
"leader_launcher_args_attach_existing_alias",
|
|
64
|
+
&["--attach-existing"],
|
|
65
|
+
LeaderLauncherArgs {
|
|
66
|
+
attach_existing: true,
|
|
67
|
+
..Default::default()
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
(
|
|
71
|
+
"leader_launcher_args_external_leader_opt_out",
|
|
72
|
+
&["--external-leader", "--", "--model", "opus"],
|
|
73
|
+
LeaderLauncherArgs {
|
|
74
|
+
provider_args: vec!["--model".to_string(), "opus".to_string()],
|
|
75
|
+
external_leader: true,
|
|
76
|
+
..Default::default()
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"leader_launcher_args_attach_session_spaced",
|
|
81
|
+
&["--attach-session", "mysess"],
|
|
82
|
+
LeaderLauncherArgs {
|
|
83
|
+
attach_session: Some("mysess".to_string()),
|
|
84
|
+
..Default::default()
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"leader_launcher_args_attach_session_equals",
|
|
89
|
+
&["--attach-session=mysess"],
|
|
90
|
+
LeaderLauncherArgs {
|
|
91
|
+
attach_session: Some("mysess".to_string()),
|
|
92
|
+
..Default::default()
|
|
93
|
+
},
|
|
94
|
+
),
|
|
63
95
|
// Known Team Agent launcher flags after `--` are rejected by the error table.
|
|
64
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
(
|
|
97
|
+
"leader_launcher_args_dashdash_passthrough_strips_separator",
|
|
98
|
+
&["--attach", "--", "-x", "--provider-confirm"],
|
|
99
|
+
LeaderLauncherArgs {
|
|
100
|
+
provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
|
|
101
|
+
attach_existing: true,
|
|
102
|
+
..Default::default()
|
|
103
|
+
},
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
"leader_launcher_args_unknown_tokens_collect_as_provider_args",
|
|
107
|
+
&["foo", "--attach", "bar"],
|
|
108
|
+
LeaderLauncherArgs {
|
|
109
|
+
provider_args: vec!["foo".to_string(), "bar".to_string()],
|
|
110
|
+
attach_existing: true,
|
|
111
|
+
..Default::default()
|
|
112
|
+
},
|
|
113
|
+
),
|
|
74
114
|
];
|
|
75
115
|
for (id, input, expected) in cases {
|
|
76
116
|
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
@@ -97,7 +137,10 @@ fn leader_launcher_args_error_cases() {
|
|
|
97
137
|
for (id, input, expected) in CASES {
|
|
98
138
|
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
99
139
|
let err = leader_launcher_args(&argv).expect_err(id);
|
|
100
|
-
assert!(
|
|
140
|
+
assert!(
|
|
141
|
+
err.to_string().contains(*expected),
|
|
142
|
+
"{id}: unexpected error: {err}"
|
|
143
|
+
);
|
|
101
144
|
}
|
|
102
145
|
}
|
|
103
146
|
|
|
@@ -110,14 +153,31 @@ fn send_target_cases() {
|
|
|
110
153
|
// (old case-id, targets, target, complete expected enum).
|
|
111
154
|
let cases: &[(&str, Option<&str>, Option<&str>, MessageTarget)] = &[
|
|
112
155
|
(
|
|
113
|
-
"send_target_fanout_strips_and_filters_empty",
|
|
156
|
+
"send_target_fanout_strips_and_filters_empty",
|
|
157
|
+
Some("a, b ,,c"),
|
|
158
|
+
None,
|
|
114
159
|
MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
|
|
115
160
|
),
|
|
116
|
-
(
|
|
161
|
+
(
|
|
162
|
+
"send_target_single_target",
|
|
163
|
+
None,
|
|
164
|
+
Some("agent_x"),
|
|
165
|
+
MessageTarget::Single("agent_x".to_string()),
|
|
166
|
+
),
|
|
117
167
|
// A bare "*" means broadcast to the whole team.
|
|
118
|
-
(
|
|
168
|
+
(
|
|
169
|
+
"send_target_broadcast_star",
|
|
170
|
+
None,
|
|
171
|
+
Some("*"),
|
|
172
|
+
MessageTarget::Broadcast,
|
|
173
|
+
),
|
|
119
174
|
// targets="" is falsy in Python, so it falls through to args.target.
|
|
120
|
-
(
|
|
175
|
+
(
|
|
176
|
+
"send_target_empty_targets_falls_through_to_target",
|
|
177
|
+
Some(""),
|
|
178
|
+
Some("fallback"),
|
|
179
|
+
MessageTarget::Single("fallback".to_string()),
|
|
180
|
+
),
|
|
121
181
|
];
|
|
122
182
|
for (id, targets, target, expected) in cases {
|
|
123
183
|
assert_eq!(send_target(*targets, *target), *expected, "{id}");
|
|
@@ -133,18 +193,52 @@ fn send_target_cases() {
|
|
|
133
193
|
fn classify_agent_bucket_cases() {
|
|
134
194
|
// (old case-id, all original (raw, health) inputs, expected bucket).
|
|
135
195
|
const CASES: &[(&str, &[(&str, &str)], SummaryBucket)] = &[
|
|
136
|
-
(
|
|
137
|
-
|
|
138
|
-
|
|
196
|
+
(
|
|
197
|
+
"classify_failed_takes_priority",
|
|
198
|
+
&[("failed", ""), ("error", ""), ("running", "error")],
|
|
199
|
+
SummaryBucket::Failed,
|
|
200
|
+
),
|
|
201
|
+
(
|
|
202
|
+
"classify_stopped",
|
|
203
|
+
&[("stopped", ""), ("done", ""), ("running", "done")],
|
|
204
|
+
SummaryBucket::Stopped,
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
"classify_busy",
|
|
208
|
+
&[("busy", ""), ("", "running"), ("", "working")],
|
|
209
|
+
SummaryBucket::Busy,
|
|
210
|
+
),
|
|
139
211
|
// hstatus==idle precedes the raw==running branch.
|
|
140
|
-
(
|
|
141
|
-
|
|
212
|
+
(
|
|
213
|
+
"classify_hstatus_idle_beats_raw_running",
|
|
214
|
+
&[("running", "idle")],
|
|
215
|
+
SummaryBucket::Idle,
|
|
216
|
+
),
|
|
217
|
+
(
|
|
218
|
+
"classify_pure_running",
|
|
219
|
+
&[("running", "")],
|
|
220
|
+
SummaryBucket::Running,
|
|
221
|
+
),
|
|
142
222
|
// bug-071/077/085: unmatched and blocked/stuck/missing are Unknown, NEVER idle.
|
|
143
|
-
(
|
|
223
|
+
(
|
|
224
|
+
"classify_blocked_and_unmatched_are_unknown_never_idle",
|
|
225
|
+
&[
|
|
226
|
+
("blocked", ""),
|
|
227
|
+
("stuck", ""),
|
|
228
|
+
("", "missing"),
|
|
229
|
+
("weird_value", ""),
|
|
230
|
+
("", ""),
|
|
231
|
+
],
|
|
232
|
+
SummaryBucket::Unknown,
|
|
233
|
+
),
|
|
144
234
|
];
|
|
145
235
|
for (id, inputs, expected) in CASES {
|
|
146
236
|
for &(raw, health) in inputs.iter() {
|
|
147
|
-
assert_eq!(
|
|
237
|
+
assert_eq!(
|
|
238
|
+
classify_agent_bucket(raw, health),
|
|
239
|
+
*expected,
|
|
240
|
+
"{id}: raw={raw:?}, health={health:?}"
|
|
241
|
+
);
|
|
148
242
|
}
|
|
149
243
|
}
|
|
150
244
|
}
|
|
@@ -395,7 +489,7 @@ fn cli_error_payload_plain_runtime() {
|
|
|
395
489
|
assert_eq!(payload.error, "some other error");
|
|
396
490
|
assert_eq!(
|
|
397
491
|
payload.action,
|
|
398
|
-
"
|
|
492
|
+
"先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
|
|
399
493
|
);
|
|
400
494
|
assert_eq!(payload.log, "/tmp/y.log");
|
|
401
495
|
assert_eq!(payload.reason, None);
|
|
@@ -417,19 +511,14 @@ fn cli_error_payload_tmux_conflict_quick_start_enrichment() {
|
|
|
417
511
|
// context reset is only through restart --allow-fresh with explicit consent.
|
|
418
512
|
assert_eq!(
|
|
419
513
|
payload.action,
|
|
420
|
-
"
|
|
421
|
-
To resume it use `team-agent restart`. \
|
|
422
|
-
If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. \
|
|
423
|
-
Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. \
|
|
424
|
-
Never terminate existing tmux sessions from quick-start."
|
|
425
|
-
);
|
|
426
|
-
assert_eq!(
|
|
427
|
-
payload.next_actions,
|
|
428
|
-
Some(vec![
|
|
429
|
-
"If this is your existing team, resume it with `team-agent restart`.".to_string(),
|
|
430
|
-
"If you want a separate team, change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string(),
|
|
431
|
-
])
|
|
514
|
+
"终端会话 `my-team` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
|
|
432
515
|
);
|
|
516
|
+
let next = payload.next_actions.as_ref().unwrap();
|
|
517
|
+
assert_eq!(next.len(), 2);
|
|
518
|
+
assert!(next[0].contains("team-agent restart"));
|
|
519
|
+
assert!(
|
|
520
|
+
next[1].contains("TEAM.md") && next[1].contains("name") && next[1].contains("quick-start")
|
|
521
|
+
);
|
|
433
522
|
}
|
|
434
523
|
|
|
435
524
|
#[test]
|
|
@@ -440,16 +529,11 @@ fn cli_error_payload_tmux_conflict_non_quick_start_enrichment() {
|
|
|
440
529
|
assert_eq!(payload.session_name.as_deref(), Some("my-team"));
|
|
441
530
|
assert_eq!(
|
|
442
531
|
payload.action,
|
|
443
|
-
"
|
|
444
|
-
Do not terminate existing tmux sessions from startup; \
|
|
445
|
-
use a different team name or runtime.session_name and start again."
|
|
446
|
-
);
|
|
447
|
-
assert_eq!(
|
|
448
|
-
payload.next_actions,
|
|
449
|
-
Some(vec![
|
|
450
|
-
"Use a different team name or runtime.session_name before starting again.".to_string()
|
|
451
|
-
])
|
|
532
|
+
"终端会话 `my-team` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
|
|
452
533
|
);
|
|
534
|
+
let next = payload.next_actions.as_ref().unwrap();
|
|
535
|
+
assert_eq!(next.len(), 1);
|
|
536
|
+
assert!(next[0].contains("队伍") && next[0].contains("名称"));
|
|
453
537
|
}
|
|
454
538
|
|
|
455
539
|
#[test]
|
|
@@ -641,19 +725,42 @@ fn cmd_status_summary_with_agent_rejected() {
|
|
|
641
725
|
// =========================================================================
|
|
642
726
|
|
|
643
727
|
#[test]
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
let
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
let
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
728
|
+
#[serial_test::serial(env)]
|
|
729
|
+
fn cmd_leader_passthrough_help_is_pure_human_guidance() {
|
|
730
|
+
let env = crate::cli::hermetic_test_support::HermeticTestEnv::enter("launcher-help-unit");
|
|
731
|
+
let cwd = env.workspace("empty");
|
|
732
|
+
let tools = [
|
|
733
|
+
"pi", "codex", "claude", "copilot", "grok", "agent", "cursor",
|
|
734
|
+
];
|
|
735
|
+
let bin = env.root().join("canaries");
|
|
736
|
+
std::fs::create_dir(&bin).unwrap();
|
|
737
|
+
#[cfg(unix)]
|
|
738
|
+
let _path = {
|
|
739
|
+
use std::os::unix::fs::PermissionsExt;
|
|
740
|
+
for tool in tools {
|
|
741
|
+
let path = bin.join(tool);
|
|
742
|
+
std::fs::write(&path, "#!/bin/sh\nprintf called > \"$0.called\"\nexit 0\n").unwrap();
|
|
743
|
+
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
744
|
+
}
|
|
745
|
+
env.with_env("PATH", &bin.to_string_lossy())
|
|
746
|
+
};
|
|
747
|
+
for provider in ["pi", "codex", "claude", "copilot", "grok", "cursor"] {
|
|
748
|
+
for alias in ["-h", "--help"] {
|
|
749
|
+
let result = cmd_leader_passthrough(provider, &[alias.into()], &cwd).unwrap();
|
|
750
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
751
|
+
assert!(
|
|
752
|
+
matches!(&result.output, CmdOutput::Human(text) if text.contains(provider) && text.contains("用法") && text.contains("Examples") && text.contains("下一步"))
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
|
|
757
|
+
assert!(env.registry_entries().is_empty());
|
|
758
|
+
for tool in tools {
|
|
759
|
+
assert!(
|
|
760
|
+
!bin.join(format!("{tool}.called")).exists(),
|
|
761
|
+
"native tool invoked"
|
|
762
|
+
);
|
|
763
|
+
}
|
|
657
764
|
}
|
|
658
765
|
|
|
659
766
|
#[test]
|
|
@@ -257,8 +257,7 @@ fn inbox_excludes_leader_stage_result_notifications() {
|
|
|
257
257
|
// the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
|
|
258
258
|
#[test]
|
|
259
259
|
fn comms_selftest_golden_boundary_scope_and_check_shapes() {
|
|
260
|
-
let boundary = "
|
|
261
|
-
Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
260
|
+
let boundary = "这里只检查终端连接是否一致,不实际发送消息,也不证明队友已回复。";
|
|
262
261
|
let ws = tmp_workspace();
|
|
263
262
|
let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
|
|
264
263
|
let obj = v.as_object().expect("comms dict");
|
|
@@ -100,11 +100,10 @@ fn cli_shutdown_kills_team_session_real_teardown() {
|
|
|
100
100
|
|
|
101
101
|
// =========================================================================
|
|
102
102
|
// WAVE-2 Lane B — CLI leader handler delegation byte-parity (leader_port::*).
|
|
103
|
-
// The
|
|
103
|
+
// The retained CLI verbs are thin pass-throughs (cli/commands.py:152-157):
|
|
104
104
|
// cmd_takeover -> runtime.takeover(ws, team, confirm)
|
|
105
105
|
// cmd_claim_leader -> runtime.claim_leader(ws, team, confirm) (Family A)
|
|
106
|
-
//
|
|
107
|
-
// leader_port::{takeover,claim_leader,leader_identity} are STUBS returning
|
|
106
|
+
// leader_port::{takeover,claim_leader} are STUBS returning
|
|
108
107
|
// the WRONG shape today -> these LOCK the golden dict so the porter wires
|
|
109
108
|
// them into leader::* / runtime.* and matches byte-for-byte.
|
|
110
109
|
// Golden re-probed @ team-agent-public (probe_claim.py / probe_rtclaim.py /
|
|
@@ -297,89 +296,6 @@ fn leader_port_claim_leader_no_pane_refuses_caller_pane_missing_family_a() {
|
|
|
297
296
|
);
|
|
298
297
|
}
|
|
299
298
|
|
|
300
|
-
// RED — leader_identity(): CLI directly emits leader.leader_identity's 9-key
|
|
301
|
-
// dict (runtime.leader_identity is imported from leader). golden probe_lid.py
|
|
302
|
-
// keys: ok, uuid_prefix, machine_fingerprint, workspace_abspath, os_user,
|
|
303
|
-
// team_id, current_pane_id, last_seen_at, source. Current stub returns
|
|
304
|
-
// {ok:true, team:...} (wrong shape) -> RED.
|
|
305
|
-
#[test]
|
|
306
|
-
fn leader_port_leader_identity_emits_nine_key_dict() {
|
|
307
|
-
let ws = leader_port_ws("identity");
|
|
308
|
-
std::fs::create_dir_all(crate::model::paths::runtime_dir(&ws)).unwrap();
|
|
309
|
-
let v = super::leader_port::leader_identity(&ws, None).unwrap();
|
|
310
|
-
assert_eq!(v["ok"], json!(true));
|
|
311
|
-
let obj = v.as_object().expect("identity → JSON object");
|
|
312
|
-
for key in [
|
|
313
|
-
"ok",
|
|
314
|
-
"uuid_prefix",
|
|
315
|
-
"machine_fingerprint",
|
|
316
|
-
"workspace_abspath",
|
|
317
|
-
"os_user",
|
|
318
|
-
"team_id",
|
|
319
|
-
"current_pane_id",
|
|
320
|
-
"last_seen_at",
|
|
321
|
-
"source",
|
|
322
|
-
] {
|
|
323
|
-
assert!(
|
|
324
|
-
obj.contains_key(key),
|
|
325
|
-
"golden identity dict must carry '{key}', got {obj:?}"
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
// no override/state uuid → source is the leader-plan "derived" string.
|
|
329
|
-
assert_eq!(v["source"], json!("derived"));
|
|
330
|
-
// uuid_prefix is exactly 12 hex chars (derive[:12]).
|
|
331
|
-
let prefix = v["uuid_prefix"].as_str().expect("uuid_prefix str");
|
|
332
|
-
assert_eq!(prefix.len(), 12, "uuid_prefix == derived[:12]");
|
|
333
|
-
assert!(prefix.chars().all(|c| c.is_ascii_hexdigit()));
|
|
334
|
-
// no team registered + no TMUX_PANE/receiver → these are JSON null.
|
|
335
|
-
if std::env::var_os("TMUX_PANE").is_none() {
|
|
336
|
-
assert_eq!(v["current_pane_id"], serde_json::Value::Null);
|
|
337
|
-
}
|
|
338
|
-
assert_eq!(v["last_seen_at"], serde_json::Value::Null);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ── cmd_watch (cli/adapters.rs:58) [RED] — today a CmdResult::none() no-op ───────────────────────
|
|
342
|
-
// Golden cmd_watch (cli/commands.py:103-109) DELEGATES to run_watch(workspace.resolve(), team) and
|
|
343
|
-
// exits 0. Golden run_watch (watch/__init__.py:25-37) is a `while True` LIVE TAIL that streams
|
|
344
|
-
// render_event_line output (collect_watch_lines) for the watched team. The Rust cmd_watch returns
|
|
345
|
-
// CmdResult::none() — a no-op that never touches the watch subsystem. The watch LINE SHAPE is itself
|
|
346
|
-
// byte-locked by coordinator::render_event_line (coordinator/tests.rs GROUP H); the cli contract here
|
|
347
|
-
// is that cmd_watch must DELEGATE and surface those rendered lines. Seeded a result_received event ->
|
|
348
|
-
// golden render = "result_received: <agent> -> <summary>" (render_event_line: agent_id + summary[:80]).
|
|
349
|
-
//
|
|
350
|
-
// RED confirmed TODAY: cmd_watch=none() returns immediately (no watch line) -> the assertion fails
|
|
351
|
-
// (and does NOT hang). PORTER: wire cmd_watch -> coordinator::run_watch(resolved workspace, team,
|
|
352
|
-
// interval, sink). Since golden's tail is `while True`, the cli port needs a TERMINATING / bounded
|
|
353
|
-
// entry to be both byte-parity AND unit-testable (collect the current watch lines into the CmdResult,
|
|
354
|
-
// or stream to stdout with a bounded test seam) — a blocking unit test is unacceptable.
|
|
355
|
-
#[test]
|
|
356
|
-
fn cmd_watch_delegates_and_surfaces_rendered_watch_lines_not_noop() {
|
|
357
|
-
let ws = tmp_workspace();
|
|
358
|
-
let logs = crate::model::paths::logs_dir(&ws);
|
|
359
|
-
std::fs::create_dir_all(&logs).unwrap();
|
|
360
|
-
std::fs::write(
|
|
361
|
-
logs.join("events.jsonl"),
|
|
362
|
-
"{\"event\":\"result_received\",\"agent_id\":\"alpha\",\"summary\":\"did the thing\"}\n",
|
|
363
|
-
)
|
|
364
|
-
.unwrap();
|
|
365
|
-
let r = cmd_watch(&WatchArgs {
|
|
366
|
-
workspace: ws.clone(),
|
|
367
|
-
team: None,
|
|
368
|
-
})
|
|
369
|
-
.expect("cmd_watch returns a CmdResult");
|
|
370
|
-
let text = match &r.output {
|
|
371
|
-
CmdOutput::Human(s) => s.clone(),
|
|
372
|
-
CmdOutput::Json(v) => v.to_string(),
|
|
373
|
-
CmdOutput::None => String::new(),
|
|
374
|
-
};
|
|
375
|
-
assert!(
|
|
376
|
-
text.contains("result_received: alpha -> did the thing"),
|
|
377
|
-
"cmd_watch must DELEGATE to the watch subsystem (run_watch -> render_event_line) and surface the \
|
|
378
|
-
rendered watch line; today it is a CmdResult::none() no-op. got output={:?}",
|
|
379
|
-
r.output
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
299
|
// D7 [WARN] — WAVE-2 Lane B: empty caller binds refuse as caller_pane_missing.
|
|
384
300
|
#[test]
|
|
385
301
|
#[serial_test::serial(env)]
|
|
@@ -175,58 +175,7 @@ fn inbox_matches_sender_or_recipient_and_excludes_others() {
|
|
|
175
175
|
);
|
|
176
176
|
let _ = std::fs::remove_dir_all(&ws);
|
|
177
177
|
}
|
|
178
|
-
|
|
179
|
-
// NOT a stored `pane_id` field. A live worker present in state (with a `window`, on the team's `-L`
|
|
180
|
-
// socket) must NOT be fabricated as "agent pane not found". ──────────────────────────────────────
|
|
181
|
-
// Golden status/peek.py:35-44: agent = state["agents"][id]; window = agent.get("window", id);
|
|
182
|
-
// if not session_name or not _tmux_window_exists(session_name, window): raise "agent terminal is
|
|
183
|
-
// not available: <id>"; else `tmux capture-pane -t session:window`. It NEVER reads a stored pane_id.
|
|
184
|
-
// Probed live (/tmp/probe_peek.py): a present worker whose window is absent on the socket raises
|
|
185
|
-
// `agent terminal is not available: w1`; a missing agent raises `unknown agent id: <id>`. Rust
|
|
186
|
-
// cmd_peek (adapters.rs:231 + agent_pane_id:279) keys off agent_state pane_id/pane/tmux_pane_id and
|
|
187
|
-
// returns {ok:false,error:"agent pane not found"} when absent — so a NORMAL live worker (window in
|
|
188
|
-
// state, no pane_id field) is mis-reported as not found. That is the CP-1 pane-resolution divergence.
|
|
189
|
-
//
|
|
190
|
-
// Deterministic without real tmux: on a host with no live session, the golden-correct peek resolves
|
|
191
|
-
// session:window, finds the window absent on the socket, and yields "agent terminal is not available:
|
|
192
|
-
// w1" — NOT "agent pane not found". (The window-on-socket -> real raw-screen capture positive case is
|
|
193
|
-
// real-machine; see the #[ignore] dispatch_routes_peek_real_machine.)
|
|
194
|
-
#[test]
|
|
195
|
-
fn peek_resolves_live_worker_via_session_window_not_pane_id_field() {
|
|
196
|
-
let ws = tmp_workspace();
|
|
197
|
-
// a live worker: present in state with a `window`, session_name set, but NO stored pane_id field.
|
|
198
|
-
// session name is unique so a real tmux session on the dev host can't accidentally satisfy it.
|
|
199
|
-
crate::state::persist::save_runtime_state(
|
|
200
|
-
&ws,
|
|
201
|
-
&json!({
|
|
202
|
-
"session_name": "team-peek-red-probe-x9q",
|
|
203
|
-
"agents": {"w1": {"status": "running", "provider": "codex", "window": "w1"}}
|
|
204
|
-
}),
|
|
205
|
-
)
|
|
206
|
-
.unwrap();
|
|
207
|
-
let args = PeekArgs {
|
|
208
|
-
agent: "w1".to_string(),
|
|
209
|
-
workspace: ws.clone(),
|
|
210
|
-
tail: 20,
|
|
211
|
-
head: None,
|
|
212
|
-
search: None,
|
|
213
|
-
allow_raw_screen: true,
|
|
214
|
-
json: true,
|
|
215
|
-
};
|
|
216
|
-
let text = outcome_text(cmd_peek(&args));
|
|
217
|
-
assert!(
|
|
218
|
-
!text.contains("agent pane not found"),
|
|
219
|
-
"peek keys off a stored pane_id field and fabricates 'agent pane not found' for a live worker \
|
|
220
|
-
that has a `window` in state; golden resolves session:window and never reads pane_id. got: {text}"
|
|
221
|
-
);
|
|
222
|
-
assert!(
|
|
223
|
-
text.contains("agent terminal is not available: w1"),
|
|
224
|
-
"golden status/peek.py: a worker whose window is not on the socket yields \
|
|
225
|
-
'agent terminal is not available: w1' (window-existence via session:window), NOT a \
|
|
226
|
-
pane_id-keyed error. got: {text}"
|
|
227
|
-
);
|
|
228
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
229
|
-
}
|
|
178
|
+
|
|
230
179
|
#[test]
|
|
231
180
|
fn ux_doctor_secret_scan_is_present_and_non_triggering_for_normal_paths() {
|
|
232
181
|
let ws = tmp_workspace();
|
|
@@ -295,44 +244,16 @@ fn ux_doctor_secret_scan_findings_name_the_exact_trigger() {
|
|
|
295
244
|
let _ = std::fs::remove_dir_all(&ws);
|
|
296
245
|
}
|
|
297
246
|
#[test]
|
|
298
|
-
fn
|
|
299
|
-
let
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
"tasks": [{"id": "t1", "assignee": "w1", "status": "pending"}],
|
|
306
|
-
"leader_receiver": {"status": "attached"},
|
|
307
|
-
}),
|
|
308
|
-
)
|
|
309
|
-
.unwrap();
|
|
310
|
-
let value = json_output(
|
|
311
|
-
cmd_wait_ready(&WaitReadyArgs {
|
|
312
|
-
workspace: ws.clone(),
|
|
313
|
-
timeout: 0.0,
|
|
314
|
-
json: true,
|
|
315
|
-
team: None,
|
|
316
|
-
})
|
|
317
|
-
.expect("wait-ready"),
|
|
318
|
-
);
|
|
319
|
-
assert_eq!(
|
|
320
|
-
value["ok"],
|
|
321
|
-
json!(false),
|
|
322
|
-
"wait-ready must not fake success before workers are ready"
|
|
323
|
-
);
|
|
324
|
-
assert_eq!(value.pointer("/readiness/ready"), Some(&json!(false)));
|
|
325
|
-
assert!(
|
|
326
|
-
value["summary"]
|
|
327
|
-
.as_str()
|
|
328
|
-
.unwrap_or("")
|
|
329
|
-
.contains("not ready"),
|
|
330
|
-
"wait-ready false state should explain not-ready status, got {value:?}"
|
|
331
|
-
);
|
|
332
|
-
let _ = std::fs::remove_dir_all(&ws);
|
|
247
|
+
fn runtime_readiness_rejects_starting_workers() {
|
|
248
|
+
let value = crate::cli::diagnose::wait_readiness(&json!({
|
|
249
|
+
"agents": {"w1": {"status": "starting"}},
|
|
250
|
+
"leader_receiver": {"status": "attached"},
|
|
251
|
+
}));
|
|
252
|
+
assert_eq!(value["ready"], json!(false));
|
|
253
|
+
assert_eq!(value["all_spawned"], json!(false));
|
|
333
254
|
}
|
|
334
255
|
#[test]
|
|
335
|
-
fn
|
|
256
|
+
fn runtime_readiness_counts_mcp_config_and_task_prompt_delivery() {
|
|
336
257
|
let ws = tmp_workspace();
|
|
337
258
|
let mcp_config = ws
|
|
338
259
|
.join(".team")
|
|
@@ -342,10 +263,9 @@ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
|
|
|
342
263
|
.join("mcp_config.json");
|
|
343
264
|
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
344
265
|
std::fs::write(&mcp_config, r#"{"mcpServers":{"team-agent":{}}}"#).unwrap();
|
|
345
|
-
|
|
346
|
-
&ws,
|
|
347
|
-
&json!({
|
|
266
|
+
let state = json!({
|
|
348
267
|
"session_name": "team-fake-ready",
|
|
268
|
+
"messages": {"delivered": 1},
|
|
349
269
|
"agents": {
|
|
350
270
|
"fake_impl": {
|
|
351
271
|
"status": "running",
|
|
@@ -359,30 +279,9 @@ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
|
|
|
359
279
|
"status": "pending",
|
|
360
280
|
}],
|
|
361
281
|
"leader_receiver": {"status": "attached"},
|
|
362
|
-
})
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
366
|
-
store
|
|
367
|
-
.create_message(
|
|
368
|
-
Some("task_impl"),
|
|
369
|
-
"leader",
|
|
370
|
-
"fake_impl",
|
|
371
|
-
"initial task prompt",
|
|
372
|
-
None,
|
|
373
|
-
true,
|
|
374
|
-
None,
|
|
375
|
-
)
|
|
376
|
-
.unwrap();
|
|
377
|
-
let value = json_output(
|
|
378
|
-
cmd_wait_ready(&WaitReadyArgs {
|
|
379
|
-
workspace: ws.clone(),
|
|
380
|
-
timeout: 0.0,
|
|
381
|
-
json: true,
|
|
382
|
-
team: None,
|
|
383
|
-
})
|
|
384
|
-
.expect("wait-ready"),
|
|
385
|
-
);
|
|
282
|
+
});
|
|
283
|
+
let readiness = crate::cli::diagnose::wait_readiness(&state);
|
|
284
|
+
let value = json!({"readiness": readiness});
|
|
386
285
|
assert_eq!(
|
|
387
286
|
value.pointer("/readiness/mcp_ready"),
|
|
388
287
|
Some(&json!(true)),
|
|
@@ -400,31 +299,7 @@ fn wait_ready_fake_quick_start_counts_mcp_config_and_task_prompt_delivery() {
|
|
|
400
299
|
);
|
|
401
300
|
let _ = std::fs::remove_dir_all(&ws);
|
|
402
301
|
}
|
|
403
|
-
|
|
404
|
-
json!({
|
|
405
|
-
"schema_version": "result_envelope_v1",
|
|
406
|
-
"task_id": "task_impl",
|
|
407
|
-
"agent_id": "fake_impl",
|
|
408
|
-
"status": "success",
|
|
409
|
-
"summary": "done",
|
|
410
|
-
"artifacts": [],
|
|
411
|
-
"changes": [],
|
|
412
|
-
"tests": [{"command": "cargo test", "status": "passed"}],
|
|
413
|
-
"risks": [],
|
|
414
|
-
"next_actions": []
|
|
415
|
-
})
|
|
416
|
-
}
|
|
417
|
-
fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
|
|
418
|
-
let store = crate::message_store::MessageStore::open(ws).unwrap();
|
|
419
|
-
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
420
|
-
conn.execute(
|
|
421
|
-
"insert into results(
|
|
422
|
-
result_id, owner_team_id, task_id, agent_id, envelope, status, created_at
|
|
423
|
-
) values (?1, null, 'task_impl', 'fake_impl', ?2, 'success', '2026-06-02T10:00:00+00:00')",
|
|
424
|
-
rusqlite::params![result_id, valid_result_envelope().to_string()],
|
|
425
|
-
)
|
|
426
|
-
.unwrap();
|
|
427
|
-
}
|
|
302
|
+
|
|
428
303
|
fn json_output(result: CmdResult) -> serde_json::Value {
|
|
429
304
|
match result.output {
|
|
430
305
|
CmdOutput::Json(v) => v,
|