main
Drew Owens 0.9.10 source: presence baseline holds only bridge-put documents (adversary pass 46); manifest as published bf99cca 19d ago
//! ADVERSARIAL REVIEW OF 0.9.9 — pass forty-six: the user-presence guard only.
//!
//! presence.rs, the guard's call sites and FakeOs compile here and were
//! exercised. LiveOs (`GetForegroundWindow`, `GetLastInputInfo`) is
//! cfg(windows): READ, NOT RUN.
//!
//! WHAT CHECKS OUT
//!  * (3) takenOverByUser lives in `presence.json` in the DATA ROOT, keyed
//!    pid:creationTime: it survives a com-host respawn and a bridge update, and
//!    a new session (new creation time) does not inherit it. With the creation
//!    time unknown the key is `pid:?`, which errs present. Never cleared, but
//!    keyed, and pruned at 20 entries — harmless.
//!  * (4) The override needs userPresentOverride:true AND userAuthorized:true
//!    AND a non-empty reason; userAuthorized alone is not enough. None of the
//!    three is underscore-prefixed, so the dispatcher's stripping neither
//!    removes nor supplies them, and nothing defaults them true. The ledger
//!    line records the verb, the reason and the evidence — not WHO; there is no
//!    caller identity in it.
//!  * (6) Read, not run: a NULL foreground window becomes `Ok(None)` —
//!    "readable, and not SOLIDWORKS" — rather than unknown. Windows returns NULL
//!    on the secure desktop (a UAC prompt, the lock screen) and in a service or
//!    disconnected session, where GetLastInputInfo also reports that session's
//!    stale input. So a bridge in those contexts reads the user as ABSENT. Under
//!    a UAC prompt the user is certainly present. Treat a NULL foreground as
//!    unreadable (which already counts as present).
//!  * (5) The pre-priming re-check cannot be pinned through the verb surface:
//!    the guard runs before the ladder, so any presence evidence planted before
//!    the call refuses there, and FakeOs has no hook to change foreground or
//!    input DURING a verb. For the document case it is also redundant: the
//!    prime's own `documentCount == 0` check (mutant MH, pass 45: caught)
//!    refuses a session in which a document appeared. Its unique case — the
//!    user brings SOLIDWORKS forward between the declined rung and the prime,
//!    with no document — needs a mid-verb fixture.
//!
//! THE FINDINGS are both in the FALSE-ABSENT direction: the starting-set rule.

mod common;
use common::*;
use serde_json::{json, Value};

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

struct Sim {
    dir: TempDir,
}

impl Sim {
    fn new(tag: &str) -> Sim {
        let dir = TempDir::new(tag);
        std::fs::write(dir.join("quit-script.json"), json!({"a": "exited"}).to_string()).unwrap();
        std::fs::write(
            dir.join("sldworks.json"),
            json!({"responding": true, "hwnd": 6622700, "privateMB": 900, "handles": 4000}).to_string(),
        )
        .unwrap();
        std::fs::write(dir.join("windows.json"), "[]").unwrap();
        Sim { dir }
    }

    fn boot_with(&self, scratch: TempDir) -> Bridge {
        let b = Bridge::start_in(
            scratch,
            &[
                ("SWX_LAUNCH_FAKE_DIR", Box::leak(self.dir.path().into_boxed_str())),
                ("SWX_ALLOW_USER_WRITES", "1"),
                ("SWX_QUIT_SETTLE_MS", "80"),
                ("SWX_QUIT_EXIT_WAIT_SECS", "3"),
            ],
        );
        b.call("swx_start", json!({}));
        b
    }

    fn user_busy_in_another_app(&self) {
        std::fs::write(
            self.dir.join("foreground.json"),
            json!({"hwnd": 777, "pid": 4321, "title": "Drawing ASM-CRATE.pdf - Acrobat", "className": "AcrobatSDIWindow"})
                .to_string(),
        )
        .unwrap();
        std::fs::write(self.dir.join("last-input-secs"), "5").unwrap();
    }
}

/// SEVERITY 2 — A SESSION THE USER STARTED, WITH THE USER'S DOCUMENTS OPEN, IS
/// SWEPT THE FIRST TIME THE BRIDGE LOOKS AWAY.
///
/// The guard has two positive signals: SOLIDWORKS foreground with input in the
/// last 120 s, or documents that were not in the set the bridge last looked
/// at. The second needs a baseline, and with NO baseline `foreign` is empty —
/// "on the bridge's first look at a session whatever is open is accepted as the
/// starting set". Then `!present` writes the user's documents in AS that set.
///
/// For a session the USER launched, every document in it is the user's by
/// definition. The guard does not consult launch attribution at all, so the
/// whole decision for "user-launched session, user's assembly open, user reading
/// a PDF of its drawing in another window" is:
///   (a) foreground is Acrobat -> not at keyboard;
///   (b) no baseline -> nothing foreign;
///   (c) never taken over;
///   -> ABSENT -> swx_close_all_safely proceeds: preserves, then CLOSES the
///   user's documents.
/// Nothing is lost — the sweep saves first, and swx_stop would still refuse on
/// open documents / unsaved work — which is why this is sev-2. But it is the
/// exact harm this guard was built after on 10-01: ending a person's working
/// session while they are working in it, avoided that day by luck of timing.
///
/// Fix: a session this bridge did not launch (attribution says so) starts with
/// an EMPTY baseline — everything open in it is the user's. Only a bridge-
/// launched session may adopt a starting set, and only at launch.
#[test]
fn the_users_own_session_is_not_swept_because_the_bridge_had_not_looked_before() {
    let m = Sim::new("adv44-user-session");
    let scratch = TempDir::new("adv44-user-session-scratch");
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({"running": true, "untitled": 0, "active": "C:\\Users\\Public\\adom-crate\\ASM-CRATE.SLDASM",
               "docs": [
                   {"docType": "assembly", "path": "C:\\Users\\Public\\adom-crate\\ASM-CRATE.SLDASM",
                    "title": "ASM-CRATE.SLDASM", "saved": true, "dirty": false},
                   {"docType": "part", "path": "C:\\Users\\Public\\adom-crate\\PANEL-SIDE.SLDPRT",
                    "title": "PANEL-SIDE.SLDPRT", "saved": true, "dirty": false}
               ],
               "files": {}}).to_string(),
    )
    .unwrap();
    let b = m.boot_with(scratch);
    m.user_busy_in_another_app();
    let before = b.call("swx_docs", json!({}));
    assert!(before["count"].as_u64().unwrap_or(0) == 2, "precondition: the user's two documents are open: {}", s(&before));

    let r = b.call("swx_close_all_safely", json!({"reason": "leave the machine as found"}));
    let after = b.call("swx_docs", json!({}));
    assert!(
        r["errorCode"] == json!("user_is_active"),
        "the user's own session — their assembly and a part open, the user reading the drawing in \
         another window five seconds ago — was judged ABSENT and swept: with no baseline, nothing \
         is foreign, and the guard never asks whether the bridge launched this session. Documents \
         before: {}, after: {}. Reply: {}",
        before["count"],
        after["count"],
        s(&r)
    );
}

/// SEVERITY 2 — ON A BRIDGE-LAUNCHED SESSION, A DOCUMENT THE USER OPENS BEFORE
/// THE BRIDGE'S FIRST DOCUMENT VERB IS ADOPTED AS THE BRIDGE'S STARTING SET.
///
/// `note_bridge_looked` runs after bridge verbs that can change documents.
/// Launch-and-attach (swx_start) is not one of them, so the first baseline is
/// written by the first guarded verb's own measurement — and it adopts whatever
/// is open, including what the user opened in the window between launch-ready
/// and that verb. That is exactly the 10-01 shape: a bridge-launched empty
/// session, the user starts working in it, the harness's "leave the machine as
/// found" verb arrives. Measured here with the user in another app: the user's
/// assembly is swept.
#[test]
fn a_document_opened_between_launch_and_the_first_bridge_verb_is_the_users() {
    let m = Sim::new("adv44-launch-gap");
    let b = m.boot_with(TempDir::new("adv44-launch-gap-scratch"));
    m.user_busy_in_another_app();
    // The user opens their assembly in the bridge-launched session.
    std::fs::write(b.scratch.join("mock-user-opens-document"), "").unwrap();
    // The simulator adds the user's document on an app_state read.
    let _ = b.call("swx_get_app_state", json!({}));
    let before = b.call("swx_docs", json!({}));
    assert!(s(&before).contains("ASM-CRATE"), "precondition: the user's assembly is open: {}", s(&before));
    let _ = std::fs::remove_file(b.scratch.join("mock-user-opens-document"));

    let r = b.call("swx_close_all_safely", json!({"reason": "leave the machine as found"}));
    let after = b.call("swx_docs", json!({}));
    assert!(
        r["errorCode"] == json!("user_is_active"),
        "SOLIDWORKS was launched by the bridge and empty; the user opened ASM-CRATE.SLDASM before \
         the bridge's first document verb. The first guarded verb took its baseline from what was \
         open — the user's assembly — so nothing was foreign, and the sweep proceeded. Take the \
         baseline at launch-ready (empty for a bridge-launched session). After: {}. Reply: {}",
        s(&after),
        s(&r)
    );
}

/// SEVERITY 2 — ANY MUTATING BRIDGE VERB LAUNDERS THE USER'S DOCUMENTS INTO THE
/// BASELINE.
///
/// After every mutating, non-exempt verb, verbs.rs:3268 calls
/// `note_bridge_looked`, which sets the baseline to EVERYTHING open now. The
/// stated limit is "a document the user opens DURING a bridge verb cannot be
/// attributed". The real window is wider: everything the user opened SINCE THE
/// PREVIOUS bridge verb is adopted by the next one, whatever that verb was. So
/// the 10-01 sequence with one extra step defeats the guard:
///   1. the bridge makes a part (baseline = {part});
///   2. the user opens ASM-CRATE.SLDASM (foreign — the guard would see it);
///   3. the bridge makes another part (baseline := {part, part, ASM-CRATE});
///   4. "leave the machine as found": the user is reading in another window,
///      nothing is foreign -> the sweep closes their assembly.
/// The refresh should ADD what the verb itself changed (the documents it
/// created or opened, by their reply), not re-read the world.
#[test]
fn a_later_bridge_verb_does_not_adopt_the_users_document_as_its_own() {
    let m = Sim::new("adv44-launder");
    let b = m.boot_with(TempDir::new("adv44-launder-scratch"));
    let mine = b.call("swx_new_document", json!({"type": "part"}));
    assert!(mine["success"] == json!(true), "precondition: the bridge made a part: {}", s(&mine));
    std::fs::write(b.scratch.join("mock-user-opens-document"), "").unwrap();
    let _ = b.call("swx_get_app_state", json!({}));
    let _ = std::fs::remove_file(b.scratch.join("mock-user-opens-document"));
    // NOT swx_user_presence here: measuring presence marks the session
    // taken-over (sticky) when it sees the foreign document, which would make
    // the sweep below refuse for that reason instead. (Measured: at this point
    // the guard does report ASM-CRATE.SLDASM as foreign, count 1.) A harness
    // that does not happen to ask in between gets no such protection.
    let open_now = b.call("swx_docs", json!({}));
    assert!(s(&open_now).contains("ASM-CRATE"), "precondition: the user's assembly is open: {}", s(&open_now));
    let mine2 = b.call("swx_new_document", json!({"type": "part"}));
    assert!(mine2["success"] == json!(true), "precondition: the bridge made another part: {}", s(&mine2));
    m.user_busy_in_another_app();

    let r = b.call("swx_close_all_safely", json!({"reason": "leave the machine as found"}));
    let after = b.call("swx_docs", json!({}));
    assert!(
        r["errorCode"] == json!("user_is_active"),
        "the guard saw the user's ASM-CRATE.SLDASM as foreign, then the bridge ran ONE more \
         document verb of its own and the baseline was re-read from everything open — adopting the \
         user's assembly. The sweep then closed it. Refresh the baseline with what the verb \
         changed, not with the world. After: {}. Reply: {}",
        s(&after),
        s(&r)
    );
}