fix(TASK-046): engine polish + wizard bypass + ETXTBSY retry
- unify hook base-env assembly on hooks::base_envs_parts (drop the hand-copied hook_base_envs in function/agents.rs; graph executor and spawn-result hooks now share the single construction site, test-pinned) - factor the duplicated job.started dispatch in jobs.rs handle_start into one fire_job_started helper - COYOTE_LLM_ERROR now carries the immediate error (to_string()), matching COYOTE_TOOL_ERROR; test pins the new format - remove all stale #[allow(dead_code)] markers in src/hooks.rs - test infra: rename install_warn_collector -> install_log_collector, add poison-symmetric warn_snapshot/debug_snapshot helpers, hoist a shared EnvVarGuard (TestConfigDirGuard now builds on it), add a same-fire payload-uniqueness test, and give global_tools env overrides the same malformed-JSON debug diagnostic as global_hooks - inspection-only flags (--info et al) bypass the first-run wizard: missing config + info_flag returns Config::default() without writing anything, in both the sandbox and interactive arms (tests included) - bound the shim-relocation test's exec in an ETXTBSY retry (10x50ms) - document the intended 'roles' reserved-name asymmetry at resolve_bundle_name's gate
This commit is contained in:
12 files changed
+282
-191
No files matched your search
+7
-4
@@ -1019,10 +1019,13 @@ impl AgentConfig {
|
||||
if let Some(v) = read_env_value::<String>(&with_prefix("reasoning_effort")) {
|
||||
self.reasoning_effort = v;
|
||||
}
|
||||
if let Ok(v) = env::var(with_prefix("global_tools"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.global_tools = v;
|
||||
if let Ok(v) = env::var(with_prefix("global_tools")) {
|
||||
match serde_json::from_str(&v) {
|
||||
Ok(v) => self.global_tools = v,
|
||||
Err(err) => {
|
||||
debug!("Ignoring malformed global_tools env override for agent '{name}': {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(v) = env::var(with_prefix("global_hooks")) {
|
||||
match serde_json::from_str(&v) {
|
||||
|
||||
@@ -241,6 +241,10 @@ impl BundleStore {
|
||||
same_name_other_source: None,
|
||||
};
|
||||
|
||||
// Reserved names are the `--install-builtins` categories. `roles` is
|
||||
// deliberately NOT among them: builtin roles are not installable that
|
||||
// way (some are coupled to flag-specific coyote behavior), so the
|
||||
// asymmetry with InstallFilter is intended.
|
||||
let reserved = AssetCategory::parse(&base).is_some();
|
||||
let collision = self.source_of_other_bundle(&base, &canonical);
|
||||
if reserved || collision.is_some() {
|
||||
|
||||
@@ -734,6 +734,11 @@ impl Config {
|
||||
|
||||
if env::var_os(SANDBOX_ENV_FLAG).is_some() {
|
||||
if !config_path.exists() {
|
||||
// Inspection-only flags are look-don't-touch: no first-run
|
||||
// config creation, nothing written.
|
||||
if info_flag {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
create_config_file(&config_path).await?;
|
||||
}
|
||||
|
||||
@@ -748,6 +753,12 @@ impl Config {
|
||||
{
|
||||
Some(v) => (Self::load_dynamic(&v)?, String::new()),
|
||||
None => {
|
||||
// Same look-don't-touch rule: never run the interactive
|
||||
// first-run wizard (or write a config) for a flag that
|
||||
// only inspects state.
|
||||
if info_flag {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
if *IS_STDOUT_TERMINAL {
|
||||
create_config_file(&config_path).await?;
|
||||
}
|
||||
@@ -1640,6 +1651,60 @@ hooks:
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn info_flag_with_missing_config_skips_wizard_and_writes_nothing() {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let tmp_dir = env::temp_dir().join(format!("coyote-info-bypass-{unique}"));
|
||||
create_dir_all(&tmp_dir).unwrap();
|
||||
let _config_dir = crate::testing::EnvVarGuard::set(get_env_name("config_dir"), &tmp_dir);
|
||||
let _config_file = crate::testing::EnvVarGuard::unset(get_env_name("config_file"));
|
||||
let _sandbox = crate::testing::EnvVarGuard::unset(crate::sandbox::SANDBOX_ENV_FLAG);
|
||||
let _provider = crate::testing::EnvVarGuard::unset(get_env_name("provider"));
|
||||
let _platform = crate::testing::EnvVarGuard::unset(get_env_name("platform"));
|
||||
|
||||
let result = Config::load_with_interpolation(true).await;
|
||||
|
||||
let leftover: Vec<_> = std::fs::read_dir(&tmp_dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().file_name())
|
||||
.collect();
|
||||
let _ = std::fs::remove_dir_all(&tmp_dir);
|
||||
result.expect("inspection flags must load a default config without a config file");
|
||||
assert!(
|
||||
leftover.is_empty(),
|
||||
"inspection flags must not write anything: {leftover:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn sandbox_info_flag_with_missing_config_writes_nothing() {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let tmp_dir = env::temp_dir().join(format!("coyote-sandbox-info-bypass-{unique}"));
|
||||
create_dir_all(&tmp_dir).unwrap();
|
||||
let config_path = tmp_dir.join("config.yaml");
|
||||
let _config_file =
|
||||
crate::testing::EnvVarGuard::set(get_env_name("config_file"), &config_path);
|
||||
let _sandbox = crate::testing::EnvVarGuard::set(crate::sandbox::SANDBOX_ENV_FLAG, "1");
|
||||
|
||||
let result = Config::load_with_interpolation(true).await;
|
||||
|
||||
let created = config_path.exists();
|
||||
let _ = std::fs::remove_dir_all(&tmp_dir);
|
||||
result.expect("inspection flags must load a default config in sandbox mode too");
|
||||
assert!(
|
||||
!created,
|
||||
"inspection flags must not create the sandbox config file"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unloadable_override_is_stale() {
|
||||
assert!(models_override_is_stale(false, Some(SystemTime::now()), 24));
|
||||
|
||||
@@ -918,7 +918,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::mcp_tool_policy::LayerSource;
|
||||
use crate::function::ToolCall;
|
||||
use crate::testing::{install_warn_collector, warn_messages};
|
||||
use crate::testing::{install_log_collector, warn_snapshot};
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
#[test]
|
||||
@@ -1083,7 +1083,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_items_warns_when_resource_listing_fails() {
|
||||
install_warn_collector();
|
||||
install_log_collector();
|
||||
let fixture = FixtureServer {
|
||||
resources_capability: true,
|
||||
fail_resource_listings: true,
|
||||
@@ -1093,7 +1093,7 @@ mod tests {
|
||||
|
||||
runtime.catalog_items("fixture").await.unwrap();
|
||||
|
||||
let messages = warn_messages().lock().unwrap();
|
||||
let messages = warn_snapshot();
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
@@ -1547,7 +1547,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn prompt_catalog_degrades_when_one_server_fails() {
|
||||
install_warn_collector();
|
||||
install_log_collector();
|
||||
let healthy = FixtureServer {
|
||||
prompts_capability: true,
|
||||
..Default::default()
|
||||
@@ -1564,7 +1564,7 @@ mod tests {
|
||||
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].server, "fixture");
|
||||
let messages = warn_messages().lock().unwrap();
|
||||
let messages = warn_snapshot();
|
||||
assert!(
|
||||
messages
|
||||
.iter()
|
||||
@@ -1794,7 +1794,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn catalog_items_warns_on_dead_context_patterns() {
|
||||
install_warn_collector();
|
||||
install_log_collector();
|
||||
let (mut runtime, _server) = fixture_runtime(FixtureServer::default()).await;
|
||||
let mut filter = single_layer_filter(LayerSource::Global, &["*"]);
|
||||
filter.push_layer(LayerSource::Session, &["zzz_*".to_string()]);
|
||||
@@ -1802,7 +1802,7 @@ mod tests {
|
||||
|
||||
runtime.catalog_items("fixture").await.unwrap();
|
||||
|
||||
let messages = warn_messages().lock().unwrap();
|
||||
let messages = warn_snapshot();
|
||||
assert!(
|
||||
messages.iter().any(|msg| msg.contains("'zzz_*'")
|
||||
&& msg.contains("session (.set)")
|
||||
|
||||
Reference in new issue
Block a user