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.0 — pass thirty-one.
//!
//! WHAT CHECKS OUT.
//!
//!   * The toggle is read INSIDE `cmd_import` (com.rs:3558), so it is read on
//!     EVERY import, not once per host life. No cache, nothing stale, and a user
//!     who changes the setting between two imports gets the route they asked for
//!     on the second one.
//!   * The toggle is never written — not set, not flipped-and-restored. Right,
//!     and for the stated reason.
//!   * `register_created` fires the moment `OpenDoc6` hands back a document
//!     (com.rs:3394), before the recount, so the pathless interconnect part is
//!     owned at birth exactly as the translator route learned to do.
//!   * `import_diagnostics` (abwin.rs:118) matches `"import diagnostic"` /
//!     `"import diagnos"`, which is specific enough that a title has to be about
//!     import diagnostics to reach it, and the classifier returns
//!     `(kind, blocking, never_answer)` with both flags TRUE — so a wedged
//!     import reports `stoppedByDialog` and no answer is ever sent. I could not
//!     construct a plausible non-diagnostics SOLIDWORKS window that collides.
//!
//! WHAT DOES NOT. Four findings; two provable here, two needing a simulator
//! fault that does not exist yet, flagged as such rather than asserted.

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

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

fn a_step_file(scratch: &TempDir, name: &str) -> String {
    let p = scratch.join(name);
    std::fs::write(&p, b"ISO-10303-21;\nHEADER;\nENDSEC;\nEND-ISO-10303-21;\n").unwrap();
    p.to_string_lossy().to_string()
}

/// SEVERITY 1 — `dissolve:true` IS SILENTLY IGNORED ON THE TRANSLATOR ROUTE.
///
/// The refusal lives INSIDE `import_via_interconnect` (com.rs:3349), which is
/// entered only when `interconnect == Some(true)`. With the toggle OFF — or
/// UNREADABLE, see below — `cmd_import` goes straight to `LoadFile4` and the
/// `dissolve` argument is never looked at again. No refusal, no mention in the
/// reply. The simulator repeats the shape exactly (mock.rs:2542): the refusal is
/// `fault(...) || (interconnect && dissolve)`.
///
/// The defence would be that there is no link to dissolve on the translator
/// route, so ignoring it is harmless. That is the argument this codebase has
/// rejected every other time it came up. The caller asked for a specific,
/// irreversible-sounding operation and got back a success that says nothing
/// about it; `swx_import`'s own reply carries `interconnect`, `linked` and
/// `openOptionsMeasured` precisely so nothing has to be inferred. An argument
/// the bridge did not honour is a fact about what happened.
///
/// It is worse in combination with the next finding: an UNREADABLE toggle also
/// routes to the translator, so a caller asking for `dissolve:true` on Drew's
/// machine — where 3D Interconnect is ON and every import really does produce a
/// linked part — would get a success with an intact link and no indication that
/// the request was dropped.
///
/// Either refuse `dissolve:true` before the route is chosen, or echo
/// `dissolveRequested: true, dissolved: false` with the reason.
#[test]
fn dissolve_is_not_silently_dropped_when_the_translator_route_is_taken() {
    let scratch = TempDir::new("adv30-dissolve");
    std::fs::write(scratch.join("mock-3d-interconnect-off"), "").unwrap();
    let file = a_step_file(&scratch, "smoke.step");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let r = b.call("swx_import", json!({"filePath": file, "dissolve": true}));

    assert!(
        r["errorCode"] == json!("dissolve_not_measured")
            || r.get("dissolveRequested").is_some()
            || s(&r).to_ascii_lowercase().contains("dissolve"),
        "swx_import was asked for dissolve:true and returned without refusing it, without \
         honouring it, and without mentioning it. The refusal sits inside \
         import_via_interconnect (com.rs:3349), so it only fires when the toggle reads ON; the \
         translator route never looks at the argument. On this user's machine the toggle IS on, \
         so a caller who reaches the translator route — because the setting was OFF, or because \
         it could not be READ — is told the import succeeded while the link it asked to break \
         either never existed or is still there. Reply: {}",
        s(&r)
    );
}

/// SEVERITY 2 — AN UNREADABLE TOGGLE IS TREATED AS "OFF", NOT AS UNKNOWN.
///
/// ```ignore
/// if interconnect == Some(true) {
///     return self.import_via_interconnect(…);
/// }
/// // ── ROUTE TWO: THE TRANSLATOR.
/// ```
///
/// `None` and `Some(false)` take the same branch. `pref_toggle`'s doc comment
/// says "an unreadable preference is UNKNOWN, and the import reports it as such
/// rather than assuming the default" — and the REPORT is honest, `interconnect`
/// comes back `null`. The ROUTE is not: unknown silently becomes off.
///
/// On this install that is the wrong half of the coin. 3D Interconnect is ON, so
/// an unreadable toggle sends the import down `LoadFile4`, which on this machine
/// returns no document — and the failure is then reported as a translator
/// failure, pointing the next diagnosis at the translator when the cause was an
/// unreadable preference. For a release whose entire purpose is measurement,
/// that is the expensive direction to guess in.
///
/// This bridge's own doctrine says so in this very file: "a silent second
/// attempt would make the next live run ambiguous… a guess nobody can see is
/// worse than a failure somebody can read." An unreadable toggle should refuse
/// and name the preference, or try the interconnect route and say which it used
/// — not take the off-branch silently.
///
/// NOT PROVEN BY THIS TEST. There is no simulator fault for a preference
/// SOLIDWORKS will not answer, so the `None` branch cannot be reached from a
/// test at all — which is itself the finding's second half: the UNKNOWN case
/// 0.8.0 introduced has no coverage of any kind. This test pins the reachable
/// precondition, that the route is reported per import, so that 0.8.1's
/// measurement overrides have something to sit on.
#[test]
fn the_import_reply_says_which_route_it_took_and_why() {
    let scratch = TempDir::new("adv30-route");
    std::fs::write(scratch.join("mock-3d-interconnect-off"), "").unwrap();
    let file = a_step_file(&scratch, "smoke.step");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let r = b.call("swx_import", json!({"filePath": file}));
    assert!(r["success"] == json!(true), "precondition: the import ran: {}", s(&r));

    assert!(
        r.get("interconnectSettingReadable").is_some()
            || (r.get("interconnect").map(|v| !v.is_null()).unwrap_or(false)
                && r.get("routeReason").is_some()),
        "the reply reports `interconnect` but not whether that value was READ or merely absent. \
         null means \"SOLIDWORKS would not answer\" and false means \"the user turned it off\", \
         and they take the SAME route while meaning opposite things about how much to trust the \
         result. A caller cannot distinguish them, and on this install they have opposite \
         consequences. Reply: {}",
        s(&r)
    );
}

/// SEVERITY 2 — THE `OpenDoc6` THROW PATH NEVER RE-COUNTS.
///
/// The recount the Builder added lands on the returns-null path: `after` is
/// taken, `net` is computed, and the detail says the count went `before -> after`.
/// It does NOT land on the THROW path. `.map_err(…)?` at com.rs:3367-3387
/// short-circuits the whole function before `document_count` is ever called
/// again, so a `OpenDoc6` that raises after opening something leaves a document
/// that is unowned, uncounted and unmentioned — and `register_created` is two
/// lines below the `?`, so it does not fire either.
///
/// The translator route has an ActiveDoc recovery branch for exactly this shape.
/// The interconnect route has none, on either failure path: even where the
/// recount DOES happen, `net > 0` with `opened == None` returns an error and
/// takes no ownership of whatever appeared.
///
/// Live relevance: `OpenDoc6` currently comes back with `errors = 2097152`
/// (`swFileRequiresRepairError`) and no document. That is the returns-null
/// shape, so today the recount runs — but it runs and then discards what it
/// learned, which is the half worth fixing now.
///
/// NOT PROVEN BY THIS TEST: there is no simulator fault that makes `OpenDoc6`
/// throw. The test pins the reachable neighbour — that a failed interconnect
/// import reports what the document count did — so the recount cannot be
/// removed silently.
#[test]
fn a_failed_interconnect_import_reports_what_the_document_count_did() {
    let scratch = TempDir::new("adv30-recount");
    std::fs::write(scratch.join("mock-import-translator-under-interconnect"), "").unwrap();
    let file = a_step_file(&scratch, "smoke.step");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    let r = b.call("swx_import", json!({"filePath": file}));
    assert!(r["success"] == json!(false), "precondition: the import failed: {}", s(&r));

    let h = &r["hostError"];
    assert!(
        h.get("documentsOpened").is_some() || h.get("documentCount").is_some(),
        "a failed import reports errors, warnings and the path, but not what the open-document \
         COUNT did — the one number that says whether a document was left behind. It is in the \
         prose detail on one path and nowhere on the other, and it is the fact ownership turns \
         on. hostError: {}",
        s(h)
    );
}

/// SEVERITY 1 — THE SIMULATOR'S INTERCONNECT IMPORT IS NOT PATHLESS, AND THE
/// PRODUCTION CODE SAYS THE REAL ONE IS.
///
/// com.rs:3391, in the words of the route itself:
///
/// ```ignore
/// // OWN IT THE MOMENT IT EXISTS … The document this produces is PATHLESS (its
/// // title comes from the source file), so without registration at birth the
/// // drain could not identify it as the bridge's own.
/// ```
///
/// The simulator gives it a path: two imports of `smoke.step` come back as
/// `…\Temp\swx4242\IC~~\smoke-1.SLDPRT` and `…smoke-2.SLDPRT`. So every test
/// covering the drain and the sweep over this route is exercising the
/// FIND-BY-TEMP-PATH branch, which is the one that cannot run on Drew's machine,
/// while the ownership branch the comment says is load-bearing — `register_created`
/// plus identity-at-birth — is never the thing under test.
///
/// It is the release's central risk and it is the one thing the suite cannot
/// see. The drain's behaviour here looks correct and is: it finds both
/// documents, refuses to close either because `CloseDoc` resolves by name and
/// two documents share `smoke.SLDPRT`, and reports `titleCollisions` with both
/// full paths so the user can act. But it got the title collision from the
/// paths. With no paths — the live shape — the same two documents are
/// indistinguishable by every key the drain has except the registration made at
/// birth, and nothing here proves that key works.
///
/// Make the simulator's interconnect route pathless, and let it be the ownership
/// registry that tells the two apart. If the drain still refuses, it refuses for
/// the reason it will refuse live.
///
/// (Minor, from the same probe: `titleCollisions` reports one collision TWICE,
/// once per participating document, with `paths` in a different order each time.
/// A caller counting entries double-counts.)
#[test]
fn the_interconnect_route_produces_a_pathless_document() {
    let scratch = TempDir::new("adv30-pathless");
    let file = a_step_file(&scratch, "smoke.step");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);

    // 0.8.2 stopped letting `auto` prefer this route — OpenDoc6 is a MEASURED
    // failure under 3D Interconnect — so it is asked for by name. The property
    // under test is unchanged: what this route opens has no path, so ownership
    // must come from the registry.
    let r = b.call("swx_import", json!({"filePath": file, "route": "opendoc6"}));
    assert!(
        r["via"].as_str().unwrap_or("").contains("Interconnect"),
        "precondition: the interconnect route was taken: {}",
        s(&r)
    );

    let path = r["imported"]["path"].as_str().unwrap_or("").to_string();
    // BUILDER NOTE, 0.8.3: the finding is right and is fixed at the layer it is
    // about — the HOST now hands back a pathless document on this route, so the
    // drain's find-by-temp-path branch is no longer what these tests exercise.
    // The reply still carries a path because the SERVER then gives the document
    // an identity at birth (a scratch file), which is production behaviour since
    // 0.6.0 and happens live too: a bridge-created document that cannot be named
    // cannot be addressed by any verb. So the assertion is what the finding
    // actually requires — NO `IC~~` conversion temp, i.e. ownership cannot have
    // come from the temp-path branch — plus the identity that replaces it.
    assert!(
        !path.contains("IC~~"),
        "the simulator's 3D Interconnect import came back with path {path:?}. The production \
         route says in its own comment that this document is PATHLESS and that registration at \
         birth is therefore what makes it drainable. With a temp path in the fixture, every \
         drain and sweep test over this route takes the find-by-temp-path branch — the one that \
         cannot run on the live machine — and the ownership branch that will run there is \
         untested. This is the release's central risk and the suite cannot currently see it."
    );
    // AND THE OWNERSHIP THAT MUST REPLACE IT.
    let identity = r["documentIdentity"].clone();
    assert!(
        identity["identity"] == json!("path") && identity["scratchFile"] == json!(true),
        "the pathless document was not given an identity at birth, so nothing but its title \
         names it — and two imports of one file share a title. identity: {}",
        s(&identity)
    );
}