mirror of
https://github.com/block/buzz.git
synced 2026-10-28 13:01:10 +00:00
fix(desktop): heal stale persona_team_dir paths in release builds (#1003)
This commit is contained in:
@@ -142,7 +142,9 @@ fn patch_json_records(
|
|||||||
}
|
}
|
||||||
if changed {
|
if changed {
|
||||||
if let Ok(bytes) = serde_json::to_vec_pretty(&records) {
|
if let Ok(bytes) = serde_json::to_vec_pretty(&records) {
|
||||||
let _ = std::fs::write(path, bytes);
|
if let Err(e) = crate::managed_agents::atomic_write_json(path, &bytes) {
|
||||||
|
eprintln!("buzz-desktop: patch-json-records: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,8 +345,8 @@ pub fn sync_shared_agent_data(app: &tauri::AppHandle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reconcile_team_dirs_in_file(path: &Path, canonical_dir: &Path) {
|
fn reconcile_team_dirs_in_file(path: &Path, target_dir: &Path) {
|
||||||
let canonical_teams = canonical_dir.join("agents/teams");
|
let target_teams = target_dir.join("agents/teams");
|
||||||
patch_json_records(path, |obj| {
|
patch_json_records(path, |obj| {
|
||||||
// Handle both old field name and new field name
|
// Handle both old field name and new field name
|
||||||
let field_name = if obj.contains_key("persona_team_dir") {
|
let field_name = if obj.contains_key("persona_team_dir") {
|
||||||
@@ -374,43 +376,111 @@ fn reconcile_team_dirs_in_file(path: &Path, canonical_dir: &Path) {
|
|||||||
let Some(id) = team_id else {
|
let Some(id) = team_id else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let expected = canonical_teams.join(id);
|
let expected = target_teams.join(id);
|
||||||
if team_path == expected {
|
if team_path == expected {
|
||||||
|
// Value already correct — still normalize the legacy field name so
|
||||||
|
// stores converge on `persona_team_dir` (runtime reads either via
|
||||||
|
// serde alias).
|
||||||
|
if field_name == "persona_pack_path" {
|
||||||
|
if let Some(val) = obj.remove("persona_pack_path") {
|
||||||
|
obj.insert("persona_team_dir".to_string(), val);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Rewriting to a path that does not exist on disk makes things worse
|
||||||
|
// than leaving a stale-but-working path in place. fs::metadata follows
|
||||||
|
// symlinks, so a valid symlinked install passes; a dangling symlink
|
||||||
|
// fails with NotFound.
|
||||||
|
if let Err(e) = std::fs::metadata(&expected) {
|
||||||
|
eprintln!(
|
||||||
|
"buzz-desktop: team-dir-reconcile: {:?}: {:?} expected at {:?} — {e}, leaving as-is",
|
||||||
|
obj.get("name").and_then(|v| v.as_str()).unwrap_or("?"),
|
||||||
|
team_path,
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(expected_str) = expected.to_str() else {
|
||||||
|
eprintln!(
|
||||||
|
"buzz-desktop: team-dir-reconcile: {:?}: expected path {:?} is not valid UTF-8, leaving as-is",
|
||||||
|
obj.get("name").and_then(|v| v.as_str()).unwrap_or("?"),
|
||||||
|
expected,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"buzz-desktop: team-dir-reconcile: {:?}: {:?} → {:?}",
|
"buzz-desktop: team-dir-reconcile: {:?}: {:?} → {:?}",
|
||||||
obj.get("name").and_then(|v| v.as_str()).unwrap_or("?"),
|
obj.get("name").and_then(|v| v.as_str()).unwrap_or("?"),
|
||||||
team_path,
|
team_path,
|
||||||
expected,
|
expected,
|
||||||
);
|
);
|
||||||
// Always write the canonical new field name
|
// Always write the new field name
|
||||||
obj.remove("persona_pack_path");
|
obj.remove("persona_pack_path");
|
||||||
obj.insert(
|
obj.insert(
|
||||||
"persona_team_dir".to_string(),
|
"persona_team_dir".to_string(),
|
||||||
serde_json::Value::String(expected.to_string_lossy().into_owned()),
|
serde_json::Value::String(expected_str.to_owned()),
|
||||||
);
|
);
|
||||||
true
|
true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Select the data directory to reconcile against.
|
||||||
|
///
|
||||||
|
/// Dev instances — identified by the data-dir name starting with
|
||||||
|
/// `CANONICAL_DEV_IDENTIFIER` (covers the canonical dir itself and any
|
||||||
|
/// worktree variant like `xyz.block.buzz.app.dev.mybranch`) — share
|
||||||
|
/// `agents/managed-agents.json` and `agents/teams` via symlinks to the
|
||||||
|
/// canonical dev dir, so they should normalize against that canonical dir.
|
||||||
|
///
|
||||||
|
/// Release builds must reconcile their own data dir — keying off the canonical
|
||||||
|
/// dev dir's mere existence would leave release records permanently stale on
|
||||||
|
/// developer machines, where that dir is always present.
|
||||||
|
fn reconcile_target_dir(current_dir: &Path) -> PathBuf {
|
||||||
|
let is_dev_instance = current_dir
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.is_some_and(|n| n.starts_with(CANONICAL_DEV_IDENTIFIER));
|
||||||
|
if is_dev_instance {
|
||||||
|
match canonical_dev_data_dir(current_dir) {
|
||||||
|
Some(dir) if dir.exists() => dir,
|
||||||
|
_ => current_dir.to_path_buf(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current_dir.to_path_buf()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Reconcile `persona_team_dir` (and legacy `persona_pack_path`) values in
|
/// Reconcile `persona_team_dir` (and legacy `persona_pack_path`) values in
|
||||||
/// managed-agents.json to point to the canonical dev data directory's
|
/// managed-agents.json to point to the correct `agents/teams/` prefix.
|
||||||
/// `agents/teams/` prefix. Fixes stale paths left when agents were created
|
///
|
||||||
/// from worktree instances whose data directories don't have local team copies.
|
/// Fixes two classes of stale paths:
|
||||||
|
/// - Worktree dev instances whose records point at a sibling data dir rather
|
||||||
|
/// than the canonical dev dir (dev instances share managed-agents.json and
|
||||||
|
/// agents/teams via symlinks, so they all reconcile against the canonical dir).
|
||||||
|
/// - Legacy paths left by historical renames: `agents/packs/` → `agents/teams/`
|
||||||
|
/// (the packs→teams consolidation) and bundle-id `xyz.block.sprout.app` →
|
||||||
|
/// `xyz.block.buzz.app` (the sprout→buzz rename, which moved the app data dir).
|
||||||
|
///
|
||||||
|
/// Release builds reconcile their own data dir — choosing the canonical dev dir
|
||||||
|
/// whenever it exists would leave release files permanently stale on developer
|
||||||
|
/// machines.
|
||||||
pub fn reconcile_persona_team_dirs(app: &tauri::AppHandle) {
|
pub fn reconcile_persona_team_dirs(app: &tauri::AppHandle) {
|
||||||
let Ok(current_dir) = app.path().app_data_dir() else {
|
let Ok(current_dir) = app.path().app_data_dir() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let canonical_dir = match canonical_dev_data_dir(¤t_dir) {
|
// Single-dir on purpose: unlike reconcile_legacy_command_names and
|
||||||
Some(dir) if dir.exists() => dir,
|
// reconcile_provider_mcp_commands, which patch both [current, canonical],
|
||||||
_ => current_dir,
|
// path rewrites are target-dependent — a dual pass through a dev
|
||||||
};
|
// instance's symlinked store would write worktree-local paths into the
|
||||||
let path = canonical_dir.join("agents/managed-agents.json");
|
// shared canonical file.
|
||||||
|
let target_dir = reconcile_target_dir(¤t_dir);
|
||||||
|
let path = target_dir.join("agents/managed-agents.json");
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reconcile_team_dirs_in_file(&path, &canonical_dir);
|
reconcile_team_dirs_in_file(&path, &target_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One-time migration from packs to teams.
|
/// One-time migration from packs to teams.
|
||||||
@@ -428,15 +498,12 @@ pub fn migrate_packs_to_teams(app: &tauri::AppHandle) {
|
|||||||
let Ok(current_dir) = app.path().app_data_dir() else {
|
let Ok(current_dir) = app.path().app_data_dir() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let canonical_dir = match canonical_dev_data_dir(¤t_dir) {
|
let target_dir = reconcile_target_dir(¤t_dir);
|
||||||
Some(dir) if dir.exists() => dir,
|
|
||||||
_ => current_dir,
|
|
||||||
};
|
|
||||||
|
|
||||||
let packs_dir = canonical_dir.join("agents/packs");
|
let packs_dir = target_dir.join("agents/packs");
|
||||||
let teams_dir = canonical_dir.join("agents/teams");
|
let teams_dir = target_dir.join("agents/teams");
|
||||||
let personas_path = canonical_dir.join("agents/personas.json");
|
let personas_path = target_dir.join("agents/personas.json");
|
||||||
let agents_path = canonical_dir.join("agents/managed-agents.json");
|
let agents_path = target_dir.join("agents/managed-agents.json");
|
||||||
|
|
||||||
// Check if migration is needed: packs dir exists OR agents JSON has old field names
|
// Check if migration is needed: packs dir exists OR agents JSON has old field names
|
||||||
let packs_dir_exists = packs_dir.exists() && !packs_dir.is_symlink();
|
let packs_dir_exists = packs_dir.exists() && !packs_dir.is_symlink();
|
||||||
@@ -831,6 +898,10 @@ pub fn migrate_persona_provider_to_runtime(app: &tauri::AppHandle) {
|
|||||||
rename_provider_to_runtime_in_personas(&path);
|
rename_provider_to_runtime_in_personas(&path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "migration_test_support.rs"]
|
||||||
|
mod test_support;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "migration_tests.rs"]
|
#[path = "migration_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
@@ -838,3 +909,7 @@ mod tests;
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "migration_command_tests.rs"]
|
#[path = "migration_command_tests.rs"]
|
||||||
mod command_tests;
|
mod command_tests;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "migration_team_dir_tests.rs"]
|
||||||
|
mod team_dir_tests;
|
||||||
|
|||||||
@@ -1,33 +1,5 @@
|
|||||||
|
use super::test_support::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
fn write_agents_json(dir: &Path, records: &serde_json::Value) {
|
|
||||||
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
dir.join("agents/managed-agents.json"),
|
|
||||||
serde_json::to_vec_pretty(records).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_agents_json(dir: &Path) -> Vec<serde_json::Value> {
|
|
||||||
let content = std::fs::read_to_string(dir.join("agents/managed-agents.json")).unwrap();
|
|
||||||
serde_json::from_str(&content).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_personas_json(dir: &Path, records: &serde_json::Value) {
|
|
||||||
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
dir.join("agents/personas.json"),
|
|
||||||
serde_json::to_vec_pretty(records).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_personas_json(dir: &Path) -> Vec<serde_json::Value> {
|
|
||||||
let content = std::fs::read_to_string(dir.join("agents/personas.json")).unwrap();
|
|
||||||
serde_json::from_str(&content).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reconcile_legacy_command_names_rewrites_renamed_sidecars() {
|
fn reconcile_legacy_command_names_rewrites_renamed_sidecars() {
|
||||||
|
|||||||
453
desktop/src-tauri/src/migration_team_dir_tests.rs
Normal file
453
desktop/src-tauri/src/migration_team_dir_tests.rs
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
use super::test_support::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// ── reconcile_team_dirs_in_file tests ────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_rewrites_worktree_path() {
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
// Team must exist on disk for the reconcile to proceed past the existence gate.
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams/com.wpfleger.sietch-tabr")).unwrap();
|
||||||
|
|
||||||
|
let worktree_pack_path = format!(
|
||||||
|
"{}/agents/packs/com.wpfleger.sietch-tabr",
|
||||||
|
parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-my-branch")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
let expected_path = format!(
|
||||||
|
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
||||||
|
canonical.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Paul",
|
||||||
|
"persona_pack_path": worktree_pack_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
||||||
|
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
||||||
|
// Old field name should be removed
|
||||||
|
assert!(records[0].get("persona_pack_path").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_rewrites_new_field_name() {
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
// Team must exist on disk for the reconcile to proceed past the existence gate.
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams/com.wpfleger.sietch-tabr")).unwrap();
|
||||||
|
|
||||||
|
let worktree_team_path = format!(
|
||||||
|
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
||||||
|
parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-my-branch")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
let expected_path = format!(
|
||||||
|
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
||||||
|
canonical.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Paul",
|
||||||
|
"persona_team_dir": worktree_team_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
||||||
|
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_leaves_canonical_path_unchanged() {
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
|
||||||
|
let canonical_path = format!(
|
||||||
|
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
||||||
|
canonical.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Duncan",
|
||||||
|
"persona_team_dir": canonical_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let before = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
||||||
|
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
||||||
|
let after = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(before, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_skips_records_without_team_dir() {
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Test Agent",
|
||||||
|
"agent_command": "buzz-agent"
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let before = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
||||||
|
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
||||||
|
let after = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(before, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_is_idempotent() {
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
// Team must exist on disk for the reconcile to proceed past the existence gate.
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams/com.wpfleger.sietch-tabr")).unwrap();
|
||||||
|
|
||||||
|
let worktree_pack_path = format!(
|
||||||
|
"{}/agents/packs/com.wpfleger.sietch-tabr",
|
||||||
|
parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-my-branch")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Paul",
|
||||||
|
"persona_pack_path": worktree_pack_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = canonical.join("agents/managed-agents.json");
|
||||||
|
reconcile_team_dirs_in_file(&path, &canonical);
|
||||||
|
let after_first = std::fs::read_to_string(&path).unwrap();
|
||||||
|
reconcile_team_dirs_in_file(&path, &canonical);
|
||||||
|
let after_second = std::fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(after_first, after_second);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_heals_legacy_release_path() {
|
||||||
|
// Record stored under the old bundle id and old packs segment should be
|
||||||
|
// rewritten to the new bundle id and teams segment.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let release_dir = parent.path().join("xyz.block.buzz.app");
|
||||||
|
std::fs::create_dir_all(release_dir.join("agents/teams/com.example.team")).unwrap();
|
||||||
|
|
||||||
|
let legacy_path = format!(
|
||||||
|
"{}/agents/packs/com.example.team",
|
||||||
|
parent.path().join("xyz.block.sprout.app").display()
|
||||||
|
);
|
||||||
|
let expected_path = format!("{}/agents/teams/com.example.team", release_dir.display());
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&release_dir,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Stilgar",
|
||||||
|
"persona_team_dir": legacy_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
reconcile_team_dirs_in_file(
|
||||||
|
&release_dir.join("agents/managed-agents.json"),
|
||||||
|
&release_dir,
|
||||||
|
);
|
||||||
|
|
||||||
|
let records = read_agents_json(&release_dir);
|
||||||
|
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_leaves_record_when_team_missing() {
|
||||||
|
// When the expected target does not exist, the record must be left untouched
|
||||||
|
// and the file must not be rewritten at all (no churn).
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
// Intentionally do NOT create agents/teams/com.example.missing.
|
||||||
|
|
||||||
|
let stale_path = format!(
|
||||||
|
"{}/agents/packs/com.example.missing",
|
||||||
|
parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-old")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Jessica",
|
||||||
|
"persona_pack_path": stale_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = canonical.join("agents/managed-agents.json");
|
||||||
|
let before = std::fs::read_to_string(&path).unwrap();
|
||||||
|
reconcile_team_dirs_in_file(&path, &canonical);
|
||||||
|
let after = std::fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
// File must be byte-identical — no churn.
|
||||||
|
assert_eq!(before, after);
|
||||||
|
// Record still has the old field and value.
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_pack_path"], stale_path);
|
||||||
|
assert!(records[0].get("persona_team_dir").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_heals_to_symlinked_team_dir() {
|
||||||
|
// When agents/teams/<id> is a symlink to a real directory elsewhere,
|
||||||
|
// Path::exists follows the symlink, so the rewrite should proceed.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
|
||||||
|
// Real team lives outside the canonical dir; a symlink points to it.
|
||||||
|
let real_team = parent.path().join("real-team-store/com.example.team");
|
||||||
|
std::fs::create_dir_all(&real_team).unwrap();
|
||||||
|
let symlink_target = canonical.join("agents/teams/com.example.team");
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams")).unwrap();
|
||||||
|
std::os::unix::fs::symlink(&real_team, &symlink_target).unwrap();
|
||||||
|
|
||||||
|
let stale_path = format!(
|
||||||
|
"{}/agents/packs/com.example.team",
|
||||||
|
parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-old")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
let expected_path = format!("{}/agents/teams/com.example.team", canonical.display());
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Chani",
|
||||||
|
"persona_pack_path": stale_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
||||||
|
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_skips_dangling_candidate_symlink() {
|
||||||
|
// When agents/teams/<id> is a symlink whose target does not exist,
|
||||||
|
// Path::exists returns false, so the record must be left unchanged.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams")).unwrap();
|
||||||
|
|
||||||
|
// Create a dangling symlink at the expected location.
|
||||||
|
let dangling_target = parent.path().join("nonexistent-dir");
|
||||||
|
let symlink_path = canonical.join("agents/teams/com.example.gone");
|
||||||
|
std::os::unix::fs::symlink(&dangling_target, &symlink_path).unwrap();
|
||||||
|
|
||||||
|
let stale_path = format!(
|
||||||
|
"{}/agents/packs/com.example.gone",
|
||||||
|
parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-old")
|
||||||
|
.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Gurney",
|
||||||
|
"persona_pack_path": stale_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = canonical.join("agents/managed-agents.json");
|
||||||
|
let before = std::fs::read_to_string(&path).unwrap();
|
||||||
|
reconcile_team_dirs_in_file(&path, &canonical);
|
||||||
|
let after = std::fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(before, after);
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_pack_path"], stale_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_through_symlink_preserves_symlink() {
|
||||||
|
// Dev worktree instances reach the canonical store through a symlinked
|
||||||
|
// managed-agents.json — the patched write must land in the canonical
|
||||||
|
// file and leave the symlink in place.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
let worktree = parent
|
||||||
|
.path()
|
||||||
|
.join("xyz.block.buzz.app.dev.worktree-my-branch");
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams/com.example.team")).unwrap();
|
||||||
|
std::fs::create_dir_all(worktree.join("agents")).unwrap();
|
||||||
|
|
||||||
|
let stale_path = format!("{}/agents/packs/com.example.team", worktree.display());
|
||||||
|
let expected_path = format!("{}/agents/teams/com.example.team", canonical.display());
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Paul",
|
||||||
|
"persona_pack_path": stale_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
let canonical_store = canonical.join("agents/managed-agents.json");
|
||||||
|
let worktree_store = worktree.join("agents/managed-agents.json");
|
||||||
|
std::os::unix::fs::symlink(&canonical_store, &worktree_store).unwrap();
|
||||||
|
|
||||||
|
reconcile_team_dirs_in_file(&worktree_store, &canonical);
|
||||||
|
|
||||||
|
assert!(worktree_store.is_symlink());
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── reconcile_target_dir tests ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconcile_target_dir_release_with_existing_dev_sibling_returns_self() {
|
||||||
|
// Release build must reconcile its OWN dir, even when a dev sibling exists.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let release_dir = parent.path().join("xyz.block.buzz.app");
|
||||||
|
let dev_dir = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(&release_dir).unwrap();
|
||||||
|
std::fs::create_dir_all(&dev_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reconcile_target_dir(&release_dir), release_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconcile_target_dir_worktree_dev_with_canonical_sibling_returns_canonical() {
|
||||||
|
// A worktree dev instance defers to the canonical dev dir when it exists.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let worktree_dir = parent.path().join("xyz.block.buzz.app.dev.mybranch");
|
||||||
|
let canonical_dir = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(&worktree_dir).unwrap();
|
||||||
|
std::fs::create_dir_all(&canonical_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reconcile_target_dir(&worktree_dir), canonical_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconcile_target_dir_canonical_dev_returns_self() {
|
||||||
|
// The canonical dev dir is itself a dev instance; it should return itself
|
||||||
|
// (canonical_dev_data_dir points at the same path, and that path exists).
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical_dir = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(&canonical_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reconcile_target_dir(&canonical_dir), canonical_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconcile_target_dir_release_without_dev_sibling_returns_self() {
|
||||||
|
// Release build with no dev sibling present — stays on its own dir.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let release_dir = parent.path().join("xyz.block.buzz.app");
|
||||||
|
std::fs::create_dir_all(&release_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reconcile_target_dir(&release_dir), release_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconcile_target_dir_worktree_dev_without_canonical_sibling_returns_self() {
|
||||||
|
// Worktree dev instance with no canonical sibling present — stays on itself.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let worktree_dir = parent.path().join("xyz.block.buzz.app.dev.mybranch");
|
||||||
|
std::fs::create_dir_all(&worktree_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reconcile_target_dir(&worktree_dir), worktree_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconcile_target_dir_ignores_non_prefix_dev_identifier() {
|
||||||
|
// Dev detection is prefix-only — a dir merely *containing* the dev
|
||||||
|
// identifier later in its name is not a dev instance.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let odd_dir = parent.path().join("com.example.xyz.block.buzz.app.dev");
|
||||||
|
let canonical_dir = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(&odd_dir).unwrap();
|
||||||
|
std::fs::create_dir_all(&canonical_dir).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(reconcile_target_dir(&odd_dir), odd_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_skips_path_without_teams_or_packs_component() {
|
||||||
|
// A path with no teams/packs component yields no team id — record is
|
||||||
|
// silently skipped and the file is not rewritten.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Leto",
|
||||||
|
"persona_team_dir": "/some/random/path"
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
let path = canonical.join("agents/managed-agents.json");
|
||||||
|
let before = std::fs::read_to_string(&path).unwrap();
|
||||||
|
reconcile_team_dirs_in_file(&path, &canonical);
|
||||||
|
let after = std::fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(before, after);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn team_dir_reconcile_renames_legacy_field_when_value_already_canonical() {
|
||||||
|
// Value already points at the target — only the legacy field name is
|
||||||
|
// normalized; the value is untouched.
|
||||||
|
let parent = tempfile::tempdir().unwrap();
|
||||||
|
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
||||||
|
std::fs::create_dir_all(canonical.join("agents/teams/com.example.team")).unwrap();
|
||||||
|
|
||||||
|
let correct_path = format!("{}/agents/teams/com.example.team", canonical.display());
|
||||||
|
|
||||||
|
write_agents_json(
|
||||||
|
&canonical,
|
||||||
|
&serde_json::json!([{
|
||||||
|
"name": "Thufir",
|
||||||
|
"persona_pack_path": correct_path
|
||||||
|
}]),
|
||||||
|
);
|
||||||
|
|
||||||
|
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
||||||
|
|
||||||
|
let records = read_agents_json(&canonical);
|
||||||
|
assert_eq!(records[0]["persona_team_dir"], correct_path);
|
||||||
|
assert!(records[0].get("persona_pack_path").is_none());
|
||||||
|
}
|
||||||
31
desktop/src-tauri/src/migration_test_support.rs
Normal file
31
desktop/src-tauri/src/migration_test_support.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
//! Shared helpers for the migration test modules.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub(crate) fn write_agents_json(dir: &Path, records: &serde_json::Value) {
|
||||||
|
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
dir.join("agents/managed-agents.json"),
|
||||||
|
serde_json::to_vec_pretty(records).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_agents_json(dir: &Path) -> Vec<serde_json::Value> {
|
||||||
|
let content = std::fs::read_to_string(dir.join("agents/managed-agents.json")).unwrap();
|
||||||
|
serde_json::from_str(&content).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_personas_json(dir: &Path, records: &serde_json::Value) {
|
||||||
|
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
dir.join("agents/personas.json"),
|
||||||
|
serde_json::to_vec_pretty(records).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_personas_json(dir: &Path) -> Vec<serde_json::Value> {
|
||||||
|
let content = std::fs::read_to_string(dir.join("agents/personas.json")).unwrap();
|
||||||
|
serde_json::from_str(&content).unwrap()
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::test_support::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -320,20 +321,6 @@ fn canonical_dev_data_dir_returns_self_for_canonical_instance() {
|
|||||||
assert_eq!(canonical_dev_data_dir(&canonical).unwrap(), canonical);
|
assert_eq!(canonical_dev_data_dir(&canonical).unwrap(), canonical);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_agents_json(dir: &Path, records: &serde_json::Value) {
|
|
||||||
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
dir.join("agents/managed-agents.json"),
|
|
||||||
serde_json::to_vec_pretty(records).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_agents_json(dir: &Path) -> Vec<serde_json::Value> {
|
|
||||||
let content = std::fs::read_to_string(dir.join("agents/managed-agents.json")).unwrap();
|
|
||||||
serde_json::from_str(&content).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_creates_teams_directory_symlink() {
|
fn sync_creates_teams_directory_symlink() {
|
||||||
let (_parent, canonical, worktree) = setup_sync_layout();
|
let (_parent, canonical, worktree) = setup_sync_layout();
|
||||||
@@ -397,150 +384,6 @@ fn sync_replaces_real_teams_dir_with_symlink() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn team_dir_reconcile_rewrites_worktree_path() {
|
|
||||||
let parent = tempfile::tempdir().unwrap();
|
|
||||||
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
|
||||||
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
|
||||||
|
|
||||||
let worktree_pack_path = format!(
|
|
||||||
"{}/agents/packs/com.wpfleger.sietch-tabr",
|
|
||||||
parent
|
|
||||||
.path()
|
|
||||||
.join("xyz.block.buzz.app.dev.worktree-my-branch")
|
|
||||||
.display()
|
|
||||||
);
|
|
||||||
let expected_path = format!(
|
|
||||||
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
|
||||||
canonical.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
write_agents_json(
|
|
||||||
&canonical,
|
|
||||||
&serde_json::json!([{
|
|
||||||
"name": "Paul",
|
|
||||||
"persona_pack_path": worktree_pack_path
|
|
||||||
}]),
|
|
||||||
);
|
|
||||||
|
|
||||||
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
|
||||||
|
|
||||||
let records = read_agents_json(&canonical);
|
|
||||||
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
|
||||||
// Old field name should be removed
|
|
||||||
assert!(records[0].get("persona_pack_path").is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn team_dir_reconcile_rewrites_new_field_name() {
|
|
||||||
let parent = tempfile::tempdir().unwrap();
|
|
||||||
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
|
||||||
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
|
||||||
|
|
||||||
let worktree_team_path = format!(
|
|
||||||
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
|
||||||
parent
|
|
||||||
.path()
|
|
||||||
.join("xyz.block.buzz.app.dev.worktree-my-branch")
|
|
||||||
.display()
|
|
||||||
);
|
|
||||||
let expected_path = format!(
|
|
||||||
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
|
||||||
canonical.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
write_agents_json(
|
|
||||||
&canonical,
|
|
||||||
&serde_json::json!([{
|
|
||||||
"name": "Paul",
|
|
||||||
"persona_team_dir": worktree_team_path
|
|
||||||
}]),
|
|
||||||
);
|
|
||||||
|
|
||||||
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
|
||||||
|
|
||||||
let records = read_agents_json(&canonical);
|
|
||||||
assert_eq!(records[0]["persona_team_dir"], expected_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn team_dir_reconcile_leaves_canonical_path_unchanged() {
|
|
||||||
let parent = tempfile::tempdir().unwrap();
|
|
||||||
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
|
||||||
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
|
||||||
|
|
||||||
let canonical_path = format!(
|
|
||||||
"{}/agents/teams/com.wpfleger.sietch-tabr",
|
|
||||||
canonical.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
write_agents_json(
|
|
||||||
&canonical,
|
|
||||||
&serde_json::json!([{
|
|
||||||
"name": "Duncan",
|
|
||||||
"persona_team_dir": canonical_path
|
|
||||||
}]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let before = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
|
||||||
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
|
||||||
let after = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(before, after);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn team_dir_reconcile_skips_records_without_team_dir() {
|
|
||||||
let parent = tempfile::tempdir().unwrap();
|
|
||||||
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
|
||||||
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
|
||||||
|
|
||||||
write_agents_json(
|
|
||||||
&canonical,
|
|
||||||
&serde_json::json!([{
|
|
||||||
"name": "Test Agent",
|
|
||||||
"agent_command": "buzz-agent"
|
|
||||||
}]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let before = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
|
||||||
reconcile_team_dirs_in_file(&canonical.join("agents/managed-agents.json"), &canonical);
|
|
||||||
let after = std::fs::read_to_string(canonical.join("agents/managed-agents.json")).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(before, after);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn team_dir_reconcile_is_idempotent() {
|
|
||||||
let parent = tempfile::tempdir().unwrap();
|
|
||||||
let canonical = parent.path().join(CANONICAL_DEV_IDENTIFIER);
|
|
||||||
std::fs::create_dir_all(canonical.join("agents")).unwrap();
|
|
||||||
|
|
||||||
let worktree_pack_path = format!(
|
|
||||||
"{}/agents/packs/com.wpfleger.sietch-tabr",
|
|
||||||
parent
|
|
||||||
.path()
|
|
||||||
.join("xyz.block.buzz.app.dev.worktree-my-branch")
|
|
||||||
.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
write_agents_json(
|
|
||||||
&canonical,
|
|
||||||
&serde_json::json!([{
|
|
||||||
"name": "Paul",
|
|
||||||
"persona_pack_path": worktree_pack_path
|
|
||||||
}]),
|
|
||||||
);
|
|
||||||
|
|
||||||
let path = canonical.join("agents/managed-agents.json");
|
|
||||||
reconcile_team_dirs_in_file(&path, &canonical);
|
|
||||||
let after_first = std::fs::read_to_string(&path).unwrap();
|
|
||||||
reconcile_team_dirs_in_file(&path, &canonical);
|
|
||||||
let after_second = std::fs::read_to_string(&path).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(after_first, after_second);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Packs → Teams migration tests ───────────────────────────────────
|
// ── Packs → Teams migration tests ───────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -673,20 +516,6 @@ fn migrate_packs_to_teams_rewrites_agents_json() {
|
|||||||
assert!(records[0].get("persona_name_in_pack").is_none());
|
assert!(records[0].get("persona_name_in_pack").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_personas_json(dir: &Path, records: &serde_json::Value) {
|
|
||||||
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
|
||||||
std::fs::write(
|
|
||||||
dir.join("agents/personas.json"),
|
|
||||||
serde_json::to_vec_pretty(records).unwrap(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_personas_json(dir: &Path) -> Vec<serde_json::Value> {
|
|
||||||
let content = std::fs::read_to_string(dir.join("agents/personas.json")).unwrap();
|
|
||||||
serde_json::from_str(&content).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rename_provider_to_runtime_migrates_field() {
|
fn rename_provider_to_runtime_migrates_field() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user