mirror of
https://github.com/block/buzz.git
synced 2026-11-10 11:21:05 +00:00
## Summary - add custom-agent catalog sharing and hide built-ins from discovery - let owners publish later catalog updates from Share or while saving edits — the save always persists locally, and the publish reports `published` or `queued` (flushed automatically once the relay is reachable again) - preserve agent type, model, and runtime across snapshot import/export - simplify agent and team entry points and tighten catalog layout - migrate the legacy global retention queue into the owner's active scope so pending catalog publishes survive the upgrade - keep the agent list and edits usable in recovery mode by degrading to unshared projections when scope resolution or the retention DB fails - track catalog provenance on copied personas, so adding an already-added foreign agent resolves to the existing copy instead of creating a duplicate - scope inbound persona events to the community relay they arrived on - page the catalog read past the relay's 1,000-row query clamp - unify the share dialog's memory-level choice into a single "What's included" selector that drives both DM-send and copy-link delivery (all six combinations preserved), group the delivery rows above the option rows, and label the catalog toggle "Not shared" / "Shared" - keep emoji avatars on catalog entries — they persist as inline percent-encoded SVG, which the catalog projection's http(s)-only URL guard used to drop, so a shared agent showed initials instead of its avatar - drop the "Active in communities" card from Agents settings, superseded by the per-channel runtime controls in the members sidebar ## Screenshots ### Agent actions  ### Team avatar stack  ### Catalog sharing  ### Publish while editing  ### Publish from Share  ### Catalog details  --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
299 lines
12 KiB
Rust
299 lines
12 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 {
|
|
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(),
|
|
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,
|
|
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 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(¤t_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(¤t_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");
|
|
}
|