@team-agent/installer 0.5.106 → 0.5.108
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 +1 -1
- package/crates/team-agent/src/cli/emit.rs +3 -3
- package/crates/team-agent/src/cli/mod.rs +87 -4
- package/crates/team-agent/src/cli/models.rs +252 -3
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +155 -37
- package/crates/team-agent/src/cli/tests/base.rs +112 -225
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +184 -0
- package/crates/team-agent/src/cli/tests/status_brief.rs +624 -0
- package/crates/team-agent/src/compiler/tests.rs +46 -59
- package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
- package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +0 -9
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -66
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -392
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
- package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
- package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
- package/crates/team-agent/src/model/name_similarity.rs +52 -67
- package/crates/team-agent/src/model/task_graph.rs +37 -124
- package/crates/team-agent/src/model/yaml/tests.rs +46 -165
- package/crates/team-agent/src/provider/model_catalog.rs +166 -3
- package/crates/team-agent/src/provider/testdata/cursor-list-models-native.stdout +250 -0
- package/crates/team-agent/src/provider/testdata/grok-models-authenticated.stdout +9 -0
- package/crates/team-agent/src/provider/testdata/grok-models-unauthenticated.stdout +7 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
- package/crates/team-agent/src/provider/tests/classify.rs +106 -198
- package/crates/team-agent/src/tmux_backend/tests.rs +178 -501
- package/crates/team-agent/src/transport/tests/wire.rs +0 -55
- package/package.json +4 -4
|
@@ -117,6 +117,14 @@ struct BriefNode {
|
|
|
117
117
|
tmux_command: Option<String>,
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
121
|
+
struct AttachContext {
|
|
122
|
+
endpoint: String,
|
|
123
|
+
session: String,
|
|
124
|
+
window: String,
|
|
125
|
+
pane: String,
|
|
126
|
+
}
|
|
127
|
+
|
|
120
128
|
#[derive(Debug)]
|
|
121
129
|
struct ProbeResult {
|
|
122
130
|
nodes: Vec<ProbeNode>,
|
|
@@ -150,34 +158,45 @@ pub(crate) fn status_brief_scoped(
|
|
|
150
158
|
state: &Value,
|
|
151
159
|
agent: Option<&str>,
|
|
152
160
|
) -> Value {
|
|
153
|
-
let
|
|
154
|
-
let probes = probe_registered_endpoints(®istered);
|
|
155
|
-
let nodes = registered
|
|
156
|
-
.iter()
|
|
157
|
-
.map(|node| project_node(node, probes.get(node.endpoint.as_deref().unwrap_or(""))))
|
|
158
|
-
.collect::<Vec<_>>();
|
|
161
|
+
let nodes = project_status_nodes(state, agent);
|
|
159
162
|
json!({
|
|
160
|
-
"nodes": nodes
|
|
163
|
+
"nodes": nodes
|
|
164
|
+
.into_iter()
|
|
165
|
+
.map(|(node, _)| node.into_json())
|
|
166
|
+
.collect::<Vec<_>>()
|
|
161
167
|
})
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
/// Human output
|
|
165
|
-
///
|
|
166
|
-
/// diagnostic or an inferred value.
|
|
170
|
+
/// Human output keeps the legacy single-node form and groups shared attach
|
|
171
|
+
/// context only when multiple nodes make that repetition useful.
|
|
167
172
|
pub(crate) fn format_status_brief(
|
|
168
|
-
|
|
173
|
+
_workspace: &Path,
|
|
169
174
|
state: &Value,
|
|
170
175
|
agent: Option<&str>,
|
|
171
176
|
) -> String {
|
|
172
|
-
let
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
let nodes = project_status_nodes(state, agent);
|
|
178
|
+
if nodes.len() <= 1 {
|
|
179
|
+
return nodes
|
|
180
|
+
.into_iter()
|
|
181
|
+
.filter_map(|(node, _)| format_brief_node(&node.into_json()))
|
|
182
|
+
.collect::<Vec<_>>()
|
|
183
|
+
.join("\n");
|
|
184
|
+
}
|
|
185
|
+
format_multi_node_brief(nodes)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn project_status_nodes(
|
|
189
|
+
state: &Value,
|
|
190
|
+
agent: Option<&str>,
|
|
191
|
+
) -> Vec<(BriefNode, Option<AttachContext>)> {
|
|
192
|
+
let registered = registered_nodes(state, agent);
|
|
193
|
+
let probes = probe_registered_endpoints(®istered);
|
|
194
|
+
registered
|
|
195
|
+
.iter()
|
|
196
|
+
.map(|node| {
|
|
197
|
+
project_node_with_attach(node, probes.get(node.endpoint.as_deref().unwrap_or("")))
|
|
198
|
+
})
|
|
199
|
+
.collect()
|
|
181
200
|
}
|
|
182
201
|
|
|
183
202
|
pub(crate) fn registered_agent_exists(state: &Value, agent: &str) -> bool {
|
|
@@ -322,6 +341,86 @@ fn format_brief_node(value: &Value) -> Option<String> {
|
|
|
322
341
|
))
|
|
323
342
|
}
|
|
324
343
|
|
|
344
|
+
fn format_multi_node_brief(nodes: Vec<(BriefNode, Option<AttachContext>)>) -> String {
|
|
345
|
+
let mut groups = Vec::<(String, String)>::new();
|
|
346
|
+
for (_, context) in &nodes {
|
|
347
|
+
let Some(context) = context else { continue };
|
|
348
|
+
if !groups.iter().any(|(endpoint, session)| {
|
|
349
|
+
endpoint == &context.endpoint && session == &context.session
|
|
350
|
+
}) {
|
|
351
|
+
groups.push((context.endpoint.clone(), context.session.clone()));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let mut lines = groups
|
|
356
|
+
.iter()
|
|
357
|
+
.enumerate()
|
|
358
|
+
.map(|(index, (endpoint, session))| {
|
|
359
|
+
format_attach_template(index + 1, endpoint, session)
|
|
360
|
+
})
|
|
361
|
+
.collect::<Vec<_>>();
|
|
362
|
+
if !groups.is_empty() {
|
|
363
|
+
lines.push(
|
|
364
|
+
"TMUX templates: replace <target> with the window.pane part of the matching ATTACH value."
|
|
365
|
+
.to_string(),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
lines.push(
|
|
369
|
+
"NAME\tPROVIDER\tMODEL\tEFFORT\tRUNTIME_STATUS\tACTIVITY\tHEALTH\tSESSION_NAME\tATTACH"
|
|
370
|
+
.to_string(),
|
|
371
|
+
);
|
|
372
|
+
for (node, context) in nodes {
|
|
373
|
+
let attach = context
|
|
374
|
+
.as_ref()
|
|
375
|
+
.and_then(|context| {
|
|
376
|
+
groups
|
|
377
|
+
.iter()
|
|
378
|
+
.position(|(endpoint, session)| {
|
|
379
|
+
endpoint == &context.endpoint && session == &context.session
|
|
380
|
+
})
|
|
381
|
+
.map(|index| format!("{}:{}.{}", index + 1, context.window, context.pane))
|
|
382
|
+
})
|
|
383
|
+
.unwrap_or_else(|| "null".to_string());
|
|
384
|
+
lines.push(
|
|
385
|
+
[
|
|
386
|
+
escape_human_cell(&node.name),
|
|
387
|
+
escape_human_cell(&node.provider),
|
|
388
|
+
escape_human_cell(node.model.as_deref().unwrap_or("null")),
|
|
389
|
+
escape_human_cell(node.effort.as_deref().unwrap_or("null")),
|
|
390
|
+
escape_human_cell(&node.runtime_status),
|
|
391
|
+
escape_human_cell(&node.activity),
|
|
392
|
+
escape_human_cell(&node.health),
|
|
393
|
+
escape_human_cell(node.session_name.as_deref().unwrap_or("null")),
|
|
394
|
+
escape_human_cell(&attach),
|
|
395
|
+
]
|
|
396
|
+
.join("\t"),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
lines.join("\n")
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fn format_attach_template(group: usize, endpoint: &str, session: &str) -> String {
|
|
403
|
+
let flag = if Path::new(endpoint).is_absolute() {
|
|
404
|
+
"-S"
|
|
405
|
+
} else {
|
|
406
|
+
"-L"
|
|
407
|
+
};
|
|
408
|
+
let target = format!("{session}:<target>");
|
|
409
|
+
format!(
|
|
410
|
+
"TMUX[{group}] tmux {flag} {} attach -t {}",
|
|
411
|
+
shell_quote(endpoint),
|
|
412
|
+
shell_quote(&target)
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
fn escape_human_cell(value: &str) -> String {
|
|
417
|
+
value
|
|
418
|
+
.replace('\\', "\\\\")
|
|
419
|
+
.replace('\t', "\\t")
|
|
420
|
+
.replace('\n', "\\n")
|
|
421
|
+
.replace('\r', "\\r")
|
|
422
|
+
}
|
|
423
|
+
|
|
325
424
|
fn display_optional(value: &Value) -> String {
|
|
326
425
|
value
|
|
327
426
|
.as_str()
|
|
@@ -1051,14 +1150,22 @@ fn non_empty(value: &Value, key: &str) -> Option<String> {
|
|
|
1051
1150
|
.map(str::to_string)
|
|
1052
1151
|
}
|
|
1053
1152
|
|
|
1153
|
+
#[cfg(test)]
|
|
1054
1154
|
fn project_node(node: &RegisteredNode, probe: Option<&Option<ProbeResult>>) -> BriefNode {
|
|
1155
|
+
project_node_with_attach(node, probe).0
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
fn project_node_with_attach(
|
|
1159
|
+
node: &RegisteredNode,
|
|
1160
|
+
probe: Option<&Option<ProbeResult>>,
|
|
1161
|
+
) -> (BriefNode, Option<AttachContext>) {
|
|
1055
1162
|
let stopped = matches!(
|
|
1056
1163
|
node.lifecycle.as_deref(),
|
|
1057
1164
|
Some("stopped" | "done" | "failed" | "error" | "terminated")
|
|
1058
1165
|
);
|
|
1059
1166
|
let matched = match_result(node, probe);
|
|
1060
1167
|
if stopped {
|
|
1061
|
-
|
|
1168
|
+
let projected = match matched {
|
|
1062
1169
|
MatchResult::Missing => BriefNode {
|
|
1063
1170
|
name: node.name.clone(),
|
|
1064
1171
|
provider: node.provider.clone(),
|
|
@@ -1072,14 +1179,15 @@ fn project_node(node: &RegisteredNode, probe: Option<&Option<ProbeResult>>) -> B
|
|
|
1072
1179
|
},
|
|
1073
1180
|
MatchResult::Unique(_) | MatchResult::Ambiguous => BriefNode::unknown(node),
|
|
1074
1181
|
};
|
|
1182
|
+
return (projected, None);
|
|
1075
1183
|
}
|
|
1076
1184
|
let MatchResult::Unique(observed) = matched else {
|
|
1077
|
-
return BriefNode::unknown(node);
|
|
1185
|
+
return (BriefNode::unknown(node), None);
|
|
1078
1186
|
};
|
|
1079
1187
|
if !node.provider.eq_ignore_ascii_case("unknown")
|
|
1080
1188
|
&& !node.provider.eq_ignore_ascii_case(&observed.provider)
|
|
1081
1189
|
{
|
|
1082
|
-
return BriefNode::unknown(node);
|
|
1190
|
+
return (BriefNode::unknown(node), None);
|
|
1083
1191
|
}
|
|
1084
1192
|
let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
|
|
1085
1193
|
&& observed.evidence_method.as_deref() == Some("pi_activity_channel");
|
|
@@ -1097,21 +1205,31 @@ fn project_node(node: &RegisteredNode, probe: Option<&Option<ProbeResult>>) -> B
|
|
|
1097
1205
|
} else {
|
|
1098
1206
|
valid_health(&observed.health)
|
|
1099
1207
|
};
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1208
|
+
let tmux_command = tmux_command(node, observed);
|
|
1209
|
+
let attach = tmux_command.as_ref().map(|_| AttachContext {
|
|
1210
|
+
endpoint: observed.socket.clone(),
|
|
1211
|
+
session: observed.session.clone(),
|
|
1212
|
+
window: observed.window.clone(),
|
|
1213
|
+
pane: observed.pane.clone(),
|
|
1214
|
+
});
|
|
1215
|
+
(
|
|
1216
|
+
BriefNode {
|
|
1217
|
+
name: node.name.clone(),
|
|
1218
|
+
provider: if node.provider.eq_ignore_ascii_case("unknown") {
|
|
1219
|
+
observed.provider.clone()
|
|
1220
|
+
} else {
|
|
1221
|
+
node.provider.clone()
|
|
1222
|
+
},
|
|
1223
|
+
model: node.model.clone(),
|
|
1224
|
+
effort: node.effort.clone(),
|
|
1225
|
+
runtime_status: "running".to_string(),
|
|
1226
|
+
activity: activity.to_string(),
|
|
1227
|
+
health: health.to_string(),
|
|
1228
|
+
session_name: observed.session_name.clone(),
|
|
1229
|
+
tmux_command,
|
|
1106
1230
|
},
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
runtime_status: "running".to_string(),
|
|
1110
|
-
activity: activity.to_string(),
|
|
1111
|
-
health: health.to_string(),
|
|
1112
|
-
session_name: observed.session_name.clone(),
|
|
1113
|
-
tmux_command: tmux_command(node, observed),
|
|
1114
|
-
}
|
|
1231
|
+
attach,
|
|
1232
|
+
)
|
|
1115
1233
|
}
|
|
1116
1234
|
|
|
1117
1235
|
fn match_result<'a>(
|
|
@@ -13,33 +13,19 @@ use serde_json::json;
|
|
|
13
13
|
// =========================================================================
|
|
14
14
|
|
|
15
15
|
#[test]
|
|
16
|
-
fn
|
|
17
|
-
// golden:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
vec!["-x".to_string(), "-y".to_string()]
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
#[test]
|
|
34
|
-
fn provider_args_empty_is_empty() {
|
|
35
|
-
// golden: _provider_args([]) == []
|
|
36
|
-
assert_eq!(provider_args(&[]), Vec::<String>::new());
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
#[test]
|
|
40
|
-
fn provider_args_lone_dashdash_yields_empty() {
|
|
41
|
-
// golden: _provider_args(["--"]) == [] (values[1:] of single-elem list)
|
|
42
|
-
assert_eq!(provider_args(&["--".into()]), Vec::<String>::new());
|
|
16
|
+
fn provider_args_cases() {
|
|
17
|
+
// (old case-id, input, Python golden); values[1:] of a lone "--" is empty.
|
|
18
|
+
const CASES: &[(&str, &[&str], &[&str])] = &[
|
|
19
|
+
("provider_args_strips_leading_dashdash", &["--", "-x"], &["-x"]),
|
|
20
|
+
("provider_args_keeps_when_no_leading_dashdash", &["-x", "-y"], &["-x", "-y"]),
|
|
21
|
+
("provider_args_empty_is_empty", &[], &[]),
|
|
22
|
+
("provider_args_lone_dashdash_yields_empty", &["--"], &[]),
|
|
23
|
+
];
|
|
24
|
+
for (id, input, expected) in CASES {
|
|
25
|
+
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
26
|
+
let expected: Vec<String> = expected.iter().map(|arg| (*arg).to_string()).collect();
|
|
27
|
+
assert_eq!(provider_args(&argv), expected, "{id}");
|
|
28
|
+
}
|
|
43
29
|
}
|
|
44
30
|
|
|
45
31
|
// =========================================================================
|
|
@@ -47,118 +33,72 @@ fn provider_args_lone_dashdash_yields_empty() {
|
|
|
47
33
|
// =========================================================================
|
|
48
34
|
|
|
49
35
|
#[test]
|
|
50
|
-
fn
|
|
51
|
-
//
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"--"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
got.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
#[test]
|
|
97
|
-
fn
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
fn leader_launcher_args_attach_session_equals() {
|
|
117
|
-
// golden: ["--attach-session=mysess"] -> attach_session="mysess"
|
|
118
|
-
let got = leader_launcher_args(&["--attach-session=mysess".into()]).unwrap();
|
|
119
|
-
assert_eq!(got.attach_session, Some("mysess".to_string()));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
#[test]
|
|
123
|
-
fn leader_launcher_args_dashdash_passthrough_strips_separator() {
|
|
124
|
-
// ["--attach","--","-x","--provider-confirm"] ->
|
|
125
|
-
// provider_args=["-x","--provider-confirm"], attach_existing=True, confirm_attach=False
|
|
126
|
-
// Known Team Agent launcher flags after `--` are rejected by a separate guard.
|
|
127
|
-
let got = leader_launcher_args(&[
|
|
128
|
-
"--attach".into(),
|
|
129
|
-
"--".into(),
|
|
130
|
-
"-x".into(),
|
|
131
|
-
"--provider-confirm".into(),
|
|
132
|
-
])
|
|
133
|
-
.unwrap();
|
|
134
|
-
assert!(got.attach_existing);
|
|
135
|
-
assert!(!got.confirm_attach);
|
|
136
|
-
assert_eq!(
|
|
137
|
-
got.provider_args,
|
|
138
|
-
vec!["-x".to_string(), "--provider-confirm".to_string()]
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
#[test]
|
|
143
|
-
fn leader_launcher_args_unknown_tokens_collect_as_provider_args() {
|
|
144
|
-
// golden: ["foo","--attach","bar"] -> provider_args=["foo","bar"], attach_existing=True
|
|
145
|
-
let got = leader_launcher_args(&["foo".into(), "--attach".into(), "bar".into()]).unwrap();
|
|
146
|
-
assert_eq!(
|
|
147
|
-
got.provider_args,
|
|
148
|
-
vec!["foo".to_string(), "bar".to_string()]
|
|
149
|
-
);
|
|
150
|
-
assert!(got.attach_existing);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
#[test]
|
|
154
|
-
fn leader_launcher_args_attach_session_missing_value_errors() {
|
|
155
|
-
// golden: ["--attach-session"] raises RuntimeError("--attach-session requires a tmux session name")
|
|
156
|
-
let err = leader_launcher_args(&["--attach-session".into()]).unwrap_err();
|
|
157
|
-
let msg = err.to_string();
|
|
158
|
-
assert!(
|
|
159
|
-
msg.contains("--attach-session requires a tmux session name"),
|
|
160
|
-
"expected exact missing-value message, got: {msg}"
|
|
161
|
-
);
|
|
36
|
+
fn leader_launcher_args_success_cases() {
|
|
37
|
+
// (old case-id, argv, complete expected structure), including default false flags.
|
|
38
|
+
let cases: &[(&str, &[&str], LeaderLauncherArgs)] = &[
|
|
39
|
+
("leader_launcher_args_empty_all_default", &[], LeaderLauncherArgs::default()),
|
|
40
|
+
("leader_launcher_args_attach_and_confirm", &["--attach", "--confirm"], LeaderLauncherArgs {
|
|
41
|
+
attach_existing: true,
|
|
42
|
+
confirm_attach: true,
|
|
43
|
+
..Default::default()
|
|
44
|
+
}),
|
|
45
|
+
// --attach-existing is an alias of --attach.
|
|
46
|
+
("leader_launcher_args_attach_existing_alias", &["--attach-existing"], LeaderLauncherArgs {
|
|
47
|
+
attach_existing: true,
|
|
48
|
+
..Default::default()
|
|
49
|
+
}),
|
|
50
|
+
("leader_launcher_args_external_leader_opt_out", &["--external-leader", "--", "--model", "opus"], LeaderLauncherArgs {
|
|
51
|
+
provider_args: vec!["--model".to_string(), "opus".to_string()],
|
|
52
|
+
external_leader: true,
|
|
53
|
+
..Default::default()
|
|
54
|
+
}),
|
|
55
|
+
("leader_launcher_args_attach_session_spaced", &["--attach-session", "mysess"], LeaderLauncherArgs {
|
|
56
|
+
attach_session: Some("mysess".to_string()),
|
|
57
|
+
..Default::default()
|
|
58
|
+
}),
|
|
59
|
+
("leader_launcher_args_attach_session_equals", &["--attach-session=mysess"], LeaderLauncherArgs {
|
|
60
|
+
attach_session: Some("mysess".to_string()),
|
|
61
|
+
..Default::default()
|
|
62
|
+
}),
|
|
63
|
+
// Known Team Agent launcher flags after `--` are rejected by the error table.
|
|
64
|
+
("leader_launcher_args_dashdash_passthrough_strips_separator", &["--attach", "--", "-x", "--provider-confirm"], LeaderLauncherArgs {
|
|
65
|
+
provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
|
|
66
|
+
attach_existing: true,
|
|
67
|
+
..Default::default()
|
|
68
|
+
}),
|
|
69
|
+
("leader_launcher_args_unknown_tokens_collect_as_provider_args", &["foo", "--attach", "bar"], LeaderLauncherArgs {
|
|
70
|
+
provider_args: vec!["foo".to_string(), "bar".to_string()],
|
|
71
|
+
attach_existing: true,
|
|
72
|
+
..Default::default()
|
|
73
|
+
}),
|
|
74
|
+
];
|
|
75
|
+
for (id, input, expected) in cases {
|
|
76
|
+
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
77
|
+
let got = leader_launcher_args(&argv).unwrap_or_else(|err| panic!("{id}: {err}"));
|
|
78
|
+
assert_eq!(got, *expected, "{id}");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn leader_launcher_args_error_cases() {
|
|
84
|
+
// Preserve the original error-substring contract, rather than requiring exact equality.
|
|
85
|
+
const CASES: &[(&str, &[&str], &str)] = &[
|
|
86
|
+
(
|
|
87
|
+
"leader_launcher_args_external_leader_after_dashdash_errors",
|
|
88
|
+
&["--", "--external-leader"],
|
|
89
|
+
"Team Agent launcher flag --external-leader must appear before --",
|
|
90
|
+
),
|
|
91
|
+
(
|
|
92
|
+
"leader_launcher_args_attach_session_missing_value_errors",
|
|
93
|
+
&["--attach-session"],
|
|
94
|
+
"--attach-session requires a tmux session name",
|
|
95
|
+
),
|
|
96
|
+
];
|
|
97
|
+
for (id, input, expected) in CASES {
|
|
98
|
+
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
99
|
+
let err = leader_launcher_args(&argv).expect_err(id);
|
|
100
|
+
assert!(err.to_string().contains(*expected), "{id}: unexpected error: {err}");
|
|
101
|
+
}
|
|
162
102
|
}
|
|
163
103
|
|
|
164
104
|
// =========================================================================
|
|
@@ -166,37 +106,22 @@ fn leader_launcher_args_attach_session_missing_value_errors() {
|
|
|
166
106
|
// =========================================================================
|
|
167
107
|
|
|
168
108
|
#[test]
|
|
169
|
-
fn
|
|
170
|
-
//
|
|
171
|
-
let
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
#[test]
|
|
188
|
-
fn send_target_broadcast_star() {
|
|
189
|
-
// skeleton contract: bare "*" target -> Broadcast (send.py interprets "*" as全队广播)
|
|
190
|
-
assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
#[test]
|
|
194
|
-
fn send_target_empty_targets_falls_through_to_target() {
|
|
195
|
-
// golden: targets="" is falsy in Python -> returns args.target ("fallback")
|
|
196
|
-
assert_eq!(
|
|
197
|
-
send_target(Some(""), Some("fallback")),
|
|
198
|
-
MessageTarget::Single("fallback".to_string())
|
|
199
|
-
);
|
|
109
|
+
fn send_target_cases() {
|
|
110
|
+
// (old case-id, targets, target, complete expected enum).
|
|
111
|
+
let cases: &[(&str, Option<&str>, Option<&str>, MessageTarget)] = &[
|
|
112
|
+
(
|
|
113
|
+
"send_target_fanout_strips_and_filters_empty", Some("a, b ,,c"), None,
|
|
114
|
+
MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
|
|
115
|
+
),
|
|
116
|
+
("send_target_single_target", None, Some("agent_x"), MessageTarget::Single("agent_x".to_string())),
|
|
117
|
+
// A bare "*" means broadcast to the whole team.
|
|
118
|
+
("send_target_broadcast_star", None, Some("*"), MessageTarget::Broadcast),
|
|
119
|
+
// targets="" is falsy in Python, so it falls through to args.target.
|
|
120
|
+
("send_target_empty_targets_falls_through_to_target", Some(""), Some("fallback"), MessageTarget::Single("fallback".to_string())),
|
|
121
|
+
];
|
|
122
|
+
for (id, targets, target, expected) in cases {
|
|
123
|
+
assert_eq!(send_target(*targets, *target), *expected, "{id}");
|
|
124
|
+
}
|
|
200
125
|
}
|
|
201
126
|
|
|
202
127
|
// =========================================================================
|
|
@@ -205,61 +130,23 @@ fn send_target_empty_targets_falls_through_to_target() {
|
|
|
205
130
|
// =========================================================================
|
|
206
131
|
|
|
207
132
|
#[test]
|
|
208
|
-
fn
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
SummaryBucket::Stopped
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
#[test]
|
|
230
|
-
fn classify_busy() {
|
|
231
|
-
// raw==busy OR hstatus in {running,working} -> Busy
|
|
232
|
-
assert_eq!(classify_agent_bucket("busy", ""), SummaryBucket::Busy);
|
|
233
|
-
assert_eq!(classify_agent_bucket("", "running"), SummaryBucket::Busy);
|
|
234
|
-
assert_eq!(classify_agent_bucket("", "working"), SummaryBucket::Busy);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
#[test]
|
|
238
|
-
fn classify_hstatus_idle_beats_raw_running() {
|
|
239
|
-
// golden: raw=running, h=idle -> idle (hstatus==idle branch precedes raw==running branch)
|
|
240
|
-
assert_eq!(
|
|
241
|
-
classify_agent_bucket("running", "idle"),
|
|
242
|
-
SummaryBucket::Idle
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
#[test]
|
|
247
|
-
fn classify_pure_running() {
|
|
248
|
-
// raw==running, no overriding hstatus -> Running
|
|
249
|
-
assert_eq!(classify_agent_bucket("running", ""), SummaryBucket::Running);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
#[test]
|
|
253
|
-
fn classify_blocked_and_unmatched_are_unknown_never_idle() {
|
|
254
|
-
// bug-071/077/085: blocked/stuck/missing AND any unmatched value -> Unknown, NOT idle.
|
|
255
|
-
assert_eq!(classify_agent_bucket("blocked", ""), SummaryBucket::Unknown);
|
|
256
|
-
assert_eq!(classify_agent_bucket("stuck", ""), SummaryBucket::Unknown);
|
|
257
|
-
assert_eq!(classify_agent_bucket("", "missing"), SummaryBucket::Unknown);
|
|
258
|
-
assert_eq!(
|
|
259
|
-
classify_agent_bucket("weird_value", ""),
|
|
260
|
-
SummaryBucket::Unknown
|
|
261
|
-
);
|
|
262
|
-
assert_eq!(classify_agent_bucket("", ""), SummaryBucket::Unknown);
|
|
133
|
+
fn classify_agent_bucket_cases() {
|
|
134
|
+
// (old case-id, all original (raw, health) inputs, expected bucket).
|
|
135
|
+
const CASES: &[(&str, &[(&str, &str)], SummaryBucket)] = &[
|
|
136
|
+
("classify_failed_takes_priority", &[("failed", ""), ("error", ""), ("running", "error")], SummaryBucket::Failed),
|
|
137
|
+
("classify_stopped", &[("stopped", ""), ("done", ""), ("running", "done")], SummaryBucket::Stopped),
|
|
138
|
+
("classify_busy", &[("busy", ""), ("", "running"), ("", "working")], SummaryBucket::Busy),
|
|
139
|
+
// hstatus==idle precedes the raw==running branch.
|
|
140
|
+
("classify_hstatus_idle_beats_raw_running", &[("running", "idle")], SummaryBucket::Idle),
|
|
141
|
+
("classify_pure_running", &[("running", "")], SummaryBucket::Running),
|
|
142
|
+
// bug-071/077/085: unmatched and blocked/stuck/missing are Unknown, NEVER idle.
|
|
143
|
+
("classify_blocked_and_unmatched_are_unknown_never_idle", &[("blocked", ""), ("stuck", ""), ("", "missing"), ("weird_value", ""), ("", "")], SummaryBucket::Unknown),
|
|
144
|
+
];
|
|
145
|
+
for (id, inputs, expected) in CASES {
|
|
146
|
+
for &(raw, health) in inputs.iter() {
|
|
147
|
+
assert_eq!(classify_agent_bucket(raw, health), *expected, "{id}: raw={raw:?}, health={health:?}");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
263
150
|
}
|
|
264
151
|
|
|
265
152
|
#[test]
|