@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
|
@@ -58,6 +58,165 @@ fn production_state(agents: Value) -> Value {
|
|
|
58
58
|
})
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
struct BriefWorkspaceGuard(PathBuf);
|
|
62
|
+
|
|
63
|
+
impl std::ops::Deref for BriefWorkspaceGuard {
|
|
64
|
+
type Target = Path;
|
|
65
|
+
|
|
66
|
+
fn deref(&self) -> &Self::Target {
|
|
67
|
+
&self.0
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl Drop for BriefWorkspaceGuard {
|
|
72
|
+
fn drop(&mut self) {
|
|
73
|
+
let _ = std::fs::remove_dir_all(&self.0);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn guarded_brief_workspace(tag: &str) -> BriefWorkspaceGuard {
|
|
78
|
+
BriefWorkspaceGuard(brief_workspace(tag))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn status_agent(
|
|
82
|
+
name: &str,
|
|
83
|
+
pane: &str,
|
|
84
|
+
endpoint: &str,
|
|
85
|
+
session: &str,
|
|
86
|
+
model: &str,
|
|
87
|
+
effort: &str,
|
|
88
|
+
) -> Value {
|
|
89
|
+
let mut agent = production_agent(name, pane);
|
|
90
|
+
agent["tmux_endpoint"] = json!(endpoint);
|
|
91
|
+
agent["session_name"] = json!(session);
|
|
92
|
+
agent["model"] = json!(model);
|
|
93
|
+
agent["effort"] = json!(effort);
|
|
94
|
+
agent
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[cfg(unix)]
|
|
98
|
+
fn producer_report_nodes(socket: &str, rows: &[(String, String, String, String)]) -> String {
|
|
99
|
+
let nodes = rows
|
|
100
|
+
.iter()
|
|
101
|
+
.map(|(session, window, pane, native_session)| {
|
|
102
|
+
json!({
|
|
103
|
+
"socket": socket,
|
|
104
|
+
"workspace_path": "/ws",
|
|
105
|
+
"project_name": "demo",
|
|
106
|
+
"session": session,
|
|
107
|
+
"window_index": 1,
|
|
108
|
+
"window_name": window,
|
|
109
|
+
"pane_id": pane,
|
|
110
|
+
"name": window,
|
|
111
|
+
"provider": "pi",
|
|
112
|
+
"state": "idle",
|
|
113
|
+
"activity": "idle",
|
|
114
|
+
"session_name": native_session,
|
|
115
|
+
"health": "normal",
|
|
116
|
+
"background_tasks": {"running": 0},
|
|
117
|
+
"evidence": {"method": "pi_activity_channel", "detail": "channel"}
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
.collect::<Vec<_>>();
|
|
121
|
+
serde_json::to_string(&json!({
|
|
122
|
+
"schema_version": 1,
|
|
123
|
+
"socket": socket,
|
|
124
|
+
"sampled_at": "2026-01-01T00:00:00Z",
|
|
125
|
+
"nodes": nodes
|
|
126
|
+
}))
|
|
127
|
+
.unwrap()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[cfg(unix)]
|
|
131
|
+
fn write_nodeprobe_reports(dir: &Path, reports: &[(String, String)]) -> (PathBuf, PathBuf) {
|
|
132
|
+
std::fs::create_dir_all(dir).unwrap();
|
|
133
|
+
let log = dir.join("calls.log");
|
|
134
|
+
let mut body = format!(
|
|
135
|
+
"printf '%s\\t%s\\n' \"$1\" \"$2\" >> {}\ncase \"$2\" in\n",
|
|
136
|
+
shell_quote_for_status_test(&log.to_string_lossy())
|
|
137
|
+
);
|
|
138
|
+
for (index, (endpoint, report)) in reports.iter().enumerate() {
|
|
139
|
+
let path = dir.join(format!("report-{index}.json"));
|
|
140
|
+
std::fs::write(&path, report).unwrap();
|
|
141
|
+
body.push_str(&format!(
|
|
142
|
+
" {} ) cat {} ;;\n",
|
|
143
|
+
shell_quote_for_status_test(endpoint),
|
|
144
|
+
shell_quote_for_status_test(&path.to_string_lossy())
|
|
145
|
+
));
|
|
146
|
+
}
|
|
147
|
+
body.push_str(" *) exit 2 ;;\nesac\n");
|
|
148
|
+
(write_nodeprobe(dir, &body), log)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn shell_quote_for_status_test(value: &str) -> String {
|
|
152
|
+
format!("'{}'", value.replace('\'', "'\\''"))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn logged_probe_calls(log: &Path) -> Vec<(String, String)> {
|
|
156
|
+
std::fs::read_to_string(log)
|
|
157
|
+
.unwrap()
|
|
158
|
+
.lines()
|
|
159
|
+
.map(|line| {
|
|
160
|
+
let (flag, endpoint) = line.split_once('\t').expect("probe call fields");
|
|
161
|
+
(flag.to_string(), endpoint.to_string())
|
|
162
|
+
})
|
|
163
|
+
.collect()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn human_text(result: CmdResult) -> String {
|
|
167
|
+
match result.output {
|
|
168
|
+
CmdOutput::Human(text) => text,
|
|
169
|
+
other => panic!("expected human status output, got {other:?}"),
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn legacy_human_node(node: &Value) -> String {
|
|
174
|
+
let text = |key: &str| {
|
|
175
|
+
node.get(key)
|
|
176
|
+
.and_then(Value::as_str)
|
|
177
|
+
.unwrap_or("null")
|
|
178
|
+
.to_string()
|
|
179
|
+
};
|
|
180
|
+
format!(
|
|
181
|
+
"name: {} provider: {} model: {} effort: {} runtime_status: {} activity: {} health: {} session_name: {} tmux_command: {}",
|
|
182
|
+
text("name"),
|
|
183
|
+
text("provider"),
|
|
184
|
+
text("model"),
|
|
185
|
+
text("effort"),
|
|
186
|
+
text("runtime_status"),
|
|
187
|
+
text("activity"),
|
|
188
|
+
text("health"),
|
|
189
|
+
text("session_name"),
|
|
190
|
+
text("tmux_command")
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn human_token_count(text: &str, expected: &str) -> usize {
|
|
195
|
+
text.split(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_'))
|
|
196
|
+
.filter(|token| token.eq_ignore_ascii_case(expected))
|
|
197
|
+
.count()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn status_table_header(text: &str) -> Option<&str> {
|
|
201
|
+
text.lines().find(|line| {
|
|
202
|
+
!line.trim_start().starts_with("name:")
|
|
203
|
+
&& [
|
|
204
|
+
"name",
|
|
205
|
+
"provider",
|
|
206
|
+
"model",
|
|
207
|
+
"effort",
|
|
208
|
+
"activity",
|
|
209
|
+
"health",
|
|
210
|
+
"session_name",
|
|
211
|
+
]
|
|
212
|
+
.iter()
|
|
213
|
+
.all(|field| human_token_count(line, field) == 1)
|
|
214
|
+
&& human_token_count(line, "runtime_status") + human_token_count(line, "runtime")
|
|
215
|
+
== 1
|
|
216
|
+
&& human_token_count(line, "tmux_command") + human_token_count(line, "attach") == 1
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
61
220
|
fn status_args(ws: &Path, json_out: bool, agent: Option<&str>, team: Option<&str>) -> StatusArgs {
|
|
62
221
|
StatusArgs {
|
|
63
222
|
agent: agent.map(str::to_string),
|
|
@@ -212,6 +371,471 @@ fn cmd_status_dedupes_same_endpoint_across_agents() {
|
|
|
212
371
|
let _ = std::fs::remove_dir_all(&ws);
|
|
213
372
|
}
|
|
214
373
|
|
|
374
|
+
#[cfg(unix)]
|
|
375
|
+
#[test]
|
|
376
|
+
#[serial(status_brief)]
|
|
377
|
+
fn issue271_status_human_dedupes_six_nodes_and_preserves_json() {
|
|
378
|
+
let ws = guarded_brief_workspace("issue271-six");
|
|
379
|
+
let endpoint = "/tmp/271 shared.sock";
|
|
380
|
+
let session = "team-271";
|
|
381
|
+
let mut agents = serde_json::Map::new();
|
|
382
|
+
let mut rows = Vec::new();
|
|
383
|
+
for index in 1..=6 {
|
|
384
|
+
let name = format!("worker-{index}");
|
|
385
|
+
let pane = format!("%{}", index + 20);
|
|
386
|
+
agents.insert(
|
|
387
|
+
name.clone(),
|
|
388
|
+
status_agent(
|
|
389
|
+
&name,
|
|
390
|
+
&pane,
|
|
391
|
+
endpoint,
|
|
392
|
+
session,
|
|
393
|
+
&format!("gpt-5.6-luna-{index}"),
|
|
394
|
+
if index % 2 == 0 { "high" } else { "max" },
|
|
395
|
+
),
|
|
396
|
+
);
|
|
397
|
+
rows.push((
|
|
398
|
+
session.to_string(),
|
|
399
|
+
name.clone(),
|
|
400
|
+
pane,
|
|
401
|
+
format!("native-{name}"),
|
|
402
|
+
));
|
|
403
|
+
}
|
|
404
|
+
write_state(
|
|
405
|
+
&ws,
|
|
406
|
+
&json!({
|
|
407
|
+
"session_name": session,
|
|
408
|
+
"tmux_endpoint": endpoint,
|
|
409
|
+
"active_team_key": "demo",
|
|
410
|
+
"agents": agents
|
|
411
|
+
}),
|
|
412
|
+
);
|
|
413
|
+
let report = producer_report_nodes(endpoint, &rows);
|
|
414
|
+
let (bin, log) = write_nodeprobe_reports(
|
|
415
|
+
&ws.join("probe"),
|
|
416
|
+
&[(endpoint.to_string(), report)],
|
|
417
|
+
);
|
|
418
|
+
let nodes = status_port::with_test_nodeprobe(bin.clone(), || {
|
|
419
|
+
json_nodes(cmd_status(&status_args(&ws, true, None, None)).expect("JSON status"))
|
|
420
|
+
});
|
|
421
|
+
assert_eq!(nodes.len(), 6);
|
|
422
|
+
for node in &nodes {
|
|
423
|
+
assert_eq!(node.as_object().unwrap().len(), 9);
|
|
424
|
+
for key in [
|
|
425
|
+
"name",
|
|
426
|
+
"provider",
|
|
427
|
+
"model",
|
|
428
|
+
"effort",
|
|
429
|
+
"runtime_status",
|
|
430
|
+
"activity",
|
|
431
|
+
"health",
|
|
432
|
+
"session_name",
|
|
433
|
+
"tmux_command",
|
|
434
|
+
] {
|
|
435
|
+
assert!(node.get(key).is_some(), "missing JSON field {key}");
|
|
436
|
+
}
|
|
437
|
+
let name = node["name"].as_str().unwrap();
|
|
438
|
+
let index = name.strip_prefix("worker-").unwrap().parse::<u32>().unwrap();
|
|
439
|
+
let pane = index + 20;
|
|
440
|
+
assert_eq!(node["provider"], "pi");
|
|
441
|
+
assert_eq!(node["model"], format!("gpt-5.6-luna-{index}"));
|
|
442
|
+
assert_eq!(node["effort"], if index % 2 == 0 { "high" } else { "max" });
|
|
443
|
+
assert_eq!(node["runtime_status"], "running");
|
|
444
|
+
assert_eq!(node["activity"], "idle");
|
|
445
|
+
assert_eq!(node["health"], "normal");
|
|
446
|
+
assert_eq!(node["session_name"], format!("native-{name}"));
|
|
447
|
+
let target = format!("{session}:{name}.%{pane}");
|
|
448
|
+
assert_eq!(
|
|
449
|
+
node["tmux_command"],
|
|
450
|
+
json!(format!(
|
|
451
|
+
"tmux -S {} attach -t {}",
|
|
452
|
+
shell_quote_for_status_test(endpoint),
|
|
453
|
+
shell_quote_for_status_test(&target)
|
|
454
|
+
))
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
let before = nodes
|
|
458
|
+
.iter()
|
|
459
|
+
.map(legacy_human_node)
|
|
460
|
+
.collect::<Vec<_>>()
|
|
461
|
+
.join("\n");
|
|
462
|
+
let human = status_port::with_test_nodeprobe(bin, || {
|
|
463
|
+
human_text(cmd_status(&status_args(&ws, false, None, None)).expect("human status"))
|
|
464
|
+
});
|
|
465
|
+
let prefix = format!("tmux -S {}", shell_quote_for_status_test(endpoint));
|
|
466
|
+
let prefix_count = human.matches(&prefix).count();
|
|
467
|
+
let labels_once = status_table_header(&human).is_some();
|
|
468
|
+
let targets_once = nodes.iter().all(|node| {
|
|
469
|
+
let name = node["name"].as_str().unwrap();
|
|
470
|
+
let index = name.strip_prefix("worker-").unwrap().parse::<u32>().unwrap();
|
|
471
|
+
let pane = index + 20;
|
|
472
|
+
let target = format!("{name}.%{pane}");
|
|
473
|
+
let rows = human.lines().filter(|line| line.contains(&target)).collect::<Vec<_>>();
|
|
474
|
+
rows.len() == 1 && rows[0].contains("1:")
|
|
475
|
+
});
|
|
476
|
+
let calls = logged_probe_calls(&log);
|
|
477
|
+
assert_eq!(calls.len(), 2, "one native query per status invocation: {calls:?}");
|
|
478
|
+
assert!(
|
|
479
|
+
calls.iter().all(|(flag, called)| flag == "-S" && called == endpoint),
|
|
480
|
+
"{calls:?}"
|
|
481
|
+
);
|
|
482
|
+
assert!(
|
|
483
|
+
prefix_count == 1 && labels_once && targets_once && human.len() * 4 < before.len() * 3,
|
|
484
|
+
"six-node human output must dedupe shared text and retain targets; prefix_count={prefix_count}, labels_once={labels_once}, targets_once={targets_once}, before_bytes={}, after_bytes={}\n{human}",
|
|
485
|
+
before.len(),
|
|
486
|
+
human.len()
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#[cfg(unix)]
|
|
491
|
+
#[test]
|
|
492
|
+
#[serial(status_brief)]
|
|
493
|
+
fn issue271_status_human_groups_socket_session_pairs_without_cross_routing() {
|
|
494
|
+
let ws = guarded_brief_workspace("issue271-groups");
|
|
495
|
+
let endpoint_a = "/tmp/271 socket 'α.sock";
|
|
496
|
+
let endpoint_b = "shared named socket";
|
|
497
|
+
let session_red = "red team α";
|
|
498
|
+
let session_blue = "blue's team";
|
|
499
|
+
let specs = [
|
|
500
|
+
("a-red", endpoint_a, session_red, "%31", 1),
|
|
501
|
+
("b-red", endpoint_a, session_red, "%32", 1),
|
|
502
|
+
("c-other", endpoint_a, session_blue, "%33", 2),
|
|
503
|
+
("d-named", endpoint_b, session_red, "%34", 3),
|
|
504
|
+
("e-named", endpoint_b, session_red, "%35", 3),
|
|
505
|
+
];
|
|
506
|
+
let mut agents = serde_json::Map::new();
|
|
507
|
+
let mut rows_a = Vec::new();
|
|
508
|
+
let mut rows_b = Vec::new();
|
|
509
|
+
for &(name, endpoint, session, pane, _) in &specs {
|
|
510
|
+
agents.insert(
|
|
511
|
+
name.to_string(),
|
|
512
|
+
status_agent(name, pane, endpoint, session, "model", "high"),
|
|
513
|
+
);
|
|
514
|
+
let row = (
|
|
515
|
+
session.to_string(),
|
|
516
|
+
name.to_string(),
|
|
517
|
+
pane.to_string(),
|
|
518
|
+
format!("native-{name}"),
|
|
519
|
+
);
|
|
520
|
+
if endpoint == endpoint_a {
|
|
521
|
+
rows_a.push(row);
|
|
522
|
+
} else {
|
|
523
|
+
rows_b.push(row);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
write_state(
|
|
527
|
+
&ws,
|
|
528
|
+
&json!({
|
|
529
|
+
"session_name": session_red,
|
|
530
|
+
"tmux_endpoint": endpoint_a,
|
|
531
|
+
"active_team_key": "demo",
|
|
532
|
+
"agents": agents
|
|
533
|
+
}),
|
|
534
|
+
);
|
|
535
|
+
let reports = [
|
|
536
|
+
(
|
|
537
|
+
endpoint_a.to_string(),
|
|
538
|
+
producer_report_nodes(endpoint_a, &rows_a),
|
|
539
|
+
),
|
|
540
|
+
(
|
|
541
|
+
endpoint_b.to_string(),
|
|
542
|
+
producer_report_nodes(endpoint_b, &rows_b),
|
|
543
|
+
),
|
|
544
|
+
];
|
|
545
|
+
let (bin, log) = write_nodeprobe_reports(&ws.join("probe"), &reports);
|
|
546
|
+
let nodes = status_port::with_test_nodeprobe(bin.clone(), || {
|
|
547
|
+
json_nodes(cmd_status(&status_args(&ws, true, None, None)).expect("JSON status"))
|
|
548
|
+
});
|
|
549
|
+
assert_eq!(nodes.len(), specs.len());
|
|
550
|
+
for node in &nodes {
|
|
551
|
+
let name = node["name"].as_str().unwrap();
|
|
552
|
+
let (_, endpoint, session, pane, _) =
|
|
553
|
+
*specs.iter().find(|spec| spec.0 == name).unwrap();
|
|
554
|
+
let target = format!("{session}:{name}.{pane}");
|
|
555
|
+
assert_eq!(
|
|
556
|
+
node["tmux_command"],
|
|
557
|
+
json!(format!(
|
|
558
|
+
"tmux {} {} attach -t {}",
|
|
559
|
+
if Path::new(endpoint).is_absolute() { "-S" } else { "-L" },
|
|
560
|
+
shell_quote_for_status_test(endpoint),
|
|
561
|
+
shell_quote_for_status_test(&target)
|
|
562
|
+
))
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
let human = status_port::with_test_nodeprobe(bin, || {
|
|
566
|
+
human_text(cmd_status(&status_args(&ws, false, None, None)).expect("human status"))
|
|
567
|
+
});
|
|
568
|
+
let abs_prefix = format!("tmux -S {}", shell_quote_for_status_test(endpoint_a));
|
|
569
|
+
let named_prefix = format!("tmux -L {}", shell_quote_for_status_test(endpoint_b));
|
|
570
|
+
let calls = logged_probe_calls(&log);
|
|
571
|
+
assert_eq!(calls.len(), 4, "one probe per endpoint per invocation: {calls:?}");
|
|
572
|
+
assert_eq!(calls.iter().filter(|(_, value)| value == endpoint_a).count(), 2);
|
|
573
|
+
assert_eq!(calls.iter().filter(|(_, value)| value == endpoint_b).count(), 2);
|
|
574
|
+
assert!(calls.iter().all(|(flag, value)| {
|
|
575
|
+
(value == endpoint_a && flag == "-S") || (value == endpoint_b && flag == "-L")
|
|
576
|
+
}));
|
|
577
|
+
let expected_groups = [
|
|
578
|
+
("a-red", "%31", 1),
|
|
579
|
+
("b-red", "%32", 1),
|
|
580
|
+
("c-other", "%33", 2),
|
|
581
|
+
("d-named", "%34", 3),
|
|
582
|
+
("e-named", "%35", 3),
|
|
583
|
+
];
|
|
584
|
+
let rows_routed = expected_groups.iter().all(|(name, pane, group)| {
|
|
585
|
+
let target = format!("{name}.{pane}");
|
|
586
|
+
let matching = human
|
|
587
|
+
.lines()
|
|
588
|
+
.filter(|line| line.contains(&target))
|
|
589
|
+
.collect::<Vec<_>>();
|
|
590
|
+
matching.len() == 1 && matching[0].contains(&format!("{group}:"))
|
|
591
|
+
});
|
|
592
|
+
assert!(
|
|
593
|
+
human.matches(&abs_prefix).count() == 2
|
|
594
|
+
&& human.matches(&named_prefix).count() == 1
|
|
595
|
+
&& human.matches(session_red).count() == 2
|
|
596
|
+
&& human.matches("blue").count() == 1
|
|
597
|
+
&& rows_routed,
|
|
598
|
+
"each (endpoint, session) needs one stable group and its own targets; abs={}, named={}, red={}, blue={}, rows_routed={rows_routed}\n{human}",
|
|
599
|
+
human.matches(&abs_prefix).count(),
|
|
600
|
+
human.matches(&named_prefix).count(),
|
|
601
|
+
human.matches(session_red).count(),
|
|
602
|
+
human.matches("blue").count()
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
#[cfg(unix)]
|
|
607
|
+
#[test]
|
|
608
|
+
#[serial(status_brief)]
|
|
609
|
+
fn issue271_status_single_empty_unknown_and_stopped_compatibility() {
|
|
610
|
+
let endpoint = "/tmp/271-single.sock";
|
|
611
|
+
let single = guarded_brief_workspace("issue271-single");
|
|
612
|
+
write_state(
|
|
613
|
+
&single,
|
|
614
|
+
&json!({
|
|
615
|
+
"session_name": "team-single",
|
|
616
|
+
"tmux_endpoint": endpoint,
|
|
617
|
+
"agents": {
|
|
618
|
+
"solo": status_agent(
|
|
619
|
+
"solo",
|
|
620
|
+
"%7",
|
|
621
|
+
endpoint,
|
|
622
|
+
"team-single",
|
|
623
|
+
"gpt-5.6-luna-xhigh",
|
|
624
|
+
"high"
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
}),
|
|
628
|
+
);
|
|
629
|
+
let single_report = producer_report_nodes(
|
|
630
|
+
endpoint,
|
|
631
|
+
&[(
|
|
632
|
+
"team-single".to_string(),
|
|
633
|
+
"solo".to_string(),
|
|
634
|
+
"%7".to_string(),
|
|
635
|
+
"pi-native-solo".to_string(),
|
|
636
|
+
)],
|
|
637
|
+
);
|
|
638
|
+
let (single_bin, single_log) =
|
|
639
|
+
write_nodeprobe_reports(&single.join("probe"), &[(endpoint.to_string(), single_report)]);
|
|
640
|
+
let single_nodes = status_port::with_test_nodeprobe(single_bin.clone(), || {
|
|
641
|
+
json_nodes(cmd_status(&status_args(&single, true, None, None)).expect("single JSON"))
|
|
642
|
+
});
|
|
643
|
+
let single_human = status_port::with_test_nodeprobe(single_bin, || {
|
|
644
|
+
human_text(cmd_status(&status_args(&single, false, None, None)).expect("single human"))
|
|
645
|
+
});
|
|
646
|
+
assert_eq!(single_nodes.len(), 1);
|
|
647
|
+
assert_eq!(single_human, legacy_human_node(&single_nodes[0]));
|
|
648
|
+
assert_eq!(logged_probe_calls(&single_log).len(), 2);
|
|
649
|
+
let unknown_agent = cmd_status(&status_args(&single, false, Some("ghost"), None))
|
|
650
|
+
.unwrap_err()
|
|
651
|
+
.to_string();
|
|
652
|
+
assert!(unknown_agent.contains("unknown agent id: ghost"), "{unknown_agent}");
|
|
653
|
+
|
|
654
|
+
let empty = guarded_brief_workspace("issue271-empty");
|
|
655
|
+
write_state(&empty, &production_state(json!({})));
|
|
656
|
+
assert!(
|
|
657
|
+
human_text(cmd_status(&status_args(&empty, false, None, None)).expect("empty")).is_empty()
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
let mixed = guarded_brief_workspace("issue271-mixed");
|
|
661
|
+
let mut agents = serde_json::Map::new();
|
|
662
|
+
agents.insert(
|
|
663
|
+
"worker".to_string(),
|
|
664
|
+
status_agent("worker", "%7", endpoint, "team-mixed", "gpt-worker", "max"),
|
|
665
|
+
);
|
|
666
|
+
agents.insert(
|
|
667
|
+
"unknown".to_string(),
|
|
668
|
+
status_agent("unknown", "%8", endpoint, "team-mixed", "gpt-unknown", "high"),
|
|
669
|
+
);
|
|
670
|
+
let mut stopped = status_agent(
|
|
671
|
+
"stopped",
|
|
672
|
+
"%9",
|
|
673
|
+
endpoint,
|
|
674
|
+
"team-mixed",
|
|
675
|
+
"gpt-stopped",
|
|
676
|
+
"low",
|
|
677
|
+
);
|
|
678
|
+
stopped["status"] = json!("stopped");
|
|
679
|
+
agents.insert("stopped".to_string(), stopped);
|
|
680
|
+
write_state(
|
|
681
|
+
&mixed,
|
|
682
|
+
&json!({
|
|
683
|
+
"session_name": "team-mixed",
|
|
684
|
+
"tmux_endpoint": endpoint,
|
|
685
|
+
"agents": agents
|
|
686
|
+
}),
|
|
687
|
+
);
|
|
688
|
+
let mixed_report = producer_report_nodes(
|
|
689
|
+
endpoint,
|
|
690
|
+
&[(
|
|
691
|
+
"team-mixed".to_string(),
|
|
692
|
+
"worker".to_string(),
|
|
693
|
+
"%7".to_string(),
|
|
694
|
+
"pi-native-worker".to_string(),
|
|
695
|
+
)],
|
|
696
|
+
);
|
|
697
|
+
let (mixed_bin, mixed_log) =
|
|
698
|
+
write_nodeprobe_reports(&mixed.join("probe"), &[(endpoint.to_string(), mixed_report)]);
|
|
699
|
+
let mixed_nodes = status_port::with_test_nodeprobe(mixed_bin.clone(), || {
|
|
700
|
+
json_nodes(cmd_status(&status_args(&mixed, true, None, None)).expect("mixed JSON"))
|
|
701
|
+
});
|
|
702
|
+
let node = |name: &str| mixed_nodes.iter().find(|node| node["name"] == name).unwrap();
|
|
703
|
+
assert_eq!(node("worker")["runtime_status"], "running");
|
|
704
|
+
assert_eq!(node("worker")["session_name"], "pi-native-worker");
|
|
705
|
+
assert_eq!(node("unknown")["runtime_status"], "unknown");
|
|
706
|
+
assert_eq!(node("unknown")["model"], "gpt-unknown");
|
|
707
|
+
assert_eq!(node("unknown")["effort"], "high");
|
|
708
|
+
assert!(node("unknown")["tmux_command"].is_null());
|
|
709
|
+
assert!(node("unknown")["session_name"].is_null());
|
|
710
|
+
assert_eq!(node("stopped")["runtime_status"], "stopped");
|
|
711
|
+
assert_eq!(node("stopped")["model"], "gpt-stopped");
|
|
712
|
+
assert_eq!(node("stopped")["effort"], "low");
|
|
713
|
+
assert!(node("stopped")["tmux_command"].is_null());
|
|
714
|
+
let mixed_human = status_port::with_test_nodeprobe(mixed_bin.clone(), || {
|
|
715
|
+
human_text(cmd_status(&status_args(&mixed, false, None, None)).expect("mixed human"))
|
|
716
|
+
});
|
|
717
|
+
let agent_human = status_port::with_test_nodeprobe(mixed_bin, || {
|
|
718
|
+
human_text(
|
|
719
|
+
cmd_status(&status_args(&mixed, false, Some("worker"), None))
|
|
720
|
+
.expect("status AGENT"),
|
|
721
|
+
)
|
|
722
|
+
});
|
|
723
|
+
assert_eq!(agent_human, legacy_human_node(node("worker")));
|
|
724
|
+
let calls = logged_probe_calls(&mixed_log);
|
|
725
|
+
assert_eq!(calls.len(), 3, "one native query per status invocation");
|
|
726
|
+
assert!(
|
|
727
|
+
status_table_header(&mixed_human).is_some()
|
|
728
|
+
&& mixed_human
|
|
729
|
+
.matches(&format!("tmux -S {}", shell_quote_for_status_test(endpoint)))
|
|
730
|
+
.count()
|
|
731
|
+
== 1,
|
|
732
|
+
"mixed output must retain one shared header and only the observed worker target:\n{mixed_human}"
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
#[cfg(unix)]
|
|
737
|
+
#[test]
|
|
738
|
+
#[serial(status_brief)]
|
|
739
|
+
fn issue271_status_human_escapes_fields_and_quotes_shared_template() {
|
|
740
|
+
let ws = guarded_brief_workspace("issue271-escape");
|
|
741
|
+
let endpoint = "/tmp/271 space '雪.sock";
|
|
742
|
+
let session = "team '雪";
|
|
743
|
+
let window_a = "win '甲";
|
|
744
|
+
let window_b = "win β";
|
|
745
|
+
let mut agent_a = status_agent(
|
|
746
|
+
"node-a",
|
|
747
|
+
"%51",
|
|
748
|
+
endpoint,
|
|
749
|
+
session,
|
|
750
|
+
"gpt\tα\nmax",
|
|
751
|
+
"xhigh\nhigh",
|
|
752
|
+
);
|
|
753
|
+
agent_a["window"] = json!(window_a);
|
|
754
|
+
let mut agent_b = status_agent(
|
|
755
|
+
"node-b",
|
|
756
|
+
"%52",
|
|
757
|
+
endpoint,
|
|
758
|
+
session,
|
|
759
|
+
"grok\tβ\nxhigh",
|
|
760
|
+
"high\tmax\nlow",
|
|
761
|
+
);
|
|
762
|
+
agent_b["window"] = json!(window_b);
|
|
763
|
+
write_state(
|
|
764
|
+
&ws,
|
|
765
|
+
&json!({
|
|
766
|
+
"session_name": session,
|
|
767
|
+
"tmux_endpoint": endpoint,
|
|
768
|
+
"agents": {"node-a": agent_a, "node-b": agent_b}
|
|
769
|
+
}),
|
|
770
|
+
);
|
|
771
|
+
let report = producer_report_nodes(
|
|
772
|
+
endpoint,
|
|
773
|
+
&[
|
|
774
|
+
(
|
|
775
|
+
session.to_string(),
|
|
776
|
+
window_a.to_string(),
|
|
777
|
+
"%51".to_string(),
|
|
778
|
+
"pi\tα\nchannel".to_string(),
|
|
779
|
+
),
|
|
780
|
+
(
|
|
781
|
+
session.to_string(),
|
|
782
|
+
window_b.to_string(),
|
|
783
|
+
"%52".to_string(),
|
|
784
|
+
"pi β native".to_string(),
|
|
785
|
+
),
|
|
786
|
+
],
|
|
787
|
+
);
|
|
788
|
+
let (bin, log) = write_nodeprobe_reports(
|
|
789
|
+
&ws.join("probe"),
|
|
790
|
+
&[(endpoint.to_string(), report)],
|
|
791
|
+
);
|
|
792
|
+
let nodes = status_port::with_test_nodeprobe(bin.clone(), || {
|
|
793
|
+
json_nodes(cmd_status(&status_args(&ws, true, None, None)).expect("JSON status"))
|
|
794
|
+
});
|
|
795
|
+
let node_a = nodes.iter().find(|node| node["name"] == "node-a").unwrap();
|
|
796
|
+
assert_eq!(node_a["model"], "gpt\tα\nmax");
|
|
797
|
+
assert_eq!(node_a["effort"], "xhigh\nhigh");
|
|
798
|
+
assert_eq!(node_a["session_name"], "pi\tα\nchannel");
|
|
799
|
+
let target_a = format!("{session}:{window_a}.%51");
|
|
800
|
+
assert_eq!(
|
|
801
|
+
node_a["tmux_command"],
|
|
802
|
+
json!(format!(
|
|
803
|
+
"tmux -S {} attach -t {}",
|
|
804
|
+
shell_quote_for_status_test(endpoint),
|
|
805
|
+
shell_quote_for_status_test(&target_a)
|
|
806
|
+
))
|
|
807
|
+
);
|
|
808
|
+
let human = status_port::with_test_nodeprobe(bin, || {
|
|
809
|
+
human_text(cmd_status(&status_args(&ws, false, None, None)).expect("human status"))
|
|
810
|
+
});
|
|
811
|
+
let row_a = human
|
|
812
|
+
.lines()
|
|
813
|
+
.find(|line| line.contains("node-a"))
|
|
814
|
+
.unwrap_or("");
|
|
815
|
+
let row_b = human
|
|
816
|
+
.lines()
|
|
817
|
+
.find(|line| line.contains("node-b"))
|
|
818
|
+
.unwrap_or("");
|
|
819
|
+
let calls = logged_probe_calls(&log);
|
|
820
|
+
assert_eq!(calls.len(), 2, "one native query per status invocation");
|
|
821
|
+
assert!(
|
|
822
|
+
human
|
|
823
|
+
.matches(&format!("tmux -S {}", shell_quote_for_status_test(endpoint)))
|
|
824
|
+
.count()
|
|
825
|
+
== 1
|
|
826
|
+
&& row_a.contains("gpt\\tα\\nmax")
|
|
827
|
+
&& row_a.contains("xhigh\\nhigh")
|
|
828
|
+
&& row_a.contains("pi\\tα\\nchannel")
|
|
829
|
+
&& row_a.contains("win '甲")
|
|
830
|
+
&& row_a.contains("%51")
|
|
831
|
+
&& row_b.contains("grok\\tβ\\nxhigh")
|
|
832
|
+
&& row_b.contains("high\\tmax\\nlow")
|
|
833
|
+
&& row_a.matches("node-a").count() == 1
|
|
834
|
+
&& row_b.matches("node-b").count() == 1,
|
|
835
|
+
"human fields must be escaped without splitting node rows and the shared template must quote its context:\n{human}"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
215
839
|
#[test]
|
|
216
840
|
#[serial(status_brief)]
|
|
217
841
|
fn cmd_status_is_readonly_on_legacy_and_multi_team_state() {
|