Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 76 additions & 76 deletions desktop/src-tauri/src/commands/agent_config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,7 @@ fn with_no_goose_config<T>(body: impl FnOnce() -> T) -> T {
}

fn goose_runtime() -> &'static KnownAcpRuntime {
&KnownAcpRuntime {
id: "goose",
label: "Goose",
commands: &["goose"],
aliases: &[],
avatar_url: "",
mcp_command: None,
mcp_hooks: false,
underlying_cli: None,
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
supports_acp_model_switching: false,
model_env_var: Some("GOOSE_MODEL"),
provider_env_var: Some("GOOSE_PROVIDER"),
provider_locked: false,
default_env: &[],
config_file_path: Some("~/.config/goose/config.yaml"),
config_file_format: Some("yaml"),
supports_acp_native_config: true,
thinking_env_var: Some("GOOSE_THINKING_EFFORT"),
max_tokens_env_var: Some("GOOSE_MAX_TOKENS"),
context_limit_env_var: Some("GOOSE_CONTEXT_LIMIT"),
max_rounds_env_var: None,
required_normalized_fields: &["model", "provider"],
login_hint: None,
auth_probe_args: None,
}
crate::managed_agents::known_acp_runtime_exact("goose").expect("goose must be in catalog")
}

fn agent_record() -> ManagedAgentRecord {
Expand Down Expand Up @@ -175,13 +143,15 @@ fn linked_stale_record_model_never_outranks_persona_model() {
record.model = Some("stale-explicit-model".to_string());
let personas = vec![persona_with_model("persona-model")];

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
)
});

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("persona-model"));
Expand All @@ -205,7 +175,9 @@ fn linked_blank_definition_model_falls_through_to_global_default() {
..Default::default()
};

let surface = resolve_config_surface(record, &personas, Some(goose_runtime()), None, &global);
let surface = with_no_goose_config(|| {
resolve_config_surface(record, &personas, Some(goose_runtime()), None, &global)
});

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("global-model"));
Expand All @@ -222,13 +194,15 @@ fn definition_less_explicit_record_model_keeps_buzz_explicit_origin() {
record.model = Some("explicit-model".to_string());
let personas = vec![persona_with_model("persona-model")];

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
)
});

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("explicit-model"));
Expand All @@ -249,13 +223,15 @@ fn pending_pick_keeps_explicit_x_and_does_not_surface_live_y() {
let personas: Vec<AgentDefinition> = vec![];
let cache = session_cache("model-y", false);

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
)
});
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-x"));
Expand All @@ -277,13 +253,15 @@ fn genuine_explicit_live_switch_renders_y_over_x_buzz_explicit_secondary() {
let personas: Vec<AgentDefinition> = vec![];
let cache = session_cache("model-y", true);

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
)
});
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-y"));
Expand Down Expand Up @@ -340,13 +318,15 @@ fn persona_linked_live_switch_keeps_persona_default_secondary() {
let personas = vec![persona_with_model("persona-model")];
let cache = session_cache("model-y", true);

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
)
});
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-y"));
Expand Down Expand Up @@ -375,13 +355,15 @@ fn global_default_live_switch_renders_global_model_as_secondary_global_default()
..Default::default()
};

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&global,
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&global,
)
});
let model = surface.normalized.model.expect("model resolved");

// Live model wins as primary.
Expand Down Expand Up @@ -623,6 +605,21 @@ fn baked_env_thinking_effort_is_unmasked() {
assert!(!effort.masked);
}

#[test]
fn baked_env_goose_thinking_effort_is_unmasked() {
// GOOSE_THINKING_EFFORT is a non-secret canonical enum (off/low/medium/high/max) —
// must be revealed so the native baked lookup in `bakedEnvHelpers.ts` can read
// it as inherited effort for Goose agents (plan v3 pass-3 ★ pin).
let entries = baked_env_from_map(&[("GOOSE_THINKING_EFFORT", "high")]);
assert_eq!(entries.len(), 1);
let effort = entries
.iter()
.find(|e| e.key == "GOOSE_THINKING_EFFORT")
.unwrap();
assert_eq!(effort.value, "high");
assert!(!effort.masked);
}

#[test]
fn baked_env_allowlist_is_case_insensitive() {
// Known-safe keys — case-insensitive match must allow them.
Expand All @@ -632,6 +629,9 @@ fn baked_env_allowlist_is_case_insensitive() {
assert!(super::is_safe_to_reveal("BUZZ_AGENT_MODEL"));
assert!(super::is_safe_to_reveal("buzz_agent_thinking_effort"));
assert!(super::is_safe_to_reveal("BUZZ_AGENT_THINKING_EFFORT"));
// Goose native effort key — derived from runtime declarations via all_known_effort_keys().
assert!(super::is_safe_to_reveal("goose_thinking_effort"));
assert!(super::is_safe_to_reveal("GOOSE_THINKING_EFFORT"));
assert!(super::is_safe_to_reveal("databricks_host"));
assert!(super::is_safe_to_reveal("DATABRICKS_HOST"));
assert!(super::is_safe_to_reveal("databricks_model"));
Expand Down
10 changes: 5 additions & 5 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ pub(crate) fn plan_adapter_install<'c>(
{
None
}
// Codex adapter is outdated: uninstall the old package first so npm
// doesn't hit EEXIST on the shared `codex-acp` bin-link, then install.
// Codex adapter outdated: uninstall old package first (npm refuses to overwrite a bin from another pkg).
Some(_) => Some(vec![
"npm uninstall -g @zed-industries/codex-acp",
"npm install -g @agentclientprotocol/codex-acp",
Expand Down Expand Up @@ -133,9 +132,8 @@ pub async fn save_custom_harness(
std::fs::create_dir_all(&custom_dir)
.map_err(|e| format!("failed to create custom_harnesses dir: {e}"))?;

// ── Phase 2+3: backup-swap write + rename (Windows-safe, rollback on failure)
// `save_and_warm` holds the persist mutex for the write + registry-warm pair
// so concurrent saves never produce a stale registry snapshot (B-6).
// Phase 2+3: backup-swap write + rename (Windows-safe, rollback on failure).
// `save_and_warm` holds the persist mutex so concurrent saves never produce a stale registry.
custom_harnesses::save_and_warm(&custom_dir, &definition, rename_old_id.as_deref())?;

// Resolve availability for the returned catalog entry.
Expand Down Expand Up @@ -165,6 +163,8 @@ pub async fn save_custom_harness(
model_env_var: None,
provider_env_var: None,
thinking_env_var: None,
accepted_effort_values: None,
effort_aliases: None,
max_tokens_env_var: None,
context_limit_env_var: None,
max_rounds_env_var: None,
Expand Down
65 changes: 65 additions & 0 deletions desktop/src-tauri/src/commands/agents_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,69 @@ mod tests {
assert_eq!(launch["policy_env"]["BUZZ_ACP_AGENTS"], "4");
assert_eq!(launch["owner_pubkey"], "owner-hex");
}

/// Deploy parity (plan v3 Delta 4): for a legacy-only Goose record, the bridged
/// descriptor feeds `launch.env` with the native key, while the separately-merged
/// top-level `env_vars` retains the legacy key untouched.
///
/// Contract: providers execute `launch`; top-level `env_vars` is compatibility
/// bookkeeping. This test pins that boundary.
#[test]
fn deploy_parity_launch_env_carries_native_goose_effort() {
use crate::managed_agents::known_acp_runtime_exact;
use crate::managed_agents::{
global_config::GlobalAgentConfig, resolve_effective_agent_env,
};

// Goose record with only legacy BUZZ_AGENT_THINKING_EFFORT.
let record: ManagedAgentRecord = serde_json::from_value(serde_json::json!({
"pubkey": "pk",
"name": "goose-agent",
"private_key_nsec": "",
"relay_url": "",
"acp_command": "goose-acp",
"agent_command": "goose",
"agent_args": [],
"mcp_command": "",
"turn_timeout_seconds": 320,
"parallelism": 1,
"respond_to": "owner-only",
"respond_to_allowlist": [],
"env_vars": { "BUZZ_AGENT_THINKING_EFFORT": "high" },
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}))
.unwrap();

let runtime = known_acp_runtime_exact("goose");
let global = GlobalAgentConfig::default();
// The descriptor (= what launch.env uses) comes from resolve_effective_agent_env.
let descriptor = resolve_effective_agent_env(&record, &[], runtime, &global);

// launch.env: legacy key translated → native key.
assert_eq!(
descriptor
.env
.get("GOOSE_THINKING_EFFORT")
.map(String::as_str),
Some("high"),
"launch.env must carry native GOOSE_THINKING_EFFORT"
);
assert!(
!descriptor.env.contains_key("BUZZ_AGENT_THINKING_EFFORT"),
"launch.env must not carry legacy key"
);

// top-level env_vars (unmodified raw input): legacy key is still there.
// (The deploy payload's `env_vars` field is merged_user_env of the raw record —
// the bridge only affects the descriptor/launch path.)
assert_eq!(
record
.env_vars
.get("BUZZ_AGENT_THINKING_EFFORT")
.map(String::as_str),
Some("high"),
"top-level env_vars retains legacy key as compatibility bookkeeping"
);
}
}
Loading
Loading