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.0 — import / document-lifecycle robustness.
//!
//! The drain verb is well guarded where it counts: it re-reads each document's
//! path immediately before closing and re-checks `is_import_temp`, so a
//! document saved out of the temp dir between enumerate and close is skipped
//! (TOCTOU covered); it is bounded per call by `max` and a 40s wall; and the
//! server loop stops on no-progress, so 1342 cannot wedge or spin the host.
//! `cmd_import` fails closed on an unreadable count, so it never double-imports.
//!
//! What is NOT sound is the ownership PROOF itself. `is_import_temp` requires
//! two path markers to be PRESENT, not to be in the launcher's actual
//! `…\Temp\swx<pid>\IC~~\…` relationship — so a path can carry both markers in
//! the wrong order, or in unrelated folders, and be closed WITHOUT A SAVE
//! PROMPT. Closing a user's document is data loss.
//!
//! Every #[test] here FAILS on 33d56d7.

mod common;

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

/// THE RESOLVED `%TEMP%` these assertions are about.
///
/// `is_import_temp` anchors to the temp root the process resolves, and on this
/// Linux host that is `/tmp` — which no Windows path can ever sit under. The
/// rule under test is "is this path under the USER'S resolved temp dir", so the
/// test says which dir that is. (0.5.3: the anchor used to have a shape-test
/// fallback that made this unnecessary, and that fallback qualified a restored
/// profile backup on another volume.)
fn with_live_temp() {
    std::env::set_var("SWX_TEMP_ROOT", swx_bridge::comhost::mock::MOCK_TEMP_ROOT);
}

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

// ════════════════════════════════════════════════════════════════════════════
// 1. THE OWNERSHIP PROOF — markers present is not markers in relationship
// ════════════════════════════════════════════════════════════════════════════

/// A REAL import temp is always `…\Temp\swx<pid>\…\IC~~\…` — the `IC~~`
/// conversion folder is a DESCENDANT of the `swx<pid>` temp folder. `is_import_temp`
/// checks only that `\temp\swx` and `\ic~~\` each appear SOMEWHERE, in any order.
/// A path with `\IC~~\` BEFORE `\Temp\swx` cannot be a launcher temp, yet it
/// qualifies — and `cmd_close_import_temps` closes what qualifies, with
/// `CloseDoc` (no save prompt).
#[test]
fn markers_in_the_wrong_order_are_not_an_import_temp() {
    with_live_temp();
    // IC~~ segment first, temp\swx second: impossible for a real launcher temp.
    let contrived = r"C:\work\IC~~\drafts\Temp\swx_export\customer-master.sldprt";
    assert!(
        !is_import_temp(contrived),
        "'{contrived}' was accepted as a drainable import temp although its IC~~ segment PRECEDES \
         its Temp\\swx segment — the reverse of the launcher's real layout. is_import_temp \
         requires the two markers to be present, not to stand in the swx<pid>\\…\\IC~~ \
         relationship, so a user path carrying both tokens is closed without a save prompt."
    );
}

/// The same looseness with a plausible user directory: someone keeps design
/// backups under a folder they named `swx-archive` inside a `Temp` folder, with
/// an `IC~~` subfolder of their own. Both markers, adjacent to nothing the
/// launcher made — and the file has a real, saved location the user would lose.
#[test]
fn a_user_folder_that_happens_to_contain_both_tokens_is_not_a_temp() {
    with_live_temp();
    let user = r"D:\Temp\swx-archive\2026\IC~~\customer-final.sldprt";
    assert!(
        !is_import_temp(user),
        "'{user}' — a user's own saved file under folders they named — matched the import-temp \
         proof and would be closed without a save prompt. The proof must be the CONTIGUOUS \
         launcher pattern (Temp\\swx<digits>\\…\\IC~~\\), not 'both tokens appear'."
    );
}

/// And the proof should be anchored to the launcher's pid form: `swx` followed
/// by digits. `\temp\swx` matches `\temp\swxanything`, so a user folder literally
/// named `swxstuff` under any `Temp` qualifies the whole path.
#[test]
fn the_swx_marker_should_be_the_launchers_pid_form_not_any_swx_prefix() {
    with_live_temp();
    let user = r"C:\Users\drew\AppData\Local\Temp\swxnotes\IC~~\keep.sldprt";
    assert!(
        !is_import_temp(user),
        "'{user}' qualified because '\\temp\\swx' matches 'swxnotes'. The launcher's folder is \
         swx<pid> — swx followed by DIGITS. Matching any swx-prefixed folder name widens the \
         proof past what the launcher actually writes."
    );
}

/// The cases that MUST still match, so a fix does not throw the baby out — a
/// real temp with forward slashes, 8.3 short name, and mixed case. These pass
/// today and must keep passing.
#[test]
fn genuine_temps_still_match_after_any_tightening() {
    with_live_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\SWX107688\IC~~\FIRST.STEP.SLDPRT",
    ] {
        assert!(is_import_temp(p), "a genuine launcher temp must still be recognised: {p}");
    }
}

// ════════════════════════════════════════════════════════════════════════════
// 4. THE MOCK/REAL DIVERGENCE the Builder flagged
// ════════════════════════════════════════════════════════════════════════════

/// The real backend's `doc_info` reads `GetPathName`, which for an import temp
/// returns the `…\IC~~\…` path — NON-EMPTY. The mock's `doc_info` returns
/// `path:""` for the same document (unsaved → empty), and its comment claims
/// "both backends give it". They do not: `is_import_temp`, the write gate's
/// "destination exists" check, `save_as`, and path-keyed ownership all read
/// `doc_info.path`, and for a temp the mock feeds them "" while the real
/// backend feeds them the temp path. Every path-keyed consumer is therefore
/// UNTESTED for a temp, and could treat a real temp (non-empty path) differently
/// from what the mock proves.
#[test]
fn a_mock_import_temp_carries_the_path_a_real_one_would() {
    with_live_temp();
    let b = Bridge::start();
    b.call("swx_start", json!({"reason": "importing a STEP file"}));
    // A fixture the importer will accept in mock mode.
    let step = TempDir::new("adv5-step");
    let file = step.join("First.step");
    std::fs::write(&file, b"ISO-10303-21;\n").unwrap();

    let imp = b.call("swx_import", json!({"filePath": file.to_string_lossy()}));
    assert!(imp["success"] == true, "precondition: import ran: {}", s(&imp));

    let docs = b.call("swx_docs", json!({}));
    let list = docs["docs"].as_array().cloned().unwrap_or_default();
    let temp = list.iter().find(|d| {
        d.get("title").and_then(|t| t.as_str()).map(|t| t.contains("First")).unwrap_or(false)
    });
    assert!(temp.is_some(), "precondition: the imported doc is listed: {}", s(&docs));
    let path = temp.unwrap().get("path").and_then(|p| p.as_str()).unwrap_or("");
    assert!(
        is_import_temp(path),
        "the mock reports path='{path}' for an import temp, so `is_import_temp` — the ownership \
         proof the whole drain relies on — is FALSE against the mock's own doc_info. The real \
         backend's GetPathName returns the IC~~ path, so the path-keyed consumers (the drain \
         match, the write gate's exists-check, save_as, ownership-by-path) are exercised on a \
         value the real backend never produces for a temp.\ndoc: {}",
        s(temp.unwrap())
    );
}