Files
buzz/desktop/src-tauri/src/migration_avatar_tests.rs
Logan Johnson dd1c2afc2a Discover alternate Buzz ACP commands (#6948)
🤖 I’m Larry.

## Summary
Managed-agent definitions can now choose which installed ACP transport
will launch the agent. The choice sits beside **Agent harness** while
creating or editing an agent, so it is made before first deployment and
remains visible afterward.

Buzz discovers executable commands named `buzz-*-acp`, keeps **Buzz ACP
(default)** as the safe default, and preserves existing custom command
values. Discovery and launch share the same resolver, so a command
offered in the picker is the command Buzz will execute.

## UX walkthrough

### 1. The ACP command picker appears while creating the agent
The default is explicit, and the helper text explains that this
selection controls deployment.

![Create-agent form with the ACP command picker beside Agent
harness](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/01-persona-create-acp-selector.png)

### 2. Installed wrapper commands are offered by name
Here Buzz has discovered `buzz-janet-acp` alongside the stock transport.

![ACP command menu offering Buzz ACP and
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/02-discovered-wrapper-dropdown.png)

### 3. The wrapper is selected before deployment

![Create-agent form with buzz-janet-acp
selected](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/03-wrapper-selected-before-deploy.png)

### 4. The deployed profile reports the effective command

![Deployed agent Runtime profile showing
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/04-deployed-profile-effective-acp.png)

### 5. Editing the deployed agent reopens its definition with the choice
preserved

![Edit-agent form retaining
buzz-janet-acp](https://d24qwcpro867f5.cloudfront.net/repos/buzz/prs/6948/05-edit-shows-persisted-wrapper.png)

## Details
- Definitions carry the selected ACP command through relay publication,
snapshots, imports, team adoption, deployment, and later edits.
- Existing definitions without this field continue to use `buzz-acp`.
- Unknown persisted commands remain visible but unavailable rather than
being silently replaced.
- Discovery is read-only and never executes candidates. It filters
non-files and non-executables, supports Windows `.exe`, `.cmd`, and
`.bat` shims, deduplicates aliases, and returns a stable sorted list.
- Command names remain portable; Buzz does not persist machine-specific
absolute paths.

---------

Signed-off-by: Logan Johnson <loganj@squareup.com>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@nostr>
Co-authored-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
2026-09-24 11:33:24 -04:00

337 lines
13 KiB
Rust

use super::*;
#[test]
fn refresh_builtin_agent_avatars_updates_seeded_values_and_preserves_customizations() {
use sha2::{Digest as _, Sha256};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let old_fizz = "data:image/png;base64,old-fizz";
let old_honey = "data:image/png;base64,old-honey";
let fizz_hash = hex::encode(Sha256::digest(old_fizz.as_bytes()));
let honey_hash = hex::encode(Sha256::digest(old_honey.as_bytes()));
let legacy_avatars = [
LegacyBuiltInAvatar {
persona_id: "builtin:fizz",
data_url_sha256: fizz_hash.as_str(),
sanitized_media_sha256: "",
persona_content_hash: "",
},
LegacyBuiltInAvatar {
persona_id: "builtin:honey",
data_url_sha256: honey_hash.as_str(),
sanitized_media_sha256: "",
persona_content_hash: "",
},
];
let definition = crate::managed_agents::AgentDefinition {
session_policy: Default::default(),
description: None,
id: "builtin:fizz".to_string(),
display_name: "Fizz".to_string(),
avatar_url: Some(old_fizz.to_string()),
system_prompt: "A customized built-in prompt".to_string(),
acp_command: None,
runtime: Some("goose".to_string()),
model: Some("test-model".to_string()),
provider: Some("test-provider".to_string()),
name_pool: vec!["Fizzy".to_string()],
is_builtin: true,
is_active: true,
shared: false,
source_team: None,
source_team_persona_slug: None,
catalog_source: None,
team_catalog_source: None,
env_vars: Default::default(),
respond_to: None,
respond_to_allowlist: Vec::new(),
parallelism: None,
created_at: "before".to_string(),
updated_at: "before".to_string(),
};
let old_persona_version = crate::managed_agents::persona_events::persona_content_hash(
&crate::managed_agents::persona_events::persona_event_content(&definition),
);
let mut definition_record = serde_json::to_value(definition.into_agent_record()).unwrap();
definition_record["future_definition_field"] = serde_json::json!("preserved");
let instance =
|pubkey: &str, persona_id: &str, avatar_url: &str, persona_source_version: &str| {
serde_json::json!({
"name": pubkey,
"pubkey": pubkey,
"persona_id": persona_id,
"relay_url": "ws://localhost:3000",
"avatar_url": avatar_url,
"acp_command": "buzz-acp",
"agent_command": "goose",
"agent_args": [],
"mcp_command": "",
"turn_timeout_seconds": 320,
"parallelism": 4,
"system_prompt": "A customized built-in prompt",
"model": "test-model",
"provider": "test-provider",
"persona_source_version": persona_source_version,
"env_vars": {},
"start_on_app_launch": true,
"created_at": "before",
"updated_at": "before",
"last_started_at": null,
"last_stopped_at": null,
"last_exit_code": null,
"last_error": null
})
};
let mut synced_instance = instance(
"fizz-instance",
"builtin:fizz",
old_fizz,
&old_persona_version,
);
synced_instance["future_instance_field"] = serde_json::json!("preserved");
let records = serde_json::Value::Array(vec![
definition_record,
synced_instance,
instance(
"drifted-fizz-instance",
"builtin:fizz",
old_fizz,
"genuinely-drifted-version",
),
instance(
"honey-instance",
"builtin:honey",
"data:image/png;base64,user-customized",
"honey-version",
),
instance("custom-instance", "custom:fizz", old_fizz, "custom-version"),
]);
std::fs::write(&path, serde_json::to_vec_pretty(&records).unwrap()).unwrap();
refresh_builtin_agent_avatars_in_file(&path, &legacy_avatars, "after");
let migrated: Vec<serde_json::Value> =
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
let new_fizz = crate::managed_agents::built_in_persona_avatar_url("builtin:fizz").unwrap();
assert_eq!(migrated[0]["avatar_url"], new_fizz);
assert_eq!(migrated[0]["updated_at"], "after");
assert_eq!(migrated[0]["future_definition_field"], "preserved");
let migrated_definition: crate::managed_agents::ManagedAgentRecord =
serde_json::from_value(migrated[0].clone()).unwrap();
let new_persona_version = crate::managed_agents::persona_events::persona_content_hash(
&crate::managed_agents::persona_events::persona_event_content(
&migrated_definition.to_definition_view().unwrap(),
),
);
assert_ne!(new_persona_version, old_persona_version);
assert_eq!(migrated[1]["avatar_url"], new_fizz);
assert_eq!(migrated[1]["updated_at"], "after");
assert_eq!(migrated[1]["persona_source_version"], new_persona_version);
assert_eq!(migrated[1]["future_instance_field"], "preserved");
assert_eq!(migrated[2]["avatar_url"], new_fizz);
assert_eq!(migrated[2]["updated_at"], "after");
assert_eq!(
migrated[2]["persona_source_version"],
"genuinely-drifted-version"
);
assert_eq!(
migrated[3]["avatar_url"],
"data:image/png;base64,user-customized"
);
assert_eq!(migrated[3]["updated_at"], "before");
assert_eq!(migrated[4]["avatar_url"], old_fizz);
assert_eq!(migrated[4]["updated_at"], "before");
let once = std::fs::read(&path).unwrap();
refresh_builtin_agent_avatars_in_file(&path, &legacy_avatars, "later");
assert_eq!(std::fs::read(&path).unwrap(), once);
}
#[test]
fn current_builtin_agent_avatars_do_not_match_legacy_hashes() {
use sha2::{Digest as _, Sha256};
for legacy in LEGACY_BUILTIN_AVATARS {
let current =
crate::managed_agents::built_in_persona_avatar_url(legacy.persona_id).unwrap();
assert_ne!(
hex::encode(Sha256::digest(current.as_bytes())),
legacy.data_url_sha256
);
}
}
#[test]
fn legacy_avatar_match_checks_every_known_generation_for_a_persona() {
use sha2::{Digest as _, Sha256};
let prior_avatar = "data:image/png;base64,prior-fizz";
let prior_hash = hex::encode(Sha256::digest(prior_avatar.as_bytes()));
let legacy_avatars = [
LegacyBuiltInAvatar {
persona_id: "builtin:fizz",
data_url_sha256: "older-fizz-hash",
sanitized_media_sha256: "older-upload-hash",
persona_content_hash: "older-persona-version",
},
LegacyBuiltInAvatar {
persona_id: "builtin:fizz",
data_url_sha256: prior_hash.as_str(),
sanitized_media_sha256: "prior-upload-hash",
persona_content_hash: "prior-persona-version",
},
];
let record = serde_json::json!({
"slug": "builtin:fizz",
"avatar_url": prior_avatar,
});
let matched = legacy_avatar_match(&record, &legacy_avatars).unwrap();
assert_eq!(
matched.metadata.persona_content_hash,
"prior-persona-version"
);
assert!(!matched.was_uploaded);
}
#[test]
fn refresh_builtin_agent_avatars_updates_versions_without_stored_definitions() {
use sha2::{Digest as _, Sha256};
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let old_fizz = "data:image/png;base64,old-fizz";
let fizz_hash = hex::encode(Sha256::digest(old_fizz.as_bytes()));
let mut old_definition =
crate::managed_agents::built_in_persona_definition("builtin:fizz", "before").unwrap();
old_definition.avatar_url = Some(old_fizz.to_string());
let old_version = crate::managed_agents::persona_events::persona_content_hash(
&crate::managed_agents::persona_events::persona_event_content(&old_definition),
);
let legacy_avatars = [LegacyBuiltInAvatar {
persona_id: "builtin:fizz",
data_url_sha256: fizz_hash.as_str(),
sanitized_media_sha256: "",
persona_content_hash: old_version.as_str(),
}];
let records = serde_json::json!([
{
"name": "synced-fizz",
"pubkey": "synced-fizz",
"persona_id": "builtin:fizz",
"avatar_url": old_fizz,
"persona_source_version": old_version,
"updated_at": "before"
},
{
"name": "drifted-fizz",
"pubkey": "drifted-fizz",
"persona_id": "builtin:fizz",
"avatar_url": old_fizz,
"persona_source_version": "genuinely-drifted-version",
"updated_at": "before"
}
]);
std::fs::write(&path, serde_json::to_vec_pretty(&records).unwrap()).unwrap();
refresh_builtin_agent_avatars_in_file(&path, &legacy_avatars, "after");
let migrated: Vec<serde_json::Value> =
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
let current_definition =
crate::managed_agents::built_in_persona_definition("builtin:fizz", "after").unwrap();
let current_version = crate::managed_agents::persona_events::persona_content_hash(
&crate::managed_agents::persona_events::persona_event_content(&current_definition),
);
let new_fizz = crate::managed_agents::built_in_persona_avatar_url("builtin:fizz").unwrap();
assert_eq!(migrated[0]["avatar_url"], new_fizz);
assert_eq!(migrated[0]["persona_source_version"], current_version);
assert_eq!(migrated[0]["updated_at"], "after");
assert_eq!(migrated[1]["avatar_url"], new_fizz);
assert_eq!(
migrated[1]["persona_source_version"],
"genuinely-drifted-version"
);
assert_eq!(migrated[1]["updated_at"], "after");
}
#[test]
fn refresh_builtin_agent_avatars_updates_uploaded_media_urls() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("managed-agents.json");
let media_sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let different_sha256 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let old_persona_version = "legacy-persona-version";
let legacy_avatars = [LegacyBuiltInAvatar {
persona_id: "builtin:fizz",
data_url_sha256: "not-a-data-url-hash",
sanitized_media_sha256: media_sha256,
persona_content_hash: old_persona_version,
}];
let matching_url = format!("https://relay.example/media/{media_sha256}.png?download=1");
let custom_url = format!("https://relay.example/media/{different_sha256}.png");
let embedded_hash_url = format!("https://relay.example/media/avatar-{media_sha256}.png");
let records = serde_json::json!([
{
"name": "synced-fizz",
"pubkey": "synced-fizz",
"persona_id": "builtin:fizz",
"avatar_url": matching_url,
"persona_source_version": old_persona_version,
"updated_at": "before"
},
{
"name": "drifted-fizz",
"pubkey": "drifted-fizz",
"persona_id": "builtin:fizz",
"avatar_url": matching_url,
"persona_source_version": "genuinely-drifted-version",
"updated_at": "before"
},
{
"name": "custom-fizz",
"pubkey": "custom-fizz",
"persona_id": "builtin:fizz",
"avatar_url": custom_url,
"persona_source_version": old_persona_version,
"updated_at": "before"
},
{
"name": "embedded-hash-fizz",
"pubkey": "embedded-hash-fizz",
"persona_id": "builtin:fizz",
"avatar_url": embedded_hash_url,
"persona_source_version": old_persona_version,
"updated_at": "before"
}
]);
std::fs::write(&path, serde_json::to_vec_pretty(&records).unwrap()).unwrap();
refresh_builtin_agent_avatars_in_file(&path, &legacy_avatars, "after");
let migrated: Vec<serde_json::Value> =
serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
let current_definition =
crate::managed_agents::built_in_persona_definition("builtin:fizz", "after").unwrap();
let current_version = crate::managed_agents::persona_events::persona_content_hash(
&crate::managed_agents::persona_events::persona_event_content(&current_definition),
);
let new_fizz = crate::managed_agents::built_in_persona_avatar_url("builtin:fizz").unwrap();
assert_eq!(migrated[0]["avatar_url"], new_fizz);
assert_eq!(migrated[0]["persona_source_version"], current_version);
assert_eq!(migrated[0]["updated_at"], "after");
assert_eq!(migrated[1]["avatar_url"], new_fizz);
assert_eq!(
migrated[1]["persona_source_version"],
"genuinely-drifted-version"
);
assert_eq!(migrated[1]["updated_at"], "after");
assert_eq!(migrated[2]["avatar_url"], custom_url);
assert_eq!(migrated[2]["updated_at"], "before");
assert_eq!(migrated[3]["avatar_url"], embedded_hash_url);
assert_eq!(migrated[3]["updated_at"], "before");
}