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
@@ -1475,6 +1475,33 @@ global_hooks:
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn load_envs_ignores_malformed_global_tools_and_logs_debug() {
|
||||
crate::testing::install_log_collector();
|
||||
let name = "gt-malformed-envtest";
|
||||
let yaml = format!("name: {name}\ninstructions: hi\nglobal_tools:\n - keep_tool.sh\n");
|
||||
let mut config: AgentConfig = serde_yaml::from_str(&yaml).unwrap();
|
||||
let env_name = normalize_env_name(&format!("{name}_global_tools"));
|
||||
let _guard = crate::testing::EnvVarGuard::set(&env_name, "{not-json");
|
||||
|
||||
config.load_envs(&AppConfig::default());
|
||||
|
||||
assert_eq!(
|
||||
config.global_tools,
|
||||
vec!["keep_tool.sh"],
|
||||
"a malformed override must leave the existing value untouched"
|
||||
);
|
||||
let debugs = crate::testing::debug_snapshot();
|
||||
assert!(
|
||||
debugs.iter().any(|message| {
|
||||
message.contains("Ignoring malformed global_tools env override")
|
||||
&& message.contains(name)
|
||||
}),
|
||||
"expected a debug log for the malformed override: {debugs:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_graph_carries_hooks_and_global_hooks() {
|
||||
let yaml = "\
|
||||
|
||||
@@ -210,6 +210,18 @@ impl Default for AppConfig {
|
||||
|
||||
impl AppConfig {
|
||||
pub fn from_config(config: super::Config) -> Result<Self> {
|
||||
Self::from_config_impl(config, true)
|
||||
}
|
||||
|
||||
/// Like [`Self::from_config`], but tolerates a config with no resolvable
|
||||
/// model: `model_id` stays empty instead of failing with "No available
|
||||
/// model". For inspection readouts (--info, --list-*, --mcp-list,
|
||||
/// --list-secrets), which render model fields as absent/default.
|
||||
pub fn from_config_lenient(config: super::Config) -> Result<Self> {
|
||||
Self::from_config_impl(config, false)
|
||||
}
|
||||
|
||||
fn from_config_impl(config: super::Config, require_model: bool) -> Result<Self> {
|
||||
let mut app_config = Self {
|
||||
model_id: config.model_id,
|
||||
temperature: config.temperature,
|
||||
@@ -306,7 +318,13 @@ impl AppConfig {
|
||||
}
|
||||
app_config.setup_document_loaders();
|
||||
app_config.setup_user_agent();
|
||||
app_config.resolve_model()?;
|
||||
if require_model {
|
||||
app_config.resolve_model()?;
|
||||
} else {
|
||||
// Model-less is fine here: model_id stays empty and inspection
|
||||
// callers substitute Model::default().
|
||||
let _ = app_config.resolve_model();
|
||||
}
|
||||
app_config.validate_reasoning_effort()?;
|
||||
Ok(app_config)
|
||||
}
|
||||
@@ -1269,6 +1287,18 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_errors_when_no_model_available() {
|
||||
let result = AppConfig::from_config(Config::default());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_config_lenient_tolerates_missing_model() {
|
||||
let app = AppConfig::from_config_lenient(Config::default()).unwrap();
|
||||
assert!(app.model_id.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_model_keeps_explicit_model_id() {
|
||||
let mut app = AppConfig {
|
||||
|
||||
@@ -43,6 +43,7 @@ impl AppState {
|
||||
log_path: Option<PathBuf>,
|
||||
start_mcp_servers: bool,
|
||||
lenient_vault: bool,
|
||||
inspection: bool,
|
||||
abort_signal: AbortSignal,
|
||||
) -> Result<Self> {
|
||||
let vault = if lenient_vault {
|
||||
@@ -74,7 +75,13 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
let mut functions = Functions::init(config.visible_tools.as_deref())?;
|
||||
// Inspection-only runs are look-don't-touch readouts: skip the
|
||||
// function-binary prune/build, which writes into the config dir.
|
||||
let mut functions = if inspection {
|
||||
Functions::default()
|
||||
} else {
|
||||
Functions::init(config.visible_tools.as_deref())?
|
||||
};
|
||||
if !mcp_registry.is_empty() && config.mcp_server_support {
|
||||
functions.append_mcp_meta_functions(mcp_registry.server_features());
|
||||
}
|
||||
|
||||
@@ -417,7 +417,13 @@ impl RequestContext {
|
||||
working_mode: WorkingMode,
|
||||
info_flag: bool,
|
||||
) -> Result<Self> {
|
||||
let model = Model::retrieve_model(&app.config, &app.config.model_id, ModelType::Chat)?;
|
||||
// A lenient AppConfig (inspection-only run, no resolvable model)
|
||||
// leaves model_id empty; render those readouts with a default model.
|
||||
let model = if info_flag && app.config.model_id.is_empty() {
|
||||
Model::default()
|
||||
} else {
|
||||
Model::retrieve_model(&app.config, &app.config.model_id, ModelType::Chat)?
|
||||
};
|
||||
|
||||
let mut functions = app.functions.clone();
|
||||
if working_mode.is_repl() {
|
||||
|
||||
Reference in new issue
Block a user