feat: add --no-workspace-macros opt-out for workspace macro loading
Mirrors --no-workspace-mcp exactly: a CLI-only flag backed by an AppConfig field (default false) that disables .coyote/macros in both the resolved macro policy and Macro::load's workspace-then-global preference, so the two always agree (custom-commands design §5).
This commit is contained in:
@@ -96,6 +96,9 @@ pub struct Cli {
|
||||
/// Disable loading workspace MCP servers from .coyote/mcp.json, .coyote/.mcp.json, or .mcp.json
|
||||
#[arg(long)]
|
||||
pub no_workspace_mcp: bool,
|
||||
/// Disable loading workspace macros from .coyote/macros
|
||||
#[arg(long)]
|
||||
pub no_workspace_macros: bool,
|
||||
/// Disable memory for this invocation
|
||||
#[arg(long)]
|
||||
pub no_memory: bool,
|
||||
|
||||
@@ -98,6 +98,7 @@ pub struct AppConfig {
|
||||
pub user_agent: Option<String>,
|
||||
pub save_shell_history: bool,
|
||||
pub no_workspace_mcp: bool,
|
||||
pub no_workspace_macros: bool,
|
||||
pub sync_models_url: Option<String>,
|
||||
|
||||
pub clients: Vec<ClientConfig>,
|
||||
@@ -181,6 +182,7 @@ impl Default for AppConfig {
|
||||
user_agent: None,
|
||||
save_shell_history: true,
|
||||
no_workspace_mcp: false,
|
||||
no_workspace_macros: false,
|
||||
sync_models_url: None,
|
||||
|
||||
clients: vec![],
|
||||
@@ -266,6 +268,7 @@ impl AppConfig {
|
||||
user_agent: config.user_agent,
|
||||
save_shell_history: config.save_shell_history,
|
||||
no_workspace_mcp: false,
|
||||
no_workspace_macros: false,
|
||||
sync_models_url: config.sync_models_url,
|
||||
|
||||
clients: config.clients,
|
||||
|
||||
@@ -855,6 +855,39 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn effective_honors_no_workspace_macros() {
|
||||
with_macro_dirs(|workspace_root, global| {
|
||||
let macros_subdir = workspace_root.join("macros");
|
||||
fs::create_dir_all(¯os_subdir).unwrap();
|
||||
write_macro(¯os_subdir, "shared", VALID_YAML);
|
||||
write_macro(¯os_subdir, "ws-only", VALID_YAML);
|
||||
write_macro(global, "shared", VALID_YAML);
|
||||
write_macro(global, "global-only", VALID_YAML);
|
||||
|
||||
with_macro_dir_envs(workspace_root, global, || {
|
||||
let config = AppConfig::default();
|
||||
|
||||
let policy = MacroPolicy::effective(&config, None, None, None, &[], false);
|
||||
assert_eq!(
|
||||
policy.find("shared").unwrap().source,
|
||||
Some(MacroSource::Workspace)
|
||||
);
|
||||
assert!(policy.find("ws-only").is_some());
|
||||
assert!(policy.find("global-only").is_some());
|
||||
|
||||
let policy = MacroPolicy::effective(&config, None, None, None, &[], true);
|
||||
assert_eq!(
|
||||
policy.find("shared").unwrap().source,
|
||||
Some(MacroSource::Global)
|
||||
);
|
||||
assert!(policy.find("ws-only").is_none());
|
||||
assert!(policy.find("global-only").is_some());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn effective_resolves_role_session_and_global_levels() {
|
||||
|
||||
+68
-3
@@ -25,7 +25,7 @@ pub async fn macro_execute(
|
||||
if ctx.in_non_isolated_macro() {
|
||||
bail!("nested macros not allowed in non-isolated mode");
|
||||
}
|
||||
let macro_value = Macro::load(name)?;
|
||||
let macro_value = Macro::load(name, ctx.app.config.no_workspace_macros)?;
|
||||
let (mut new_args, text) = split_args_text(args.unwrap_or_default(), cfg!(windows));
|
||||
if !text.is_empty() {
|
||||
new_args.push(text.to_string());
|
||||
@@ -140,9 +140,9 @@ pub struct Macro {
|
||||
}
|
||||
|
||||
impl Macro {
|
||||
pub fn load(name: &str) -> Result<Macro> {
|
||||
pub fn load(name: &str, no_workspace_macros: bool) -> Result<Macro> {
|
||||
let workspace_path = paths::workspace_macros_dir().join(format!("{name}.yaml"));
|
||||
let path = if workspace_path.exists() {
|
||||
let path = if !no_workspace_macros && workspace_path.exists() {
|
||||
workspace_path
|
||||
} else {
|
||||
paths::macro_file(name)
|
||||
@@ -307,6 +307,71 @@ mod tests {
|
||||
write(&path, content).unwrap();
|
||||
}
|
||||
|
||||
/// Sets up a temp workspace macros dir and a temp global macros dir, each
|
||||
/// containing a `shared` macro whose `description` names its source, and
|
||||
/// points the workspace/global dir env overrides at them for `f`.
|
||||
fn with_macro_load_envs<F: FnOnce()>(f: F) {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let root = env::temp_dir().join(format!("coyote-macro-load-tests-{unique}"));
|
||||
let workspace_root = root.join("workspace");
|
||||
let workspace_macros = workspace_root.join("macros");
|
||||
let global = root.join("global");
|
||||
create_dir_all(&workspace_macros).unwrap();
|
||||
create_dir_all(&global).unwrap();
|
||||
write(
|
||||
workspace_macros.join("shared.yaml"),
|
||||
"description: workspace\nsteps:\n - \".help\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
write(
|
||||
global.join("shared.yaml"),
|
||||
"description: global\nsteps:\n - \".help\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ws_env = get_env_name("workspace_config_dir");
|
||||
let global_env = get_env_name("macros_dir");
|
||||
let prev_ws = env::var_os(&ws_env);
|
||||
let prev_global = env::var_os(&global_env);
|
||||
unsafe {
|
||||
env::set_var(&ws_env, &workspace_root);
|
||||
env::set_var(&global_env, &global);
|
||||
}
|
||||
f();
|
||||
unsafe {
|
||||
match prev_ws {
|
||||
Some(v) => env::set_var(&ws_env, v),
|
||||
None => env::remove_var(&ws_env),
|
||||
}
|
||||
match prev_global {
|
||||
Some(v) => env::set_var(&global_env, v),
|
||||
None => env::remove_var(&global_env),
|
||||
}
|
||||
}
|
||||
let _ = remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn load_prefers_workspace_over_global_by_default() {
|
||||
with_macro_load_envs(|| {
|
||||
let loaded = Macro::load("shared", false).unwrap();
|
||||
assert_eq!(loaded.description.as_deref(), Some("workspace"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn load_skips_workspace_when_no_workspace_macros() {
|
||||
with_macro_load_envs(|| {
|
||||
let loaded = Macro::load("shared", true).unwrap();
|
||||
assert_eq!(loaded.description.as_deref(), Some("global"));
|
||||
});
|
||||
}
|
||||
|
||||
/// Drives a macro-execution future to completion on a thread with extra
|
||||
/// stack headroom: nested `run_repl_command` poll frames are deep in
|
||||
/// debug builds and overflow the 2 MiB default test-thread stack.
|
||||
|
||||
@@ -2505,7 +2505,7 @@ impl RequestContext {
|
||||
self.agent.as_ref(),
|
||||
self.session.as_ref(),
|
||||
&crate::repl::builtin_command_names(),
|
||||
false,
|
||||
self.app.config.no_workspace_macros,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -222,6 +222,9 @@ async fn main() -> Result<()> {
|
||||
if cli.no_workspace_mcp {
|
||||
app_config.no_workspace_mcp = true;
|
||||
}
|
||||
if cli.no_workspace_macros {
|
||||
app_config.no_workspace_macros = true;
|
||||
}
|
||||
let app_config: Arc<AppConfig> = Arc::new(app_config);
|
||||
let app_state: Arc<AppState> = Arc::new(
|
||||
AppState::init(
|
||||
|
||||
Reference in New Issue
Block a user