main
Drew Owens 0.9.10 source: presence baseline holds only bridge-put documents (adversary pass 46); manifest as published bf99cca 19d ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532
//! SEV-1, MEASURED 2026-09-30: WHERE THE BRIDGE'S DATA LIVES.
//!
//! Until 0.9.0 every file this bridge wrote on its own initiative — the recovery
//! folders that hold the ONLY copy of work the user had not saved, the action
//! ledger, the ownership registry — lived in `bridges-cache\solidworks`, the
//! directory ab INSTALLS the bridge into. ab's updater re-extracts that
//! directory, and on Drew's machine it was found CREATED 2026-09-18 09:42 by the
//! auto-install of 0.8.9: every recovery folder from 09-17 was gone. Only test
//! documents were in them. The next time it would not have been.
//!
//! Every test here drives the real binary with the two locations laid out the
//! way ab lays them out under `%LOCALAPPDATA%`, and asserts one of the four
//! things the fix promises:
//!
//!   * the data root is NOT the install cache, and nothing is written there;
//!   * what an older bridge left in the install cache is COPIED out, verified
//!     byte for byte, never over anything, with the originals left in place —
//!     each file once;
//!   * `swx_recover_list` still reads the install cache, and says which of its
//!     rescues is the only copy;
//!   * a sweep REFUSES to place recovery under an install location, however the
//!     path is spelled — and closes nothing when it refuses.

//
// 0.9.10: the documents these sessions are seeded with were not opened by the
// bridge, so the user-presence guard counts them as the USER's (adversary pass
// 46: the baseline may contain only documents the bridge put there). Every
// sweep below passes `userPresentOverride` + `userAuthorized`, standing in for
// the user's explicit request — which is what these sweeps model.
mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};
use std::path::{Path, PathBuf};

fn s(v: &Value) -> String {
    serde_json::to_string_pretty(v).unwrap_or_default()
}

/// A machine whose window census can be read, so the sweep is allowed to close.
fn a_visible_machine(tag: &str) -> TempDir {
    let d = TempDir::new(tag);
    std::fs::write(d.join("windows.json"), "[]").unwrap();
    d
}

/// `%LOCALAPPDATA%`, with ab's layout under it — and, beside it, the one
/// directory only this bridge writes to.
struct Layout {
    local: TempDir,
}

impl Layout {
    fn new(tag: &str) -> Layout {
        Layout { local: TempDir::new(tag) }
    }
    /// The data root since 0.9.1: a SIBLING of `Adom Bridge\`.
    fn data(&self) -> PathBuf {
        self.local.0.join("adom-solidworks-bridge")
    }
    /// ab's own directory: its installer's and its uninstaller's.
    fn ab(&self) -> PathBuf {
        self.local.0.join("Adom Bridge")
    }
    fn cache(&self) -> PathBuf {
        self.ab().join("bridges-cache")
    }
    /// The first legacy root: 0.8.x's install cache.
    fn legacy(&self) -> PathBuf {
        self.cache().join("solidworks")
    }
    /// The second legacy root: 0.9.0's data root, inside ab's directory.
    fn legacy_data(&self) -> PathBuf {
        self.ab().join("solidworks-data")
    }
}

/// Unsaved work the sweep has to write somewhere: a pathless import and a saved
/// document with changes.
fn a_session_with_unsaved_work(root: &Path) {
    std::fs::create_dir_all(root).unwrap();
    std::fs::write(
        root.join("mock-solidworks-open-docs.json"),
        json!({
            "running": true, "untitled": 1, "active": "untitled:a",
            "docs": [
                {"docType": "part", "path": "untitled:a", "title": "Imported.step.SLDPRT",
                 "saved": false, "dirty": true},
                {"docType": "part", "path": "C:\\Users\\drew\\work\\bracket.SLDPRT",
                 "title": "bracket.SLDPRT", "saved": true, "dirty": true}
            ],
            "files": {}
        })
        .to_string(),
    )
    .unwrap();
}

/// A file that looks like what SOLIDWORKS writes: four varying bytes, the format
/// marker at offset 4, and a body.
fn a_document(len: usize) -> Vec<u8> {
    let mut v = vec![0x9f, 0xe4, 0x18, 0x9f, 0x00, 0x00, 0x00, 0x04];
    v.resize(len, b'x');
    v
}

fn write(path: &Path, bytes: &[u8]) {
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(path, bytes).unwrap();
}

/// A sweep folder the way a pre-0.9.0 bridge left it: the rescue and its record.
fn a_legacy_sweep(recovery: &Path, sweep: &str, name: &str, bytes: &[u8]) -> PathBuf {
    a_legacy_sweep_of(recovery, sweep, &[(name, bytes)])
}

/// ...with several rescues in it.
fn a_legacy_sweep_of(recovery: &Path, sweep: &str, rescues: &[(&str, &[u8])]) -> PathBuf {
    let dir = recovery.join(sweep);
    let mut entries: Vec<Value> = Vec::new();
    for (name, bytes) in rescues {
        write(&dir.join(name), bytes);
        entries.push(json!({
            "name": name,
            "recoveryPath": dir.join(name).to_string_lossy(),
            "title": name, "originalPath": Value::Null, "kind": "part",
            "namingFallback": Value::Null, "originalBytes": Value::Null,
            "recoveryBytes": bytes.len(), "signatureOk": true,
            "verified": true, "verdictReason": Value::Null, "preserved": true,
            "closed": true, "wasDirty": true, "wasPathless": true
        }));
    }
    write(
        &dir.join("manifest.json"),
        s(&json!({
            "manifestVersion": 1,
            "sweep": sweep,
            "reason": "the user asked me to close SOLIDWORKS",
            "failures": [],
            "files": entries
        }))
        .as_bytes(),
    );
    dir
}

fn sweeps_named<'a>(list: &'a Value, sweep: &str) -> Vec<&'a Value> {
    list["sweeps"]
        .as_array()
        .map(|a| a.iter().filter(|x| x["sweep"] == sweep).collect())
        .unwrap_or_default()
}

fn ledger_count(root: &Path, action: &str) -> usize {
    std::fs::read_to_string(root.join("ledger.jsonl"))
        .unwrap_or_default()
        .lines()
        .filter(|l| l.contains(&format!("\"action\":\"{action}\"")))
        .count()
}

// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn recovery_is_written_to_the_data_root_never_into_the_install_cache() {
    let lay = Layout::new("dataroot-default");
    a_session_with_unsaved_work(&lay.data());
    let machine = a_visible_machine("dataroot-default-machine");
    let local = lay.local.path();
    // NO OVERRIDE: the root comes from %LOCALAPPDATA%, as it does on Drew's
    // machine. (An empty SWX_SCRATCH_DIR is the harness's way of not setting it.)
    let b = Bridge::start_with(&[
        ("SWX_SCRATCH_DIR", ""),
        ("LOCALAPPDATA", &local),
        ("SWX_LAUNCH_FAKE_DIR", &machine.path()),
    ]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));
    check!(b, "swx_close_all_safely: the sweep runs in the default layout", r["success"] == true && r["done"] == true, s(&r));

    let dir = PathBuf::from(r["recoveryDir"].as_str().unwrap_or(""));
    check!(
        b,
        "swx_close_all_safely: the rescue is written under the DATA root",
        dir.starts_with(lay.data().join("recovery")),
        format!("{} vs {}", dir.display(), lay.data().display())
    );
    check!(
        b,
        "swx_close_all_safely: ...and NOT under ab's install cache, which an update empties",
        !dir.starts_with(lay.cache()),
        dir.display().to_string()
    );
    let written: Vec<PathBuf> = r["recovered"]
        .as_array()
        .cloned()
        .unwrap_or_default()
        .iter()
        .filter_map(|x| x["recoveryPath"].as_str().map(PathBuf::from))
        .collect();
    check!(b, "swx_close_all_safely: both unsaved documents were written", written.len() == 2, s(&r));
    for f in &written {
        check!(
            b,
            "swx_close_all_safely: every rescue exists, in the data root",
            f.exists() && f.starts_with(lay.data()),
            f.display().to_string()
        );
    }
    // THE REST OF THE DATA WENT WITH IT.
    check!(
        b,
        "swx_close_all_safely: the ledger is in the data root",
        lay.data().join("ledger.jsonl").exists(),
        lay.data().display().to_string()
    );
    check!(
        b,
        "swx_close_all_safely: and nothing at all was written into ab's directory",
        !lay.ab().exists(),
        lay.ab().display().to_string()
    );

    let l = b.call("swx_recover_list", json!({}));
    check!(
        b,
        "swx_recover_list: reads the data root",
        l["recoveryRoot"] == json!(lay.data().join("recovery").to_string_lossy()),
        s(&l)
    );
    check!(
        b,
        "swx_recover_list: ...and names both legacy locations it also reads, oldest first",
        l["legacyRecoveryRoot"] == json!(lay.legacy().join("recovery").to_string_lossy())
            && l["legacyRecoveryRoots"]
                == json!([
                    {"path": lay.legacy().join("recovery").to_string_lossy(), "location": "legacyInstallCache"},
                    {"path": lay.legacy_data().join("recovery").to_string_lossy(), "location": "legacyDataRoot"},
                ]),
        s(&l)
    );
    let sweep = l["sweeps"].as_array().and_then(|a| a.first()).cloned().unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: a sweep in the data root is not reported at risk",
        sweep["location"] == "data" && sweep["atRiskFromUpdate"] == false && sweep["warning"].is_null(),
        s(&sweep)
    );
}

#[test]
fn what_an_older_bridge_left_in_the_install_cache_is_copied_out_verified_and_left_in_place() {
    let lay = Layout::new("dataroot-migrate");
    let legacy = lay.legacy();
    let rescue = a_document(64 * 1024);
    let stuck_bytes = a_document(32 * 1024);
    let sweep_dir = a_legacy_sweep_of(
        &legacy.join("recovery"),
        "20260917-101500",
        &[("bracket.SLDPRT", &rescue), ("stuck.SLDPRT", &stuck_bytes)],
    );
    let manifest_bytes = std::fs::read(sweep_dir.join("manifest.json")).unwrap();
    let ledger = b"{\"at\":1789000000,\"action\":\"document_created\",\"detail\":{}}\n{\"at\":1789000100,\"action\":\"close_all_safely\",\"detail\":{}}\n".to_vec();
    write(&legacy.join("ledger.jsonl"), &ledger);
    let timings = br#"{"swx_docs":{"count":3,"secs":[0.1,0.2,0.3]}}"#.to_vec();
    write(&legacy.join("timings.json"), &timings);
    let scratch_doc = a_document(20 * 1024);
    write(&legacy.join("exports").join("scratch").join("Part1.SLDPRT"), &scratch_doc);
    write(&legacy.join("owned-docs.json"), br#"{"documents":[]}"#);
    // WHAT IS NOT DATA: the bridge's own install. Never copied.
    write(&legacy.join("swx-bridge.exe"), b"MZ not really");
    write(&legacy.join("bridge.json"), br#"{"name":"solidworks"}"#);
    // A FILE THAT IS ALREADY IN THE DATA ROOT is never overwritten.
    write(&legacy.join("settings.json"), br#"{"badge":false}"#);
    write(&lay.data().join("settings.json"), br#"{"badge":true}"#);
    // AND ONE THAT CANNOT BE READ: its copy fails, and must leave nothing behind.
    let stuck = legacy.join("recovery").join("20260917-101500").join("stuck.SLDPRT");
    let unreadable = {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&stuck, std::fs::Permissions::from_mode(0o000)).unwrap();
        // Root reads anything; the failure half of this test needs a user that
        // cannot.
        std::fs::read(&stuck).is_err()
    };

    let local = lay.local.path();
    let env = [("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())];
    let b = Bridge::start_with(&env);
    // BEFORE ANY VERB: this is what the migration left, untouched by anything else.
    let data = lay.data();
    let same = |rel: &str, want: &[u8]| std::fs::read(data.join(rel)).map(|got| got == want).unwrap_or(false);
    check!(b, "migration: a rescue is copied byte for byte", same("recovery/20260917-101500/bracket.SLDPRT", &rescue), data.display().to_string());
    check!(b, "migration: ...with the sweep's own record", same("recovery/20260917-101500/manifest.json", &manifest_bytes), data.display().to_string());
    check!(b, "migration: the bridge's scratch documents come too", same("exports/scratch/Part1.SLDPRT", &scratch_doc), data.display().to_string());
    check!(b, "migration: and its timings", same("timings.json", &timings), data.display().to_string());
    check!(b, "migration: and the ownership registry", data.join("owned-docs.json").exists(), data.display().to_string());
    let migrated_ledger = std::fs::read(data.join("ledger.jsonl")).unwrap_or_default();
    check!(
        b,
        "migration: the ledger is carried over whole, and the move is recorded in it",
        migrated_ledger.starts_with(&ledger)
            && ledger_count(&data, "data_migrated_from_install_cache") == 1,
        String::from_utf8_lossy(&migrated_ledger).to_string()
    );
    check!(
        b,
        "migration: the bridge's own install is not data and is not copied",
        !data.join("swx-bridge.exe").exists() && !data.join("bridge.json").exists(),
        data.display().to_string()
    );
    check!(
        b,
        "migration: a file already in the data root is NEVER overwritten",
        same("settings.json", br#"{"badge":true}"#),
        data.display().to_string()
    );
    // THE ORIGINALS ARE LEFT EXACTLY WHERE THEY WERE.
    check!(
        b,
        "migration: every original is left in place, unchanged",
        std::fs::read(sweep_dir.join("bracket.SLDPRT")).ok().as_deref() == Some(&rescue[..])
            && std::fs::read(legacy.join("ledger.jsonl")).ok().as_deref() == Some(&ledger[..])
            && std::fs::read(legacy.join("exports/scratch/Part1.SLDPRT")).ok().as_deref() == Some(&scratch_doc[..])
            && std::fs::read(legacy.join("settings.json")).ok().as_deref() == Some(&br#"{"badge":false}"#[..]),
        legacy.display().to_string()
    );
    let marker: Value = serde_json::from_str(
        &std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    let listed = |key: &str, file: &str| {
        marker[key].as_array().map(|a| a.iter().any(|e| e["file"] == file)).unwrap_or(false)
    };
    // SAME NAME, DIFFERENT BYTES: the legacy settings went BESIDE the existing
    // file — never over it, and never dropped either.
    let beside = marker["copied"]
        .as_array()
        .and_then(|a| a.iter().find(|e| e["file"] == "settings.json").cloned())
        .unwrap_or(json!({}));
    let beside_as = beside["copiedAs"].as_str().unwrap_or("").to_string();
    check!(
        b,
        "migration: a different file of the same name is copied BESIDE the existing one, not dropped",
        beside["outcome"] == "copiedBeside"
            && beside_as.starts_with("settings.json.legacy-")
            && same(&beside_as, br#"{"badge":false}"#),
        s(&marker)
    );
    check!(
        b,
        "migration: the record says what was copied, by root, size, mtime and digest, and that the originals stayed",
        marker["originalsLeftInPlace"] == true
            && marker["markerVersion"] == 2
            && listed("copied", "recovery/20260917-101500/bracket.SLDPRT")
            && marker["copied"].as_array().map(|a| a.iter().all(|e| {
                e["legacyRoot"].is_string() && e["bytes"].is_u64() && e["modifiedMs"].is_u64()
                    && e["sha256"].as_str().map(|h| h.len() == 64).unwrap_or(false)
            })).unwrap_or(false),
        s(&marker)
    );
    if unreadable {
        check!(
            b,
            "migration: a copy that cannot be made is reported, and leaves NOTHING behind in the data root",
            listed("failed", "recovery/20260917-101500/stuck.SLDPRT")
                && !data.join("recovery/20260917-101500/stuck.SLDPRT").exists(),
            s(&marker)
        );
    }

    // THE LISTING, AFTER: the copy is the one to use, and the original says so.
    let l = b.call("swx_recover_list", json!({}));
    let both = sweeps_named(&l, "20260917-101500");
    let ours = both.iter().find(|x| x["location"] == "data").cloned().cloned().unwrap_or(json!({}));
    let theirs = both.iter().find(|x| x["location"] == "legacyInstallCache").cloned().cloned().unwrap_or(json!({}));
    let bracket = ours["files"]
        .as_array()
        .and_then(|a| a.iter().find(|f| f["name"] == "bracket.SLDPRT").cloned())
        .unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: the migrated rescue is listed from the data root, with the sweep's own verdict",
        bracket["source"] == "manifest" && bracket["verified"] == true && ours["atRiskFromUpdate"] == false,
        s(&l)
    );
    check!(
        b,
        "swx_recover_list: the original is listed from the install cache, AT RISK",
        theirs["atRiskFromUpdate"] == true,
        s(&l)
    );
    if unreadable {
        // One of its files did not copy, so this is still the only copy of it.
        check!(
            b,
            "swx_recover_list: a legacy sweep with a file that did not copy claims NO copy",
            theirs["copiedTo"].is_null()
                && theirs["warning"].as_str().unwrap_or("").contains("NO copy"),
            s(&theirs)
        );
        // AND THE DATA-ROOT SIDE DOES NOT CALL IT GONE. Its manifest came across
        // and names the file; the file is still in the old location.
        let stuck_here = ours["files"]
            .as_array()
            .and_then(|a| a.iter().find(|f| f["name"] == "stuck.SLDPRT").cloned())
            .unwrap_or(json!({}));
        check!(
            b,
            "swx_recover_list: a file whose copy failed is NOT COPIED YET, pointing at its only copy — not GONE",
            stuck_here["notCopiedYet"] == true
                && stuck_here["present"] == false
                && stuck_here["legacyPath"] == json!(stuck.to_string_lossy())
                && stuck_here["howToOpen"].as_str().unwrap_or("").contains("NOT COPIED YET"),
            s(&ours)
        );
    }
    drop(b);

    // ── SECOND START. The user deleted one migrated copy; the unreadable file
    //    became readable; and an older bridge (a rollback) wrote a new rescue.
    std::fs::remove_file(data.join("exports/scratch/Part1.SLDPRT")).unwrap();
    {
        use std::os::unix::fs::PermissionsExt;
        std::fs::set_permissions(&stuck, std::fs::Permissions::from_mode(0o644)).unwrap();
    }
    let late = a_document(48 * 1024);
    a_legacy_sweep(&legacy.join("recovery"), "20260930-120000", "late.SLDPRT", &late);
    let b = Bridge::start_with(&env);
    check!(
        b,
        "migration: a rescue that appeared since is copied on the next start",
        same("recovery/20260930-120000/late.SLDPRT", &late),
        data.display().to_string()
    );
    check!(
        b,
        "migration: a copy the user deleted is NOT brought back — each file is migrated once",
        !data.join("exports/scratch/Part1.SLDPRT").exists(),
        data.display().to_string()
    );
    if unreadable {
        check!(
            b,
            "migration: a copy that failed is retried on the next start",
            data.join("recovery/20260917-101500/stuck.SLDPRT").exists(),
            data.display().to_string()
        );
        let l = b.call("swx_recover_list", json!({}));
        let theirs = sweeps_named(&l, "20260917-101500")
            .into_iter()
            .find(|x| x["location"] == "legacyInstallCache")
            .cloned()
            .unwrap_or(json!({}));
        let ours = sweeps_named(&l, "20260917-101500")
            .into_iter()
            .find(|x| x["location"] == "data")
            .cloned()
            .unwrap_or(json!({}));
        let stuck_here = ours["files"]
            .as_array()
            .and_then(|a| a.iter().find(|f| f["name"] == "stuck.SLDPRT").cloned())
            .unwrap_or(json!({}));
        check!(
            b,
            "swx_recover_list: ...and the retried copy is listed as a rescue in the data root",
            stuck_here["source"] == "manifest" && stuck_here["verified"] == true,
            s(&ours)
        );
        check!(
            b,
            "swx_recover_list: once every file has a copy, the original points at it",
            theirs["copiedTo"] == json!(data.join("recovery").join("20260917-101500").to_string_lossy())
                && theirs["warning"].as_str().unwrap_or("").contains("use that one"),
            s(&theirs)
        );
    }
    check!(
        b,
        "migration: the second run is on the record too",
        ledger_count(&data, "data_migrated_from_install_cache") == 2,
        std::fs::read_to_string(data.join("ledger.jsonl")).unwrap_or_default()
    );
    drop(b);

    // ── THIRD START, nothing new: nothing is copied and nothing is recorded.
    let b = Bridge::start_with(&env);
    b.call("swx_recover_list", json!({}));
    check!(
        b,
        "migration: a start with nothing new to copy does nothing",
        ledger_count(&data, "data_migrated_from_install_cache") == 2
            && !data.join("exports/scratch/Part1.SLDPRT").exists(),
        std::fs::read_to_string(data.join("ledger.jsonl")).unwrap_or_default()
    );
}

/// A sweep whose recovery folder would land under an install location — the
/// install directory itself, or ab's install cache — is refused before anything
/// is written or closed, however the path is spelled.
#[test]
fn a_sweep_refuses_to_place_recovery_under_an_install_location() {
    let t = TempDir::new("dataroot-refuse");
    let inst = t.join("inst");
    std::fs::create_dir_all(&inst).unwrap();
    std::os::unix::fs::symlink(&inst, t.join("link")).unwrap();
    std::fs::create_dir_all(t.join("elsewhere")).unwrap();
    let p = |x: &Path| x.to_string_lossy().to_string();
    let unrelated = p(&t.join("unrelated"));
    // ab's own directory, as `%LOCALAPPDATA%\Adom Bridge` — given only to the
    // case that needs it; everywhere else LOCALAPPDATA is empty, i.e. unset.
    let local = t.join("local");
    // (label, data root, install dir, ab's install cache, LOCALAPPDATA)
    let cases: Vec<(&str, String, String, String, String)> = vec![
        ("inside the install directory", p(&inst.join("data")), p(&inst), unrelated.clone(), String::new()),
        ("through a link to it", p(&t.join("link").join("data")), p(&inst), unrelated.clone(), String::new()),
        ("spelled with ..", format!("{}/../inst/data", p(&t.join("elsewhere"))), p(&inst), unrelated.clone(), String::new()),
        ("spelled in another case", p(&inst.join("data")).replace("/inst/", "/INST/"), p(&inst), unrelated.clone(), String::new()),
        ("AS the install directory", p(&t.join("eq")), p(&t.join("eq").join("recovery")), unrelated.clone(), String::new()),
        ("inside ab's install cache", p(&t.join("cache").join("data")), unrelated.clone(), p(&t.join("cache")), String::new()),
        // 0.9.0's OWN ROOT: inside the directory ab's installer and uninstaller own
        // (adversary pass 39). Not the cache, not the install directory — refused
        // for whose directory it is.
        ("inside ab's own directory", p(&local.join("Adom Bridge").join("solidworks-data")), unrelated.clone(), unrelated.clone(), p(&local)),
    ];
    for (label, root, install, cache, localappdata) in cases {
        // The mock session lives in the data root; for the case-spelled variant,
        // at the path as the filesystem sees it.
        let seed = PathBuf::from(if label == "spelled in another case" { p(&inst.join("data")) } else { root.clone() });
        a_session_with_unsaved_work(&seed);
        let machine = a_visible_machine("dataroot-refuse-machine");
        let b = Bridge::start_with(&[
            ("SWX_SCRATCH_DIR", &root),
            ("SWX_INSTALL_DIR", &install),
            ("SWX_LEGACY_DIR", &cache),
            ("LOCALAPPDATA", &localappdata),
            ("SWX_LAUNCH_FAKE_DIR", &machine.path()),
        ]);
        b.call("swx_start", json!({}));
        let before = b.call("swx_docs", json!({}));
        let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));
        check!(
            b,
            &format!("swx_close_all_safely: refuses a recovery folder {label}"),
            r["success"] == false && r["errorCode"] == "recovery_inside_install_dir",
            s(&r)
        );
        let after = b.call("swx_docs", json!({}));
        check!(
            b,
            &format!("swx_close_all_safely: ...and closes NOTHING when it refuses ({label})"),
            before["count"].as_u64().is_some() && before["count"] == after["count"],
            format!("before {} after {}", s(&before), s(&after))
        );
        check!(
            b,
            &format!("swx_close_all_safely: ...and writes nothing ({label})"),
            !seed.join("recovery").exists(),
            seed.display().to_string()
        );
        check!(
            b,
            &format!("swx_close_all_safely: the bridge said so in its log at start ({label})"),
            b.log().contains("WARNING recovery is inside an install location"),
            b.log()
        );
    }

    // A SIBLING THAT MERELY SHARES A PREFIX is not inside anything.
    let sibling = p(&t.join("inst-data"));
    a_session_with_unsaved_work(Path::new(&sibling));
    let machine = a_visible_machine("dataroot-refuse-sibling");
    let b = Bridge::start_with(&[
        ("SWX_SCRATCH_DIR", &sibling),
        ("SWX_INSTALL_DIR", &p(&inst)),
        ("SWX_LEGACY_DIR", &unrelated),
        ("SWX_LAUNCH_FAKE_DIR", &machine.path()),
    ]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));
    check!(
        b,
        "swx_close_all_safely: a data root beside the install directory is not refused",
        r["success"] == true && r["done"] == true,
        s(&r)
    );
}

/// A rescue that exists ONLY in the install cache — written by an older bridge
/// after this one started, say — is listed, and the listing says it is the only
/// copy and that the next update deletes it.
#[test]
fn recover_list_finds_a_rescue_left_only_in_the_install_cache() {
    let t = TempDir::new("dataroot-legacy-list");
    let data = t.join("data");
    let cache = t.join("cache");
    std::fs::create_dir_all(&data).unwrap();
    let b = Bridge::start_with(&[
        ("SWX_SCRATCH_DIR", &data.to_string_lossy()),
        ("SWX_LEGACY_DIR", &cache.to_string_lossy()),
    ]);
    let rescue = a_document(40 * 1024);
    a_legacy_sweep(&cache.join("recovery"), "20260929-080000", "gear.SLDPRT", &rescue);

    let l = b.call("swx_recover_list", json!({}));
    let sweep = sweeps_named(&l, "20260929-080000").first().cloned().cloned().unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: a rescue left in the install cache is listed",
        sweep["location"] == "legacyInstallCache",
        s(&l)
    );
    check!(
        b,
        "swx_recover_list: ...marked at risk from the next update, as the ONLY copy",
        sweep["atRiskFromUpdate"] == true
            && sweep["copiedTo"].is_null()
            && sweep["warning"].as_str().unwrap_or("").contains("NO copy"),
        s(&sweep)
    );
    let gear = sweep["files"]
        .as_array()
        .and_then(|a| a.iter().find(|f| f["name"] == "gear.SLDPRT").cloned())
        .unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: ...and its verdict is read from its own record, as anywhere else",
        gear["source"] == "manifest" && gear["verified"] == true && gear["bytes"] == rescue.len(),
        s(&gear)
    );
}

/// A COPY THAT DOES NOT MATCH ITS ORIGINAL IS NOT A COPY. It is removed — a
/// half-copied rescue must never sit in the data root looking like a whole one —
/// the failure is on the record, the original stays where it was, the listing
/// says the original is the only copy, and the next start tries again.
#[test]
fn a_copy_that_does_not_match_its_original_is_discarded_and_retried() {
    let lay = Layout::new("dataroot-corrupt");
    let rescue = a_document(64 * 1024);
    let sweep_dir = a_legacy_sweep(&lay.legacy().join("recovery"), "20260917-090000", "shaft.SLDPRT", &rescue);
    let data = lay.data();
    std::fs::create_dir_all(&data).unwrap();
    std::fs::write(data.join("mock-migrate-corrupts-copy"), "").unwrap();
    let local = lay.local.path();
    let env = [("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())];

    let b = Bridge::start_with(&env);
    let marker: Value = serde_json::from_str(
        &std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    let failed = |m: &Value, f: &str| m["failed"].as_array().map(|a| a.iter().any(|e| e["file"] == f)).unwrap_or(false);
    check!(
        b,
        "migration: a copy that does not verify is reported as failed",
        failed(&marker, "recovery/20260917-090000/shaft.SLDPRT"),
        s(&marker)
    );
    check!(
        b,
        "migration: ...and REMOVED, so no partial rescue sits in the data root",
        !data.join("recovery/20260917-090000/shaft.SLDPRT").exists(),
        data.display().to_string()
    );
    check!(
        b,
        "migration: ...and the original is untouched",
        std::fs::read(sweep_dir.join("shaft.SLDPRT")).ok().as_deref() == Some(&rescue[..]),
        sweep_dir.display().to_string()
    );
    let l = b.call("swx_recover_list", json!({}));
    check!(
        b,
        "migration: a sweep none of whose files copied leaves no empty folder to be listed beside it",
        sweeps_named(&l, "20260917-090000").len() == 1
            && !data.join("recovery").join("20260917-090000").exists(),
        s(&l)
    );
    let theirs = sweeps_named(&l, "20260917-090000")
        .into_iter()
        .find(|x| x["location"] == "legacyInstallCache")
        .cloned()
        .unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: the original is the only copy, and says so",
        theirs["atRiskFromUpdate"] == true
            && theirs["copiedTo"].is_null()
            && theirs["warning"].as_str().unwrap_or("").contains("NO copy"),
        s(&l)
    );
    drop(b);

    // THE DISK RECOVERS. The next start retries, and this time it lands.
    std::fs::remove_file(data.join("mock-migrate-corrupts-copy")).unwrap();
    let b = Bridge::start_with(&env);
    let copy = data.join("recovery/20260917-090000/shaft.SLDPRT");
    check!(
        b,
        "migration: a copy that failed is retried on the next start, and verified",
        std::fs::read(&copy).ok().as_deref() == Some(&rescue[..]),
        copy.display().to_string()
    );
    let marker: Value = serde_json::from_str(
        &std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    check!(
        b,
        "migration: ...and the record no longer lists it as failing",
        !failed(&marker, "recovery/20260917-090000/shaft.SLDPRT")
            && marker["copied"].as_array().map(|a| a.iter().any(|e| e["file"] == "recovery/20260917-090000/shaft.SLDPRT")).unwrap_or(false),
        s(&marker)
    );
}

/// THE MIGRATION RUNS BEFORE THE BRIDGE READS ITS OWN STATE.
///
/// `Ctx::new` restores the ownership registry from the data root. A migration
/// that ran after it would copy the registry into place and leave the running
/// bridge owning nothing until its NEXT restart — so the first bridge after an
/// update would refuse to close or write to every document it had created
/// before it, which is the update-shaped version of bug #5.
#[test]
fn ownership_an_older_bridge_recorded_is_restored_by_the_first_start_after_the_move() {
    let lay = Layout::new("dataroot-ownership");
    let legacy = lay.legacy();
    std::fs::create_dir_all(&legacy).unwrap();
    // AN OLDER BRIDGE, writing into the install cache, creates a document.
    {
        let old = Bridge::start_in(TempDir::borrowed(&legacy), &[]);
        old.call("swx_start", json!({}));
        let made = old.call("swx_new_document", json!({"type": "part", "keepUnsaved": true}));
        check!(old, "precondition: the older bridge created a document", made["success"] == true, s(&made));
        check!(
            old,
            "precondition: ...and recorded that it owns it, in the install cache",
            std::fs::read_to_string(legacy.join("owned-docs.json")).unwrap_or_default().contains("Part"),
            legacy.display().to_string()
        );
    }
    // SOLIDWORKS KEEPS RUNNING ACROSS THE UPDATE. The simulator keeps its session
    // in the bridge's root, so it is carried over by hand — that is the
    // simulator's plumbing, not the bridge's data, and not part of what is tested.
    let data = lay.data();
    std::fs::create_dir_all(&data).unwrap();
    for e in std::fs::read_dir(&legacy).unwrap().flatten() {
        let name = e.file_name().to_string_lossy().to_string();
        if name.starts_with("mock-") && e.path().is_file() {
            std::fs::copy(e.path(), data.join(&name)).unwrap();
        }
    }
    let local = lay.local.path();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let st = b.call("swx_status", json!({}));
    check!(
        b,
        "migration: the FIRST start after the move already owns what the older bridge created",
        b.log().contains("ownership registry restored") && st["success"] == true,
        b.log()
    );
}

/// A START THAT IS EXPECTED TO DIE, under the mock-only interruption fault.
/// Spawned directly — the harness rightly treats a bridge that never answers as a
/// failure — and never waited on forever: if the fault were not seen, the bridge
/// would serve until killed, and a suite that HANGS instead of failing hides the
/// very regression it exists to catch. Returns the exit code, or `None` if it
/// had to be killed.
fn a_start_that_should_die(local: &str) -> Option<i32> {
    let mut child = std::process::Command::new(common::BIN)
        .args(["--port", &common::free_port().to_string()])
        .env("SWX_MOCK", "1")
        .env("ADOM_BIND_HOST", "127.0.0.1")
        .env("SWX_SCRATCH_DIR", "")
        .env("LOCALAPPDATA", local)
        .env_remove("ADOM_DIRECT_API_URL")
        .env_remove("ADOM_BRIDGE_TOKEN")
        .stdout(std::process::Stdio::null())
        .stderr(std::process::Stdio::null())
        .spawn()
        .expect("run the bridge");
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
    loop {
        if let Ok(Some(status)) = child.try_wait() {
            return status.code();
        }
        if std::time::Instant::now() > deadline {
            let _ = child.kill();
            let _ = child.wait();
            return None;
        }
        std::thread::sleep(std::time::Duration::from_millis(50));
    }
}

/// A START THAT DIES MID-COPY leaves nothing under the real name.
///
/// The migration runs at start-up, and a start can die at any point — a crash, a
/// power cut, ab killing a bridge that is slow to answer `/status`. A copy made
/// straight to the real name would leave a TRUNCATED file there, and the
/// never-overwrite rule would then protect the truncated copy forever while the
/// next update deleted the only whole one. So every copy is made under a
/// temporary name and renamed into place once it verifies. And if the original
/// is gone by the time anyone looks, the half-copied bytes are listed as what
/// they are: incomplete, and possibly all that is left.
#[test]
fn a_start_that_dies_mid_copy_leaves_no_truncated_file_under_the_real_name() {
    let lay = Layout::new("dataroot-interrupted");
    let rescue = a_document(64 * 1024);
    // `zeta` sorts after `manifest.json`, and the start is let through ONE copy:
    // so it dies with the record already across and the rescue half-written —
    // the state in which a listing could call the rescue GONE.
    let legacy_sweep = a_legacy_sweep(&lay.legacy().join("recovery"), "20260917-080000", "zeta.SLDPRT", &rescue);
    let data = lay.data();
    std::fs::create_dir_all(&data).unwrap();
    std::fs::write(data.join("mock-migrate-dies-mid-copy"), "1").unwrap();
    let local = lay.local.path();

    // THE START THAT DIES. Spawned directly: the harness rightly treats a bridge
    // that never answers as a failure.
    assert_eq!(a_start_that_should_die(&local), Some(3), "precondition: the start died mid-copy");
    let here = data.join("recovery").join("20260917-080000");
    let left: Vec<String> = std::fs::read_dir(&here)
        .map(|d| d.flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect())
        .unwrap_or_default();
    assert!(
        left.iter().any(|n| n == "manifest.json")
            && left.iter().any(|n| n == "zeta.SLDPRT.migrating")
            && !left.iter().any(|n| n == "zeta.SLDPRT"),
        "a start that died mid-copy must leave its half-written bytes ONLY under a temporary name, \
         never under the real one — the never-overwrite rule would protect a truncated rescue \
         forever: {left:?}"
    );

    // THE UPDATE ARRIVES before the bridge starts again, and takes the original.
    std::fs::remove_dir_all(&legacy_sweep).unwrap();
    std::fs::remove_file(data.join("mock-migrate-dies-mid-copy")).unwrap();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let l = b.call("swx_recover_list", json!({}));
    let ours = sweeps_named(&l, "20260917-080000").first().cloned().cloned().unwrap_or(json!({}));
    let files = ours["files"].as_array().cloned().unwrap_or_default();
    // TWO entries, and they agree: the partial bytes, which are not a rescue,
    // and the file the manifest names, which says ONLY A PARTIAL COPY of it
    // exists — not GONE, and not "moved or deleted by something else", because
    // nothing moved it: the copy never finished.
    let partial = files.iter().find(|f| f["name"] == "zeta.SLDPRT.migrating").cloned().unwrap_or(json!({}));
    let named = files.iter().find(|f| f["name"] == "zeta.SLDPRT").cloned().unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: the half-copied bytes are listed as a PARTIAL COPY, never as a rescue",
        files.len() == 2
            && partial["partialCopy"] == true
            && partial["copyOf"] == "zeta.SLDPRT"
            && partial["verified"] == false
            && partial["howToOpen"].as_str().unwrap_or("").contains("PARTIAL COPY"),
        s(&l)
    );
    let how = named["howToOpen"].as_str().unwrap_or("");
    check!(
        b,
        "swx_recover_list: ...and the file it was copying says ONLY A PARTIAL COPY exists — not GONE",
        named["present"] == false
            && named["partialCopyOnly"] == true
            && named["partialCopy"] == "zeta.SLDPRT.migrating"
            && named["verified"] == false
            && how.contains("ONLY A PARTIAL COPY EXISTS")
            && !how.contains("GONE")
            && !how.contains("moved or deleted by something else"),
        s(&named)
    );
    drop(b);

    // AND WHEN THE ORIGINAL IS STILL THERE, the next start simply copies it.
    let lay = Layout::new("dataroot-interrupted-retry");
    let legacy_sweep = a_legacy_sweep(&lay.legacy().join("recovery"), "20260917-081500", "gear.SLDPRT", &rescue);
    let data = lay.data();
    std::fs::create_dir_all(&data).unwrap();
    std::fs::write(data.join("mock-migrate-dies-mid-copy"), "").unwrap();
    let local = lay.local.path();
    assert_eq!(a_start_that_should_die(&local), Some(3), "precondition: the start died mid-copy");
    std::fs::remove_file(data.join("mock-migrate-dies-mid-copy")).unwrap();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let here = data.join("recovery").join("20260917-081500");
    let names: Vec<String> = std::fs::read_dir(&here)
        .map(|d| d.flatten().map(|e| e.file_name().to_string_lossy().to_string()).collect())
        .unwrap_or_default();
    check!(
        b,
        "migration: the next start copies the file whole and leaves no temporary behind",
        std::fs::read(here.join("gear.SLDPRT")).ok().as_deref() == Some(&rescue[..])
            && std::fs::read(here.join("manifest.json")).ok()
                == std::fs::read(legacy_sweep.join("manifest.json")).ok()
            && !names.iter().any(|n| n.ends_with(".migrating")),
        format!("{names:?}")
    );
}

/// THE DATA ROOT IS A DIRECTORY ONLY THIS BRIDGE WRITES TO (adversary pass 39).
///
/// 0.9.0 moved the data root out of the directory ab re-extracts on every bridge
/// update, and into `%LOCALAPPDATA%\Adom Bridge\solidworks-data` — inside the
/// directory ab's INSTALLER owns ("installs to %LOCALAPPDATA%\Adom Bridge\", "the
/// uninstaller lives at %LOCALAPPDATA%\Adom Bridge\uninstall.exe", per ab's
/// docs). What an ab uninstall, a repair or "delete the Adom Bridge folder and
/// reinstall" does to the rest of that directory is undocumented and unmeasured.
/// So the root is a SIBLING, named for this bridge alone: not under any path
/// that contains `Adom Bridge`, and not under the install cache.
#[test]
fn the_data_root_is_a_directory_only_this_bridge_writes() {
    let lay = Layout::new("dataroot-sibling");
    let local = lay.local.path();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let l = b.call("swx_recover_list", json!({}));
    let recovery = PathBuf::from(l["recoveryRoot"].as_str().unwrap_or(""));
    let root = recovery.parent().map(|p| p.to_path_buf()).unwrap_or_default();
    check!(
        b,
        "swx_recover_list: the data root is %LOCALAPPDATA%\\adom-solidworks-bridge",
        root == lay.data(),
        format!("{} vs {}", root.display(), lay.data().display())
    );
    check!(
        b,
        "swx_recover_list: ...not under ANY path containing `Adom Bridge`",
        !root
            .components()
            .any(|c| c.as_os_str().to_string_lossy().to_ascii_lowercase().contains("adom bridge")),
        root.display().to_string()
    );
    check!(
        b,
        "swx_recover_list: ...and not under ab's directory or its install cache",
        !root.starts_with(lay.ab()) && !root.starts_with(lay.cache()),
        root.display().to_string()
    );
}

/// TWO LEGACY ROOTS, OLDEST FIRST. 0.9.0's data root — `Adom Bridge\solidworks-data`
/// — is migrated like 0.8.x's install cache, after it. Rescues with distinct
/// names simply move. A LEDGER in both is MERGED (0.9.2): 0.9.1 gave the real
/// name to the first root's copy and put the second BESIDE it, and adversary pass
/// 40 showed that picks the stale one on exactly Drew's timeline — the cache
/// re-created by 0.8.9 after 0.9.0 had copied it and kept appending, so the 0.9.0
/// era sat in a side file `swx_action_ledger` never reads. This test pinned that
/// naming rule; it is changed ON PURPOSE: the live ledger is now the union of
/// both, ordered by `at`, and both originals are kept beside it. 0.9.0's own
/// migration record is not data and does not come along.
#[test]
fn the_0_9_0_data_root_is_migrated_after_the_install_cache_and_nothing_is_lost() {
    let lay = Layout::new("dataroot-two-legacies");
    let cache_ledger = b"{\"at\":1788000000,\"action\":\"document_created\",\"detail\":{\"title\":\"FROM-0.8.9\"}}\n".to_vec();
    let mut data_ledger = cache_ledger.clone();
    data_ledger.extend_from_slice(b"{\"at\":1789000000,\"action\":\"close_all_safely\",\"detail\":{\"title\":\"FROM-0.9.0\"}}\n");
    write(&lay.legacy().join("ledger.jsonl"), &cache_ledger);
    write(&lay.legacy_data().join("ledger.jsonl"), &data_ledger);
    write(&lay.legacy_data().join("owned-docs.json"), br#"{"documents":[]}"#);
    write(&lay.legacy_data().join("migrated-from-legacy.json"), br#"{"copied":[],"why":"0.9.0's own record"}"#);
    let old = a_document(48 * 1024);
    let newer = a_document(56 * 1024);
    a_legacy_sweep(&lay.legacy().join("recovery"), "20260917-090000", "old.SLDPRT", &old);
    let newer_sweep = a_legacy_sweep(&lay.legacy_data().join("recovery"), "20260930-170000", "newer.SLDPRT", &newer);
    // THE SAME SWEEP AND NAME IN BOTH, WITH DIFFERENT BYTES: contrived, and the
    // case where "never over it, never dropped" has to hold for a rescue.
    let mut other = a_document(48 * 1024);
    other[100] = b'y';
    write(&lay.legacy_data().join("recovery").join("20260917-090000").join("old.SLDPRT"), &other);

    let local = lay.local.path();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let data = lay.data();
    let read = |rel: &str| std::fs::read(data.join(rel)).unwrap_or_default();
    check!(
        b,
        "migration: rescues from BOTH older locations are copied into the data root",
        read("recovery/20260917-090000/old.SLDPRT") == old
            && read("recovery/20260930-170000/newer.SLDPRT") == newer
            && read("owned-docs.json") == br#"{"documents":[]}"#.to_vec(),
        data.display().to_string()
    );
    let marker: Value = serde_json::from_str(
        &std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    let ledger_besides: Vec<String> = std::fs::read_dir(&data)
        .map(|d| {
            d.flatten()
                .map(|e| e.file_name().to_string_lossy().to_string())
                .filter(|n| n.starts_with("ledger.jsonl.legacy-"))
                .collect()
        })
        .unwrap_or_default();
    // THE LIVE LEDGER IS THE MERGE: every line of both, each once, in `at` order
    // — here that is exactly 0.9.0's (a superset of the cache's), followed by
    // whatever this start itself recorded.
    let live = read("ledger.jsonl");
    let mut beside_contents: Vec<Vec<u8>> = ledger_besides.iter().map(|n| read(n)).collect();
    beside_contents.sort();
    let mut want = vec![cache_ledger.clone(), data_ledger.clone()];
    want.sort();
    check!(
        b,
        "migration: two ledgers are MERGED into the live one, and BOTH originals are kept beside it",
        live.starts_with(&data_ledger) && beside_contents == want,
        format!("{ledger_besides:?}\n{}", s(&marker))
    );
    let merge = marker["merged"].as_array().and_then(|a| a.iter().find(|m| m["file"] == "ledger.jsonl").cloned()).unwrap_or(json!({}));
    check!(
        b,
        "migration: ...and the merge is on the record, with where each original went and what the result holds",
        merge["kind"] == "ledger"
            && merge["result"]["entries"] == 2
            && merge["liveBefore"]["copiedAs"].as_str().map(|n| n.starts_with("ledger.jsonl.legacy-")).unwrap_or(false)
            && merge["incoming"]["copiedAs"].as_str().map(|n| n.starts_with("ledger.jsonl.legacy-")).unwrap_or(false)
            && ledger_count(&data, "legacy_files_merged") == 1,
        s(&marker)
    );
    check!(
        b,
        "migration: the order is recorded, oldest first, and 0.9.0's own record is not copied as data",
        marker["from"]
            == json!([lay.legacy().to_string_lossy(), lay.legacy_data().to_string_lossy()])
            && marker["markerVersion"] == 2,
        s(&marker)
    );
    // A FILE ONLY THE SECOND LEGACY ROOT HOLDS, named by a record in the data
    // root (built after the start, so the migration cannot dissolve the state).
    write(&lay.legacy_data().join("recovery").join("20260930-170000").join("late.SLDPRT"), &a_document(40 * 1024));
    write(
        &data.join("recovery").join("20260930-170000").join("manifest.json"),
        json!({"manifestVersion": 1, "files": [
            {"name": "newer.SLDPRT", "title": "newer.SLDPRT", "kind": "part", "recoveryBytes": newer.len(), "verified": true, "preserved": true, "closed": true},
            {"name": "late.SLDPRT", "title": "late.SLDPRT", "kind": "part", "recoveryBytes": 40 * 1024, "verified": true, "preserved": true, "closed": true},
        ]}).to_string().as_bytes(),
    );
    let l = b.call("swx_recover_list", json!({}));
    let ours = sweeps_named(&l, "20260930-170000")
        .into_iter()
        .find(|x| x["location"] == "data")
        .cloned()
        .unwrap_or(json!({}));
    let late = ours["files"].as_array().and_then(|a| a.iter().find(|f| f["name"] == "late.SLDPRT").cloned()).unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: a file only 0.9.0's location holds is NOT COPIED YET, pointing there",
        late["notCopiedYet"] == true
            && late["verified"].is_null()
            && late["legacyPath"]
                == json!(lay.legacy_data().join("recovery").join("20260930-170000").join("late.SLDPRT").to_string_lossy()),
        s(&ours)
    );
    let first = sweeps_named(&l, "20260917-090000")
        .into_iter()
        .find(|x| x["location"] == "data")
        .cloned()
        .unwrap_or(json!({}));
    let beside = first["files"]
        .as_array()
        .and_then(|a| a.iter().find(|f| f["copiedBeside"] == true).cloned())
        .unwrap_or(json!({}));
    check!(
        b,
        "migration + swx_recover_list: a same-named rescue with different bytes is kept BESIDE, and listed as such",
        read("recovery/20260917-090000/old.SLDPRT") == old
            && beside["besideOf"] == "old.SLDPRT"
            && std::fs::read(beside["path"].as_str().unwrap_or("")).ok() == Some(other.clone())
            && beside["howToOpen"].as_str().unwrap_or("").contains("A COPY KEPT BESIDE")
            && !first["files"].as_array().map(|a| a.iter().any(|f| f["name"].as_str().unwrap_or("").starts_with("manifest.json"))).unwrap_or(true),
        s(&first)
    );
    let theirs = sweeps_named(&l, "20260930-170000")
        .into_iter()
        .find(|x| x["location"] == "legacyDataRoot")
        .cloned()
        .unwrap_or(json!({}));
    // `late.SLDPRT` has no copy, so this sweep is still the only copy of it.
    check!(
        b,
        "swx_recover_list: a rescue in 0.9.0's location is listed, at risk (ab's directory), honest about its copy",
        theirs["atRisk"] == true
            && theirs["atRiskFromUpdate"] == false
            && theirs["copiedTo"].is_null()
            && theirs["warning"].as_str().unwrap_or("").contains("ab's own directory")
            && theirs["directory"] == json!(newer_sweep.to_string_lossy()),
        s(&theirs)
    );
    let older = sweeps_named(&l, "20260917-090000")
        .into_iter()
        .find(|x| x["location"] == "legacyInstallCache")
        .cloned()
        .unwrap_or(json!({}));
    check!(
        b,
        "swx_recover_list: a fully copied legacy sweep points at its copy",
        older["copiedTo"] == json!(data.join("recovery").join("20260917-090000").to_string_lossy()),
        s(&older)
    );
}

/// A LEGACY FILE THAT CHANGES AFTER ITS COPY IS COPIED AGAIN — BESIDE THE FIRST
/// COPY, NEVER OVER IT — AND SAID TO HAVE CHANGED (adversary pass 39, sev-2).
///
/// 0.9.0 settled a migrated file by its NAME, so the two files that change by
/// nature — the append-only ledger and the rewritten ownership registry — were
/// never looked at again: a rollback to an older bridge appends to the OLD
/// ledger, and the next update deletes it. Now each copy is recorded by (legacy
/// root, path, size, mtime, sha256 of the copy), and a later start that finds the
/// legacy file different copies it again as `<name>.legacy-<stamp>` and reports
/// it — in the migration record, in the ledger, and in `swx_recover_list`.
#[test]
fn a_legacy_file_that_changes_after_its_copy_is_copied_again_beside_it() {
    let lay = Layout::new("dataroot-changed");
    let legacy_ledger = lay.legacy().join("ledger.jsonl");
    let legacy_owned = lay.legacy().join("owned-docs.json");
    let before = b"{\"at\":1789000000,\"action\":\"document_created\",\"detail\":{\"title\":\"BEFORE\"}}\n".to_vec();
    write(&legacy_ledger, &before);
    write(&legacy_owned, br#"{"documents":[]}"#);
    let legacy_settings = lay.legacy().join("settings.json");
    write(&legacy_settings, br#"{"badge":false}"#);
    write(&lay.legacy().join("timings.json"), br#"{"swx_docs":{"count":3,"x":1}}"#);
    let local = lay.local.path();
    let env = [("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())];
    let data = lay.data();
    let besides = |prefix: &str| -> Vec<String> {
        let mut v: Vec<String> = std::fs::read_dir(&data)
            .map(|d| {
                d.flatten()
                    .map(|e| e.file_name().to_string_lossy().to_string())
                    .filter(|n| n.starts_with(prefix))
                    .collect()
            })
            .unwrap_or_default();
        v.sort();
        v
    };
    let marker = || -> Value {
        serde_json::from_str(&std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default())
            .unwrap_or(json!({}))
    };
    drop(Bridge::start_with(&env));
    let first_copy = std::fs::read(data.join("ledger.jsonl")).unwrap_or_default();
    assert!(first_copy.starts_with(&before), "precondition: the first start copied the legacy ledger");

    // A ROLLBACK: the older bridge appends to its ledger and rewrites its registry.
    let mut rolled = before.clone();
    rolled.extend_from_slice(b"{\"at\":1790000000,\"action\":\"document_closed\",\"detail\":{\"title\":\"DURING-ROLLBACK\"}}\n");
    write(&legacy_ledger, &rolled);
    write(&legacy_owned, br#"{"documents":[{"title":"Part9","session":4242}]}"#);
    // A DIFFERENT SIZE WITH THE SAME MTIME: a restore that keeps timestamps, or a
    // filesystem whose mtime is coarser than the two writes (FAT32 keeps two
    // seconds). Only the size can tell.
    let legacy_timings = lay.legacy().join("timings.json");
    let timings_mtime = std::fs::metadata(&legacy_timings).unwrap().modified().unwrap();
    write(&legacy_timings, br#"{"swx_docs":{"count":4}}"#);
    std::fs::File::options()
        .write(true)
        .open(&legacy_timings)
        .unwrap()
        .set_modified(timings_mtime)
        .unwrap();
    // SAME SIZE, DIFFERENT CONTENT, LATER MTIME: only the mtime can tell.
    let settings_was = std::fs::metadata(&legacy_settings).unwrap().modified().unwrap();
    write(&legacy_settings, br#"{"badge":"no!"}"#);
    std::fs::File::options()
        .write(true)
        .open(&legacy_settings)
        .unwrap()
        .set_modified(settings_was + std::time::Duration::from_secs(60))
        .unwrap();

    let b = Bridge::start_with(&env);
    let ledger_besides = besides("ledger.jsonl.legacy-");
    let owned_besides = besides("owned-docs.json.legacy-");
    // 0.9.2: A CHANGED LEDGER IS MERGED INTO THE LIVE ONE (adversary pass 40).
    // 0.9.1 kept the rollback's entries only in a side file, which
    // `swx_action_ledger` never reads — the same defect as picking one of two
    // roots' ledgers. Now the live ledger holds both histories, and BOTH originals
    // are kept beside it: the live file as it was before the merge, and the
    // changed legacy version.
    let live_text = String::from_utf8_lossy(&std::fs::read(data.join("ledger.jsonl")).unwrap_or_default()).to_string();
    check!(
        b,
        "migration: the CHANGED legacy ledger is MERGED into the live one — both histories, each line once",
        live_text.contains("BEFORE")
            && live_text.contains("DURING-ROLLBACK")
            && live_text.matches("\"title\":\"BEFORE\"").count() == 1,
        live_text.clone()
    );
    let beside_ledgers: Vec<Vec<u8>> = ledger_besides.iter().map(|n| std::fs::read(data.join(n)).unwrap_or_default()).collect();
    check!(
        b,
        "migration: ...and both versions are kept beside it: the live file as it was, and the changed one",
        ledger_besides.len() == 2
            && beside_ledgers.iter().any(|v| v == &rolled)
            && beside_ledgers.iter().any(|v| v.starts_with(&before) && !String::from_utf8_lossy(v).contains("DURING-ROLLBACK")),
        format!("{ledger_besides:?}")
    );
    let timings_besides = besides("timings.json.legacy-");
    check!(
        b,
        "migration: a size change with the mtime put back is caught by its size and kept beside",
        timings_besides.len() == 1
            && std::fs::read(data.join(&timings_besides[0])).ok() == Some(br#"{"swx_docs":{"count":4}}"#.to_vec()),
        format!("{timings_besides:?}")
    );
    let settings_besides = besides("settings.json.legacy-");
    check!(
        b,
        "migration: a same-size rewrite is caught by its mtime and kept beside too",
        settings_besides.len() == 1
            && std::fs::read(data.join(&settings_besides[0])).ok() == Some(br#"{"badge":"no!"}"#.to_vec()),
        format!("{settings_besides:?}")
    );
    // THE REGISTRY IS UNITED, not picked: the live registry now proposes the
    // rollback's claim too (restore re-validates every claim against the live
    // session), and both versions are kept beside it.
    let live_owned: Value = serde_json::from_str(&std::fs::read_to_string(data.join("owned-docs.json")).unwrap_or_default()).unwrap_or(json!({}));
    check!(
        b,
        "migration: the rewritten ownership registry is UNITED with the live one, both versions kept beside",
        live_owned["documents"].as_array().map(|a| a.iter().any(|d| d["title"] == "Part9" && d["session"] == 4242)).unwrap_or(false)
            && owned_besides.len() == 2
            && owned_besides.iter().any(|n| {
                std::fs::read(data.join(n)).ok()
                    == Some(br#"{"documents":[{"title":"Part9","session":4242}]}"#.to_vec())
            }),
        format!("{owned_besides:?} {}", s(&live_owned))
    );
    let m = marker();
    let changed = m["changedSinceCopy"].as_array().cloned().unwrap_or_default();
    let ledger_change = changed.iter().find(|e| e["file"] == "ledger.jsonl").cloned().unwrap_or(json!({}));
    check!(
        b,
        "migration: the record says it CHANGED SINCE ITS COPY — what it was, what it is, where it went",
        ledger_change["was"]["bytes"] == before.len()
            && ledger_change["now"]["bytes"] == rolled.len()
            && ledger_change["now"]["sha256"].as_str().map(|h| h.len() == 64).unwrap_or(false)
            && ledger_change["copiedAs"]
                .as_str()
                .map(|c| ledger_besides.iter().any(|n| n == c) && std::fs::read(data.join(c)).ok() == Some(rolled.clone()))
                .unwrap_or(false)
            && ledger_change["mergedInto"] == "ledger.jsonl"
            && changed.iter().any(|e| e["file"] == "owned-docs.json"),
        s(&m)
    );
    check!(
        b,
        "migration: ...and the ledger says so in lines of its own — the change, and the merge",
        ledger_count(&data, "legacy_file_changed_since_copy") == 1
            && ledger_count(&data, "legacy_files_merged") == 1,
        std::fs::read_to_string(data.join("ledger.jsonl")).unwrap_or_default()
    );
    let l = b.call("swx_recover_list", json!({}));
    check!(
        b,
        "swx_recover_list: reports what changed since its copy",
        l["migration"]["changedSinceCopy"].as_array().map(|a| a.len()) == Some(4),
        s(&l["migration"])
    );
    drop(b);

    // NOTHING CHANGED: nothing is copied again.
    drop(Bridge::start_with(&env));
    let b = Bridge::start_with(&env);
    check!(
        b,
        "migration: an unchanged legacy file is not copied or merged again",
        besides("ledger.jsonl.legacy-").len() == 2
            && marker()["changedSinceCopy"].as_array().map(|a| a.len()) == Some(4)
            && ledger_count(&data, "legacy_files_merged") == 1,
        s(&marker())
    );
    drop(b);

    // IT CHANGES AGAIN: merged again, two more copies beside — and the earlier
    // copies beside are never overwritten.
    let earlier: Vec<(String, Vec<u8>)> =
        ledger_besides.iter().map(|n| (n.clone(), std::fs::read(data.join(n)).unwrap_or_default())).collect();
    let mut again = rolled.clone();
    again.extend_from_slice(b"{\"at\":1791000000,\"action\":\"document_closed\",\"detail\":{\"title\":\"AGAIN\"}}\n");
    write(&legacy_ledger, &again);
    let b = Bridge::start_with(&env);
    let ledger_besides_now = besides("ledger.jsonl.legacy-");
    let live_now = std::fs::read_to_string(data.join("ledger.jsonl")).unwrap_or_default();
    check!(
        b,
        "migration: a second change is merged too, and every earlier copy beside stays as it was",
        ledger_besides_now.len() == 4
            && earlier.iter().all(|(n, v)| std::fs::read(data.join(n)).ok().as_ref() == Some(v))
            && ledger_besides_now.iter().any(|n| std::fs::read(data.join(n)).ok() == Some(again.clone()))
            && live_now.contains("AGAIN")
            && live_now.matches("DURING-ROLLBACK").count() == 1,
        format!("{ledger_besides_now:?}\n{live_now}")
    );
}

/// NOT WHERE THE OS CLEANS (adversary pass 39, sev-3).
///
/// With neither `LOCALAPPDATA` nor `USERPROFILE`, the only location left is the
/// temp directory — which Storage Sense and Disk Cleanup empty by design. The
/// root may still point there (the bridge has to keep its notes somewhere), but
/// NO SWEEP writes the only copy of unsaved work there: it refuses with
/// `data_root_is_temp` before writing or closing anything, and the start-up log
/// says why. With `USERPROFILE`, the profile is used instead — before temp, always.
#[test]
fn with_no_profile_location_every_sweep_refuses_rather_than_write_to_temp() {
    let t = TempDir::new("dataroot-temp");
    let child_tmp = t.join("child-tmp");
    std::fs::create_dir_all(&child_tmp).unwrap();
    let c = child_tmp.to_string_lossy().to_string();
    let temp_root = child_tmp.join("adom-solidworks-bridge");
    a_session_with_unsaved_work(&temp_root);
    let machine = a_visible_machine("dataroot-temp-machine");
    let b = Bridge::start_with(&[
        ("SWX_SCRATCH_DIR", ""),
        ("LOCALAPPDATA", ""),
        ("USERPROFILE", ""),
        ("TMPDIR", &c),
        ("TMP", &c),
        ("TEMP", &c),
        ("SWX_LAUNCH_FAKE_DIR", &machine.path()),
    ]);
    check!(
        b,
        "start-up: a data root in the temp directory is announced, with what it means",
        b.log().contains("WARNING data root") && b.log().contains("data_root_is_temp"),
        b.log()
    );
    b.call("swx_start", json!({}));
    let before = b.call("swx_docs", json!({}));
    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));
    check!(
        b,
        "swx_close_all_safely: REFUSES to put the only copy of unsaved work in the temp directory",
        r["success"] == false && r["errorCode"] == "data_root_is_temp",
        s(&r)
    );
    let after = b.call("swx_docs", json!({}));
    check!(
        b,
        "swx_close_all_safely: ...closing nothing and writing nothing",
        before["count"].as_u64().is_some()
            && before["count"] == after["count"]
            && !temp_root.join("recovery").exists(),
        format!("before {} after {}", s(&before), s(&after))
    );
    drop(b);

    // WITH A PROFILE: the profile, not temp — and the sweep runs.
    let profile = t.join("profile");
    let profile_root = profile.join("adom-solidworks-bridge");
    a_session_with_unsaved_work(&profile_root);
    let pr = profile.to_string_lossy().to_string();
    let b = Bridge::start_with(&[
        ("SWX_SCRATCH_DIR", ""),
        ("LOCALAPPDATA", ""),
        ("USERPROFILE", &pr),
        ("TMPDIR", &c),
        ("TMP", &c),
        ("TEMP", &c),
        ("SWX_LAUNCH_FAKE_DIR", &machine.path()),
    ]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));
    check!(
        b,
        "swx_close_all_safely: with USERPROFILE the root is the profile's, and the sweep runs there",
        r["success"] == true
            && PathBuf::from(r["recoveryDir"].as_str().unwrap_or("")).starts_with(profile_root.join("recovery"))
            && !b.log().contains("WARNING data root"),
        s(&r)
    );
}

/// TWO REGISTRIES ARE UNITED, NOT PICKED — and one that cannot be merged is
/// still never dropped (adversary pass 40, E1).
///
/// The ownership registry in each older location holds the claims of the bridge
/// that wrote it. Keeping one copy under the real name disowns the other's
/// documents (fail-safe, but wrong); the union proposes every claim, and
/// `owned::restore` re-validates each against the live session anyway. A
/// registry that is not one this bridge wrote cannot be merged: it is copied
/// beside the live one, reported `mergeFailed`, and the live one is untouched.
#[test]
fn two_registries_are_united_and_an_unmergeable_one_is_kept_beside() {
    let lay = Layout::new("dataroot-registries");
    write(&lay.legacy().join("owned-docs.json"), br#"{"documents":[{"title":"PartA","session":1}]}"#);
    write(&lay.legacy_data().join("owned-docs.json"), br#"{"documents":[{"title":"PartB","session":2}]}"#);
    write(&lay.legacy().join("com-host-created.json"), br#"{"a":1}"#);
    // Not a registry (no `documents` list): the merge must refuse and keep it.
    write(&lay.legacy().join("settings.json"), br#"{"badge":true}"#);
    write(&lay.legacy_data().join("settings.json"), br#"{"badge":false}"#);
    let local = lay.local.path();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let data = lay.data();
    let live: Value =
        serde_json::from_str(&std::fs::read_to_string(data.join("owned-docs.json")).unwrap_or_default())
            .unwrap_or(json!({}));
    let titles: Vec<String> = live["documents"]
        .as_array()
        .map(|a| a.iter().filter_map(|d| d["title"].as_str().map(|t| t.to_string())).collect())
        .unwrap_or_default();
    check!(
        b,
        "migration: the live registry is the UNION of both older registries",
        titles == vec!["PartA".to_string(), "PartB".to_string()],
        s(&live)
    );
    let marker: Value = serde_json::from_str(
        &std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    check!(
        b,
        "migration: ...recorded as a registry merge with both originals beside it",
        marker["merged"]
            .as_array()
            .map(|a| a.iter().any(|m| m["file"] == "owned-docs.json" && m["kind"] == "registry" && m["result"]["entries"] == 2))
            .unwrap_or(false),
        s(&marker)
    );
    // settings.json is not a mergeable file: the second version goes beside, the
    // first keeps the name — as before 0.9.2.
    let settings_beside: Vec<String> = std::fs::read_dir(&data)
        .map(|d| {
            d.flatten()
                .map(|e| e.file_name().to_string_lossy().to_string())
                .filter(|n| n.starts_with("settings.json.legacy-"))
                .collect()
        })
        .unwrap_or_default();
    check!(
        b,
        "migration: a file that is not a ledger or a registry is still kept beside, not merged",
        std::fs::read(data.join("settings.json")).ok() == Some(br#"{"badge":true}"#.to_vec())
            && settings_beside.len() == 1
            && std::fs::read(data.join(&settings_beside[0])).ok() == Some(br#"{"badge":false}"#.to_vec()),
        format!("{settings_beside:?}")
    );
    drop(b);

    // A REGISTRY THAT CANNOT BE MERGED. A third location is not needed: make the
    // older registry change into something unmergeable, and start again.
    write(&lay.legacy_data().join("owned-docs.json"), b"{not json");
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let after: Value =
        serde_json::from_str(&std::fs::read_to_string(data.join("owned-docs.json")).unwrap_or_default())
            .unwrap_or(json!({}));
    let kept_beside = std::fs::read_dir(&data)
        .map(|d| {
            d.flatten().any(|e| {
                e.file_name().to_string_lossy().starts_with("owned-docs.json.legacy-")
                    && std::fs::read(e.path()).ok() == Some(b"{not json".to_vec())
            })
        })
        .unwrap_or(false);
    let marker2: Value = serde_json::from_str(
        &std::fs::read_to_string(data.join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    check!(
        b,
        "migration: an unmergeable registry leaves the live one untouched and is kept beside, never dropped",
        after == live
            && kept_beside
            && marker2["mergeFailed"].as_array().map(|a| a.iter().any(|e| e["file"] == "owned-docs.json")).unwrap_or(false),
        format!("{}\n{}", s(&after), s(&marker2))
    );
}

/// THE SAME SIZE AND MTIME: HASHED UNDER THE CAP, SAID OUT LOUD OVER IT
/// (adversary pass 40, E2). Every record says how its file is compared.
#[test]
fn a_record_says_whether_its_file_is_compared_by_hash_or_only_by_size_and_mtime() {
    let lay = Layout::new("dataroot-hashcap");
    write(&lay.legacy().join("owned-docs.json"), br#"{"documents":[]}"#);
    let big = vec![b'x'; 16 * 1024 * 1024 + 1];
    write(&lay.legacy().join("exports").join("big.step"), &big);
    let local = lay.local.path();
    let b = Bridge::start_with(&[("SWX_SCRATCH_DIR", ""), ("LOCALAPPDATA", local.as_str())]);
    let marker: Value = serde_json::from_str(
        &std::fs::read_to_string(lay.data().join("migrated-from-legacy.json")).unwrap_or_default(),
    )
    .unwrap_or(json!({}));
    let rec = |f: &str| {
        marker["copied"]
            .as_array()
            .and_then(|a| a.iter().find(|r| r["file"] == f).cloned())
            .unwrap_or(json!({}))
    };
    check!(
        b,
        "migration: a small file is compared by size, mtime AND sha256 of the legacy file",
        rec("owned-docs.json")["comparedBy"] == "size+mtime+sha256"
            && rec("owned-docs.json")["legacySha256"].as_str().map(|h| h.len() == 64).unwrap_or(false),
        s(&marker)
    );
    check!(
        b,
        "migration: a file over the 16 MiB cap is compared by size and mtime only — and its record SAYS so",
        rec("exports/big.step")["comparedBy"] == "size+mtime (over the 16 MiB hash cap)"
            && marker["hashCapBytes"] == 16 * 1024 * 1024,
        s(&rec("exports/big.step"))
    );
}

/// A RECOVERY FOLDER THAT CANNOT BE MADE IS REFUSED ONCE, BY NAME, BEFORE THE
/// FIRST SAVE (adversary pass 40, E3) — and nothing is written or closed.
#[test]
fn a_recovery_folder_that_cannot_be_made_is_refused_once_by_name() {
    let scratch = TempDir::new("dataroot-unwritable");
    a_session_with_unsaved_work(&scratch.0);
    std::fs::write(scratch.join("recovery"), b"a file where the folder must go").unwrap();
    let machine = a_visible_machine("dataroot-unwritable-machine");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_close_all_safely", json!({"userPresentOverride": true, "userAuthorized": true, "reason": "the user asked me to close SOLIDWORKS"}));
    check!(
        b,
        "swx_close_all_safely: refuses with recovery_dir_unwritable, naming the cause once",
        r["success"] == false
            && r["errorCode"] == "recovery_dir_unwritable"
            && r["recovered"].is_null(),
        s(&r)
    );
    check!(
        b,
        "swx_close_all_safely: ...before anything was written: the file in the way is untouched",
        std::fs::read(scratch.join("recovery")).ok() == Some(b"a file where the folder must go".to_vec()),
        String::new()
    );
}