Skip to content
Merged
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
460 changes: 84 additions & 376 deletions desktop/src-tauri/src/managed_agents/personas.rs

Large diffs are not rendered by default.

115 changes: 56 additions & 59 deletions desktop/src-tauri/src/managed_agents/personas/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ fn merge_personas_adds_missing_built_ins() {
.iter()
.map(|record| record.display_name.as_str())
.collect();
assert_eq!(display_names, vec!["Solo", "Kit", "Scout"]);
assert_eq!(display_names, vec!["Fizz"]);
}

#[test]
Expand All @@ -52,7 +52,7 @@ fn merge_personas_preserves_custom_records() {

#[test]
fn merge_personas_restores_builtin_defaults() {
let mut edited_builtin = custom_persona("builtin:solo", "My Solo");
let mut edited_builtin = custom_persona("builtin:fizz", "My Fizz");
edited_builtin.is_builtin = true;
edited_builtin.is_active = true;
let original_created_at = edited_builtin.created_at.clone();
Expand All @@ -61,19 +61,19 @@ fn merge_personas_restores_builtin_defaults() {
let (records, changed) = merge_personas(vec![edited_builtin], "2026-03-19T00:00:00Z");

assert!(changed);
let solo = records
let fizz = records
.iter()
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist");
.find(|record| record.id == "builtin:fizz")
.expect("fizz built-in should exist");
let canonical = BUILT_IN_PERSONAS
.iter()
.find(|persona| persona.id == "builtin:solo")
.expect("solo built-in definition should exist");
assert_eq!(solo.display_name, canonical.display_name);
assert_eq!(solo.avatar_url.as_deref(), canonical.avatar_url,);
assert_eq!(solo.created_at, original_created_at);
assert_eq!(solo.updated_at, original_updated_at);
assert!(solo.is_active);
.find(|persona| persona.id == "builtin:fizz")
.expect("fizz built-in definition should exist");
assert_eq!(fizz.display_name, canonical.display_name);
assert_eq!(fizz.avatar_url.as_deref(), canonical.avatar_url,);
assert_eq!(fizz.created_at, original_created_at);
assert_eq!(fizz.updated_at, original_updated_at);
assert!(fizz.is_active);
}

#[test]
Expand All @@ -82,7 +82,7 @@ fn merge_personas_restores_builtin_env_vars() {
// the canonical (empty) env on merge. Built-ins are intended immutable —
// if a user wants per-persona credentials, they create or duplicate to a
// custom persona.
let mut tampered = custom_persona("builtin:solo", "Solo");
let mut tampered = custom_persona("builtin:fizz", "Fizz");
tampered.is_builtin = true;
tampered.avatar_url = None;
tampered.is_active = true;
Expand All @@ -92,48 +92,48 @@ fn merge_personas_restores_builtin_env_vars() {
let (records, changed) = merge_personas(vec![tampered], "2026-03-19T00:00:00Z");

assert!(changed);
let solo = records
let fizz = records
.iter()
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist");
.find(|record| record.id == "builtin:fizz")
.expect("fizz built-in should exist");
// Built-in persona definitions have no `env_vars` field — they are
// always empty. The merge reset should clear the tampered key entirely.
assert!(
solo.env_vars.is_empty(),
fizz.env_vars.is_empty(),
"expected empty, got {:?}",
solo.env_vars
fizz.env_vars
);
}

#[test]
fn merge_personas_restores_builtin_name_pool_and_preserves_is_active() {
let mut solo = custom_persona("builtin:solo", "Solo");
solo.is_builtin = true;
solo.avatar_url = None;
solo.is_active = true;
solo.name_pool = vec!["Definitely Not Solo".to_string()];
let mut fizz = custom_persona("builtin:fizz", "Fizz");
fizz.is_builtin = true;
fizz.avatar_url = None;
fizz.is_active = true;
fizz.name_pool = vec!["Definitely Not Fizz".to_string()];

let (records, changed) = merge_personas(vec![solo], "2026-03-19T00:00:00Z");
let (records, changed) = merge_personas(vec![fizz], "2026-03-19T00:00:00Z");

assert!(changed);
let solo = records
let fizz = records
.iter()
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist");
.find(|record| record.id == "builtin:fizz")
.expect("fizz built-in should exist");
let expected_name_pool = BUILT_IN_PERSONAS
.iter()
.find(|persona| persona.id == "builtin:solo")
.expect("solo built-in definition should exist")
.find(|persona| persona.id == "builtin:fizz")
.expect("fizz built-in definition should exist")
.name_pool
.iter()
.map(|name| (*name).to_string())
.collect::<Vec<_>>();
assert_eq!(solo.name_pool, expected_name_pool);
assert!(solo.is_active);
assert_eq!(fizz.name_pool, expected_name_pool);
assert!(fizz.is_active);
}

#[test]
fn merge_personas_backfills_new_builtins_for_existing_store() {
fn merge_personas_adds_fizz_and_retires_old_builtins_for_existing_store() {
let mut legacy_builtins = vec![custom_persona("builtin:solo", "Solo")];
for persona in &mut legacy_builtins {
persona.is_builtin = true;
Expand All @@ -143,23 +143,20 @@ fn merge_personas_backfills_new_builtins_for_existing_store() {
let (records, changed) = merge_personas(legacy_builtins, "2026-03-19T00:00:00Z");

assert!(changed);
assert!(records.iter().any(|record| record.id == "builtin:kit"));
assert!(records.iter().any(|record| record.id == "builtin:scout"));
assert!(records.iter().any(|record| record.id == "builtin:solo"));
assert!(
records
.iter()
.find(|record| record.id == "builtin:solo")
.expect("solo built-in should exist")
.is_active
);
assert!(
records
.iter()
.find(|record| record.id == "builtin:kit")
.expect("kit built-in should exist")
.is_active
);
let fizz = records
.iter()
.find(|record| record.id == "builtin:fizz")
.expect("fizz built-in should exist");
assert!(fizz.is_builtin);
assert!(fizz.is_active);

let solo = records
.iter()
.find(|record| record.id == "builtin:solo")
.expect("old solo record should be retained as retired custom persona");
assert!(!solo.is_builtin);
assert!(!solo.is_active);
assert_eq!(solo.display_name, "Solo (retired)");
}

#[test]
Expand Down Expand Up @@ -196,15 +193,15 @@ fn ensure_persona_is_active_rejects_missing_personas() {

#[test]
fn ensure_persona_is_active_rejects_inactive_personas() {
let mut persona = custom_persona("builtin:solo", "Solo");
let mut persona = custom_persona("builtin:fizz", "Fizz");
persona.is_builtin = true;
persona.is_active = false;

let err = ensure_persona_is_active(&[persona], "builtin:solo").unwrap_err();
let err = ensure_persona_is_active(&[persona], "builtin:fizz").unwrap_err();

assert_eq!(
err,
"Solo is not in My Agents. Choose it from Persona Catalog first."
"Fizz is not in My Agents. Choose it from Persona Catalog first."
);
}

Expand Down Expand Up @@ -236,33 +233,33 @@ fn validate_persona_activation_change_rejects_non_builtins() {

#[test]
fn validate_persona_activation_change_rejects_managed_agent_references() {
let mut persona = custom_persona("builtin:solo", "Solo");
let mut persona = custom_persona("builtin:fizz", "Fizz");
persona.is_builtin = true;

let err = validate_persona_activation_change(&persona, false, true, false).unwrap_err();

assert_eq!(
err,
"Solo is still assigned to a managed agent. Remove or reassign those agents first."
"Fizz is still assigned to a managed agent. Remove or reassign those agents first."
);
}

#[test]
fn validate_persona_activation_change_rejects_team_references() {
let mut persona = custom_persona("builtin:solo", "Solo");
let mut persona = custom_persona("builtin:fizz", "Fizz");
persona.is_builtin = true;

let err = validate_persona_activation_change(&persona, false, false, true).unwrap_err();

assert_eq!(
err,
"Solo is still referenced by a team. Remove it from those teams first."
"Fizz is still referenced by a team. Remove it from those teams first."
);
}

#[test]
fn validate_persona_activation_change_allows_safe_builtin_updates() {
let mut persona = custom_persona("builtin:solo", "Solo");
let mut persona = custom_persona("builtin:fizz", "Fizz");
persona.is_builtin = true;

assert!(validate_persona_activation_change(&persona, true, false, false).is_ok());
Expand All @@ -271,7 +268,7 @@ fn validate_persona_activation_change_allows_safe_builtin_updates() {

#[test]
fn validate_persona_deletion_rejects_builtins() {
let mut persona = custom_persona("builtin:solo", "Solo");
let mut persona = custom_persona("builtin:fizz", "Fizz");
persona.is_builtin = true;

let err = validate_persona_deletion(&persona, false).unwrap_err();
Expand Down Expand Up @@ -358,7 +355,7 @@ fn pack_id_rejects_too_long() {
#[test]
fn migrate_retires_unmodified_personas() {
let now = "2026-04-01T00:00:00Z";
// Simulate a store from before the Solo/Kit/Scout transition: all 6
// Simulate a store from before the Fizz transition: all 6
// retired personas with original system prompts.
let mut stored: Vec<PersonaRecord> = RETIRED_PERSONAS
.iter()
Expand Down
6 changes: 3 additions & 3 deletions desktop/src-tauri/src/managed_agents/team_repair.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ mod tests {

#[test]
fn team_persona_key_falls_back_to_id() {
let t = team("builtin-team:kit-scout");
assert_eq!(team_persona_key(&t), "builtin-team:kit-scout");
let t = team("builtin-team:fizz");
assert_eq!(team_persona_key(&t), "builtin-team:fizz");
}

// ── backfill_source_dirs ─────────────────────────────────────────────
Expand Down Expand Up @@ -375,7 +375,7 @@ mod tests {
let teams_dir = tmp.path();
std::fs::create_dir(teams_dir.join("com.test.pack")).unwrap();

let mut builtin = team("builtin-team:kit-scout");
let mut builtin = team("builtin-team:fizz");
builtin.is_builtin = true;
builtin.persona_ids = vec!["p1".to_string()];

Expand Down
44 changes: 22 additions & 22 deletions desktop/src-tauri/src/managed_agents/teams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,10 @@ struct BuiltInTeam {
}

const BUILT_IN_TEAMS: &[BuiltInTeam] = &[BuiltInTeam {
id: "builtin-team:kit-scout",
name: "Kit & Scout",
description: Some("Kit orchestrates and builds; Scout researches, plans, and reviews."),
persona_ids: &["builtin:kit", "builtin:scout"],
id: "builtin-team:fizz",
name: "Fizz",
description: Some("Fizz works carefully and collaboratively."),
persona_ids: &["builtin:fizz"],
}];

fn built_in_team_records(now: &str) -> Vec<TeamRecord> {
Expand Down Expand Up @@ -890,12 +890,12 @@ mod tests {
let json = serde_json::json!({
"version": 1,
"type": "team",
"name": "Solo",
"name": "Fizz",
});
let bytes = serde_json::to_vec(&json).unwrap();
let parsed = parse_team_json(&bytes).unwrap();
assert!(parsed.personas.is_empty());
assert_eq!(parsed.name, "Solo");
assert_eq!(parsed.name, "Fizz");
}

// -----------------------------------------------------------------------
Expand All @@ -910,24 +910,24 @@ mod tests {
assert_eq!(records.len(), BUILT_IN_TEAMS.len());
assert!(records.iter().all(|record| record.is_builtin));
let names: Vec<&str> = records.iter().map(|t| t.name.as_str()).collect();
assert_eq!(names, vec!["Kit & Scout"]);
assert_eq!(names, vec!["Fizz"]);
}

#[test]
fn merge_teams_preserves_user_customizations_to_builtin() {
let mut customized = team("builtin-team:kit-scout", "Kit & Scout (mine)");
let mut customized = team("builtin-team:fizz", "Fizz (mine)");
customized.is_builtin = true;
customized.persona_ids = vec!["builtin:kit".to_string()];
customized.persona_ids = vec!["builtin:fizz".to_string()];

let (records, _changed) = merge_teams(vec![customized], "2026-05-07T00:00:00Z");

let kit_scout = records
let fizz = records
.iter()
.find(|t| t.id == "builtin-team:kit-scout")
.expect("Kit & Scout built-in should exist");
assert_eq!(kit_scout.name, "Kit & Scout (mine)");
assert_eq!(kit_scout.persona_ids, vec!["builtin:kit".to_string()]);
assert!(kit_scout.is_builtin);
.find(|t| t.id == "builtin-team:fizz")
.expect("Fizz built-in should exist");
assert_eq!(fizz.name, "Fizz (mine)");
assert_eq!(fizz.persona_ids, vec!["builtin:fizz".to_string()]);
assert!(fizz.is_builtin);
}

#[test]
Expand All @@ -936,7 +936,7 @@ mod tests {
let (records, _changed) = merge_teams(vec![user_team], "2026-05-07T00:00:00Z");

assert!(records.iter().any(|t| t.id == "user-uuid"));
assert!(records.iter().any(|t| t.id == "builtin-team:kit-scout"));
assert!(records.iter().any(|t| t.id == "builtin-team:fizz"));
}

#[test]
Expand All @@ -959,22 +959,22 @@ mod tests {
fn merge_teams_repromotes_existing_builtin_marked_as_custom() {
// If someone hand-edits the store and flips is_builtin to false on a
// canonical built-in id, merge_teams should restore the flag.
let mut downgraded = team("builtin-team:kit-scout", "Kit & Scout");
let mut downgraded = team("builtin-team:fizz", "Fizz");
downgraded.is_builtin = false;

let (records, changed) = merge_teams(vec![downgraded], "2026-05-07T00:00:00Z");

assert!(changed);
let kit_scout = records
let fizz = records
.iter()
.find(|t| t.id == "builtin-team:kit-scout")
.expect("Kit & Scout should exist");
assert!(kit_scout.is_builtin);
.find(|t| t.id == "builtin-team:fizz")
.expect("Fizz should exist");
assert!(fizz.is_builtin);
}

#[test]
fn validate_team_deletion_rejects_built_ins() {
let mut built_in = team("builtin-team:kit-scout", "Kit & Scout");
let mut built_in = team("builtin-team:fizz", "Fizz");
built_in.is_builtin = true;

let err = validate_team_deletion(&built_in).unwrap_err();
Expand Down
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/managed_agents/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -635,8 +635,8 @@ mod tests {
fn persona_record_defaults_active_when_field_is_missing() {
let record: PersonaRecord = serde_json::from_str(
r#"{
"id": "builtin:solo",
"display_name": "Solo",
"id": "builtin:fizz",
"display_name": "Fizz",
"avatar_url": null,
"system_prompt": "Prompt",
"created_at": "2026-03-19T00:00:00Z",
Expand Down
Loading