Files
buzz/desktop/src-tauri/src/managed_agents/scope_init_tests.rs
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 40607d4a88 fix(desktop): pass-1 review corrections (round 2) — compensation, Mesh stop, scope, CI
Item 1 — Compensation primitive: replace AtomicBool gate with lock-owning
compensate_drain that takes the caller's already-held rt_transition guard
by value, re-acquires only the store lock, validates captured scope generation,
then restores journal entries via start_pair_under_held_locks. Split
start_pair_under_held_locks out of start_pair so both the normal and
compensation paths share the spawn-and-register body. execute_drain_journal
refactored to accept an injectable stop_fn via drain_journal_with_stop;
execute_drain_journal_with_stop_fn exposed for test injection. Tests: live
SIGKILL drain, structural lock-release proof, deterministic partial-failure
test with injected stop error covering stopped prefix and remaining tail.

Item 2 — Client stop + start serialization: mesh_stop_client Tauri command
in mesh_llm_scope.rs stops only a client-mode runtime; serve/absent are
no-ops. Client start (ensure_relay_mesh_for_record) acquires
workspace_transition through runtime installation to serialize against
apply_workspace, which holds workspace_transition from before the Option A
preflight through commit. fail_if_client_mesh_active preflight runs under
workspace_transition so no new client can start in the check→commit gap.
UI: 'Stop using shared compute' button in MeshComputeSettingsCard shown when
isConsuming; calls new meshStopClient() in tauriMesh.ts. e2eBridge mock for
mesh_stop_client added.

Item 3 — Fallible migrations + atomic marker: rename_provider_to_runtime_in_personas
propagates Result; migrate_agent_keys_to_dev_service_at returns Result and
propagates from copy_agent_keys_between_stores. run_scoped_migrations uses ?
on persona-provider step. _ready marker written via temp+rename (atomic).
dev-key migration skipped in unit-test builds (#[cfg(not(test))]) to avoid
macOS Keychain dialogs. Tests: old-marker upgrade (no scope deletion), partial
migration failure withholds v1 until repair succeeds.

Item 4 — Global-config captured respawn: Phase 2 restart validates captured
scope generation under store lock before stop, and again before respawn via
start_local_agent_pairs_with_preflight_at (new captured variant using
definitions_dir). persist_last_error validates generation under store lock.

Item 5 — Snapshot imports captured operation context: both confirm_agent_snapshot_import
and confirm_team_snapshot_import capture owner keys at entry, verify against
captured_scope.owner_pubkey immediately, thread captured keys through all
mint/retention/engram phases. Re-verify owner key under store lock before
Phase 3a write. Outbound profile/memory phases use captured_scope.relay_url.

Item 6 — CI red: rustfmt applied (agents_scoped.rs, import.rs); clippy
needless_borrow at team_snapshot.rs:793 fixed; e2eBridge apply_workspace mock
returns { applied: true, degraded: [] } in both immediate and delayed branches;
mesh_stop_client mock case added. Stale 3-line doc fragment removed from
mesh_llm.rs; visibility of re-exported agents_scoped fns bumped to pub(crate).

Item 7 — Listener behavioral test: useNestNotifications.test.mjs exercises
workspace-degraded toast payload, unlisten cleanup, and boundary payloads
without requiring a real Tauri runtime. Doc comment updated: event-sync dispatch
failure does not emit workspace-degraded (shutdown-time, no toast surface).

Item 8 — Dead code (minor): backfill_persona_snapshots AppHandle shim removed;
stale scope_init.rs:388-393 comment corrected; make_base_dir test helper removed.

File-size gate: mesh_llm.rs (999), import.rs (999), team_snapshot.rs (999).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-03 22:08:42 -04:00

528 lines
22 KiB
Rust

//! Unit tests for `managed_agents/scope_init.rs`.
//!
//! Kept in a sibling file so `scope_init.rs` stays under the
//! 1000-line size gate; `#[path]`-included from there.
use super::*;
use tempfile::TempDir;
/// Returns `(TempDir, base_dir)` where `base_dir = tmp.path().join("agents")`.
///
/// Production: `managed_agents_base_dir` returns `<app-data>/agents`.
/// Tests must use that same layout so `legacy_definitions_exist`,
/// `install_staged`, and `read_or_create_canonical_claim` all see files
/// at the correct level.
fn make_base_dir_pair() -> (TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().expect("tempdir");
let base_dir = tmp.path().join("agents");
std::fs::create_dir_all(&base_dir).unwrap();
(tmp, base_dir)
}
/// Write the legacy definition files directly into `base_dir`
/// (i.e. `<app-data>/agents/managed-agents.json` etc.).
fn make_legacy_files(base_dir: &Path) {
std::fs::create_dir_all(base_dir).unwrap();
std::fs::write(base_dir.join("managed-agents.json"), b"[]").unwrap();
std::fs::write(base_dir.join("teams.json"), b"[]").unwrap();
}
#[test]
fn test_fresh_no_legacy_scope_initializes_ready() {
let (_tmp, base_dir) = make_base_dir_pair();
let scope_dir = base_dir.join("scopes").join("testscope");
ensure_scope_ready("testscope", &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir), "scope should be Ready");
// Manifest should indicate FreshNoLegacy.
let manifest: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_dir.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(matches!(manifest.init_kind, ScopeInitKind::FreshNoLegacy));
}
#[test]
fn test_adopted_legacy_scope_copies_files() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
let scope_id = "firstscope";
let scope_dir = base_dir.join("scopes").join(scope_id);
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir));
assert!(
scope_dir.join("managed-agents.json").exists(),
"legacy managed-agents.json should be copied"
);
let manifest: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_dir.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(matches!(manifest.init_kind, ScopeInitKind::AdoptedLegacy));
}
#[test]
fn test_second_scope_legacy_claimed_by_other() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
// First scope claims.
let scope_a = base_dir.join("scopes").join("scope_a");
ensure_scope_ready("scope_a", &scope_a, &base_dir, "test_owner").unwrap();
// Second scope should see LegacyClaimedByOther.
let scope_b = base_dir.join("scopes").join("scope_b");
ensure_scope_ready("scope_b", &scope_b, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_b));
let manifest: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_b.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(
matches!(
manifest.init_kind,
ScopeInitKind::LegacyClaimedByOther { .. }
),
"second scope should see LegacyClaimedByOther, got {:?}",
manifest.init_kind
);
assert!(
!scope_b.join("managed-agents.json").exists(),
"second scope should start empty"
);
}
#[test]
fn test_idempotent_double_initialize() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
let scope_dir = base_dir.join("scopes").join("idempotent");
ensure_scope_ready("idempotent", &scope_dir, &base_dir, "test_owner").unwrap();
// Second call should be a fast no-op.
ensure_scope_ready("idempotent", &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir));
}
#[test]
fn test_staging_cleanup_on_retry() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
let scope_dir = base_dir.join("scopes").join("retry");
let staging = staging_dir_for(&scope_dir);
// Simulate an interrupted staging directory.
std::fs::create_dir_all(&staging).unwrap();
std::fs::write(staging.join("partial.json"), b"garbage").unwrap();
// ensure_scope_ready should clean it up and succeed.
ensure_scope_ready("retry", &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir));
assert!(!staging.exists(), "staging dir should be cleaned up");
}
#[test]
fn test_retention_db_claim_takes_precedence() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
// Pre-plant a retention.db with scope_a's claim.
// retention.db lives at `base_dir/retention.db` (i.e. `<app-data>/agents/retention.db`).
let retention_db_path = base_dir.join("retention.db");
let conn = Connection::open(&retention_db_path).unwrap();
ensure_migration_table(&conn).unwrap();
conn.execute(
"INSERT INTO retention_migrations (name, scope_id) VALUES (?1, ?2)",
params![DEFINITIONS_MIGRATION_NAME, "scope_a"],
)
.unwrap();
drop(conn);
// scope_b activates first — retention.db says scope_a owns legacy.
let scope_b = base_dir.join("scopes").join("scope_b");
ensure_scope_ready("scope_b", &scope_b, &base_dir, "test_owner").unwrap();
let manifest: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_b.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(
matches!(
manifest.init_kind,
ScopeInitKind::LegacyClaimedByOther { ref claiming_scope_id }
if claiming_scope_id == "scope_a"
),
"retention.db claim should win, got {:?}",
manifest.init_kind
);
// scope_a now activates — should adopt legacy.
let scope_a = base_dir.join("scopes").join("scope_a");
ensure_scope_ready("scope_a", &scope_a, &base_dir, "test_owner").unwrap();
let manifest_a: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_a.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(matches!(manifest_a.init_kind, ScopeInitKind::AdoptedLegacy));
assert!(scope_a.join("managed-agents.json").exists());
}
/// Crash boundary: claim written, then crash before any file is copied into
/// staging. On retry the staging directory does not exist, so the full
/// staged install runs again. The same scope wins the claim (INSERT OR
/// IGNORE is idempotent) and legacy files are copied correctly.
#[test]
fn test_crash_after_claim_before_staging_resumes_correctly() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
// Simulate: claim was written into the fallback file but no staging dir exists yet.
// The fallback claim file lives at `base_dir/legacy-claim.json`
// (no extra "agents" join — base_dir is already `<app-data>/agents`).
let claim_path = base_dir.join(FALLBACK_CLAIM_FILE);
let claim = serde_json::json!({"scope_id": "scope_a"});
std::fs::write(&claim_path, serde_json::to_vec(&claim).unwrap()).unwrap();
// No staging dir exists — retry runs the full staged install from the claim.
let scope_a = base_dir.join("scopes").join("scope_a");
ensure_scope_ready("scope_a", &scope_a, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_a), "scope must be Ready after retry");
let manifest: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_a.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(
matches!(manifest.init_kind, ScopeInitKind::AdoptedLegacy),
"scope_a owns the claim and must adopt legacy, got {:?}",
manifest.init_kind
);
assert!(
scope_a.join("managed-agents.json").exists(),
"legacy files must be copied after retry"
);
}
/// Crash boundary: staging directory exists (copy was in progress) but the
/// atomic rename never happened. On retry the stale staging dir is cleaned
/// and the full staged install runs again. The retry must not overwrite any
/// post-crash writes that might have landed in the target (the target
/// doesn't exist yet since rename never fired, so there's nothing to
/// overwrite — staging is the only artifact).
#[test]
fn test_crash_during_staging_copy_is_cleaned_on_retry() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
let scope_dir = base_dir.join("scopes").join("scope_retry");
let staging = staging_dir_for(&scope_dir);
// Simulate interrupted staging: directory exists with partial content.
std::fs::create_dir_all(&staging).unwrap();
std::fs::write(staging.join("managed-agents.json"), b"[\"partial\"]").unwrap();
// No manifest inside staging (write didn't complete).
ensure_scope_ready("scope_retry", &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir));
assert!(
!staging.exists(),
"stale staging dir must be cleaned up on retry"
);
// The final managed-agents.json is from the legacy source, not the partial.
let content = std::fs::read(scope_dir.join("managed-agents.json")).unwrap();
assert_eq!(
content, b"[]",
"managed-agents.json must be from the legacy source after retry"
);
}
/// Crash boundary: staging complete (manifest written) but rename never
/// happened. Detected by: staging dir exists. On retry, clean staging and
/// re-run; the claim is idempotent so the same scope adopts legacy again.
#[test]
fn test_crash_after_staging_manifest_before_rename_resumes_correctly() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
let scope_dir = base_dir.join("scopes").join("scope_rename");
let staging = staging_dir_for(&scope_dir);
// Simulate: staging complete with manifest, but rename never fired.
std::fs::create_dir_all(&staging).unwrap();
let manifest = ScopeManifest {
scope_id: "scope_rename".into(),
init_kind: ScopeInitKind::AdoptedLegacy,
};
std::fs::write(
staging.join(MANIFEST_FILE),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
std::fs::write(staging.join("managed-agents.json"), b"[]").unwrap();
// Scope dir itself does not exist (rename didn't fire).
assert!(!scope_dir.exists());
ensure_scope_ready("scope_rename", &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir));
assert!(!staging.exists(), "staging must be cleaned after retry");
assert!(
scope_dir.join("managed-agents.json").exists(),
"adopted file must be present after retry"
);
}
/// Crash boundary: atomic rename happened (target dir exists with manifest
/// and legacy files) but the `_ready` marker was never written (migrations
/// didn't complete). On next activation, `ensure_scope_ready` must resume
/// migrations and write the ready marker without re-copying files.
///
/// The post-crash content must be valid JSON so `run_scoped_migrations`
/// step 10 (JSON validation gate) passes and `_ready` is written. The test
/// verifies that the content is NOT overwritten (no re-staging), which is
/// the behaviour that matters: a legitimate post-crash inbound write
/// must survive a resume. Content corruption is a separate failure mode
/// outside the scope of the crash-resume path.
#[test]
fn test_crash_after_rename_before_ready_resumes_migrations() {
let (_tmp, base_dir) = make_base_dir_pair();
make_legacy_files(&base_dir);
let scope_dir = base_dir.join("scopes").join("scope_pre_ready");
// Simulate: rename already happened — target has manifest + files but no
// _ready marker. Content is valid JSON so migrations can complete.
std::fs::create_dir_all(&scope_dir).unwrap();
let manifest = ScopeManifest {
scope_id: "scope_pre_ready".into(),
init_kind: ScopeInitKind::AdoptedLegacy,
};
std::fs::write(
scope_dir.join(MANIFEST_FILE),
serde_json::to_vec(&manifest).unwrap(),
)
.unwrap();
// Valid JSON array — simulates content written before the crash.
std::fs::write(scope_dir.join("managed-agents.json"), b"[]").unwrap();
// No _ready marker.
assert!(!scope_is_ready(&scope_dir));
ensure_scope_ready("scope_pre_ready", &scope_dir, &base_dir, "test_owner").unwrap();
assert!(
scope_is_ready(&scope_dir),
"ready marker must be written on retry"
);
// Post-crash writes in the target must not be overwritten (no staging).
let content = std::fs::read(scope_dir.join("managed-agents.json")).unwrap();
assert_eq!(
content, b"[]",
"post-crash target content must not be overwritten on retry"
);
}
/// C2: migration failure must NOT write the `_ready` marker.
///
/// If `fold_personas_in_dir` fails (e.g. due to a corrupt personas.json)
/// `run_scoped_migrations` returns `Err` and `ensure_scope_ready` must
/// propagate that error without writing `_ready`. On a subsequent call with
/// the defect corrected, migrations complete and `_ready` IS written.
#[test]
fn test_migration_failure_withholds_ready_and_retry_succeeds() {
let (_tmp, base_dir) = make_base_dir_pair();
// Create the legacy directory with a CORRUPT personas.json.
std::fs::create_dir_all(&base_dir).unwrap();
std::fs::write(base_dir.join("managed-agents.json"), b"[]").unwrap();
// Corrupt personas.json: `fold_personas_in_dir` tries to parse it and
// returns Err, which run_scoped_migrations propagates.
std::fs::write(base_dir.join("personas.json"), b"not valid json").unwrap();
let scope_id = "scope_fail_retry";
let scope_dir = base_dir.join("scopes").join(scope_id);
// First attempt: migrations fail, _ready must NOT be written.
let result = ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner");
assert!(result.is_err(), "corrupt personas.json must cause Err");
assert!(
!scope_is_ready(&scope_dir),
"_ready must NOT be written when migrations fail"
);
// Repair the corrupt file.
std::fs::write(base_dir.join("personas.json"), b"[]").unwrap();
// Also repair the scope_dir since ensure_scope_ready may have left it in
// a partial state — remove it so the state machine reruns from staging.
if scope_dir.exists() {
std::fs::remove_dir_all(&scope_dir).unwrap();
}
// Second attempt: migrations succeed, _ready IS written.
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(
scope_is_ready(&scope_dir),
"_ready must be written on successful retry"
);
}
/// Versioned `_ready` upgrade: a scope whose marker was written by an older
/// pipeline (e.g. "ready" or any non-current version) must be forced through
/// `run_pre_ready_family` again and have its marker upgraded to the current
/// version. An already-current marker is a fast no-op.
#[test]
fn test_old_ready_marker_forces_pre_ready_pipeline_and_upgrades_version() {
let (_tmp, base_dir) = make_base_dir_pair();
let scope_id = "versioned-scope";
let scope_dir = base_dir.join("scopes").join(scope_id);
// Full initialization with fresh scope → marker written at current version.
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir), "scope must be ready after init");
// Verify the marker actually carries the version string.
let marker_content = std::fs::read_to_string(scope_dir.join(READY_MARKER)).unwrap();
assert_eq!(
marker_content.trim(),
READY_MARKER_VERSION,
"marker must carry current version"
);
// Downgrade the marker to simulate a pre-existing scope from an older build.
std::fs::write(scope_dir.join(READY_MARKER), b"ready").unwrap();
assert!(
!scope_is_ready(&scope_dir),
"old-version marker must not be considered current"
);
// Re-running ensure_scope_ready must upgrade the marker.
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(
scope_is_ready(&scope_dir),
"scope must be ready after version upgrade"
);
let upgraded_content = std::fs::read_to_string(scope_dir.join(READY_MARKER)).unwrap();
assert_eq!(
upgraded_content.trim(),
READY_MARKER_VERSION,
"upgraded marker must carry current version"
);
}
/// Production-contract coverage: `base_dir` is `<app-data>/agents`
/// (the real shape from `managed_agents_base_dir`). Legacy files live
/// at `base_dir/{managed-agents,teams}.json`; the scope dir lives at
/// `base_dir/scopes/<scope_id>/`; the fallback claim file lives at
/// `base_dir/legacy-claim.json`. This test verifies the full adoption
/// path using the production layout so any future double-join regresses
/// visibly here rather than silently succeeding on a synthetic tree.
#[test]
fn test_production_shaped_adoption_finds_legacy_files() {
// `app_data_dir` is the synthetic `<app-data>` root.
let tmp = tempfile::tempdir().expect("tempdir");
let app_data_dir = tmp.path();
// Production: `managed_agents_base_dir` returns `<app-data>/agents`.
let base_dir = app_data_dir.join("agents");
std::fs::create_dir_all(&base_dir).unwrap();
// Legacy files sit directly under `base_dir`.
std::fs::write(base_dir.join("managed-agents.json"), b"[]").unwrap();
std::fs::write(base_dir.join("teams.json"), b"[]").unwrap();
// Scope dir is `base_dir/scopes/<scope_id>/`.
let scope_id = "prod-shape-scope";
let scope_dir = base_dir.join("scopes").join(scope_id);
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(scope_is_ready(&scope_dir), "scope must be Ready");
// With the correct layout the scope must adopt legacy (not start fresh).
let manifest: ScopeManifest =
serde_json::from_slice(&std::fs::read(scope_dir.join(MANIFEST_FILE)).unwrap()).unwrap();
assert!(
matches!(manifest.init_kind, ScopeInitKind::AdoptedLegacy),
"production-layout scope must adopt legacy, got {:?}",
manifest.init_kind
);
// Legacy files were copied into the scope.
assert!(
scope_dir.join("managed-agents.json").exists(),
"managed-agents.json must be present in adopted scope"
);
// Fallback claim file lives at `base_dir/legacy-claim.json`, NOT at
// `base_dir/agents/legacy-claim.json` (which would be the double-join
// path). Verify the correct location was used.
assert!(
base_dir.join(FALLBACK_CLAIM_FILE).exists(),
"fallback claim must be at base_dir/legacy-claim.json, not at a nested path"
);
assert!(
!base_dir.join("agents").join(FALLBACK_CLAIM_FILE).exists(),
"double-join claim path must NOT exist"
);
}
/// Old `_ready` marker with a migration failure: the scope stays at the old
/// version until the broken file is repaired IN PLACE (no scope deletion),
/// then the retry advances the marker to the current version.
///
/// This test covers the path where `scope_dir` already has a `MANIFEST_FILE`
/// (staged install completed on a prior run) but carries an old-version
/// `_ready` marker. `ensure_scope_ready` re-runs migrations via the fast-path
/// at `scope_init.rs:158-162`. A corrupt `personas.json` inside the scope
/// directory makes `migrate_persona_provider_to_runtime_at` return `Err`,
/// which withholds the `v1` upgrade. After the file is repaired, a retry
/// succeeds and the marker advances — without ever deleting or recreating the
/// scope directory.
#[test]
fn test_migration_failure_withholds_ready_upgrade_repair_in_place() {
let (_tmp, base_dir) = make_base_dir_pair();
let scope_id = "scope-repair-in-place";
let scope_dir = base_dir.join("scopes").join(scope_id);
// ── Phase 1: full initialization → marker at v1 ─────────────────────────
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(
scope_is_ready(&scope_dir),
"scope must be v1-ready after init"
);
// ── Phase 2: downgrade marker → simulate a pre-existing old-version scope ─
std::fs::write(scope_dir.join(READY_MARKER), b"ready").unwrap();
assert!(
!scope_is_ready(&scope_dir),
"old-version marker must not be considered current"
);
// ── Phase 3: inject a migration failure ──────────────────────────────────
// Write a corrupt personas.json into the scope_dir so
// `migrate_persona_provider_to_runtime_at` returns Err when called on
// the scope_dir by the fast-path at scope_init.rs:158-162.
std::fs::write(scope_dir.join("personas.json"), b"not valid json").unwrap();
// Re-run ensure_scope_ready — must fail because migration fails.
let result = ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner");
assert!(
result.is_err(),
"corrupt personas.json must cause migration Err: {:?}",
result
);
// The marker must NOT have been advanced.
assert!(
!scope_is_ready(&scope_dir),
"_ready must NOT be upgraded when migration fails"
);
// Crucially, the scope_dir itself must still exist (repair in place).
assert!(
scope_dir.exists(),
"scope_dir must still exist after migration failure — no deletion allowed"
);
assert!(
scope_dir.join(MANIFEST_FILE).exists(),
"MANIFEST_FILE must survive migration failure"
);
// ── Phase 4: repair the file IN PLACE (no scope deletion) ───────────────
std::fs::remove_file(scope_dir.join("personas.json")).unwrap();
// ── Phase 5: retry → marker advances to v1 ───────────────────────────────
ensure_scope_ready(scope_id, &scope_dir, &base_dir, "test_owner").unwrap();
assert!(
scope_is_ready(&scope_dir),
"scope must be ready after in-place repair and retry"
);
let upgraded = std::fs::read_to_string(scope_dir.join(READY_MARKER)).unwrap();
assert_eq!(
upgraded.trim(),
READY_MARKER_VERSION,
"marker must carry current version after upgrade"
);
}