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
//! WHEN AN IMPORT FAILS, THE TRANSLATOR'S OWN ANSWER MUST REACH THE CALLER.
//!
//! Live, 2026-09-17, on a bridge-launched session: `LoadFile4` began raising a
//! COM exception on EVERY STEP file — a real McMaster part and the gate's own
//! fixture alike — while native `OpenDoc6` opens on the same session worked and
//! the safe close worked. So it is translator state, and the only thing that
//! could say WHICH translator state is the HRESULT.
//!
//! What the caller actually saw was:
//!
//! ```text
//! error:  SOLIDWORKS could not import … (LoadFile4 opened nothing, errors=0;
//!         LoadFile2 refused)
//! detail: SOLIDWORKS rejected 'LoadFile4'
//! ```
//!
//! The HRESULT, the EXCEPINFO source and description and the failing argument
//! index were all already computed — `variant.rs` puts them on the inner error —
//! and the import kept only `e.message`. A failure diagnosed from prose that
//! omits the one number the translator returned is not diagnosed at all, and
//! `swx_com_eval` is read-only by design, so there is no way to probe around it
//! (and no reason to loosen it).

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()
}

#[test]
fn a_failed_import_carries_the_com_failure_the_caller_needs() {
    let scratch = TempDir::new("import-diagnosis");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({}));
    std::fs::write(b.scratch.join("mock-import-load4-throws"), "").unwrap();
    let step = scratch.join("F-W10.step");
    std::fs::write(&step, "ISO-10303-21;\n").unwrap();

    let r = b.call("swx_import", json!({"path": step.to_string_lossy()}));
    check!(b, "the import fails", r["success"] == false, s(&r));

    // THE PROSE IS STILL THERE — it is what a person reads first.
    check!(
        b,
        "it still says what happened in words",
        r["error"].as_str().unwrap_or("").contains("could not import"),
        s(&r)
    );
    // AND THE DETAIL NOW CARRIES THE INNER FAILURE, not just its first sentence.
    let detail = r["detail"].as_str().unwrap_or("");
    check!(
        b,
        "the detail carries the COM failure, not only 'SOLIDWORKS rejected LoadFile4'",
        detail.contains("0x") && detail.len() > "SOLIDWORKS rejected 'LoadFile4'".len(),
        s(&r)
    );

    // AND THE STRUCTURED REMAINS REACH THE CALLER, so a translator can be
    // diagnosed by its HRESULT rather than by matching prose.
    let host = r["hostError"].clone();
    check!(b, "the structured failure survives the trip to the caller", host.is_object(), s(&r));
    let l4 = host["loadFile4"].clone();
    check!(b, "...with LoadFile4's own failure as a sub-object", l4.is_object(), s(&host));
    check!(
        b,
        "...carrying the HRESULT",
        l4["hresult"].as_str().unwrap_or("").starts_with("0x")
            || l4["detail"].as_str().unwrap_or("").contains("0x"),
        s(&l4)
    );
    check!(
        b,
        "...and LoadFile2's own answer, so 'refused' and 'threw' are distinguishable",
        host["loadFile2"]["returned"] == false,
        s(&host)
    );
    check!(b, "...and the path it was asked for", host["path"].is_string(), s(&host));
}

/// A SUCCESSFUL VERB CARRIES NO SUCH FIELD — the diagnosis rides on the failure,
/// not on every reply.
#[test]
fn a_working_import_reports_no_host_failure() {
    let scratch = TempDir::new("import-diagnosis-ok");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({}));
    let step = scratch.join("smoke.step");
    std::fs::write(&step, "ISO-10303-21;\n").unwrap();
    let r = b.call("swx_import", json!({"path": step.to_string_lossy()}));
    check!(b, "the import succeeds", r["success"] == true, s(&r));
    check!(b, "and carries no failure object", r["hostError"].is_null(), s(&r));
}

/// THE ACTUAL LIVE FAILURE, and what a caller can now do with it.
///
/// 2026-09-17: `hostError.loadFile4.detail = "Type mismatch. (0x80020005)
/// (argument index 1)"`. `rgvarg` is packed REVERSED, so argument index 1 is the
/// THIRD parameter of `LoadFile4(path, "r", importData, byref errors)` —
/// `importData` — which this bridge had passed as `VT_EMPTY` since v0.2.0. The
/// fix is one variant encoding; this test is about the other half, the half that
/// made the fix findable: a caller who can read the HRESULT and the argument
/// index can locate a bug in an API they cannot see the source of.
#[test]
fn the_type_mismatch_that_named_the_argument_reaches_the_caller() {
    let scratch = TempDir::new("import-typemismatch");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({}));
    std::fs::write(b.scratch.join("mock-import-typemismatch"), "").unwrap();
    let step = scratch.join("F-W10.step");
    std::fs::write(&step, "ISO-10303-21;\n").unwrap();

    let r = b.call("swx_import", json!({"path": step.to_string_lossy()}));
    check!(b, "the import fails", r["success"] == false, s(&r));
    let l4 = r["hostError"]["loadFile4"].clone();
    check!(
        b,
        "the caller is told WHICH failure it was, by number",
        l4["hresult"] == "0x80020005" || l4["detail"].as_str().unwrap_or("").contains("0x80020005"),
        s(&r)
    );
    check!(
        b,
        "...and WHICH argument SOLIDWORKS objected to",
        l4["argumentIndex"] == 1 || l4["detail"].as_str().unwrap_or("").contains("argument index 1"),
        s(&l4)
    );
    check!(
        b,
        "...and that LoadFile2 answered rather than threw, so the two are distinguishable",
        r["hostError"]["loadFile2"]["returned"] == false
            && r["hostError"]["loadFile2"]["failure"].is_null(),
        s(&r["hostError"])
    );
}