fix(TASK-046): make inspection flags look-don't-touch and pin the contracts
- Skip the builtins bootstrap, function-binary build, and tool-scope rebuild for inspection-only runs (--info/--list-*/--mcp-list/ --list-secrets without --agent/--role): a pristine COYOTE_CONFIG_DIR now stays byte-empty (was 269 files written before dispatch) - Tolerate a model-less default config on the inspection surface via AppConfig::from_config_lenient + Model::default() in bootstrap; strict 'No available model' behavior unchanged for the REPL path and all mutating mcp/vault flags - --list-secrets degrades to an rc=0 informational message in sandbox mode (mutating vault flags still fail there), pinned by test - Add the 041.7-4 regression test: a malformed COYOTE_<NAME>_GLOBAL_TOOLS env override logs the debug message and leaves global_tools untouched - Adopt scripts/usage-probe-inspection.sh (10 flags: rc=0 + zero writes, output required only for --info) and add binary-level tests/inspection_flags.rs, incl. the agent+--info still-bootstraps pin
This commit is contained in:
9 files changed
+389
-31
No files matched your search
@@ -0,0 +1,124 @@
|
||||
//! Binary-level contract for inspection flags on a pristine config dir:
|
||||
//! exit 0 and NOTHING written into the config dir (no first-run wizard,
|
||||
//! no builtins bootstrap). Only --info guarantees output on stdout; the
|
||||
//! list flags legitimately emit an empty listing on a pristine dir, so
|
||||
//! their contract is exit 0 + zero writes. Vault-backed flags like
|
||||
//! --list-secrets depend on host state and are covered by
|
||||
//! scripts/usage-probe-inspection.sh instead. Agent runs, by contrast,
|
||||
//! must still bootstrap builtins even when combined with --info — see
|
||||
//! agent_info_on_empty_config_dir_bootstraps_builtins.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
fn fresh_config_dir(label: &str) -> PathBuf {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let tmp_dir = env::temp_dir().join(format!("coyote-inspection-{label}-{unique}"));
|
||||
fs::create_dir_all(&tmp_dir).unwrap();
|
||||
tmp_dir
|
||||
}
|
||||
|
||||
fn probe_inspection_flag(flag: &str, require_stdout: bool) {
|
||||
let tmp_dir = fresh_config_dir(flag.trim_start_matches("--"));
|
||||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_coyote"))
|
||||
.arg(flag)
|
||||
.env("COYOTE_CONFIG_DIR", &tmp_dir)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let leftover: Vec<_> = fs::read_dir(&tmp_dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().file_name())
|
||||
.collect();
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{flag}: expected exit 0 on an empty config dir, got {:?}\nstdout: {}\nstderr: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
if require_stdout {
|
||||
assert!(
|
||||
!output.stdout.is_empty(),
|
||||
"{flag}: expected some output on stdout"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
leftover.is_empty(),
|
||||
"{flag}: inspection flags must write nothing into the config dir: {leftover:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_on_empty_config_dir_writes_nothing() {
|
||||
probe_inspection_flag("--info", true);
|
||||
}
|
||||
|
||||
// List flags on a pristine dir correctly print an empty listing, so the
|
||||
// contract is exit 0 + zero writes only (no stdout assertion).
|
||||
|
||||
#[test]
|
||||
fn list_models_on_empty_config_dir_writes_nothing() {
|
||||
probe_inspection_flag("--list-models", false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_agents_on_empty_config_dir_writes_nothing() {
|
||||
probe_inspection_flag("--list-agents", false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mcp_list_on_empty_config_dir_writes_nothing() {
|
||||
probe_inspection_flag("--mcp-list", false);
|
||||
}
|
||||
|
||||
// The wizard bypass must not swallow the builtins bootstrap for agent runs:
|
||||
// `--agent <builtin> --info` on a totally fresh config dir has to install
|
||||
// the bundled definitions before it can render the agent readout.
|
||||
#[test]
|
||||
fn agent_info_on_empty_config_dir_bootstraps_builtins() {
|
||||
let tmp_dir = fresh_config_dir("agent-info");
|
||||
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_coyote"))
|
||||
.args(["--agent", "adversary", "--info"])
|
||||
.env("COYOTE_CONFIG_DIR", &tmp_dir)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let agents_dir = tmp_dir.join("agents");
|
||||
let builtins_installed =
|
||||
agents_dir.is_dir() && fs::read_dir(&agents_dir).unwrap().next().is_some();
|
||||
let _ = fs::remove_dir_all(&tmp_dir);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"--agent adversary --info: expected exit 0 on an empty config dir, got {:?}\nstdout: {}\nstderr: {}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(
|
||||
builtins_installed,
|
||||
"--agent adversary --info: expected builtin agents installed into {}",
|
||||
agents_dir.display()
|
||||
);
|
||||
// The --info readout echoes the loaded agent definition including its
|
||||
// global_tools, so a builtin tool name there proves the definitions were
|
||||
// loaded into the agent's tool scope, not just written to disk.
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
stdout.contains("fs_cat.sh"),
|
||||
"--agent adversary --info: expected builtin tool fs_cat.sh in the readout\nstdout: {stdout}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user