main
Drew Owens 0.6.9 source: launch resilience, honest counts, ownership persistence, drain, quit ladder — 21 adversary passes, 303 te… cf16621 1mo ago
//! ADVERSARIAL REVIEW OF 0.5.5 — pass nine, the drain's close key.
//!
//! The 0.5.5 walk is right where it was wrong: `walk_documents` is bounded by a
//! count AND a time box, reports `walkTruncatedByTime`, and re-reads each path as
//! it passes so a victim is identified from live evidence. `documents()` now
//! REFUSES above the cap and every caller propagates that error with `?`, so no
//! consumer turns "too many to marshal" into "none open". `importTempsElsewhere`
//! is explicitly documented as a count over the SCANNED window and is reported
//! beside `documentsScanned`. The latency fault is charged per document at the
//! points that pay a real round trip.
//!
//! But the victim is chosen BY PATH and then closed BY TITLE:
//!
//! ```text
//! for (title, path) in victims {
//!     if sw.call("CloseDoc", &[&Var::bstr(&title)]).is_ok() {
//! ```
//!
//! `ISldWorks::CloseDoc` resolves a document by NAME, and SOLIDWORKS titles are
//! not unique across paths. Drew's session right now holds saved
//! `…\validation\First.step-1.SLDPRT` files whose titles are shared by import
//! temporaries. Closing by title asks SOLIDWORKS to pick, and `CloseDoc` does not
//! prompt.
//!
//! THE MOCK CANNOT SHOW THIS. Its drain removes by `d.path` — a unique internal
//! key — so it always closes exactly the right document, and the 254 green tests
//! are not evidence about the close key. The test below therefore asserts the
//! GUARD that is missing rather than pretending the mock reproduces the fault.

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

/// Drew's live session, reduced to the two documents that matter: a SAVED file
/// he owns and an import temporary that carries the SAME TITLE.
fn session_with_a_title_collision(scratch: &TempDir) {
    let temp_root = r"C:\Users\drew\AppData\Local\Temp";
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({
            "running": true,
            "untitled": 2,
            "active": "C:\\Users\\drew\\validation\\First.step-1.SLDPRT",
            "docs": [
                // The user's own saved document. NOT a temp, and its loss is
                // unrecoverable.
                {
                    "docType": "part",
                    "path": "C:\\Users\\drew\\validation\\First.step-1.SLDPRT",
                    "title": "First.step-1.SLDPRT",
                    "saved": true,
                    "importTemp": false
                },
                // A leaked conversion temporary that happens to carry the same
                // title — which is exactly what repeated imports of one fixture
                // produce.
                {
                    "docType": "part",
                    "path": "untitled:import:1:First.step-1.SLDPRT",
                    "title": "First.step-1.SLDPRT",
                    "saved": false,
                    "importTemp": true,
                    "tempPath": format!("{temp_root}\\swx107688\\IC~~\\First.step-1.SLDPRT")
                }
            ],
            "files": {}
        })
        .to_string(),
    )
    .unwrap();
}

/// The drain must not close a victim whose title is shared by another open
/// document — because the only close key it has is that title.
///
/// Nothing in the response even lets a caller SEE the collision: no
/// `titleCollisions`, no per-victim "this title is unique among open documents",
/// no post-close verification that the document still at that path is the one
/// that went. On a real backend that is the difference between draining a
/// temporary and discarding Drew's validation part with no save prompt.
#[test]
fn the_drain_refuses_a_victim_whose_title_another_open_document_shares() {
    let scratch = TempDir::new("adv9-scratch");
    session_with_a_title_collision(&scratch);
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({"reason": "draining"}));

    let before = b.call("swx_docs", json!({}));
    let titled = |v: &Value| -> usize {
        v["docs"]
            .as_array()
            .map(|a| {
                a.iter()
                    .filter(|d| d.get("title").and_then(|t| t.as_str()) == Some("First.step-1.SLDPRT"))
                    .count()
            })
            .unwrap_or(0)
    };
    assert_eq!(
        titled(&before),
        2,
        "precondition: two open documents share the title, one saved and one a temp: {}",
        s(&before)
    );

    let r = b.call("swx_close_import_temps", json!({"reason": "draining leaked temps"}));
    let text = s(&r).to_lowercase();
    assert!(
        text.contains("titlecollision")
            || text.contains("ambiguous")
            || text.contains("shares its title")
            || r["closed"].as_u64() == Some(0),
        "the drain closed a victim by TITLE while a second open document — a SAVED file the user \
         owns at C:\\Users\\drew\\validation\\First.step-1.SLDPRT — carries the same title, and \
         the response says nothing about it. CloseDoc resolves by name and does not prompt, so on \
         the real backend SOLIDWORKS chooses which of the two goes. The victim is identified by \
         PATH; it has to be closed by something that identifies it too.\n{}",
        s(&r)
    );
}

/// And the invariant itself, pinned. This PASSES today only because the mock
/// removes by its own unique key rather than by title — so it is a regression pin
/// for whenever the mock is made faithful to `CloseDoc`, not evidence that the
/// real backend is safe.
#[test]
fn a_saved_document_sharing_a_temps_title_survives_the_drain() {
    let scratch = TempDir::new("adv9-scratch2");
    session_with_a_title_collision(&scratch);
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({"reason": "draining"}));
    b.call("swx_close_import_temps", json!({"reason": "draining leaked temps"}));

    let after = b.call("swx_docs", json!({}));
    let saved_alive = after["docs"]
        .as_array()
        .map(|a| {
            a.iter().any(|d| {
                d.get("path").and_then(|p| p.as_str())
                    == Some("C:\\Users\\drew\\validation\\First.step-1.SLDPRT")
            })
        })
        .unwrap_or(false);
    assert!(
        saved_alive,
        "the user's SAVED document at …\\validation\\First.step-1.SLDPRT is gone after a drain \
         that was supposed to touch only conversion temporaries.\n{}",
        s(&after)
    );
}

/// `walk_documents` stops on the first `GetNext` that does not answer:
///
/// ```text
/// let next = doc.obj("GetNext", &[]).ok().flatten();
/// ```
///
/// A document closed by the user (or by SOLIDWORKS) mid-walk invalidates the
/// iteration, `GetNext` fails, `.ok().flatten()` makes it `None`, and the walk
/// ENDS — with `walkTruncatedByTime:false` and therefore `done:true`. It never
/// closes a wrong document (each path is re-read as it is passed), but a walk
/// that was cut short is reported as one that finished. On a flooded session that
/// is the difference between "drained" and "stopped after four of 2144".
///
/// The fix is a comparison the verb already has both sides of: `documentsScanned`
/// against `documentCountBefore`.
#[test]
fn a_walk_that_ended_early_is_not_reported_as_complete() {
    let scratch = TempDir::new("adv9-scratch3");
    session_with_a_title_collision(&scratch);
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({"reason": "draining"}));
    let r = b.call("swx_close_import_temps", json!({"reason": "draining leaked temps"}));
    let text = s(&r).to_lowercase();
    assert!(
        text.contains("walkcomplete") || text.contains("walkincomplete"),
        "the response reports `walkTruncatedByTime` and `documentsScanned` but has no field that \
         says whether the walk actually REACHED THE END of the session. GetNext failing on an \
         invalidated iteration is indistinguishable in this output from a walk that finished, and \
         `done` is computed as if it finished.\n{}",
        s(&r)
    );
}