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.1 — pass six.
//!
//! F1 is properly fixed in SHAPE: `is_import_temp` is now a segment walk, and
//! every path my round-five tests closed on stays closed. F2 is closed in both
//! places (an unreadable count is UNKNOWN-and-truncated, never a full marshal).
//! F3's caveat is honest. `liveness_verdict` gets the hard part right — no
//! `dead` from one sample, a short span, or unknown responsiveness, and
//! `verdict_advice("dead")` puts the decision with the user.
//!
//! What is left is two things the segment walk still cannot see, and a family of
//! verbs that read an UNLISTABLE session as an EMPTY one.
//!
//! Every #[test] here FAILS on e723504 except the two positive controls, which
//! are here so a fix cannot over-tighten.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};
use swx_bridge::comhost::host::is_import_temp;
use swx_bridge::launch::liveness_verdict;

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

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

// ════════════════════════════════════════════════════════════════════════════
// 1. DATA LOSS: the walk proves a LAYOUT, not a LOCATION
// ════════════════════════════════════════════════════════════════════════════

/// The walk asks for a `temp` segment, then `swx<digits>`, then `IC~~`, then a
/// file. It never asks WHERE that chain is. `%TEMP%` is a specific directory;
/// `D:\Temp\` is a folder a user made.
///
/// The realistic route in: the 1342 leaked temps are real files still sitting on
/// disk. Recovering one — copying `…\Temp\swx107688\IC~~\First.step.SLDPRT`
/// somewhere safe that keeps the folder layout, or just opening it where it lies
/// — makes it the user's document. `cmd_close_import_temps` then closes it with
/// `CloseDoc`, which does not prompt, in the DEFAULT posture, with no write
/// opt-in and no host veto (the drain is deliberately exempt).
#[test]
fn a_temp_shaped_chain_on_another_volume_is_not_the_launchers_temp() {
    let users_own = r"D:\Temp\swx107688\IC~~\First.step.SLDPRT";
    assert!(
        !is_import_temp(users_own),
        "'{users_own}' is not under %TEMP% — it is a folder the user made (or a copy they took of \
         a leaked temp they wanted to keep), and it qualified for a no-prompt CloseDoc. The proof \
         has to be anchored to the resolved %TEMP% root, not just to the segment shape."
    );
}

/// The live evidence is unambiguous about the layout:
/// `C:\Users\drew\AppData\Local\Temp\swx107688\IC~~\First.step.SLDPRT` — `IC~~`
/// sits DIRECTLY under `swx<pid>`. The walk accepts it at any depth below, which
/// widens the surface for no benefit.
///
/// The asymmetry says which way to err: a MISS leaves a temp undrained (memory,
/// recoverable by a restart), a FALSE MATCH closes a document without a prompt
/// (data loss). Tighten to the observed layout.
#[test]
fn ic_must_sit_directly_under_the_swx_pid_folder() {
    let deep = r"C:\Users\drew\AppData\Local\Temp\swx107688\projects\2026\IC~~\customer.sldprt";
    assert!(
        !is_import_temp(deep),
        "'{deep}' matched although IC~~ is three levels below swx107688. The launcher writes \
         swx<pid>\\IC~~\\ directly; accepting any depth below buys nothing and widens a \
         no-prompt-close rule."
    );
}

/// The cases that MUST keep matching — the real layout, forward slashes, the 8.3
/// short form, mixed case. These pass today.
#[test]
fn the_real_launcher_layout_still_matches() {
    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(p), "the launcher's own layout must still be recognised: {p}");
    }
}

/// ...and the round-five closures must stay closed.
#[test]
fn the_round_five_data_loss_paths_stay_closed() {
    for p in [
        r"C:\work\IC~~\drafts\Temp\swx_export\customer-master.sldprt",
        r"D:\Temp\swx-archive\2026\IC~~\customer-final.sldprt",
        r"C:\Users\drew\AppData\Local\Temp\swxnotes\IC~~\keep.sldprt",
        // IC~~ as the FILE, not a folder.
        r"C:\Users\drew\AppData\Local\Temp\swx107688\IC~~",
    ] {
        assert!(!is_import_temp(p), "round five closed this and it must stay closed: {p}");
    }
}

// ════════════════════════════════════════════════════════════════════════════
// 2. AN UNLISTABLE SESSION READ AS AN EMPTY ONE
// ════════════════════════════════════════════════════════════════════════════

/// `resolve_document` refuses honestly with `document_list_unavailable` when the
/// open set is truncated — that fix is real. But two verbs take the same
/// truncated (therefore EMPTY) `open` list and iterate it instead of refusing:
/// `reset_workspace` and `close_all_documents`. They correctly skip the
/// ownership prune, then find nothing to close, and report `success:true` with
/// `closed:[]`.
///
/// A caller that asked the bridge to clean up its own documents is told the
/// workspace is clean. On the session this release exists for — 1342 open docs —
/// that is the most misleading possible answer.
#[test]
fn reset_workspace_refuses_when_it_cannot_see_the_session() {
    let b = Bridge::start_with(&[("SWX_DOC_ENUM_CAP", "3")]);
    b.call("swx_start", json!({"reason": "modelling"}));
    let steps = TempDir::new("adv6-steps");
    // Five bridge-created documents, over the cap of three.
    for i in 0..5 {
        let f = fixture(&steps, &format!("Part{i}.step"));
        b.call("swx_import", json!({"filePath": f}));
    }
    let docs = b.call("swx_docs", json!({}));
    assert!(
        docs["truncated"] == true,
        "precondition: the open set is over the cap and unlistable: {}",
        s(&docs)
    );

    let r = b.call("swx_reset_workspace", json!({"reason": "cleaning up after the build"}));
    let closed_none = r["closed"].as_array().map(|a| a.is_empty()).unwrap_or(true);
    assert!(
        !(r["success"] == true && closed_none),
        "swx_reset_workspace reported success having closed NOTHING, on a session holding five \
         documents this bridge created — because the open list was truncated, so the loop it \
         iterates was empty. The honest answer is document_list_unavailable, the same refusal \
         resolve_document makes.\n{}",
        s(&r)
    );
}

/// POSITIVE CONTROL, and it passes: `close_all_documents` does NOT depend on the
/// truncated list to act — it delegates to the host's single `CloseAllDocuments`
/// COM call and reports the documents the host says it closed. (An earlier
/// version of this test passed for the wrong reason, refused by the write
/// posture before it ever reached the loop; the opt-in below fixes that.)
#[test]
fn close_all_documents_refuses_when_it_cannot_see_the_session() {
    // The write opt-in is ON, so the verb reaches the loop under review rather
    // than being refused by the posture — an earlier version of this test passed
    // because the posture refused it first, which proves nothing about the hole.
    let b = Bridge::start_with(&[
        ("SWX_DOC_ENUM_CAP", "3"),
        ("SWX_ALLOW_USER_WRITES", "1"),
    ]);
    b.call("swx_start", json!({"reason": "modelling"}));
    let steps = TempDir::new("adv6-steps2");
    for i in 0..5 {
        let f = fixture(&steps, &format!("Part{i}.step"));
        b.call("swx_import", json!({"filePath": f}));
    }
    assert!(
        b.call("swx_docs", json!({}))["truncated"] == true,
        "precondition: unlistable"
    );

    let r = b.call(
        "swx_close_all_documents",
        json!({"reason": "clearing the session", "confirm": true, "allowUserWrites": true}),
    );
    assert!(
        r["errorCode"] != "user_writes_not_enabled",
        "precondition: the write posture must not refuse this before the loop is reached: {}",
        s(&r)
    );
    let says_ok = r["success"] == true;
    let closed_none = r["closed"].as_array().map(|a| a.is_empty()).unwrap_or(true);
    assert!(
        !(says_ok && closed_none),
        "swx_close_all_documents reported success having closed nothing on an unlistable session. \
         A destructive verb must refuse when it cannot see what it would act on, not report a \
         clean no-op.\n{}",
        s(&r)
    );
}

// ════════════════════════════════════════════════════════════════════════════
// 4. BUSY IS AN ABSOLUTE, NOT A RATE
// ════════════════════════════════════════════════════════════════════════════

/// `liveness_verdict` gets the asymmetry right — `dead` needs two readings, a
/// span of at least `DEAD_SAMPLE_SECS`, and no new window, and unknown
/// responsiveness is never a verdict. But `moving` is an ABSOLUTE threshold:
///
/// ```text
/// cpu_active_seconds > 0.25  ||  memory_delta_mb.abs() >= 8
/// ```
///
/// with no reference to the span. Over a ten-minute sample, 0.3 seconds of CPU
/// is 0.05% of one core — a housekeeping timer, not a rebuild — and an 8 MB
/// wobble is noise. Both read as `busy`, whose advice is "TELL THE USER TO WAIT
/// … it usually finishes … Do NOT advise ending the task". A wedged session with
/// one spinning background thread is therefore reported as "wait" for ever, and
/// there is no elapsed-time input that could ever escalate it.
#[test]
fn a_trickle_of_cpu_over_a_long_sample_is_not_evidence_of_work() {
    // 0.3s of CPU across 600s: 0.05% of a core.
    let v = liveness_verdict(Some(false), Some(0.3), Some(600.0), Some(0), Some(0));
    assert_ne!(
        v, "busy",
        "0.3s of CPU over a 600s sample is 0.05% of one core — noise, not a rebuild — and the \
         verdict was '{v}', whose advice tells the caller to wait and not to end the task. The \
         threshold is absolute; it has to be a RATE against the sample span."
    );
    // And an 8 MB drift over ten minutes is not progress either.
    let m = liveness_verdict(Some(false), Some(0.0), Some(600.0), Some(8), Some(0));
    assert_ne!(
        m, "busy",
        "an 8 MB memory change across 600s was read as work in progress ('{m}')."
    );
}

/// The properties that MUST hold, pinned so a rate fix cannot break them.
#[test]
fn the_liveness_asymmetry_holds() {
    // A real rebuild: 21s of CPU in 30s.
    assert_eq!(liveness_verdict(Some(false), Some(21.0), Some(30.0), Some(40), Some(0)), "busy");
    // Unknown responsiveness is never a verdict.
    assert_eq!(liveness_verdict(None, Some(0.0), Some(60.0), Some(0), Some(0)), "unknown");
    // One reading / too short a span is never dead.
    assert_eq!(liveness_verdict(Some(false), None, None, None, None), "unknown");
    assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(3.0), Some(0), Some(0)), "unknown");
    // A dialog on screen means something is being asked, not that nothing runs.
    assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(60.0), Some(0), Some(1)), "unknown");
    // No work over a long sample, nothing on screen: dead.
    assert_eq!(liveness_verdict(Some(false), Some(0.0), Some(60.0), Some(0), Some(0)), "dead");
}


// ════════════════════════════════════════════════════════════════════════════
// 3. ONE IMPORT, TWO OWNERSHIP ROWS
// ════════════════════════════════════════════════════════════════════════════

/// F3's fix added `remember_scratch_at` so an import temp is remembered BY PATH
/// (ownership is decided by path whenever a document has one). But the
/// unconditional `remember_scratch(title, session)` above it was kept, and the
/// two dedupe on different keys — the first on `(title, session)` with
/// `path:None`, the second on `(path, session)`. Neither sees the other, so
/// every import writes TWO rows for ONE document.
///
/// The phantom `path:None` row claims the TITLE with no path, which is a wider
/// ownership claim than what was created; `trackedScratchDocuments` reports
/// double; and `owned-docs.json` — rewritten atomically on every
/// remember/forget/rename/prune — carries twice the rows it should, which on the
/// session this release exists for (1342 temps) is 2684.
#[test]
fn imports_record_one_owned_row_each_even_when_unlistable() {
    // WHY THE CAP MATTERS HERE: with a listable session the prune that runs at
    // the top of reset_workspace drops the phantom row (its path:None cannot
    // match an open document that reports a path), so the duplication is
    // invisible. On a TRUNCATED session the prune is correctly skipped — and the
    // phantoms then accumulate unbounded, which is precisely the 1342-temp state
    // this release exists for.
    let b = Bridge::start_with(&[("SWX_DOC_ENUM_CAP", "3")]);
    b.call("swx_start", json!({"reason": "importing"}));
    let steps = TempDir::new("adv6-dup");
    for i in 0..5 {
        let f = fixture(&steps, &format!("Widget{i}.step"));
        b.call("swx_import", json!({"filePath": f}));
    }
    assert!(
        b.call("swx_docs", json!({}))["truncated"] == true,
        "precondition: the session is unlistable, so the prune is skipped"
    );

    let r = b.call("swx_reset_workspace", json!({"reason": "checking what is tracked"}));
    let tracked = r["trackedScratchDocuments"].as_array().cloned().unwrap_or_default();
    assert_eq!(
        tracked.len(),
        5,
        "five imports produced {} ownership rows. remember_scratch (title, path:None) and \
         remember_scratch_at (title, path) both fire and dedupe on different keys, so each import \
         writes TWO rows; on a truncated session nothing prunes the phantom away. owned-docs.json \
         is rewritten atomically on every remember/forget/prune, so at the 1342 temps this \
         release was written for that is 2684 rows per write — and the title-only row claims \
         ownership of the title with no path at all.\ntracked: {:?}",
        tracked.len(),
        tracked
    );
}

/// And the reason the sweep gives for skipping a temp is wrong. `reset_workspace`
/// computes `saved = path.is_some()`, but an import temp HAS a path
/// (`…\IC~~\…`) and has never been saved anywhere the user could find it. The
/// verb therefore skips its own import temps with "has been saved to disk; pass
/// force:true" — a statement that is not true of a conversion temporary, and one
/// that invites a caller to pass `force:true` on a session where the honest
/// answer is "use swx_close_import_temps".
#[test]
fn an_import_temp_is_not_described_as_saved_to_disk() {
    let b = Bridge::start();
    b.call("swx_start", json!({"reason": "importing"}));
    let steps = TempDir::new("adv6-skipreason");
    let f = fixture(&steps, "Bracket.step");
    b.call("swx_import", json!({"filePath": f}));

    let r = b.call("swx_reset_workspace", json!({"reason": "sweeping"}));
    let why: Vec<String> = r["skipped"]
        .as_array()
        .cloned()
        .unwrap_or_default()
        .iter()
        .filter_map(|x| x.get("why").and_then(|w| w.as_str()).map(String::from))
        .collect();
    assert!(
        !why.iter().any(|w| w.contains("saved to disk")),
        "the sweep skipped an import temporary saying it 'has been saved to disk'. It has a temp \
         PATH, which is not the same as being saved: nothing the user could open is on disk at a \
         location they chose. `saved = path.is_some()` conflates the two.\nskipped: {:?}\n{}",
        why,
        s(&r)
    );
}

/// `new_windows.unwrap_or(0) > 0` — a window census that FAILED becomes "no
/// dialog on screen", which is one of the three things `dead` requires. So an
/// unreadable desktop contributes positively to the gravest verdict the bridge
/// can reach, on a machine where the census failing and SOLIDWORKS being wedged
/// are correlated. Every other unknown in this function returns "unknown"; this
/// one is silently a zero.
#[test]
fn an_unreadable_window_census_cannot_support_a_dead_verdict() {
    let v = liveness_verdict(Some(false), Some(0.0), Some(60.0), Some(0), None);
    assert_ne!(
        v, "dead",
        "the window census could not be taken (new_windows:None) and the verdict was still \
         '{v}'. `unwrap_or(0)` turns 'I could not look for a dialog' into 'there is no dialog', \
         which is a precondition of `dead` — the one verdict whose advice sends the user to end \
         their CAD session."
    );
}