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.2 — pass seven, the QUIT ladder.
//!
//! The ladder's measurement discipline is the best work in this codebase. I
//! could not break any of it: `process_gone` returns `None` on an unreadable
//! census and `None` becomes `Unmeasured`, never `Quit`; `stillRunning` is
//! `null` (never `false`) when the census cannot be read; the registry is
//! cleared ONLY after a census-confirmed exit; every rung releases its
//! `IDispatch` and the server re-releases and COUNTS references before
//! measuring, warning when any are outstanding; a new dialog or a UIPI block
//! stops the ladder; rung (f) refuses without a pid AND a creation time; and
//! the default `maxRung` is "a", so the message rungs do not run unasked.
//!
//! Two things do not hold. One is the strongest claim in `src/quit.rs`. The
//! other is the %TEMP% anchor from pass six, which has a fallback that undoes it.
//!
//! Both #[test]s here FAIL on 8d0cd2d.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};
use swx_bridge::comhost::host::is_import_temp_under;

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

// ════════════════════════════════════════════════════════════════════════════
// 1. THE PRECONDITIONS ARE SWEPT ONCE, NOT BEFORE EVERY RUNG
// ════════════════════════════════════════════════════════════════════════════

/// `check_preconditions`' own doc comment:
///
/// > Every one is required and **every one is re-measured immediately before
/// > each rung** — not inherited from the caller, not cached from an earlier
/// > verb.
///
/// It is called exactly once (`verbs.rs:1144`), before the `for rung in plan`
/// loop. Nothing inside the loop re-checks the document count, the unsaved
/// count, busy, responding, or the dialog census as a PRECONDITION — the loop
/// measures only what a rung DID.
///
/// So on a multi-rung ladder the "verified zero documents" invariant holds only
/// for rung (a). A document opened while rung (a) is settling is still open when
/// rung (b) fires `RunCommand(swCommands_APP_EXIT)` at the session. The ladder
/// does stop if that raises a Save prompt — but `BlockingDialog::error_code`
/// calls a save prompt at a verified zero "impossible … a bug to report", and
/// this is how it becomes possible.
#[test]
fn preconditions_are_remeasured_before_every_rung() {
    let dir = TempDir::new("adv7-quit");
    // Rung (a) declines, rung (b) would exit — a two-rung ladder with a settle
    // gap between them that a second caller can act in.
    std::fs::write(dir.join("rungs.json"), json!({"a": "declined", "b": "exited"}).to_string())
        .unwrap();
    std::fs::write(dir.join("windows.json"), json!([]).to_string()).unwrap();
    std::fs::write(
        dir.join("sldworks.json"),
        json!({"responding": true, "hwnd": 4242, "privateMB": 800}).to_string(),
    )
    .unwrap();

    let b = std::sync::Arc::new(Bridge::start_in(
        TempDir::new("adv7-scratch"),
        &[
            ("SWX_LAUNCH_FAKE_DIR", &dir.path()),
            ("SWX_ALLOW_USER_WRITES", "1"),
            // A wide settle so the document below lands between the rungs.
            ("SWX_QUIT_SETTLE_MS", "2500"),
        ],
    ));
    b.call("swx_start", json!({}));

    let quitter = {
        let b = std::sync::Arc::clone(&b);
        std::thread::spawn(move || {
            b.call(
                "swx_stop",
                json!({"confirm": true, "allowUserWrites": true,
                       "reason": "the caller is finished with SOLIDWORKS", "maxRung": "b"}),
            )
        })
    };
    // Land a document in the gap after rung (a)'s measurement.
    std::thread::sleep(std::time::Duration::from_millis(1200));
    let made = b.call("swx_new_document", json!({"type": "part"}));
    let r = quitter.join().expect("the quit thread");

    assert!(
        made["success"] == true,
        "precondition: a document really was opened mid-ladder: {}",
        s(&made)
    );
    // MEASURE THE ACT, not the outcome. Whether rung (b) happened to succeed is
    // luck; whether it was ATTEMPTED at a session that now had a document open
    // is the safety property.
    let attempted = r["rungsTried"].as_array().map(|a| a.len()).unwrap_or(0);
    assert_eq!(
        attempted, 1,
        "the ladder attempted {attempted} rungs: it went on to fire rung (b) \
         (RunCommand(swCommands_APP_EXIT)) at a session where a document had been opened while \
         rung (a) was settling. check_preconditions is called ONCE, before the loop — its own doc \
         comment promises it runs before EVERY rung, and the 'a save prompt at a verified zero \
         document count is impossible, a bug to report' reasoning depends on that promise. The \
         ladder must re-sweep and refuse with open_documents_present instead.\n{}\n\
         --- server log ---\n{}",
        s(&r),
        b.log()
    );
}

// ════════════════════════════════════════════════════════════════════════════
// 2. THE %TEMP% ANCHOR HAS A FALLBACK THAT UNDOES IT
// ════════════════════════════════════════════════════════════════════════════

/// Pass six's D1 fix anchors the drain to the resolved `%TEMP%` — and then ORs
/// in a shape test:
///
/// ```text
/// matches_resolved || canonical      // canonical = parent ends appdata\local\temp
/// ```
///
/// The `canonical` arm makes the anchor advisory. Any path whose tail is
/// `…\AppData\Local\Temp\swx<digits>\IC~~\<file>` qualifies wherever it lives:
/// a RESTORED PROFILE BACKUP (`E:\Backups\drew\AppData\Local\Temp\…`), another
/// user's profile, a copy someone took of a leaked temp they wanted to keep. The
/// drain closes what qualifies with `CloseDoc`, which does not prompt, in the
/// default posture with no write opt-in and no host veto.
///
/// It also fires when `%TEMP%` is UNREADABLE — `matches_resolved` is
/// `unwrap_or(false)`, so `canonical` alone decides. Everywhere else in this
/// bridge an unreadable precondition refuses; here it guesses.
#[test]
fn the_temp_anchor_is_not_undone_by_a_shape_test() {
    let live_temp = r"C:\Users\drew\AppData\Local\Temp";

    // A restored profile backup on another volume. Not the live %TEMP%.
    let backup = r"E:\Backups\drew\AppData\Local\Temp\swx107688\IC~~\First.step.SLDPRT";
    assert!(
        !is_import_temp_under(backup, Some(live_temp)),
        "'{backup}' is not under the resolved %TEMP% ({live_temp}) — it is a restored profile \
         backup — and it qualified for a no-prompt CloseDoc because its tail matches \
         appdata\\local\\temp. When %TEMP% IS readable, `matches_resolved` must be the whole test."
    );

    // Another user's profile, while ours is readable.
    let other = r"C:\Users\someoneelse\AppData\Local\Temp\swx107688\IC~~\x.SLDPRT";
    assert!(
        !is_import_temp_under(other, Some(live_temp)),
        "'{other}' belongs to another user's profile and still qualified."
    );

    // %TEMP% unreadable: the shape test is then the ONLY test, so a path a user
    // could have anywhere on disk drains. An unreadable precondition must refuse.
    assert!(
        !is_import_temp_under(backup, None),
        "with %TEMP% unreadable the shape test alone qualified '{backup}'. Every other \
         unreadable precondition in this bridge refuses; this one guesses, and the act it \
         authorises is a close without a save prompt."
    );
}

/// The cases that MUST keep matching, so a fix cannot break the drain: the live
/// %TEMP% (long form), the 8.3 short profile name, forward slashes, mixed case.
#[test]
fn the_live_temp_dir_still_drains() {
    let live = r"C:\Users\drew\AppData\Local\Temp";
    for p in [
        r"C:\Users\drew\AppData\Local\Temp\swx107688\IC~~\First.step.SLDPRT",
        "C:/Users/drew/AppData/Local/Temp/swx107688/IC~~/First.step.SLDPRT",
        r"C:\USERS\DREW\APPDATA\LOCAL\TEMP\SWX107~1\IC~~\FIRST.STEP.SLDPRT",
    ] {
        assert!(
            is_import_temp_under(p, Some(live)),
            "a genuine temp under the live %TEMP% must still drain: {p}"
        );
    }
    // The 8.3 PROFILE form resolves to the same directory; it must still drain
    // even though it is not string-equal to the resolved root.
    let short = r"C:\Users\DREW~1\AppData\Local\Temp\swx107688\IC~~\First.step.SLDPRT";
    assert!(
        is_import_temp_under(short, Some(live)),
        "the 8.3 short profile form of the live %TEMP% must still drain: {short}"
    );
}