main
Drew Owens 0.9.5 source: adversary passes 42+43 folded (gated by default, log bound to session, a window's words count); manifest… b63f99e 20d ago
//! ADVERSARIAL REVIEW OF 0.9.3 — pass forty-two.
//!
//! readiness.rs, perflog.rs and session_history.rs are NOT cfg(windows): all
//! three compile and run here, driven by the simulator's `mock-startup-*`
//! faults and `SWX_PERF_LOG`. The COM side of `startup_state` (the real
//! `StartupProcessCompleted` call in com.rs) and LiveOs are cfg(windows) and
//! were NOT RUN; nothing below is a claim about them.
//!
//! WHAT CHECKS OUT
//!  * The pass-41 ledger fixes: multiset-max merge; per-line `seq` (max + 1);
//!    trim only past 2x cap, the removed lines archived to
//!    `ledger-archive-<stamp>[-n].jsonl` (collision suffix) with a
//!    `ledger_archived` line. Overlapping `seq` ranges can only arise from two
//!    0.9.3+ ledgers in two roots; the legacy roots are never written by 0.9.3,
//!    so that needs a contrived configuration. After any merge `max + 1` is
//!    still unique going forward.
//!  * Once ready, a session (by pid) stays ready, so an on-demand LOAD_ADDIN
//!    AFTER the bridge has seen it ready does not flip it back.
//!  * The prime: emptiness is checked before and after; `mark` runs before the
//!    close, so a session is primed at most once; `reset_workspace` closes only
//!    bridge-created documents, so a user document that appears between the
//!    check and the create (TOCTOU) is not closed — and the "not empty
//!    afterwards" check then stops the ladder. A prime whose close FAILS leaves
//!    the bridge's own scratch part (which has a file since 0.6.0) open and
//!    stops: safe, but reported only as "not empty afterwards", not as "the
//!    bridge's own part is still open; drain it".
//!  * session-history.json is keyed pid:creationTime on disk, so everHadDocument
//!    survives a com-host respawn and cannot transfer to a reused pid.

mod common;
use common::*;
use serde_json::{json, Value};
use std::time::{Duration, SystemTime};

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

fn a_visible_machine(tag: &str) -> TempDir {
    let d = TempDir::new(tag);
    std::fs::write(d.join("windows.json"), "[]").unwrap();
    d
}

fn session(scratch: &TempDir, docs: Value) {
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({"running": true, "untitled": 1, "active": Value::Null, "docs": docs, "files": {}}).to_string(),
    )
    .unwrap();
}

/// SEVERITY 1 (for this release's purpose) — VERBS THAT ISSUE THE SAME COM
/// COMMANDS THE CRASH LOG NAMES ARE NOT ON THE GATE.
///
/// `STARTUP_GATED` lists 15 verbs. Not on it, and each one touches a document:
///
///   * `swx_show_part` / `_assembly` / `_drawing {path}` — OPEN a document: the
///     same `open` host call `swx_open` makes, and `swx_open` IS gated.
///     `session_history::note_reply` counts them as "made one". (I first wrote
///     that they CREATE one; reading `verbs3::show` corrected me.)
///   * `swx_export` and the ten `swx_export_*` — `SaveAs3` on the active
///     document. The 2026-10-01 crash log reads `<PROCMEM> CMD:DocSave`
///     immediately before `<RUNTIME>` and `<SWERR_REPORT>`.
///   * `swx_com_eval` — arbitrary COM.
///   * `swx_set_parameter`, `swx_props_set`, `swx_rebuild_check`, `swx_bom`,
///     `swx_pack_and_go`, `swx_activate_document`, `swx_set_preference`.
///
/// So the measured crash — a document command during add-in load — is still
/// reachable through a verb whose name does not happen to be on a list. The list
/// is the wrong shape: gate every `exec`/`write` verb that reaches COM, and
/// exempt the few that must work during startup (`swx_status`, the window
/// rungs). `tests/wiring.rs` can then check the exemption list instead of the
/// gate list, so a new verb is gated by default.
///
/// Sev-1 because it defeats the release's own premise — the gate exists to stop
/// exactly this crash. 0.9.3 should still stay published: 0.9.2 gated nothing.
#[test]
fn every_verb_that_creates_or_saves_a_document_waits_for_startup() {
    let scratch = TempDir::new("adv40-ungated");
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({"running": true, "untitled": 1, "active": "C:\\Users\\drew\\work\\bracket.SLDPRT",
               "docs": [{"docType": "part", "path": "C:\\Users\\drew\\work\\bracket.SLDPRT",
                         "title": "bracket.SLDPRT", "saved": true, "dirty": false}],
               "files": {}}).to_string(),
    )
    .unwrap();
    std::fs::write(scratch.join("mock-startup-incomplete"), "").unwrap();
    let machine = a_visible_machine("adv40-ungated-m");
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);
    b.call("swx_start", json!({}));
    let gated = b.call("swx_new_document", json!({"type": "part"}));
    assert!(gated["errorCode"] == json!("host_app_starting"), "precondition: the gate is active: {}", s(&gated));

    let mut through: Vec<String> = Vec::new();
    for (verb, args) in [
        ("swx_show_part", json!({"path": "C:\\Users\\drew\\work\\other.SLDPRT"})),
        ("swx_export_step", json!({"outputPath": "C:\\out\\bracket.step"})),
        ("swx_com_eval", json!({"expr": "ActiveDoc.GetTitle", "reason": "probe"})),
    ] {
        let r = b.call(verb, args);
        if r["errorCode"] != json!("host_app_starting") {
            through.push(format!("{verb}: {}", r["errorCode"].as_str().unwrap_or(if r["success"] == json!(true) { "SUCCESS" } else { "other" })));
        }
    }
    assert!(
        through.is_empty(),
        "with SOLIDWORKS still starting, these document-touching verbs were NOT held at the \
         gate: {through:?}. swx_show_* {{path}} opens a document (swx_open's own call) and the export verbs SaveAs3 (the crash \
         log's CMD:DocSave) — the measured 2026-10-01 crash, through a verb not on a 15-name list. \
         Gate by default; list the exemptions."
    );
}

/// SEVERITY 2 — AN ADD-IN LOADED ON DEMAND BEFORE THE BRIDGE FIRST LOOKS MAKES A
/// FULLY STARTED SESSION "STARTING" FOREVER — AND UNQUITTABLE.
///
/// `perflog::startup_complete` says `Some(false)` whenever the last `LOAD_ADDIN`
/// line is newer than the last 3DEXPERIENCE marker. That marker is written once
/// per start. A user who starts SOLIDWORKS by hand and later enables an add-in
/// (Toolbox, Simulation) writes a `LOAD_ADDIN` after it; if the bridge first sees
/// the session after that, the log says "still loading" for the rest of the
/// session. READY requires every readable signal to agree, so COM's `true` is
/// out-voted, `readinessTimedOut` arrives at five minutes, and it stays
/// `starting` — by design, "never promoted to ready by the clock".
///
/// And `swx_stop`, `swx_close_all_safely` and `swx_shutdown` are on the gate.
/// So the bridge cannot close or quit a perfectly healthy session — the same
/// dead end as a session genuinely wedged at startup behind a dialog, where the
/// WINDOW-MESSAGE rungs (which need no COM and no readiness) are exactly what a
/// quit would use. Fail-safe, but it puts "ask the user to close it" back as the
/// only way out.
///
/// Fix: bind the log to the session (below), and let a positive COM
/// `StartupProcessCompleted` on a session the log cannot place win; never gate
/// the quit's window rungs on readiness.
#[test]
fn an_add_in_loaded_after_the_marker_does_not_make_a_started_session_unready_forever() {
    let scratch = TempDir::new("adv40-ondemand");
    session(&scratch, json!([]));
    let log = scratch.join("SolidWorksPerformance.log");
    std::fs::write(
        &log,
        "<LOAD_ADDIN> SolidWorks Toolbox\n<LOAD_ADDIN> 3DEXPERIENCE\n<3DXLOAD_TIME>24000</3DXLOAD_TIME>\n\
         <LOAD_ADDIN> SOLIDWORKS Simulation\n",
    )
    .unwrap();
    // BUILDER NOTE (0.9.5): the rule that tells an on-demand LOAD_ADDIN from a
    // start still loading is "COM StartupProcessCompleted=true, a marker for this
    // session, then a LOAD_ADDIN and no log activity for ON_DEMAND_QUIET_SECS
    // (60 s)". A log written this instant cannot meet that — it is exactly what a
    // start still loading add-ins looks like — so the fixture's log was last
    // written two minutes ago, after the (simulated, hour-old) process started.
    std::fs::File::options()
        .write(true)
        .open(&log)
        .unwrap()
        .set_modified(SystemTime::now() - Duration::from_secs(120))
        .unwrap();
    let logp = log.to_string_lossy().to_string();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);
    b.call("swx_start", json!({}));
    let st = b.call("swx_status", json!({}));
    let r = b.call("swx_new_document", json!({"type": "part"}));
    let stop = b.call("swx_stop", json!({"userAuthorized": true, "reason": "the user asked to quit"}));
    assert!(
        r["readiness"]["startupProcessCompleted"] == json!(true) || st.to_string().contains("\"startupProcessCompleted\":true"),
        "precondition: COM says the start completed: {} / {}", s(&r), s(&st)
    );
    assert!(
        r["errorCode"] != json!("host_app_starting") && stop["errorCode"] != json!("host_app_starting"),
        "COM says SOLIDWORKS finished starting and the 3DEXPERIENCE marker is in the log, but one \
         add-in loaded on demand after it, so the log reads \"still loading\" for the rest of the \
         session: document verbs AND swx_stop are refused, and the five-minute timeout never \
         promotes it. A healthy session the bridge cannot close or quit.\n  new_document: {}\n  \
         stop: {}",
        s(&r),
        s(&stop)
    );
}

/// SEVERITY 2 — THE PERFORMANCE LOG IS NOT BOUND TO THIS SESSION.
///
/// `perflog::path()` takes the most recently written log; `startup_complete`
/// reads its tail. Nothing compares the log with the running process — no pid,
/// no creation time, not even file mtime >= process start. A log whose last
/// entries are a COMPLETED earlier start therefore reads `Some(true)` for a
/// session that has not written a line yet. With COM unable to answer — exactly
/// the early-startup window — that one stale witness is the whole verdict, and
/// the session is READY at second 0: the 8-second mistake, with no wait at all.
/// The bridge already knows the process creation time (attribution, session
/// history); a log last modified before it cannot speak for this session.
///
/// ON THE GRACE (asked to argue it): `ready` after 60 s with neither signal is
/// the same class of guess as the 8 s that crashed, at 2.5x the one measured
/// platform load (24 s after a 29.7 s attach). It is defensible only because it
/// is labelled (`basedOn: graceAfterFirstSeen`) — but the verbs read `ready`,
/// not `basedOn`, and the module's own rule says the clock never promotes a
/// start to ready. I would keep the grace for READ verbs and keep document verbs
/// refused with `startingUnverifiable`, so the clock cannot authorise a crash.
#[test]
fn a_log_from_an_earlier_session_cannot_vouch_for_this_one() {
    let scratch = TempDir::new("adv40-stale");
    session(&scratch, json!([]));
    std::fs::write(scratch.join("mock-startup-unreadable"), "").unwrap();
    let log = scratch.join("SolidWorksPerformance.log");
    std::fs::write(&log, "<LOAD_ADDIN> 3DEXPERIENCE\n<3DXLOAD_TIME>24000</3DXLOAD_TIME>\n").unwrap();
    std::fs::File::options()
        .write(true)
        .open(&log)
        .unwrap()
        .set_modified(SystemTime::now() - Duration::from_secs(86_400))
        .unwrap();
    let logp = log.to_string_lossy().to_string();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_PERF_LOG", &logp)]);
    b.call("swx_start", json!({}));
    let r = b.call("swx_new_document", json!({"type": "part"}));
    let st = b.call("swx_status", json!({}));
    assert!(
        st.to_string().contains("\"startupProcessCompleted\":null"),
        "precondition: COM cannot answer: {}", s(&st)
    );
    assert!(
        r["errorCode"] == json!("host_app_starting"),
        "COM cannot say whether SOLIDWORKS has started, and the only other witness is a \
         performance log last written a DAY ago — a previous session's completed start. It was \
         taken as this session's: swx_new_document ran at once, with no grace. Bind the log to the \
         session (mtime >= the process creation time the bridge already knows).\n  {}",
        s(&r)
    );
}