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:
Dark-Alex-17 committed 2026-09-16 14:02:42 -06:00
1 parent 4087f3b3d0
commit 6514588e05
12 files changed
+282 -191

No files matched your search

+7 -4
View File
@@ -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) {
+4
View File
@@ -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() {
+65
View File
@@ -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));
+7 -7
View File
@@ -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)")