main
Drew Owens 0.9.0: bridge data moved out of the install cache (recovery survives updates), closed-in-place records, gates route thr… 5d02ca9 20d ago
//! ADVERSARIAL REVIEW OF 0.7.0 — pass twenty-two, the data-loss surface.
//!
//! The gates around the sweep are sound. `done = count_after == Some(0) &&
//! failed.is_empty()`, so a document that could not be saved is never closed,
//! keeps the count above zero, and forces `done:false`; an UNREADABLE count is
//! not `Some(0)` either. `swx_shutdown` attempts the quit only on
//! `success && done`, and `swx_stop`'s own `check_preconditions` is a second,
//! independent gate that refuses on a truncated count and on any dialog. Two
//! gates, neither derived from the other — that is the right shape for this.
//!
//! The saving itself is where the risk is.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};

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

/// THE RECOVERY WRITE IS VERIFIED BY EXISTENCE, NOT BY SIZE.
///
/// ```text
/// let on_disk = code == 0
///     && !now_path.trim().is_empty()
///     && std::path::Path::new(&now_path).exists();
/// ```
///
/// A file that exists and is EMPTY passes, the record says `preserved: true`,
/// and the document is then closed — discarding the only copy of the user's
/// unsaved work. A partial or zero-byte write is not a hypothetical here: the
/// machine this verb is meant to run on is down to single-digit gigabytes, and
/// SOLIDWORKS assemblies are large.
///
/// The idiom already exists in this file — `std::fs::metadata(out).map(|m| m.len())`
/// is computed for the export path — so the fix is to carry it here, refuse a
/// zero-length result, and REPORT the size so an operator can tell a real save
/// from an empty one.
///
/// I could not stage the partial write: the mock writes a fixed non-empty
/// string and its own check is `fs::write(..).is_err()`, which is STRONGER than
/// the real backend's. So this test asks for the reportable half — the size of
/// what was written — which is the minimum that makes the outcome auditable and
/// is a precondition for checking it at all.
#[test]
fn every_recovery_record_reports_the_size_it_wrote() {
    let b = Bridge::start_with(&[("SWX_ALLOW_USER_WRITES", "1")]);
    b.call("swx_start", json!({"reason": "preparing to close everything"}));
    let made = b.call("swx_new_document", json!({"type": "part", "keepUnsaved": true}));
    assert!(made["success"] == true, "precondition: a dirty pathless document exists: {}", s(&made));

    let r = b.call(
        "swx_close_all_safely",
        json!({"reason": "closing everything safely before a quit", "userAuthorized": true}),
    );
    let recs = r["recovered"]
        .as_array()
        .cloned()
        .or_else(|| r["records"].as_array().cloned())
        .unwrap_or_default();
    assert!(
        !recs.is_empty(),
        "precondition: the sweep preserved at least one document: {}",
        s(&r)
    );
    let preserved: Vec<&Value> = recs
        .iter()
        .filter(|x| x.get("preserved").and_then(|p| p.as_bool()) == Some(true))
        .collect();
    assert!(!preserved.is_empty(), "precondition: something was preserved: {}", s(&r));
    for rec in preserved {
        let has_size = rec.get("bytes").and_then(|v| v.as_u64()).is_some()
            || rec.get("recoveryBytes").and_then(|v| v.as_u64()).is_some()
            || rec.get("size").and_then(|v| v.as_u64()).is_some();
        assert!(
            has_size,
            "a record says preserved:true and does not say how many bytes reached disk. The \
             check behind it is `Path::exists()`, so a zero-byte file — an interrupted write on a \
             nearly-full disk — passes it, and the document is then CLOSED. Size is the only \
             thing that distinguishes a save from a file that merely appeared.\nrecord: {}\n{}",
            s(rec),
            s(&r)
        );
    }
}

/// THE USER'S OWN FILE IS NEVER TOUCHED — the invariant Drew actually stated.
///
/// A dirty document that already has a path is written into the recovery folder
/// under a new name; the file the user knows about must be byte-for-byte what it
/// was. This passes, and it is here because it is the one property that must
/// never regress on this surface.
///
/// Worth recording alongside it: `save_as3(doc, out, copy=false)` resolves to
/// `options = 1`, which is `swSaveAsOptions_Silent` ALONE — not Copy, which is
/// 2. The module header describes this call as "silent, copy-mode", and that is
/// wrong. The effect is a RE-POINT: the open document becomes the recovery file.
/// Nothing is overwritten, so the invariant holds — but the sweep then reads
/// `GetPathName` AFTER the save to get its close key, and that is correct only
/// BECAUSE it re-points. If someone later makes the flag match the comment and
/// passes Copy, `GetPathName` would still return the USER'S ORIGINAL path and
/// the close would be aimed at their file. The comment and the code must be made
/// to agree, in that direction.
#[test]
fn a_users_saved_file_is_byte_for_byte_unchanged_by_the_sweep() {
    let work = TempDir::new("adv21-userwork");
    let users_file = work.join("Bracket.SLDPRT");
    let original = b"the user's own work, saved last Tuesday";
    std::fs::write(&users_file, original).unwrap();
    let before = std::fs::read(&users_file).unwrap();

    let b = Bridge::start_with(&[("SWX_ALLOW_USER_WRITES", "1")]);
    b.call("swx_start", json!({"reason": "preparing"}));
    b.call(
        "swx_open",
        json!({"path": users_file.to_string_lossy(), "reason": "the user had this open"}),
    );
    b.call(
        "swx_close_all_safely",
        json!({"reason": "closing everything safely before a quit", "userAuthorized": true}),
    );

    let after = std::fs::read(&users_file).unwrap_or_default();
    assert_eq!(
        after, before,
        "the sweep modified a file the USER owns at {}. A dirty saved document must be written \
         into recovery under a new name; the file they know about is never the destination.",
        users_file.display()
    );
    assert!(
        !after.is_empty(),
        "the user's file at {} is now empty",
        users_file.display()
    );
}

/// "ANY DIALOG STOPS THE SWEEP" — BUT AN UNREADABLE CENSUS DOES NOT.
///
/// The sweep reports `dialogCensusReadable: false` and `stoppedByDialog: null`,
/// and proceeds to close every document anyway. So the rule is really "any
/// dialog I could SEE stops the sweep", and when the desktop cannot be
/// enumerated the sweep runs blind.
///
/// Everywhere else in this bridge an unreadable precondition refuses — the quit
/// ladder's P3 does exactly that ("the window census could not be taken... a
/// quit sent at a modal is a different act"). This is the same act, on the same
/// desktop, one verb earlier, and it is the verb that CLOSES things. A "Save
/// changes to Part1?" raised mid-sweep by SOLIDWORKS itself is precisely what
/// would be missed.
///
/// The recovery save runs first, so this is not by itself a loss of data — it is
/// the loss of the guard that was claimed. On the live session it also means the
/// sweep cannot notice the one dialog it most needs to.
#[test]
fn the_sweep_does_not_close_documents_with_the_dialog_census_unreadable() {
    let b = Bridge::start_with(&[("SWX_ALLOW_USER_WRITES", "1")]);
    b.call("swx_start", json!({"reason": "preparing"}));
    b.call("swx_new_document", json!({"type": "part", "keepUnsaved": true}));

    let r = b.call(
        "swx_close_all_safely",
        json!({"reason": "closing everything safely before a quit", "userAuthorized": true}),
    );
    let readable = r["dialogCensusReadable"].as_bool();
    if readable != Some(false) {
        return; // the census was readable; nothing to prove here.
    }
    assert_eq!(
        r["closed"].as_u64().unwrap_or(0),
        0,
        "the dialog census could not be read (dialogCensusReadable:false) and the sweep closed {} \
         document(s) anyway. The verb's own contract is that any dialog stops it; with the \
         desktop unreadable it cannot know whether one is up. The quit ladder refuses this exact \
         unknown one verb later — this is the same act on the same desktop, and it is the one \
         that does the closing.\n{}",
        r["closed"],
        s(&r)
    );
}