@team-agent/installer 0.5.107 → 0.5.109
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 +379 -70
- 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/provider/model_catalog.rs +343 -5
- 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/tmux_backend.rs +99 -2
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -218,7 +218,7 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
// Status brief is deliberately independent of the legacy RuntimeSnapshot:
|
|
221
|
-
// it performs one native tmux/process sample and exposes exactly
|
|
221
|
+
// it performs one native tmux/process sample and exposes exactly nine
|
|
222
222
|
// fields. `--summary` and `--detail` remain parser-compatible but do not
|
|
223
223
|
// re-enable history, runtime diagnostics, or reminder text.
|
|
224
224
|
if args.json {
|
|
@@ -290,7 +290,7 @@ pub(crate) fn default_help() -> String {
|
|
|
290
290
|
&["claim-leader", "takeover", "attach-leader"],
|
|
291
291
|
);
|
|
292
292
|
out.push_str("\nWorkflow: `team-agent models --provider pi`, then quick-start/add-agent/start-agent, then `team-agent send AGENT MESSAGE`.\n");
|
|
293
|
-
out.push_str("Model catalogs: pi, cursor_agent, codex, claude, claude_code.\n");
|
|
293
|
+
out.push_str("Model catalogs: pi, cursor_agent, codex, claude, claude_code, grok.\n");
|
|
294
294
|
out.push_str("Provider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
|
|
295
295
|
out.push_str("\nRun `team-agent <command> --help` for command flags.");
|
|
296
296
|
out
|
|
@@ -340,8 +340,8 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
340
340
|
)
|
|
341
341
|
.to_string(),
|
|
342
342
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
343
|
-
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出九字段:name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command;人读与 --json
|
|
344
|
-
Some("models") => "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, display name, and source aliases. Uses each provider's native catalog; Codex uses `codex debug models
|
|
343
|
+
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出九字段:name/provider/model/effort/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影字段(多节点人读紧凑显示 tmux_command)。--json 始终保留每节点完整可复制的 tmux_command;人读单节点保留完整旧格式,多节点按(socket, tmux session)显示每组一次共享 attach 模板,ATTACH列为<group>:<window>.<pane>,模板中的 <target> 替换为其 window.pane 部分。model/effort 为已接受启动配置,未设置为 null,不猜 provider 默认值;缺少可靠定位或原生 tmux/process 采样时显示 unknown,不推导attach信息。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
|
|
344
|
+
Some("models") => "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, display name, and source aliases. Uses each provider's native catalog; Cursor uses `agent --list-models`, Codex uses `codex debug models`, Claude uses SDK stream-json initialize, and Grok uses the PATH-first `grok models` command.".to_string(),
|
|
345
345
|
Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
|
|
346
346
|
Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
|
|
347
347
|
Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]".to_string(),
|
|
@@ -1358,6 +1358,13 @@ pub mod lifecycle_port {
|
|
|
1358
1358
|
.map(|path| json!({ "path": path }))
|
|
1359
1359
|
.collect::<Vec<_>>();
|
|
1360
1360
|
deadline.check("process_residuals")?;
|
|
1361
|
+
// Let already-protected-filtered shutdown roots converge before the final
|
|
1362
|
+
// snapshot, so a transient OS reaping window is not reported as a residue.
|
|
1363
|
+
wait_for_processes_gone(
|
|
1364
|
+
&root_pids,
|
|
1365
|
+
std::time::Duration::from_millis(500).min(deadline.remaining()),
|
|
1366
|
+
);
|
|
1367
|
+
deadline.check("process_residuals")?;
|
|
1361
1368
|
// C-①: the post-verify gets ONE fresh verification snapshot (reaps changed
|
|
1362
1369
|
// the world; #248 post-verify facts must be current, not the entry view).
|
|
1363
1370
|
let verify_table =
|
|
@@ -1936,6 +1943,10 @@ pub mod lifecycle_port {
|
|
|
1936
1943
|
}
|
|
1937
1944
|
Ok(())
|
|
1938
1945
|
}
|
|
1946
|
+
|
|
1947
|
+
fn remaining(&self) -> std::time::Duration {
|
|
1948
|
+
self.timeout.saturating_sub(self.start.elapsed())
|
|
1949
|
+
}
|
|
1939
1950
|
}
|
|
1940
1951
|
|
|
1941
1952
|
fn shutdown_state_for_team(workspace: &Path, team: Option<&str>) -> Result<Value, CliError> {
|
|
@@ -2239,9 +2250,70 @@ pub mod lifecycle_port {
|
|
|
2239
2250
|
}
|
|
2240
2251
|
}
|
|
2241
2252
|
|
|
2253
|
+
#[cfg(test)]
|
|
2254
|
+
#[derive(Clone, Copy)]
|
|
2255
|
+
struct ShutdownTestProbes {
|
|
2256
|
+
process_table: Option<fn() -> Result<Vec<String>, String>>,
|
|
2257
|
+
pid_is_alive: Option<fn(u32) -> bool>,
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
#[cfg(test)]
|
|
2261
|
+
std::thread_local! {
|
|
2262
|
+
static SHUTDOWN_TEST_PROBES: std::cell::Cell<ShutdownTestProbes> = const {
|
|
2263
|
+
std::cell::Cell::new(ShutdownTestProbes { process_table: None, pid_is_alive: None })
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
#[cfg(test)]
|
|
2268
|
+
struct ShutdownTestProbeRestore(ShutdownTestProbes);
|
|
2269
|
+
|
|
2270
|
+
#[cfg(test)]
|
|
2271
|
+
impl Drop for ShutdownTestProbeRestore {
|
|
2272
|
+
fn drop(&mut self) {
|
|
2273
|
+
SHUTDOWN_TEST_PROBES.with(|probes| probes.set(self.0));
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
#[cfg(test)]
|
|
2278
|
+
pub(super) fn with_process_table_test_probe<T>(
|
|
2279
|
+
probe: fn() -> Result<Vec<String>, String>,
|
|
2280
|
+
run: impl FnOnce() -> T,
|
|
2281
|
+
) -> T {
|
|
2282
|
+
let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
|
|
2283
|
+
let previous = probes.get();
|
|
2284
|
+
probes.set(ShutdownTestProbes { process_table: Some(probe), ..previous });
|
|
2285
|
+
previous
|
|
2286
|
+
});
|
|
2287
|
+
let _restore = ShutdownTestProbeRestore(previous);
|
|
2288
|
+
run()
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
#[cfg(test)]
|
|
2292
|
+
pub(super) fn with_pid_liveness_test_probe<T>(
|
|
2293
|
+
probe: fn(u32) -> bool,
|
|
2294
|
+
run: impl FnOnce() -> T,
|
|
2295
|
+
) -> T {
|
|
2296
|
+
let previous = SHUTDOWN_TEST_PROBES.with(|probes| {
|
|
2297
|
+
let previous = probes.get();
|
|
2298
|
+
probes.set(ShutdownTestProbes { pid_is_alive: Some(probe), ..previous });
|
|
2299
|
+
previous
|
|
2300
|
+
});
|
|
2301
|
+
let _restore = ShutdownTestProbeRestore(previous);
|
|
2302
|
+
run()
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2242
2305
|
/// swallow batch 1: the raw ps probe with an explicit error channel — a failed
|
|
2243
2306
|
/// probe must never masquerade as "no processes" (CLAUDE.md §5).
|
|
2244
2307
|
fn probed_process_table() -> Result<Vec<ProcessInfo>, String> {
|
|
2308
|
+
#[cfg(test)]
|
|
2309
|
+
if let Some(probe) = SHUTDOWN_TEST_PROBES.with(|probes| probes.get().process_table) {
|
|
2310
|
+
return probe().map(|lines| {
|
|
2311
|
+
lines
|
|
2312
|
+
.iter()
|
|
2313
|
+
.filter_map(|line| parse_process_info(line))
|
|
2314
|
+
.collect()
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2245
2317
|
match crate::os_probe::bounded_command_output_with_probe(
|
|
2246
2318
|
std::process::Command::new("ps").args(["-axo", "pid=,ppid=,pgid=,sess=,command="]),
|
|
2247
2319
|
"ps_table",
|
|
@@ -2696,20 +2768,31 @@ pub mod lifecycle_port {
|
|
|
2696
2768
|
// 1:1 to SIGTERM/SIGKILL, ProcessLiveness::Live == the previous
|
|
2697
2769
|
// `kill(pid, 0) == 0 || EPERM` branch).
|
|
2698
2770
|
|
|
2771
|
+
fn shutdown_pid_is_alive(pid: u32) -> bool {
|
|
2772
|
+
#[cfg(test)]
|
|
2773
|
+
if let Some(probe) = SHUTDOWN_TEST_PROBES.with(|probes| probes.get().pid_is_alive) {
|
|
2774
|
+
return probe(pid);
|
|
2775
|
+
}
|
|
2776
|
+
crate::platform::process::pid_is_alive(pid)
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2699
2779
|
fn wait_for_processes_gone(pids: &[u32], timeout: std::time::Duration) {
|
|
2700
2780
|
let start = std::time::Instant::now();
|
|
2701
2781
|
loop {
|
|
2702
2782
|
for pid in pids {
|
|
2703
2783
|
crate::platform::process::reap_child_if_possible(*pid);
|
|
2704
2784
|
}
|
|
2785
|
+
let elapsed = start.elapsed();
|
|
2705
2786
|
if !pids
|
|
2706
2787
|
.iter()
|
|
2707
|
-
.any(|pid|
|
|
2708
|
-
||
|
|
2788
|
+
.any(|pid| shutdown_pid_is_alive(*pid))
|
|
2789
|
+
|| elapsed >= timeout
|
|
2709
2790
|
{
|
|
2710
2791
|
return;
|
|
2711
2792
|
}
|
|
2712
|
-
std::thread::sleep(
|
|
2793
|
+
std::thread::sleep(
|
|
2794
|
+
std::time::Duration::from_millis(25).min(timeout.saturating_sub(elapsed)),
|
|
2795
|
+
);
|
|
2713
2796
|
}
|
|
2714
2797
|
}
|
|
2715
2798
|
|
|
@@ -2730,7 +2813,7 @@ pub mod lifecycle_port {
|
|
|
2730
2813
|
.collect::<std::collections::BTreeSet<_>>();
|
|
2731
2814
|
for pid in root_pids {
|
|
2732
2815
|
if !protected.contains_pid(*pid)
|
|
2733
|
-
&&
|
|
2816
|
+
&& shutdown_pid_is_alive(*pid)
|
|
2734
2817
|
&& seen.insert(*pid)
|
|
2735
2818
|
{
|
|
2736
2819
|
residuals.push(ProcessInfo {
|
|
@@ -357,8 +357,192 @@ mod tests {
|
|
|
357
357
|
cleanup_fixture(&path);
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
#[cfg(unix)]
|
|
361
|
+
#[test]
|
|
362
|
+
fn cursor_native_tip_fixture_works_through_public_cli_once() {
|
|
363
|
+
let agent = native_cli_fixture(
|
|
364
|
+
"agent",
|
|
365
|
+
include_bytes!("../provider/testdata/cursor-list-models-native.stdout"),
|
|
366
|
+
);
|
|
367
|
+
let output = run_public_models_cli(
|
|
368
|
+
&agent.program,
|
|
369
|
+
agent.response.as_deref(),
|
|
370
|
+
&["models", "--provider", "cursor_agent", "--json"],
|
|
371
|
+
);
|
|
372
|
+
let text = public_cli_text(&output);
|
|
373
|
+
let calls = std::fs::read_to_string(agent.program.with_extension("count")).unwrap_or_default();
|
|
374
|
+
drop(agent);
|
|
375
|
+
assert!(output.status.success(), "{text}");
|
|
376
|
+
assert_eq!(calls, "1:--list-models\n");
|
|
377
|
+
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
378
|
+
assert_eq!(value["schema_version"], "models.v1");
|
|
379
|
+
assert_eq!(value["provider"], "cursor_agent");
|
|
380
|
+
let models = value["models"].as_array().unwrap();
|
|
381
|
+
for exact in ["gpt-5.6-luna-low", "gpt-5.6-luna-xhigh"] {
|
|
382
|
+
assert!(models.iter().any(|model| model["model_id"] == exact));
|
|
383
|
+
}
|
|
384
|
+
assert_eq!(models[0]["model_id"], "auto");
|
|
385
|
+
assert_eq!(models[0]["default"], true);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
#[cfg(unix)]
|
|
389
|
+
#[test]
|
|
390
|
+
fn grok_public_cli_uses_native_catalog_and_keeps_shared_projection() {
|
|
391
|
+
let grok = native_cli_fixture(
|
|
392
|
+
"grok",
|
|
393
|
+
include_bytes!("../provider/testdata/grok-models-authenticated.stdout"),
|
|
394
|
+
);
|
|
395
|
+
let json = run_public_models_cli(
|
|
396
|
+
&grok.program,
|
|
397
|
+
grok.response.as_deref(),
|
|
398
|
+
&["models", "--provider", "grok", "--json"],
|
|
399
|
+
);
|
|
400
|
+
let human = run_public_models_cli(
|
|
401
|
+
&grok.program,
|
|
402
|
+
grok.response.as_deref(),
|
|
403
|
+
&["models", "--provider", "grok", "--search", "BUILD FAST"],
|
|
404
|
+
);
|
|
405
|
+
let no_match = run_public_models_cli(
|
|
406
|
+
&grok.program,
|
|
407
|
+
grok.response.as_deref(),
|
|
408
|
+
&["models", "--provider", "grok", "--search", "definitely-absent"],
|
|
409
|
+
);
|
|
410
|
+
let json_text = public_cli_text(&json);
|
|
411
|
+
let human_text = public_cli_text(&human);
|
|
412
|
+
let no_match_text = public_cli_text(&no_match);
|
|
413
|
+
let calls = std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
414
|
+
drop(grok);
|
|
415
|
+
assert!(json.status.success(), "{json_text}");
|
|
416
|
+
assert!(human.status.success(), "{human_text}");
|
|
417
|
+
assert!(no_match.status.success(), "{no_match_text}");
|
|
418
|
+
assert_eq!(calls, "1:models\n".repeat(3));
|
|
419
|
+
let value: Value = serde_json::from_str(json_text.trim()).unwrap();
|
|
420
|
+
assert_eq!(value["schema_version"], "models.v1");
|
|
421
|
+
assert_eq!(value["provider"], "grok");
|
|
422
|
+
assert_eq!(value["auth_basis"], "catalog_visibility");
|
|
423
|
+
let models = value["models"].as_array().unwrap();
|
|
424
|
+
assert_eq!(
|
|
425
|
+
models.iter().map(|model| model["model_id"].as_str().unwrap()).collect::<Vec<_>>(),
|
|
426
|
+
vec!["grok-4.7", "grok-4.7-build-fast", "grok-4.5", "grok-4.6"]
|
|
427
|
+
);
|
|
428
|
+
assert!(models.iter().all(|model| model["provider"] == "grok" && model["vendor"] == "xai" && model["aliases"] == json!([])));
|
|
429
|
+
assert_eq!(models[0]["default"], false);
|
|
430
|
+
assert_eq!(models[3]["default"], true);
|
|
431
|
+
assert!(human_text.contains("grok-4.7-build-fast"));
|
|
432
|
+
assert!(no_match_text.contains("No models matched --search"));
|
|
433
|
+
assert!(no_match_text.contains("auth: ok (catalog_visibility)"));
|
|
434
|
+
|
|
435
|
+
let grok = native_cli_fixture(
|
|
436
|
+
"grok",
|
|
437
|
+
include_bytes!("../provider/testdata/grok-models-unauthenticated.stdout"),
|
|
438
|
+
);
|
|
439
|
+
let changed_default = run_public_models_cli(
|
|
440
|
+
&grok.program,
|
|
441
|
+
grok.response.as_deref(),
|
|
442
|
+
&["models", "--provider", "grok", "--json"],
|
|
443
|
+
);
|
|
444
|
+
let changed_text = public_cli_text(&changed_default);
|
|
445
|
+
drop(grok);
|
|
446
|
+
assert!(changed_default.status.success(), "{changed_text}");
|
|
447
|
+
let changed: Value = serde_json::from_str(changed_text.trim()).unwrap();
|
|
448
|
+
assert_eq!(changed["models"][0]["model_id"], "grok-4.7");
|
|
449
|
+
assert_eq!(changed["models"][0]["default"], true);
|
|
450
|
+
assert_eq!(changed["models"][1]["default"], false);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
#[cfg(unix)]
|
|
454
|
+
#[test]
|
|
455
|
+
fn grok_invalid_native_catalogs_fail_closed_without_fallback() {
|
|
456
|
+
let cases: [(&str, Vec<u8>); 7] = [
|
|
457
|
+
("unknown row", b"You are logged in with grok.com.\n\nDefault model: grok-4.6\n\nAvailable models:\n - grok-4.6\n ? grok-4.5\n".to_vec()),
|
|
458
|
+
("duplicate", b"You are logged in with grok.com.\n\nDefault model: grok-4.6\n\nAvailable models:\n - grok-4.6\n - grok-4.6\n".to_vec()),
|
|
459
|
+
("invalid id", b"You are logged in with grok.com.\n\nDefault model: grok-4.6\n\nAvailable models:\n * grok bad (default)\n".to_vec()),
|
|
460
|
+
("control id", b"You are logged in with grok.com.\n\nDefault model: grok-4.6\n\nAvailable models:\n * grok-\x01evil (default)\n".to_vec()),
|
|
461
|
+
("empty", b"You are not authenticated.\n\nDefault model: grok-4.6\n\nAvailable models:\n".to_vec()),
|
|
462
|
+
("contradictory default", b"You are not authenticated.\n\nDefault model: grok-4.6\n\nAvailable models:\n * grok-4.7 (default)\n - grok-4.6\n".to_vec()),
|
|
463
|
+
("invalid utf8", vec![b'Y', b'o', b'u', 0xff]),
|
|
464
|
+
];
|
|
465
|
+
let mut outcomes = Vec::new();
|
|
466
|
+
for (name, bytes) in cases {
|
|
467
|
+
let grok = native_cli_fixture("grok", &bytes);
|
|
468
|
+
let output = run_public_models_cli(
|
|
469
|
+
&grok.program,
|
|
470
|
+
grok.response.as_deref(),
|
|
471
|
+
&["models", "--provider", "grok", "--json"],
|
|
472
|
+
);
|
|
473
|
+
let text = public_cli_text(&output);
|
|
474
|
+
let calls = std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
475
|
+
outcomes.push((name, output.status.success(), text, calls));
|
|
476
|
+
}
|
|
477
|
+
for (name, success, text, calls) in outcomes {
|
|
478
|
+
assert!(!success, "{name} catalog unexpectedly accepted: {text}");
|
|
479
|
+
assert_eq!(calls, "1:models\n", "{name}");
|
|
480
|
+
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
481
|
+
assert_eq!(value["schema_version"], "models.v1", "{name}");
|
|
482
|
+
assert_eq!(value["ok"], false, "{name}");
|
|
483
|
+
assert_eq!(value["models"], json!([]), "{name}");
|
|
484
|
+
assert!(!value["error"].as_str().unwrap().to_ascii_lowercase().contains("unsupported model provider"), "{name}: {text}");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#[cfg(unix)]
|
|
489
|
+
#[test]
|
|
490
|
+
fn grok_nonzero_native_command_is_not_a_successful_empty_catalog() {
|
|
491
|
+
let grok = CliFixture::new(fixture_program("grok", "printf 'private diagnostic' >&2\nexit 7"));
|
|
492
|
+
let output = run_public_models_cli(
|
|
493
|
+
&grok.program,
|
|
494
|
+
None,
|
|
495
|
+
&["models", "--provider", "grok", "--json"],
|
|
496
|
+
);
|
|
497
|
+
let text = public_cli_text(&output);
|
|
498
|
+
let calls = std::fs::read_to_string(grok.program.with_extension("count")).unwrap_or_default();
|
|
499
|
+
drop(grok);
|
|
500
|
+
assert!(!output.status.success(), "{text}");
|
|
501
|
+
assert_eq!(calls, "1:models\n");
|
|
502
|
+
assert!(!text.contains("private diagnostic"));
|
|
503
|
+
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
504
|
+
assert_eq!(value["ok"], false);
|
|
505
|
+
assert_eq!(value["models"], json!([]));
|
|
506
|
+
assert!(value["error"].as_str().unwrap().to_ascii_lowercase().contains("grok"), "{text}");
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#[cfg(unix)]
|
|
510
|
+
#[test]
|
|
511
|
+
fn grok_failure_help_and_unsupported_action_are_publicly_diagnostic() {
|
|
512
|
+
let missing = CliFixture::new(fixture_program("not-grok", "exit 0"));
|
|
513
|
+
let output = run_public_models_cli(
|
|
514
|
+
&missing.program,
|
|
515
|
+
None,
|
|
516
|
+
&["models", "--provider", "grok", "--json"],
|
|
517
|
+
);
|
|
518
|
+
let text = public_cli_text(&output);
|
|
519
|
+
let value: Value = serde_json::from_str(text.trim()).unwrap();
|
|
520
|
+
assert_eq!(value["ok"], false);
|
|
521
|
+
assert!(value["error"].as_str().unwrap().to_ascii_lowercase().contains("grok"), "{text}");
|
|
522
|
+
assert!(value["action"].as_str().unwrap().contains("grok"), "{text}");
|
|
523
|
+
assert!(crate::cli::emit::default_help().contains("grok"));
|
|
524
|
+
assert!(crate::cli::spec::command_spec("models").unwrap().usage.contains("grok"));
|
|
525
|
+
|
|
526
|
+
let unsupported = run_public_models_cli(
|
|
527
|
+
&missing.program,
|
|
528
|
+
None,
|
|
529
|
+
&["models", "--provider", "cloud", "--json"],
|
|
530
|
+
);
|
|
531
|
+
let unsupported_text = public_cli_text(&unsupported);
|
|
532
|
+
let unsupported_value: Value = serde_json::from_str(unsupported_text.trim()).unwrap();
|
|
533
|
+
drop(missing);
|
|
534
|
+
assert!(!output.status.success());
|
|
535
|
+
assert!(!unsupported.status.success());
|
|
536
|
+
assert!(unsupported_value["action"].as_str().unwrap().contains("grok"), "{unsupported_text}");
|
|
537
|
+
}
|
|
538
|
+
|
|
360
539
|
#[cfg(unix)]
|
|
361
540
|
fn fixture(body: &str) -> std::path::PathBuf {
|
|
541
|
+
fixture_program("pi", body)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
#[cfg(unix)]
|
|
545
|
+
fn fixture_program(program: &str, body: &str) -> std::path::PathBuf {
|
|
362
546
|
use std::os::unix::fs::PermissionsExt;
|
|
363
547
|
let sequence = FIXTURE_SEQUENCE.fetch_add(1, Ordering::Relaxed);
|
|
364
548
|
let root = std::env::current_exe()
|
|
@@ -366,7 +550,7 @@ mod tests {
|
|
|
366
550
|
.parent()
|
|
367
551
|
.unwrap()
|
|
368
552
|
.join(format!(
|
|
369
|
-
"team-agent-
|
|
553
|
+
"team-agent-{program}-fixture-{}-{}-{sequence}",
|
|
370
554
|
std::process::id(),
|
|
371
555
|
std::time::SystemTime::now()
|
|
372
556
|
.duration_since(std::time::UNIX_EPOCH)
|
|
@@ -374,7 +558,7 @@ mod tests {
|
|
|
374
558
|
.as_nanos()
|
|
375
559
|
));
|
|
376
560
|
std::fs::create_dir(&root).unwrap();
|
|
377
|
-
let path = root.join(
|
|
561
|
+
let path = root.join(program);
|
|
378
562
|
std::fs::write(
|
|
379
563
|
&path,
|
|
380
564
|
format!("#!/bin/sh\nprintf '%s\\n' \"$#:$1\" >> \"$0.count\"\n{body}\n"),
|
|
@@ -386,6 +570,71 @@ mod tests {
|
|
|
386
570
|
path
|
|
387
571
|
}
|
|
388
572
|
|
|
573
|
+
#[cfg(unix)]
|
|
574
|
+
struct CliFixture {
|
|
575
|
+
program: std::path::PathBuf,
|
|
576
|
+
response: Option<std::path::PathBuf>,
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#[cfg(unix)]
|
|
580
|
+
impl CliFixture {
|
|
581
|
+
fn new(program: std::path::PathBuf) -> Self {
|
|
582
|
+
Self { program, response: None }
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
#[cfg(unix)]
|
|
587
|
+
impl Drop for CliFixture {
|
|
588
|
+
fn drop(&mut self) {
|
|
589
|
+
cleanup_fixture(&self.program);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
#[cfg(unix)]
|
|
594
|
+
fn native_cli_fixture(program: &str, bytes: &[u8]) -> CliFixture {
|
|
595
|
+
let program = fixture_program(program, "exec /bin/cat \"$TEAM_AGENT_MODELS_FIXTURE\"");
|
|
596
|
+
let mut fixture = CliFixture::new(program);
|
|
597
|
+
let response = fixture.program.with_extension("stdout");
|
|
598
|
+
std::fs::write(&response, bytes).unwrap();
|
|
599
|
+
fixture.response = Some(response);
|
|
600
|
+
fixture
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#[cfg(unix)]
|
|
604
|
+
fn run_public_models_cli(
|
|
605
|
+
path: &Path,
|
|
606
|
+
response: Option<&Path>,
|
|
607
|
+
args: &[&str],
|
|
608
|
+
) -> std::process::Output {
|
|
609
|
+
let mut command = std::process::Command::new(std::env::current_exe().unwrap());
|
|
610
|
+
command
|
|
611
|
+
.args([
|
|
612
|
+
"--exact",
|
|
613
|
+
"cli::models::tests::real_dispatcher_child_helper",
|
|
614
|
+
"--nocapture",
|
|
615
|
+
])
|
|
616
|
+
.env("TEAM_AGENT_MODELS_CHILD", "1")
|
|
617
|
+
.env("TEAM_AGENT_MODELS_ARGS", serde_json::to_string(args).unwrap())
|
|
618
|
+
.env("PATH", path.parent().unwrap())
|
|
619
|
+
.env_remove("PI_PROVIDER")
|
|
620
|
+
.env_remove("PI_MODEL")
|
|
621
|
+
.env_remove("TEAM_AGENT_MODELS_FIXTURE");
|
|
622
|
+
if let Some(response) = response {
|
|
623
|
+
command.env("TEAM_AGENT_MODELS_FIXTURE", response);
|
|
624
|
+
}
|
|
625
|
+
command.output().unwrap()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
#[cfg(unix)]
|
|
629
|
+
fn public_cli_text(output: &std::process::Output) -> String {
|
|
630
|
+
String::from_utf8(output.stdout.clone())
|
|
631
|
+
.unwrap()
|
|
632
|
+
.split("__TEAM_AGENT_MODELS_CLI_OUTPUT_v1__\n")
|
|
633
|
+
.nth(1)
|
|
634
|
+
.expect("public CLI child output marker")
|
|
635
|
+
.to_string()
|
|
636
|
+
}
|
|
637
|
+
|
|
389
638
|
#[cfg(unix)]
|
|
390
639
|
fn fixture_with_sidecars(body: &str) -> std::path::PathBuf {
|
|
391
640
|
let path = fixture(body);
|
|
@@ -968,7 +1217,7 @@ mod tests {
|
|
|
968
1217
|
);
|
|
969
1218
|
assert_eq!(
|
|
970
1219
|
crate::cli::spec::command_spec("models").unwrap().usage,
|
|
971
|
-
"usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]"
|
|
1220
|
+
"usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]"
|
|
972
1221
|
);
|
|
973
1222
|
}
|
|
974
1223
|
|
|
@@ -147,7 +147,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
147
147
|
CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|
|
148
148
|
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
149
149
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
150
|
-
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
150
|
+
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
151
151
|
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
152
152
|
CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
|
|
153
153
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
|