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.4 — pass forty-three: quit::judge_window.
//!
//! `judge_window`, `dialog_census` and `abwin::classify` are not cfg(windows):
//! they are exercised here directly. The LiveOs code that MEASURES a window
//! (GW_OWNER, GWL_STYLE/EXSTYLE, the child-control walk) is cfg(windows) and was
//! READ, NOT RUN.
//!
//! WHAT CHECKS OUT
//!  * Ownerless windows fail closed. LiveOs reads the DIRECT owner
//!    (`GetWindow(GW_OWNER)`); no owner -> `owner_enabled: None` -> "could not be
//!    read" -> BLOCKING. A zero owner style is likewise treated as a failed read.
//!  * The direct owner is the right one for modality: a modal disables whatever
//!    owns it. A modal owned by another popup disables that popup and is judged
//!    blocking; a tool window owned by a still-enabled popup is a notification,
//!    while the popup's own modal (if any) is judged separately.
//!  * Hidden Button-class children COUNT (`has_button_class_child` ignores
//!    visibility). That is the safe direction; its cost is that a notice whose
//!    template carries hidden Button controls blocks every quit again — the
//!    pre-0.9.4 symptom. Visible-only would trade that for a modal whose buttons
//!    are created hidden and shown late. I would keep hidden-counts.
//!  * A real modal appearing between the precondition census and the rung is
//!    caught: verbs.rs re-censuses after every rung (1755-1764), judged, until
//!    the process is gone, a blocking dialog appears, or the wait ends.
//!  * Consequence of a FALSE notification, by verb: a sweep saves every document
//!    before closing anything, so "never discard" holds whatever a dialog was;
//!    swx_stop requires zero unsaved documents (P2, only `force` waives it), so
//!    ExitApp past a misread modeless question loses the QUESTION, not a
//!    document — and with unsaved work SOLIDWORKS raises its own MODAL save
//!    prompt, which disables the frame and is caught by the post-rung census.
//!    That bound is why finding 1 below is sev-2 and not sev-1.
//!  * The localised License Manager: if its title is not English, `classify`
//!    misses it — and it is still blocking, because it has a Button-class OK.

use swx_bridge::launchos::{DialogContents, WindowInfo, WS_EX_TOOLWINDOW};
use swx_bridge::quit::judge_window;

/// The window measured on 2026-10-01: #32770, style 0x94000044, exstyle 0x90080,
/// 301x67, owned by the enabled main frame.
fn measured_notice(title: &str) -> WindowInfo {
    WindowInfo {
        hwnd: 331_370,
        class_name: "#32770".into(),
        title: title.into(),
        visible: true,
        owner: Some(1001),
        owner_enabled: Some(true),
        style: Some(0x9400_0044),
        exstyle: Some(0x0009_0080),
        rect: Some([100, 100, 401, 167]),
    }
}

fn statics(text: &[&str]) -> DialogContents {
    DialogContents {
        text: text.iter().map(|s| s.to_string()).collect(),
        controls: text.iter().map(|_| ("Static".to_string(), true)).collect(),
        ..Default::default()
    }
}

/// SEVERITY 2 — THE WINDOW'S OWN WORDS ARE READ AND THEN IGNORED.
///
/// `judge_window` classifies by TITLE, then modality, then Button class. The
/// text it read is never consulted. SOLIDWORKS titles its prompts "SOLIDWORKS"
/// and puts the question in the body, and it draws many controls itself — the
/// very notice this release was built around draws its text in Static controls.
/// A modeless tool window, owner enabled, whose body says "Do you want to save
/// changes to Part1.SLDPRT?" and whose Save / Don't Save / Cancel are owner-drawn
/// (Static, or a custom class) is judged a NOTIFICATION: reported, never a reason
/// to stop a quit, a sweep or an import.
///
/// Bounded (see the module note): the sweep saves first; swx_stop needs zero
/// unsaved documents and SOLIDWORKS re-asks modally. But a notification is
/// "nothing anyone needs to decide", and this one is a decision. Run `classify`
/// over the read text too, and treat more than one clickable-looking caption
/// (Save/Don't Save/Cancel/Yes/No/Retry…) as something on offer, whatever its
/// class.
#[test]
fn a_custom_drawn_save_prompt_is_not_judged_a_notification() {
    let w = measured_notice("SOLIDWORKS");
    let c = statics(&[
        "Do you want to save changes to Part1.SLDPRT?",
        "Save",
        "Don't Save",
        "Cancel",
    ]);
    assert_eq!(w.exstyle.unwrap() & WS_EX_TOOLWINDOW, WS_EX_TOOLWINDOW, "precondition: a tool window");
    let (blocking, kind, why) = judge_window(&w, Some(&c));
    assert!(
        blocking,
        "a window that SAYS \"Do you want to save changes to Part1.SLDPRT?\" and offers \
         Save / Don't Save / Cancel — drawn as Static, the way this release's own measured notice \
         draws its text — was judged {kind:?}, non-blocking: {why}. judge_window reads the text \
         and never looks at it. Classify the text as well as the title."
    );
}

/// SEVERITY 3 — A TITLED 3DEXPERIENCE NOTIFICATION REGRESSES TO THE LIVE SYMPTOM.
///
/// The title check runs BEFORE the measured modality, and `classify` maps any
/// title containing "3dexperience" (also "licen", "activat") to `license`, a
/// known blocking kind — pinned that way in the abwin cases table. The notice
/// measured today was title-less, so 0.9.4 works on it. The same notice with a
/// title — "3DEXPERIENCE Platform", "Connected to …" — and identical evidence
/// (tool window, owner enabled, Static only) is BLOCKING by title and stops every
/// quit, which is exactly the symptom 0.9.4 fixed. Safe direction, availability
/// cost. For `license` the title adds nothing a real licence dialog needs (the
/// License Manager has a Button-class OK and blocks on that); let measured
/// notification evidence beat a title-only guess for the kinds that are not a
/// save, recovery or import prompt.
#[test]
fn a_titled_3dexperience_notice_with_notification_evidence_is_a_notification() {
    let w = measured_notice("3DEXPERIENCE Platform");
    let c = statics(&["Connected to OI000616940"]);
    let (blocking, kind, why) = judge_window(&w, Some(&c));
    assert!(
        !blocking,
        "the measured notice with a TITLE added — same tool window, enabled owner, Static-only \
         controls — is judged {kind:?} and BLOCKING by its title alone ({why}), so it stops \
         every quit: the 0.9.4 live symptom, back. Measured modality should beat a title-only \
         guess for kinds that are not save/recovery/import prompts."
    );
}

/// PIN — the rows of the decision that must not move. Passes today.
#[test]
fn judge_window_fails_closed_on_every_unread_or_modal_fact() {
    let notice = measured_notice("");
    let text = statics(&["Title", "Description"]);
    assert!(!judge_window(&notice, Some(&text)).0, "the measured notice is a notification");

    let mut ownerless = notice.clone();
    ownerless.owner = None;
    ownerless.owner_enabled = None;
    assert!(judge_window(&ownerless, Some(&text)).0, "ownerless: owner state unread -> blocking");

    let mut modal = notice.clone();
    modal.owner_enabled = Some(false);
    assert!(judge_window(&modal, Some(&text)).0, "owner disabled -> blocking");

    assert!(judge_window(&notice, None).0, "controls unread -> blocking");

    let mut hidden_button = text.clone();
    hidden_button.controls.push(("Button".into(), false));
    assert!(judge_window(&notice, Some(&hidden_button)).0, "a HIDDEN Button-class child still blocks");

    let mut not_tool = notice.clone();
    not_tool.exstyle = Some(0x0009_0000);
    assert!(judge_window(&not_tool, Some(&text)).0, "not a tool window -> blocking");

    let mut unread_ex = notice.clone();
    unread_ex.exstyle = None;
    assert!(judge_window(&unread_ex, Some(&text)).0, "extended style unread -> blocking");

    assert!(judge_window(&measured_notice("Save Modified Documents"), Some(&text)).0, "a save title blocks");
}