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.8.2 — pass thirty-two.
//!
//! WHAT CHECKS OUT
//!
//!   * An unknown `route` is refused BEFORE any COM call — it is the first thing
//!     `cmd_import` looks at, ahead of every other override (mock.rs:2556).
//!   * The overrides do not bypass ownership: the refusal for `dissolve` is
//!     evaluated after route selection and keyed on the TOGGLE, not the route,
//!     so `route:"opendoc6"` cannot smuggle a dissolve past it.
//!   * `interconnect_blocks_import` is keyed on `route == "auto"`, so an
//!     explicit route still measures. That is the point of the overrides and it
//!     is right.
//!   * The `auto` refusal sits AFTER the `net > 0` ActiveDoc-recovery branch
//!     returns (com.rs:3920 vs :3964), so a future edition whose LoadFile4
//!     accepts the file returns through the recovery path and IS registered and
//!     claimed. The coordinator's (3) is sound.
//!   * S1's byte binding catches a file that GREW as well as one that shrank —
//!     `was != now`, not `now < was`.
//!   * The sweep-folder fix is real and it is the root cause of the 1-in-12
//!     flake I reported in pass 30: two sweeps a fraction of a second apart
//!     shared a folder, the second manifest overwrote the first, and a second
//!     sweep that preserved nothing DELETED it. Good find.
//!
//! WHAT DOES NOT.

mod common;
use common::*;
use serde_json::json;

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

/// SEVERITY 1, CONSENT — THE REFUSAL TELLS ITS READER TO CHANGE THE USER'S
/// SETTING, IN THE IMPERATIVE, WITH NO INSTRUCTION TO ASK FIRST.
///
/// `interconnect_blocks_import` carries:
///
/// > "Turn it off in SOLIDWORKS: Tools > Options > System Options > Import >
/// > uncheck "Enable 3D Interconnect", then import again … THIS BRIDGE DOES NOT
/// > CHANGE USER SETTINGS, not even temporarily."
///
/// The second sentence is the bridge's promise about ITSELF. The first is an
/// instruction, and the thing that reads it is an AI agent with a desktop bridge
/// and the ability to drive the SOLIDWORKS UI. "Turn it off in Tools > Options"
/// followed by the exact click path is a runbook, not a caveat, and the disclaimer
/// does not tell the reader not to perform it — it says the BRIDGE will not.
///
/// 3D Interconnect is a persistent, global CAD preference that changes how every
/// future import behaves, including imports the user does by hand long after this
/// session. The standing authorization on this machine covers closing documents
/// and quitting SOLIDWORKS; it does not cover reconfiguring Options. The
/// difference between "ask the user to turn it off" and "turn it off" is the
/// whole of consent here, and it is one word.
///
/// The fix is that word: address the instruction to the user via the caller —
/// "ASK THE USER to turn it off …" — and keep the click path, which is genuinely
/// useful for the human who will do it.
#[test]
fn the_interconnect_refusal_does_not_instruct_its_reader_to_change_the_setting() {
    let scratch = TempDir::new("adv31-consent");
    std::fs::write(scratch.join("mock-interconnect-blocks-import"), "").unwrap();
    let file = scratch.join("smoke.step");
    std::fs::write(&file, b"ISO-10303-21;\nHEADER;\nENDSEC;\nEND-ISO-10303-21;\n").unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let r = b.call("swx_import", json!({"filePath": file.to_string_lossy()}));
    assert!(
        r["errorCode"] == json!("interconnect_blocks_import"),
        "precondition: the measured refusal: {}",
        s(&r)
    );

    // THE REFUSAL'S OWN WORDS ONLY. `sidecar_error` staples a generic `_hint`
    // onto every host failure and it happens to contain "(or ask the user to)"
    // — about dismissing a modal dialog, a different subject. Matching the whole
    // reply would pass on that, which is the wrong-reason failure this review
    // exists to catch.
    let own: String = [&r["error"], &r["detail"], &r["hostError"]]
        .iter()
        .map(|v| s(v))
        .collect::<Vec<_>>()
        .join(" ")
        .to_ascii_lowercase();
    assert!(
        own.contains("ask the user") || own.contains("ask drew") || own.contains("the user must"),
        "the refusal says \"Turn it off in SOLIDWORKS: Tools > Options > System Options > Import\" \
         — an imperative with the exact click path, read by an agent that can drive the \
         SOLIDWORKS UI. The disclaimer that follows is about what the BRIDGE does, not about what \
         its reader should do. 3D Interconnect is a persistent global preference affecting every \
         future import, including the user's own by hand; the standing authorization here covers \
         closing documents, not reconfiguring Options. One word fixes it: ASK the user to turn it \
         off. Reply: {}",
        s(&r)
    );
}

/// SEVERITY 1 — THE BYTE BINDING DISAPPEARS WHEN `recoveryBytes` IS ABSENT.
///
/// ```ignore
/// let changed = match (recorded, bytes) {
///     (Some(was), Some(now)) => was != now,
///     _ => false,
/// };
/// ```
///
/// `_ => false` means "no recorded size — nothing changed", so the manifest's
/// `verified:true` is taken at face value with nothing binding it to the bytes
/// on disk. That is the same trust S1 was added to remove, reachable through any
/// manifest that lacks the field: one written by a bridge that predates it, one
/// hand-edited, one truncated to null by a partial write, or a future
/// `manifestVersion` that renames it — and the listing never looks at
/// `manifestVersion` at all, only that `files` is an array.
///
/// The direction is decided elsewhere in this same release: an unknown must take
/// the strict answer. `reverified_entry` already returns `verified: null` for
/// exactly this situation — bytes that pass what can be checked, with facts the
/// verb cannot recover. A manifest entry with no recorded size is that situation
/// with extra steps, and should get the same `null`, not `true`.
#[test]
fn a_manifest_entry_without_a_recorded_size_is_not_taken_on_trust() {
    let scratch = TempDir::new("adv31-nobytes");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let root = scratch.join("recovery").join("20260917-000000");
    std::fs::create_dir_all(&root).unwrap();
    let mut body = vec![0u8; 900];
    body[4..8].copy_from_slice(&[0x00, 0x00, 0x00, 0x04]);
    // 900 bytes: far under any floor. Whatever the sweep saw, it is not this.
    std::fs::write(root.join("bracket.SLDPRT"), &body).unwrap();
    std::fs::write(
        root.join("manifest.json"),
        json!({
            "manifestVersion": 1,
            "files": [{
                "name": "bracket.SLDPRT",
                "title": "bracket.SLDPRT",
                "kind": "part",
                // NO recoveryBytes — the field a older writer never wrote.
                "verified": true,
                "preserved": true,
                "closed": true,
            }],
        })
        .to_string(),
    )
    .unwrap();

    let r = b.call("swx_recover_list", json!({}));
    let f = r["sweeps"][0]["files"][0].clone();
    assert!(f["source"] == json!("manifest"), "precondition: read from the manifest: {}", s(&f));

    assert!(
        f["verified"] != json!(true),
        "a manifest entry with NO recoveryBytes is trusted completely: verified:true for a \
         900-byte file, under every floor this bridge has, because `_ => false` reads a missing \
         size as \"unchanged\". verified:true means the document was CLOSED. The listing does not \
         check manifestVersion either, so any writer that omits or renames the field silently \
         removes the binding S1 added. reverified_entry already answers this exact question with \
         null; a manifest entry that cannot be bound deserves the same. Entry: {}",
        s(&f)
    );
}

/// SEVERITY 2 — `GATE PARTIAL` AND `GATE PASSED` ARE THE SAME EXIT CODE.
///
/// `tools/drain-gate.py` ends:
///
/// ```python
/// if FAILURES: … return 1
/// if not twin_open:
///     print("GATE PARTIAL — … The COLLISION half is UNTESTED this run …")
///     return 0
/// print("GATE PASSED — …")
/// return 0
/// ```
///
/// The gate proves two properties: that `CloseDoc(<full path>)` closes exactly
/// the document it was given, and that a same-titled document survives. When the
/// twin was never open the second is untested, the text says so clearly — and
/// the process exits 0, indistinguishable from the run that proved both. Any
/// caller reading `$?` instead of the prose treats a half-run gate as a pass,
/// which is precisely the vacuity the `--fixture new` redirect makes more
/// likely: that path exists because `interconnect_blocks_import` now blocks the
/// STEP fixture on this machine, so the fixture that cannot raise the collision
/// is becoming the usual one.
///
/// A distinct code — 3 — costs nothing and keeps the prose honest for the human
/// while making the gate honest for the script.
#[test]
fn the_drain_gate_does_not_exit_zero_on_an_untested_half() {
    let src = std::fs::read_to_string(
        std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tools/drain-gate.py"),
    )
    .expect("tools/drain-gate.py");

    let partial = src.find("GATE PARTIAL").expect("the partial verdict exists");
    let after = &src[partial..];
    let ret = after.find("return").expect("it returns something");
    let line: String = after[ret..].chars().take(12).collect();

    assert!(
        !line.contains("return 0"),
        "GATE PARTIAL returns 0, the same code as GATE PASSED. The partial verdict says the \
         COLLISION half is untested — one of the two properties the gate exists to prove — and a \
         caller reading the exit status cannot tell it from a full pass. The --fixture new \
         redirect makes this the likely path now that interconnect_blocks_import blocks the STEP \
         fixture on this machine. Found: {line:?}"
    );
}

/// SEVERITY 2 — A CODED REFUSAL CARRIES A GENERIC HINT THAT CONTRADICTS IT.
///
/// The whole reply for `interconnect_blocks_import`:
///
/// > `_hint`: "The COM host reported an error. If SOLIDWORKS is showing a modal
/// > dialog, dismiss it (or ask the user to) and retry; swx_status shows whether
/// > SOLIDWORKS is running and attached at all."
/// > `detail`: "Turn it off in SOLIDWORKS: Tools > Options > … Import …"
///
/// There is no dialog. Nothing is hung. The import was refused by a setting that
/// was read successfully, and retrying changes nothing until the user acts. The
/// `_hint` `sidecar_error` staples onto every host failure sends the reader to
/// look for a modal that does not exist, and it is the field agents read first.
///
/// This is the defect 0.7.6 fixed in `recover_list` — a blanket instruction that
/// was wrong for the specific files in the list — in a second place. The fix is
/// the same one: when the host gave a `code`, the coded reason is the hint, and
/// the generic sentence should not compete with it.
#[test]
fn a_coded_refusal_is_not_given_a_hint_about_a_dialog_that_is_not_there() {
    let scratch = TempDir::new("adv31-hint");
    std::fs::write(scratch.join("mock-interconnect-blocks-import"), "").unwrap();
    let file = scratch.join("smoke.step");
    std::fs::write(&file, b"ISO-10303-21;\n").unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let r = b.call("swx_import", json!({"filePath": file.to_string_lossy()}));
    assert!(
        r["errorCode"] == json!("interconnect_blocks_import"),
        "precondition: the measured refusal: {}",
        s(&r)
    );

    let hint = r["_hint"].as_str().unwrap_or("").to_ascii_lowercase();
    assert!(
        !hint.contains("modal dialog") && !hint.contains("dismiss it"),
        "a refusal whose cause is a USER SETTING, read successfully, is handed the generic \
         host-failure hint: \"If SOLIDWORKS is showing a modal dialog, dismiss it … and retry\". \
         There is no dialog, nothing is hung, and retrying changes nothing. `_hint` is the field \
         a caller reads first and it contradicts the `detail` directly below it. When the host \
         gave a code, the coded reason is the hint. Reply: {}",
        s(&r)
    );
}