main
Drew Owens 0.11.2 source 784acbf 5d ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492
#![recursion_limit = "256"]
//! UNATTENDED 3DEXPERIENCE SIGN-IN (0.9.13).
//!
//! The login window (class DSYSysLoginPanelWebView2Class, owned by our launcher
//! chain) is simulated by the launch simulator's `sign-in.json`; its accessible
//! tree, the vault and the UIA actions by an ab stub. THE FIXTURE PASSWORD BELOW
//! IS FAKE, and every test that types it greps every reply, the server log,
//! every file in the data root and every ab request except the one fill that
//! must carry it — and fails if it appears anywhere else.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};
use std::time::{Duration, Instant};

const PW: &str = "Fixture-P4ss!word-7731";
const USER: &str = "[email protected]";

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

struct Machine {
    dir: TempDir,
}

impl Machine {
    fn new(tag: &str) -> Machine {
        let dir = TempDir::new(tag);
        // A launch that reaches Run Begin and never attaches while the sign-in is up.
        std::fs::write(dir.join("routes.json"), json!({"shellExecute": {"chain": true, "runBeginAfterMs": 0}}).to_string()).unwrap();
        std::fs::write(dir.join("preexisting.json"), "[]").unwrap();
        std::fs::write(
            dir.join("sign-in.json"),
            json!({"hwnd": 777001, "className": "DSYSysLoginPanelWebView2Class",
                   "title": "Login | 3DEXPERIENCE ID | Dassault Systèmes"})
            .to_string(),
        )
        .unwrap();
        Machine { dir }
    }

    fn boot(&self, scratch: TempDir, ab: &common::AbStub, extra: &[(&str, &str)]) -> Bridge {
        let lnk = format!("{}/SOLIDWORKS Design.lnk", self.dir.path());
        std::fs::write(&lnk, "not really a shortcut").unwrap();
        let mut env: Vec<(&str, &str)> = vec![
            ("SWX_SOLIDWORKS_SHORTCUT", Box::leak(lnk.into_boxed_str())),
            ("SWX_LAUNCH_FAKE_DIR", Box::leak(self.dir.path().into_boxed_str())),
            ("SWX_LAUNCH_RUN_BEGIN_SECS", "2"),
            ("SWX_LAUNCH_SLDWORKS_SECS", "4"),
            ("SWX_LAUNCH_ATTACH_SECS", "8"),
            ("SWX_LAUNCH_POLL_MS", "100"),
            ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"),
            ("SWX_LAUNCH_ATTACH_POLL_MS", "150"),
            ("SWX_LAUNCH_INFLIGHT_SECS", "120"),
            ("SWX_SIGN_IN_VERIFY_SECS", "3"),
            ("ADOM_DIRECT_API_URL", Box::leak(ab.url.clone().into_boxed_str())),
        ];
        env.extend_from_slice(extra);
        Bridge::start_in(scratch, &env)
    }

    fn sign_in_up(&self) -> bool {
        self.dir.join("sign-in.json").exists()
    }
}

/// The login form as a UIA tree — what ab's desktop_ui_tree is assumed to
/// return (the real shape is unmeasured; the parser is lenient).
fn form(extra: Vec<Value>) -> Value {
    let mut kids = vec![
        json!({"controlType": "Edit", "name": "Email or username", "automationId": "username", "isPassword": false}),
        json!({"controlType": "Edit", "name": "Password", "automationId": "password", "isPassword": true}),
        json!({"controlType": "CheckBox", "name": "Remember me", "automationId": "rememberMe", "toggleState": "Off"}),
        json!({"controlType": "Button", "name": "Log in", "automationId": "login-btn"}),
        json!({"controlType": "Hyperlink", "name": "Forgot your password?"}),
    ];
    kids.extend(extra);
    json!({"success": true, "data": {"controlType": "Window", "name": "Login | 3DEXPERIENCE ID", "children": kids}})
}

fn vault_ok() -> Value {
    json!({"success": true, "data": {"username": USER, "password": PW, "consent": "perUse"}})
}

fn ab_for(tree: Value, vault: Value) -> common::AbStub {
    let ab = common::AbStub::start(json!([]));
    ab.set_response("desktop_ui_tree", tree);
    ab.set_response("pup_credential_reveal", vault);
    // The vault's host list (host + username, never a secret, no toast).
    ab.set_response("pup_credentials", json!({"success": true, "data": {"credentials": [
        {"host": "accounts.google.com", "username": "[email protected]"},
        {"host": "iam.3ds.com", "username": USER}
    ]}}));
    ab.set_response("desktop_ui_set", json!({"success": true}));
    ab.set_response("desktop_ui_click", json!({"success": true}));
    ab.set_response("notify_user", json!({"success": true}));
    ab
}

fn wait_for_sign_in_stage(b: &Bridge) -> Value {
    let deadline = Instant::now() + Duration::from_secs(10);
    loop {
        let st = b.call("swx_status", json!({}));
        // 0.9.23: `waitingOn` comes from a machine-wide census the moment the
        // window is up; the LAUNCH has reached the sign-in when it reports it
        // as its own (`signInMode`).
        // ...and its inventory is read off the monitor thread: wait for it too.
        if (st["waitingOn"] == "userSignIn" && st["signInMode"].is_string() && !st["signInInventory"].is_null()) || Instant::now() > deadline {
            return st;
        }
        std::thread::sleep(Duration::from_millis(150));
    }
}

/// Every text the bridge produced or kept, except the fill that must carry it.
fn everything_but_the_fill(b: &Bridge, ab: &common::AbStub, replies: &[Value]) -> String {
    let mut all = String::new();
    for r in replies {
        all.push_str(&r.to_string());
    }
    all.push_str(&b.log());
    fn walk(p: &std::path::Path, out: &mut String) {
        if let Ok(rd) = std::fs::read_dir(p) {
            for e in rd.flatten() {
                let p = e.path();
                if p.is_dir() {
                    walk(&p, out);
                } else if let Ok(bytes) = std::fs::read(&p) {
                    out.push_str(&String::from_utf8_lossy(&bytes));
                }
            }
        }
    }
    walk(std::path::Path::new(&b.scratch.path()), &mut all);
    for c in ab.calls() {
        // The fills: a UIA set, or real keystrokes (0.9.15).
        let is_fill = c["command"] == "desktop_ui_set" || c["command"] == "desktop_type";
        if !is_fill {
            all.push_str(&c.to_string());
        }
    }
    all
}

/// The user signs in... by the bridge: when the submit is pressed the window
/// closes (in the simulator: sign-in.json goes away).
fn close_window_on_submit(m: &Machine, ab: &common::AbStub) {
    let deadline = Instant::now() + Duration::from_secs(20);
    while Instant::now() < deadline {
        if ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["role"] == "button") {
            let _ = std::fs::remove_file(m.dir.join("sign-in.json"));
            return;
        }
        std::thread::sleep(Duration::from_millis(50));
    }
}

#[test]
fn the_bridge_signs_in_from_the_vault_and_the_password_appears_nowhere() {
    let m = Machine::new("signin-ok");
    let ab = ab_for(form(vec![]), vault_ok());
    // 0.9.26: "Remember me" is toggled in the background and READ BACK.
    ab.on_call_set("desktop_ui_toggle", "desktop_ui_tree", serde_json::from_str(&form(vec![]).to_string().replace(r#""toggleState":"Off""#, r#""toggleState":"On""#)).unwrap());
    let b = m.boot(TempDir::new("signin-ok-scratch"), &ab, &[]);
    let mut replies = vec![b.call("swx_start", json!({"waitSeconds": 0, "reason": "the user asked for SOLIDWORKS"}))];
    let st = wait_for_sign_in_stage(&b);
    check!(
        b,
        "INVENTORY FIRST: swx_status shows the login window's inventory — types, names, IsPassword, never values",
        st["waitingOn"] == "userSignIn"
            && st["signInInventory"].as_array().map(|a| a.iter().any(|e| e["isPassword"] == true)).unwrap_or(false)
            && st["signInMode"] == "wait",
        s(&st)
    );
    replies.push(st);
    let r = std::thread::scope(|sc| {
        sc.spawn(|| close_window_on_submit(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "Drew asked: sign me in"}))
    });
    check!(
        b,
        "swx_sign_in: signed in, verified by the window closing, Remember me ticked, consent reported",
        r["success"] == true && r["signedIn"] == true && r["rememberTicked"]["ticked"] == true && r["rememberTicked"]["via"] == "uiaToggle"
            && r["vault"]["consent"] == "perUse" && !m.sign_in_up(),
        s(&r)
    );
    replies.push(r);
    // The fill itself carried the secret, to the IsPassword element, marked sensitive.
    let sets = ab.calls_to("desktop_ui_set");
    check!(
        b,
        "the password was typed (ab's `text`) into the password element only, targeted by name",
        sets.iter().any(|c| c["args"]["name"] == "Password" && c["args"]["text"] == PW)
            && !sets.iter().any(|c| c["args"]["name"] != "Password" && c["args"]["text"] == PW),
        format!("{sets:?}")
    );
    replies.push(b.call("swx_status", json!({})));
    replies.push(b.call("swx_action_ledger", json!({"limit": 200})));
    let all = everything_but_the_fill(&b, &ab, &replies);
    check!(b, "THE PASSWORD APPEARS NOWHERE: no reply, log, ledger, data-root file or other ab request", !all.contains(PW), String::new());
    check!(b, "...and neither does the user name", !all.contains(USER), String::new());
    let ledger = b.call("swx_action_ledger", json!({"limit": 200}));
    check!(
        b,
        "the attempt is ledgered: who, outcome",
        ledger["actions"].as_array().map(|a| a.iter().any(|e| e["action"] == "solidworks_sign_in" && e["detail"]["outcome"] == "signedIn")).unwrap_or(false),
        s(&ledger)
    );
}

#[test]
fn a_rejected_password_is_never_retried_and_never_echoed() {
    let m = Machine::new("signin-rejected");
    let ab = ab_for(form(vec![]), vault_ok());
    let b = m.boot(TempDir::new("signin-rejected-scratch"), &ab, &[]);
    let mut replies = vec![b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}))];
    wait_for_sign_in_stage(&b);
    // After the submit the window stays, now saying the password was wrong.
    let r = std::thread::scope(|sc| {
        sc.spawn(|| {
            let deadline = Instant::now() + Duration::from_secs(20);
            while Instant::now() < deadline {
                if ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["role"] == "button") {
                    ab.set_response("desktop_ui_tree", form(vec![json!({"controlType": "Text", "name": "Incorrect email or password"})]));
                    return;
                }
                std::thread::sleep(Duration::from_millis(30));
            }
        });
        b.call("swx_sign_in", json!({"reason": "sign in"}))
    });
    check!(b, "sign_in_rejected", r["errorCode"] == "sign_in_rejected", s(&r));
    replies.push(r);
    let again = b.call("swx_sign_in", json!({"reason": "try again"}));
    check!(b, "the next call refuses: never retried", again["errorCode"] == "sign_in_rejected_before", s(&again));
    replies.push(again);
    check!(b, "...the vault was asked exactly once", ab.calls_to("pup_credential_reveal").len() == 1, String::new());
    check!(b, "...and the user was told", !ab.calls_to("notify_user").is_empty(), String::new());
    let all = everything_but_the_fill(&b, &ab, &replies);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());
}

#[test]
fn an_ab_error_that_echoes_the_password_is_scrubbed() {
    let m = Machine::new("signin-echo");
    let ab = ab_for(form(vec![]), vault_ok());
    // The user name goes in (0.9.25: before the reveal); the PASSWORD set fails
    // with an error that echoes it.
    ab.when("desktop_ui_set", json!({"name": "Password"}), json!({"success": false, "error": format!("ValuePattern.SetValue('{PW}') failed: element not enabled"), "text": PW}));
    let b = m.boot(TempDir::new("signin-echo-scratch"), &ab, &[]);
    let mut replies = vec![b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}))];
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "sign_in_fill_failed, nothing submitted, and the echoed password is redacted",
        r["errorCode"] == "sign_in_fill_failed" && r["error"].as_str().unwrap_or("").contains("[redacted]")
            && ab.calls_to("desktop_ui_click").iter().all(|c| c["args"]["role"] != "button"),
        s(&r)
    );
    replies.push(r);
    let all = everything_but_the_fill(&b, &ab, &replies);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());
}

#[test]
fn it_fails_closed_without_a_vault_entry_an_unknown_form_or_a_second_step() {
    // No vault entry: the exact import command, nothing typed.
    let m = Machine::new("signin-missing");
    let ab = ab_for(form(vec![]), vault_ok());
    ab.set_response("pup_credentials", json!({"success": true, "data": {"credentials": [{"host": "accounts.google.com", "username": "x"}]}}));
    let b = m.boot(TempDir::new("signin-missing-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "no vault entry: sign_in_credential_missing with the import command; nothing typed",
        r["errorCode"] == "sign_in_credential_missing" && r["importCommand"].as_str().unwrap_or("").contains("pup_import_browser_logins")
            && ab.calls_to("desktop_ui_set").is_empty()
            // M5: a missing login never raises the consent toast.
            && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );

    // An unknown form (a second password field): the inventory, nothing asked of the vault.
    let m = Machine::new("signin-unknown");
    let ab = ab_for(form(vec![json!({"controlType": "Edit", "name": "Confirm password", "isPassword": true})]), vault_ok());
    let b = m.boot(TempDir::new("signin-unknown-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "unrecognized: the inventory is returned, the vault never asked",
        r["errorCode"] == "sign_in_window_unrecognized" && r["inventory"].is_array()
            && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );

    // A second step: the user is told, nothing typed.
    let m = Machine::new("signin-2fa");
    let ab = ab_for(form(vec![json!({"controlType": "Text", "name": "Enter the verification code we sent you"})]), vault_ok());
    let b = m.boot(TempDir::new("signin-2fa-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "a second step: sign_in_needs_user, the user toasted, the vault never asked",
        r["errorCode"] == "sign_in_needs_user" && !ab.calls_to("notify_user").is_empty()
            && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );
}

#[test]
fn nothing_is_typed_when_no_sign_in_of_ours_is_waiting() {
    let m = Machine::new("signin-none");
    std::fs::remove_file(m.dir.join("sign-in.json")).unwrap();
    let ab = ab_for(form(vec![]), vault_ok());
    let b = m.boot(TempDir::new("signin-none-scratch"), &ab, &[]);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "no launch: sign_in_not_waiting, the vault never asked",
        r["errorCode"] == "sign_in_not_waiting" && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );
    let r = b.call("swx_sign_in", json!({}));
    check!(b, "a reason is required", r["errorCode"] == "reason_required", s(&r));
}

#[test]
fn sign_in_auto_signs_in_once_by_itself() {
    let m = Machine::new("signin-auto");
    let ab = ab_for(form(vec![]), vault_ok());
    let b = m.boot(TempDir::new("signin-auto-scratch"), &ab, &[]);
    let r = std::thread::scope(|sc| {
        sc.spawn(|| close_window_on_submit(&m, &ab));
        let r = b.call("swx_start", json!({"waitSeconds": 0, "reason": "start", "signIn": "auto"}));
        let deadline = Instant::now() + Duration::from_secs(15);
        while Instant::now() < deadline && m.sign_in_up() {
            std::thread::sleep(Duration::from_millis(100));
        }
        r
    });
    check!(b, "swx_start says the mode", r["signInMode"] == "auto", s(&r));
    check!(b, "the window closed after the bridge's own submit", !m.sign_in_up(), String::new());
    check!(b, "...the vault was asked exactly once", ab.calls_to("pup_credential_reveal").len() == 1, String::new());
    let all = everything_but_the_fill(&b, &ab, &[r]);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());
}

#[test]
fn a_respawned_bridge_still_sees_the_sign_in_and_reconciles_after_it() {
    let m = Machine::new("signin-respawn");
    let ab = ab_for(form(vec![]), vault_ok());
    let scratch = TempDir::new("signin-respawn-scratch");
    {
        let b = m.boot(TempDir::borrowed(&scratch.0), &ab, &[]);
        b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
        wait_for_sign_in_stage(&b);
    }
    // The old bridge (and its monitor) is gone; a new one reads the record.
    let b = m.boot(TempDir::borrowed(&scratch.0), &ab, &[]);
    let st = b.call("swx_status", json!({}));
    check!(
        b,
        "after a respawn mid-launch, waitingOn and the window come from a LIVE census",
        st["waitingOn"] == "userSignIn" && st["signInWindow"]["title"].as_str().unwrap_or("").contains("3DEXPERIENCE ID") && st["signInWindow"]["hwnd"].is_number(),
        s(&st)
    );
    // The user signs in by hand; SOLIDWORKS comes up and COM attaches.
    std::fs::remove_file(m.dir.join("sign-in.json")).unwrap();
    std::fs::write(
        m.dir.join("routes.json"),
        json!({"shellExecute": {"chain": true, "runBeginAfterMs": 0, "sldworksAfterMs": 0, "attachAfterMs": 0}}).to_string(),
    )
    .unwrap();
    let deadline = Instant::now() + Duration::from_secs(15);
    let mut last = json!({});
    while Instant::now() < deadline {
        // Only swx_status: no swx_start re-marks the attempt (measured: the
        // record stayed helperSignIn when only status was read).
        last = b.call("swx_status", json!({}));
        if last["com"]["running"] == true && last["launch"]["stage"] == "attached" {
            break;
        }
        std::thread::sleep(Duration::from_millis(250));
    }
    check!(
        b,
        "RECONCILED ON ATTACH: the record no longer says helperSignIn once SOLIDWORKS is attached",
        last["com"]["running"] == true && last["launch"]["stage"] == "attached" && last["waitingOn"].is_null(),
        s(&last)
    );
}

/// RECONCILED ON ATTACH (0.9.13), the measured shape: the record says
/// {stage:"helperSignIn", outcome:"abandoned"} (the bridge was respawned while
/// the user signed in), SOLIDWORKS is up and attached, and re-attribution has
/// no lineage to go on. The STAGE moves to `attached`; the outcome and the
/// attribution keep their evidence; the refusal is kept as supersededByAttach.
#[test]
fn an_abandoned_sign_in_record_is_reconciled_when_solidworks_is_attached() {
    let scratch = TempDir::new("signin-reconcile");
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({"running": true, "untitled": 1, "active": Value::Null, "docs": [], "files": {}}).to_string(),
    )
    .unwrap();
    std::fs::write(
        scratch.join("launch-state.json"),
        json!({
            "attempt": {
                "id": 7, "startedAt": 1700000000u64, "shortcut": "x.lnk", "route": "shellExecute",
                "invokedAt": 1700000000u64, "stage": "helperSignIn", "finished": true,
                "outcome": "abandoned", "runBeginSeen": false, "signInSeen": true,
                "signInWindow": "Login | 3DEXPERIENCE ID | Dassault Systèmes"
            },
            "preferredRoute": "shellExecute", "consecutiveFailures": 0
        })
        .to_string(),
    )
    .unwrap();
    let machine = TempDir::new("signin-reconcile-m");
    std::fs::write(machine.join("windows.json"), "[]").unwrap();
    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[("SWX_LAUNCH_FAKE_DIR", &machine.path())]);
    let st = b.call("swx_status", json!({}));
    check!(
        b,
        "the record is reconciled: stage attached, outcome kept, not attributed, the reason kept",
        st["com"]["running"] == true
            && st["launch"]["stage"] == "attached"
            && st["launch"]["outcome"] == "abandoned"
            && st["launch"]["attributed"] != true
            && st["launch"]["supersededByAttach"].is_string()
            && st["waitingOn"].is_null(),
        s(&st)
    );
}

/// THE MEASURED TREE (ab 2.1.163, 2026-10-02): top-level {foreground, hwnd,
/// maxNodes, nodeCount, root, truncated, window, status, success}; nodes
/// {role, name, automationId, invokable, settable, rect, children} — no
/// `controlType`, no `isPassword`, and a `value` that is never copied. The
/// password edit is recognized by its name.
fn measured_tree(truncated: bool) -> Value {
    json!({
        "success": true, "status": "ok", "hwnd": 777001, "maxNodes": 400, "nodeCount": 9, "truncated": truncated,
        "foreground": false, "window": {"title": "Login | 3DEXPERIENCE ID | Dassault Systèmes"},
        "root": {"role": "Window", "name": "Login | 3DEXPERIENCE ID | Dassault Systèmes", "automationId": "",
                 "invokable": false, "settable": false, "rect": {"x": 0, "y": 0, "w": 480, "h": 640},
                 "children": [
            {"role": "Pane", "name": "3DEXPERIENCE ID", "automationId": "", "invokable": false, "settable": false, "rect": {}, "children": [
                {"role": "Edit", "name": "Email or username", "automationId": "username", "invokable": false, "settable": true, "value": "[email protected]", "rect": {}},
                {"role": "Edit", "name": "Password", "automationId": "password", "invokable": false, "settable": true, "value": "", "rect": {}},
                {"role": "CheckBox", "name": "Remember me", "automationId": "", "invokable": true, "settable": false, "toggleState": "Off", "rect": {}},
                {"role": "Button", "name": "Log in", "automationId": "", "invokable": true, "settable": false, "rect": {}},
                {"role": "Button", "name": "Forgot your password?", "automationId": "", "invokable": true, "settable": false, "rect": {}}
            ]}
        ]}
    })
}

#[test]
fn the_measured_tree_shape_signs_in_and_a_truncated_one_is_refused() {
    let m = Machine::new("signin-measured");
    let ab = ab_for(measured_tree(false), vault_ok());
    let b = m.boot(TempDir::new("signin-measured-scratch"), &ab, &[]);
    let mut replies = vec![b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}))];
    let st = wait_for_sign_in_stage(&b);
    check!(
        b,
        "the measured shape is read by role; the password edit is found by its name; no value is copied",
        st["signInInventory"].as_array().map(|a| a.iter().any(|e| e["name"] == "Password" && e["isPassword"] == true && e["controlType"] == "edit")).unwrap_or(false)
            && !st.to_string().contains("[email protected]"),
        s(&st)
    );
    replies.push(st);
    let r = std::thread::scope(|sc| {
        sc.spawn(|| close_window_on_submit(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "sign in"}))
    });
    check!(b, "signed in on the measured shape", r["signedIn"] == true, s(&r));
    let reveal = ab.calls_to("pup_credential_reveal");
    check!(
        b,
        "the reveal names the vault host found on the 3ds.com domain and a plain reason for Drew's toast",
        reveal.len() == 1 && reveal[0]["args"]["host"] == "iam.3ds.com"
            && reveal[0]["args"]["reason"] == "Sign in to SOLIDWORKS with your saved 3DEXPERIENCE login",
        format!("{reveal:?}")
    );
    let clicks = ab.calls_to("desktop_ui_click");
    check!(b, "the submit was clicked BY NAME", clicks.iter().any(|c| c["args"]["name"] == "Log in"), format!("{clicks:?}"));
    replies.push(r);
    let all = everything_but_the_fill(&b, &ab, &replies);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());

    let m = Machine::new("signin-truncated");
    let ab = ab_for(measured_tree(true), vault_ok());
    let b = m.boot(TempDir::new("signin-truncated-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "a truncated tree is unrecognized — never a guess; the vault never asked",
        r["errorCode"] == "sign_in_window_unrecognized" && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );
}

#[test]
fn consent_is_waited_for_and_relay_forbidden_is_a_wiring_bug() {
    // The toast is answered after a few polls: pending, pending, then the login.
    let m = Machine::new("signin-consent");
    let ab = ab_for(form(vec![]), json!({"success": false, "errorCode": "consent_pending", "error": "waiting for the user to allow"}));
    let b = m.boot(TempDir::new("signin-consent-scratch"), &ab, &[("SWX_SIGN_IN_CONSENT_SECS", "20")]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = std::thread::scope(|sc| {
        sc.spawn(|| {
            let deadline = Instant::now() + Duration::from_secs(15);
            while Instant::now() < deadline {
                if ab.calls_to("pup_credential_reveal").len() >= 2 {
                    ab.set_response("pup_credential_reveal", vault_ok());
                    return;
                }
                std::thread::sleep(Duration::from_millis(50));
            }
        });
        sc.spawn(|| close_window_on_submit(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "sign in"}))
    });
    check!(b, "consent pending, then given: signed in, consent reported perUse", r["signedIn"] == true && r["vault"]["consent"] == "perUse", s(&r));

    // Never answered: sign_in_consent_pending, the password not typed (0.9.25:
    // the user name went in first — the page is prepared before the reveal).
    let m = Machine::new("signin-consent-timeout");
    let ab = ab_for(form(vec![]), json!({"success": false, "errorCode": "consent_pending", "error": "waiting for the user to allow"}));
    let b = m.boot(TempDir::new("signin-consent-timeout-scratch"), &ab, &[("SWX_SIGN_IN_CONSENT_SECS", "2")]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "an unanswered toast: sign_in_consent_pending, the password not typed, the page prepared",
        r["errorCode"] == "sign_in_consent_pending" && !ab.calls_to("desktop_ui_set").iter().any(|c| c["args"]["text"] == PW)
            && r["pagePrepared"]["userNameFilled"] == true,
        s(&r)
    );

    // relay_forbidden: reported as the bridge's wiring bug.
    let m = Machine::new("signin-relay");
    let ab = ab_for(form(vec![]), json!({"success": false, "errorCode": "relay_forbidden", "error": "a relayed (cloud container) call is refused"}));
    let b = m.boot(TempDir::new("signin-relay-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "relay_forbidden: sign_in_relay_forbidden, named a wiring bug",
        r["errorCode"] == "sign_in_relay_forbidden" && r["_hint"].as_str().unwrap_or("").contains("wiring bug"),
        s(&r)
    );
}

// ── 0.9.15: the MEASURED two-page, lazy 3DEXPERIENCE ID login ───────────────

/// The COLD read, measured 2026-10-02 (13 nodes, ONE control: Button "Close").
fn cold_tree() -> Value {
    json!({"success": true, "status": "ok", "hwnd": 777001, "maxNodes": 400, "nodeCount": 13, "truncated": false,
           "window": {"title": "Login - 3DEXPERIENCE ID - Dassault Systemes", "rect": {"x": 100, "y": 50, "w": 968, "h": 968}},
           "root": {"role": "Window", "name": "Login - 3DEXPERIENCE ID - Dassault Systemes", "automationId": "", "invokable": false, "settable": false,
                    "rect": {"x": 100, "y": 50, "w": 968, "h": 968}, "children": [
               {"role": "Pane", "name": "", "automationId": "", "invokable": false, "settable": false, "children": [
                   {"role": "Pane", "name": "", "automationId": "", "invokable": false, "settable": false},
                   {"role": "Button", "name": "Close", "automationId": "", "invokable": true, "settable": false}
               ]}
           ]}})
}

/// The WARM read of page ONE, measured 2026-10-02 (38 nodes): "Email or
/// username" + Continue, the hyperlinks, and the COOKIE BANNER — no password.
fn page_one() -> Value {
    json!({"success": true, "status": "ok", "hwnd": 777001, "maxNodes": 400, "nodeCount": 38, "truncated": false,
           "window": {"title": "Login - 3DEXPERIENCE ID - Dassault Systemes", "rect": {"x": 100, "y": 50, "w": 968, "h": 968}},
           "root": {"role": "Window", "name": "Login - 3DEXPERIENCE ID - Dassault Systemes", "automationId": "", "invokable": false, "settable": false, "children": [
               {"role": "Document", "name": "3DEXPERIENCE ID", "automationId": "", "invokable": false, "settable": false, "children": [
                   {"role": "Edit", "name": "Email or username", "automationId": "username", "invokable": false, "settable": true, "value": "", "rect": {"x": 384, "y": 450, "w": 400, "h": 40, "centerX": 584, "centerY": 470}},
                   {"role": "Button", "name": "Continue", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "Create your account", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "Forgot your password?", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "\u{e900} English", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "Help", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "Manage cookies", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "privacy policy", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Button", "name": "Accept All", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Button", "name": "Manage my preferences", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Text", "name": "Continue with only necessary cookies", "automationId": "", "invokable": false, "settable": false}
               ]},
               {"role": "Button", "name": "Close", "automationId": "", "invokable": true, "settable": false}
           ]}})
}

/// Page TWO — MEASURED 2026-10-02 (Visualize host, after a page-one fill +
/// Continue): the user-name box (prefilled), "Password " (automationId
/// password), CheckBox "Remember me " with toggleState off and an OFF-SCREEN
/// rect (centerX -14493) whose visible control is the Text "Remember me…",
/// Log in, Back, and the cookie banner still present.
fn page_two_with(toggle: &str) -> Value {
    json!({"success": true, "status": "ok", "hwnd": 777001, "nodeCount": 41, "truncated": false,
           "window": {"title": "Login - 3DEXPERIENCE ID - Dassault Systemes", "rect": {"x": 100, "y": 50, "w": 968, "h": 968}},
           "root": {"role": "Window", "name": "Login - 3DEXPERIENCE ID - Dassault Systemes", "automationId": "", "children": [
               {"role": "Edit", "name": "Email or username", "automationId": "username", "invokable": false, "settable": true, "value": "[email protected]",
                "rect": {"x": 384, "y": 380, "w": 400, "h": 40, "centerX": 584, "centerY": 400}},
               {"role": "Edit", "name": "Password ", "automationId": "password", "invokable": false, "settable": true, "value": "",
                "rect": {"x": 384, "y": 450, "w": 400, "h": 40, "centerX": 584, "centerY": 470}},
               {"role": "CheckBox", "name": "Remember me ", "automationId": "", "invokable": true, "settable": false, "toggleState": toggle,
                "rect": {"x": -14500, "y": 520, "w": 14, "h": 14, "centerX": -14493, "centerY": 527}},
               {"role": "Text", "name": "Remember me…", "automationId": "", "invokable": false, "settable": false,
                "rect": {"x": 410, "y": 518, "w": 120, "h": 18, "centerX": 470, "centerY": 527}},
               {"role": "Button", "name": "Log in", "automationId": "", "invokable": true, "settable": false},
               {"role": "Button", "name": "Back", "automationId": "", "invokable": true, "settable": false},
               {"role": "Button", "name": "Accept All", "automationId": "", "invokable": true, "settable": false,
                "rect": {"x": 700, "y": 900, "w": 120, "h": 40}},
               {"role": "Button", "name": "Manage my preferences", "automationId": "", "invokable": true, "settable": false,
                "rect": {"x": 840, "y": 900, "w": 200, "h": 40}},
               {"role": "Button", "name": "Close", "automationId": "", "invokable": true, "settable": false}
           ]}})
}

fn page_two() -> Value {
    page_two_with("Off")
}

/// Drive the simulated window: lazy until a point is queried; page two (lazy
/// again) after Continue; closed after Log in. `to_page_two:false` = the page
/// never changes after Continue.
fn drive_two_pages(m: &Machine, ab: &common::AbStub, to_page_two: bool) {
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut state = 0;
    let mut fp_at_continue = 0;
    while Instant::now() < deadline {
        let fp = ab.calls_to("desktop_ui_from_point").len();
        let clicks = ab.calls_to("desktop_ui_click");
        let clicked = |n: &str| clicks.iter().any(|c| c["args"]["name"] == n);
        match state {
            0 if fp >= 1 => {
                ab.set_response("desktop_ui_tree", page_one());
                state = 1;
            }
            1 if clicked("Continue") => {
                if !to_page_two {
                    return;
                }
                ab.set_response("desktop_ui_tree", cold_tree());
                fp_at_continue = ab.calls_to("desktop_ui_from_point").len();
                state = 2;
            }
            2 if fp > fp_at_continue => {
                ab.set_response("desktop_ui_tree", page_two());
                state = 3;
            }
            3 if clicked("Log in") => {
                let _ = std::fs::remove_file(m.dir.join("sign-in.json"));
                return;
            }
            _ => {}
        }
        std::thread::sleep(Duration::from_millis(20));
    }
}

#[test]
fn the_measured_two_page_lazy_login_is_one_flow_with_one_consent() {
    assert!(
        swx_bridge::launch::is_sign_in_window("WindowsForms10.Window.8.app.0.141b42a_r6_ad1", "Login - 3DEXPERIENCE ID - Dassault Systemes"),
        "the Visualize-hosted login window (measured 2026-10-02) is a sign-in window"
    );
    let m = Machine::new("signin-two-page");
    std::fs::write(
        m.dir.join("sign-in.json"),
        json!({"hwnd": 777001, "className": "WindowsForms10.Window.8.app.0.141b42a_r6_ad1",
               "title": "Login - 3DEXPERIENCE ID - Dassault Systemes"}).to_string(),
    )
    .unwrap();
    let ab = ab_for(cold_tree(), vault_ok());
    let b = m.boot(TempDir::new("signin-two-page-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "60"), ("SWX_SIGN_IN_VERIFY_SECS", "8")]);
    let mut replies = vec![b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}))];
    let (st, r) = std::thread::scope(|sc| {
        sc.spawn(|| drive_two_pages(&m, &ab, true));
        // The window is up; the monitor's inventory read wakes it.
        let deadline = Instant::now() + Duration::from_secs(10);
        let mut st = wait_for_sign_in_stage(&b);
        while st["launch"]["signInWake"].is_null() && Instant::now() < deadline {
            std::thread::sleep(Duration::from_millis(100));
            st = b.call("swx_status", json!({}));
        }
        let r = b.call("swx_sign_in", json!({"reason": "Drew: sign me in"}));
        (st, r)
    });
    replies.push(st);
    check!(
        b,
        "ONE flow over two pages: signed in, two-page, the cookie banner reported, verified by the window closing",
        r["signedIn"] == true && r["twoPage"] == true && r["cookieBanner"] == true && !m.sign_in_up(),
        s(&r)
    );
    check!(b, "the consent toast was raised ONCE (one reveal for both pages)", ab.calls_to("pup_credential_reveal").len() == 1, String::new());
    let clicks = ab.calls_to("desktop_ui_click");
    check!(
        b,
        "the cookie choice was never made: no click on Accept All or Manage my preferences",
        !clicks.iter().any(|c| c["args"]["name"] == "Accept All" || c["args"]["name"] == "Manage my preferences"),
        format!("{clicks:?}")
    );
    let sets = ab.calls_to("desktop_ui_set");
    check!(
        b,
        "the user name went into 'Email or username' on page one, the password into 'Password' on page two",
        sets.len() == 2 && sets[0]["args"]["name"] == "Email or username" && sets[0]["args"]["text"] == USER
            && sets[1]["args"]["name"] == "Password " && sets[1]["args"]["text"] == PW,
        format!("{sets:?}")
    );
    let pages = r["pages"].as_array().cloned().unwrap_or_default();
    // Whoever read page one first (the launch monitor, the moment the window
    // appeared, or swx_sign_in) woke it; the FIRST capture holds both counts.
    let woke = |w: &Value| w["nodeCounts"].as_array().map(|a| a.first() == Some(&json!(13)) && a.contains(&json!(38))).unwrap_or(false);
    let first_wake = replies[1]["launch"]["signInWake"].clone();
    check!(
        b,
        "THE LAZY TREE: page one was woken, both node counts recorded (13 cold, 38 warm); page two's inventory captured",
        pages.len() == 2
            && (woke(&pages[0]["wake"]) || woke(&first_wake))
            && pages[1]["inventory"].as_array().map(|a| a.iter().any(|e| e["name"] == "Password " && e["isPassword"] == true)).unwrap_or(false),
        s(&r["pages"])
    );
    replies.push(r);
    let all = everything_but_the_fill(&b, &ab, &replies);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());
    check!(b, "...nor the user name", !all.contains(USER), String::new());
}

#[test]
fn a_password_page_that_never_comes_types_no_password() {
    let m = Machine::new("signin-no-page-two");
    let ab = ab_for(page_one(), vault_ok());
    let b = m.boot(TempDir::new("signin-no-page-two-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20"), ("SWX_SIGN_IN_VERIFY_SECS", "2")]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "no password page: sign_in_unconfirmed, the password never typed — and never even asked for (0.9.25: the reveal waits for the password page)",
        r["errorCode"] == "sign_in_unconfirmed"
            && !ab.calls_to("desktop_ui_set").iter().any(|c| c["args"]["text"] == PW)
            && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );
}

#[test]
fn a_cookie_banner_in_the_way_is_the_users_choice() {
    let m = Machine::new("signin-cookie");
    let ab = ab_for(page_one(), vault_ok());
    ab.set_response("desktop_ui_set", json!({"success": false, "error": "element is not visible (obscured)"}));
    let b = m.boot(TempDir::new("signin-cookie-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    // 0.9.26: A FAILED SET IS NEVER EVIDENCE OF A BANNER (measured 2026-10-12:
    // a read-only refusal was blamed on a banner that did not exist, and the
    // user was told to answer it). The banner is reported, never answered, and
    // the user is told about it only when a field is seen COVERED by it.
    check!(
        b,
        "a failed set with a banner on the page: sign_in_fill_failed, the banner reported, no cookie toast, the banner never answered",
        r["errorCode"] == "sign_in_fill_failed" && r["cookieBanner"] == true
            && !ab.calls_to("notify_user").iter().any(|c| c["args"]["message"].as_str().unwrap_or("").to_lowercase().contains("cookie"))
            && !ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["name"] == "Accept All" || c["args"]["name"] == "Manage my preferences"),
        s(&r)
    );
}

// ── K1–K5 (2026-10-02): the page refuses a background set; real keystrokes ──

fn no_value_pattern() -> Value {
    json!({"success": false, "error": "control found but has NO ValuePattern (can't set text via UIA — try a foregrounded desktop_type)"})
}

fn with_username_value(v: &str) -> Value {
    fn walk(n: &mut Value, v: &str) {
        match n {
            Value::Array(a) => a.iter_mut().for_each(|x| walk(x, v)),
            Value::Object(m) => {
                if m.get("automationId") == Some(&json!("username")) {
                    m.insert("value".into(), json!(v));
                }
                m.values_mut().for_each(|x| walk(x, v));
            }
            _ => {}
        }
    }
    let mut t = page_one();
    walk(&mut t, v);
    t
}

fn keystroke_ab() -> common::AbStub {
    let ab = ab_for(page_one(), vault_ok());
    ab.set_response("desktop_ui_set", no_value_pattern());
    ab.set_response("desktop_raise_window", json!({"success": true}));
    ab.set_response("desktop_click", json!({"success": true}));
    ab.set_response("desktop_press_key", json!({"success": true}));
    ab.set_response("desktop_type", json!({"success": true}));
    ab
}

/// The machine reacts like the measured page: a click on the field brings the
/// login window to the front; typing the user name puts `typed_lands` in the
/// field; Continue shows page two; Log in closes the window.
fn drive_keystrokes(m: &Machine, ab: &common::AbStub, typed_lands: Option<String>, focus_follows: bool) {
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut state = 0;
    let mut focused = false;
    while Instant::now() < deadline {
        let clicks = ab.calls_to("desktop_ui_click");
        let clicked = |n: &str| clicks.iter().any(|c| c["args"]["name"] == n);
        // Written ONCE: rewriting it every tick truncates the file under a
        // reader, which then sees "no window has the foreground" (a harness
        // race seen under full-suite load, 2026-10-12).
        if focus_follows && !focused && !ab.calls_to("desktop_raise_window").is_empty() {
            let tmp = m.dir.join("foreground.json.tmp");
            std::fs::write(&tmp, json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();
            std::fs::rename(&tmp, m.dir.join("foreground.json")).unwrap();
            focused = true;
        }
        match state {
            0 if !ab.calls_to("desktop_type").is_empty() => {
                if let Some(t) = &typed_lands {
                    ab.set_response("desktop_ui_tree", with_username_value(t));
                }
                state = 1;
            }
            1 if clicked("Continue") => {
                ab.set_response("desktop_ui_tree", page_two());
                state = 2;
            }
            2 if ab.calls_to("desktop_click").iter().any(|c| c["args"]["x"] == 470 && c["args"]["y"] == 527) => {
                // The LABEL was clicked: the hidden checkbox turns on.
                ab.set_response("desktop_ui_tree", page_two_with("On"));
                state = 3;
            }
            2 | 3 if clicked("Log in") => {
                let _ = std::fs::remove_file(m.dir.join("sign-in.json"));
                return;
            }
            _ => {}
        }
        std::thread::sleep(Duration::from_millis(15));
    }
}

#[test]
fn the_page_that_refuses_a_background_set_is_typed_safely_and_read_back() {
    let m = Machine::new("signin-keys");
    let ab = keystroke_ab();
    let b = m.boot(TempDir::new("signin-keys-scratch"), &ab, &[("SWX_SIGN_IN_VERIFY_SECS", "8")]);
    let mut replies = vec![b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}))];
    wait_for_sign_in_stage(&b);
    // P2: the page RE-LAYS as the background set is refused (the window was
    // moved): the email box is now centred at (600, 520). A click at the first
    // read's point would miss it.
    let moved: Value = serde_json::from_str(&page_one().to_string().replace(
        r#""centerX":584,"centerY":470"#, r#""centerX":600,"centerY":520"#)).unwrap();
    ab.on_call_set("desktop_ui_set", "desktop_ui_tree", moved);
    // The login window in front from the start (the raise is still asserted):
    // a watcher-thread focus flip raced the settle loop under full-suite load.
    std::fs::write(m.dir.join("foreground.json"),
        json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();
    let r = std::thread::scope(|sc| {
        sc.spawn(|| drive_keystrokes(&m, &ab, Some(USER.to_string()), true));
        b.call("swx_sign_in", json!({"reason": "sign in"}))
    });
    check!(b, "signed in by keystrokes over two pages", r["signedIn"] == true && r["twoPage"] == true, s(&r));
    check!(
        b,
        "P2: the user-name click landed where the field IS NOW (a fresh read), not where it was",
        ab.calls_to("desktop_click").iter().any(|c| c["args"]["x"] == 600 && c["args"]["y"] == 520),
        format!("{:?}", ab.calls_to("desktop_click"))
    );
    check!(
        b,
        "P1: Remember me ticked by its visible LABEL (the checkbox is off-screen), verified by the toggle state",
        r["rememberTicked"]["via"] == "labelClick" && r["rememberTicked"]["ticked"] == true,
        s(&r["rememberTicked"])
    );
    let steps = r["steps"].to_string();
    check!(
        b,
        "the user name was typed and READ BACK (matched); the password was typed and never read back",
        steps.contains("\"readBack\":\"matched\"") && steps.contains("notReadPassword"),
        steps.clone()
    );
    let keys = ab.calls_to("desktop_press_key");
    check!(
        b,
        "select-all before every typing; Enter NEVER pressed (the submit is a background Invoke); every call carries a reason",
        keys.len() == 2 && keys.iter().all(|k| k["args"]["keys"] == json!(["ctrl+a"]) && k["args"]["reason"].is_string())
            && ab.calls_to("desktop_type").iter().all(|t| t["args"]["reason"].is_string())
            // P2: clicks at the field's OWN centre, in screen px, after a fresh read.
            && ab.calls_to("desktop_click").iter().filter(|c| c["args"]["x"] == 584 || c["args"]["x"] == 600).count() == 2
            // P3: the login window was RAISED before the click.
            && !ab.calls_to("desktop_raise_window").is_empty(),
        format!("{keys:?} {:?}", ab.calls_to("desktop_click"))
    );
    replies.push(r);
    let all = everything_but_the_fill(&b, &ab, &replies);
    check!(b, "the password appears nowhere but the fill", !all.contains(PW), String::new());
}

#[test]
fn no_keystroke_is_sent_while_the_user_is_typing() {
    let m = Machine::new("signin-collision");
    std::fs::write(m.dir.join("last-input-secs"), "0").unwrap();
    let ab = keystroke_ab();
    let b = m.boot(TempDir::new("signin-collision-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "the user typed 0 s ago: sign_in_needs_user_idle, the user asked for five seconds hands-off, NOTHING typed or clicked",
        r["errorCode"] == "sign_in_needs_user_idle"
            && !ab.calls_to("notify_user").is_empty()
            && ab.calls_to("desktop_type").is_empty()
            && ab.calls_to("desktop_click").is_empty()
            && ab.calls_to("desktop_press_key").is_empty(),
        s(&r)
    );
}

#[test]
fn a_collided_user_name_stops_before_continue_and_says_what_was_seen() {
    let m = Machine::new("signin-ogin");
    let ab = keystroke_ab();
    let b = m.boot(TempDir::new("signin-ogin-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    // Deterministic (no watcher thread — it flaked under full-suite load): the
    // login window is already in front, and the moment the bridge types, the
    // field holds "ogin" — MEASURED: the user's keystrokes, not ours.
    std::fs::write(m.dir.join("foreground.json"),
        json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();
    ab.on_call_set("desktop_type", "desktop_ui_tree", with_username_value("ogin"));
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "read-back mismatch: sign_in_fill_failed with what was observed; Continue never pressed; the password never typed",
        r["errorCode"] == "sign_in_fill_failed" && r["observed"]["fieldText"] == "ogin"
            && !ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["name"] == "Continue")
            && !ab.calls_to("desktop_type").iter().any(|c| c["args"]["text"] == PW),
        s(&r)
    );
}

#[test]
fn keystrokes_stop_when_the_login_window_is_not_in_front() {
    let m = Machine::new("signin-focus");
    let ab = keystroke_ab();
    let b = m.boot(TempDir::new("signin-focus-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    // The click does not bring the login window forward: focus stays elsewhere.
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "focus is elsewhere after the click: sign_in_fill_failed naming the foreground, NOTHING typed",
        r["errorCode"] == "sign_in_fill_failed" && r["error"].as_str().unwrap_or("").contains("foreground")
            && ab.calls_to("desktop_type").is_empty() && ab.calls_to("desktop_press_key").is_empty(),
        s(&r)
    );
}

#[test]
fn a_missing_login_names_every_way_to_grow_the_vault() {
    let m = Machine::new("signin-grow");
    let ab = ab_for(page_one(), vault_ok());
    ab.set_response("pup_credentials", json!({"success": true, "data": {"credentials": []}}));
    let b = m.boot(TempDir::new("signin-grow-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    let ways = r["growTheVault"].to_string();
    check!(
        b,
        "credential_missing names the import, a pup-window sign-in, and credential_set",
        r["errorCode"] == "sign_in_credential_missing" && ways.contains("pup_import_browser_logins")
            && ways.contains("pup window") && ways.contains("credential_set")
            && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );
}

// ── 0.9.16: any login window on the machine, under the user's authorization ──

fn visualize_machine(tag: &str, windows: Value) -> Machine {
    let m = Machine::new(tag);
    std::fs::write(
        m.dir.join("preexisting.json"),
        json!([{"pid": 56488, "image": "SLDWORKSVisualize.exe", "ageSeconds": 600},
               {"pid": 70002, "image": "SWXDesktopLauncher.exe", "ageSeconds": 60}]).to_string(),
    )
    .unwrap();
    std::fs::write(m.dir.join("sign-in.json"), windows.to_string()).unwrap();
    m
}

fn visualize_window() -> Value {
    json!({"hwnd": 4262148, "ownerPid": 56488, "className": "WindowsForms10.Window.8.app.0.141b42a_r6_ad1",
           "title": "Login - 3DEXPERIENCE ID - Dassault Systemes"})
}

#[test]
fn the_visualize_login_is_answered_only_on_the_users_authorization() {
    assert!(
        swx_bridge::launchos::WATCHED_IMAGES.contains(&"SLDWORKSVisualize.exe"),
        "the live census must see SOLIDWORKS Visualize, or its login window is never found"
    );
    let m = visualize_machine("signin-visualize", visualize_window());
    let ab = ab_for(form(vec![]), vault_ok());
    let b = m.boot(TempDir::new("signin-visualize-scratch"), &ab, &[]);
    // No launch by this bridge: without the user's authorization, nothing.
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(b, "without userAuthorized: sign_in_not_waiting, the vault never asked", r["errorCode"] == "sign_in_not_waiting" && ab.calls_to("pup_credential_reveal").is_empty(), s(&r));
    let r = std::thread::scope(|sc| {
        sc.spawn(|| close_window_on_submit(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "Drew: sign Visualize in", "userAuthorized": true, "window": "visualize"}))
    });
    check!(
        b,
        "with userAuthorized: the Visualize login is signed in, and the reply says which process owned it",
        r["signedIn"] == true && r["window"]["ownerImage"] == "SLDWORKSVisualize.exe" && r["window"]["host"] == "visualize"
            && r["window"]["ownedByThisBridgesLaunch"] == false,
        s(&r)
    );
    let all = everything_but_the_fill(&b, &ab, &[r]);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());
}

#[test]
fn several_login_windows_need_a_choice() {
    let m = visualize_machine("signin-several", json!([
        visualize_window(),
        {"hwnd": 1002, "ownerPid": 70002, "className": "DSYSysLoginPanelWebView2Class", "title": "Login | 3DEXPERIENCE ID | Dassault Systèmes"}
    ]));
    let ab = ab_for(form(vec![]), vault_ok());
    let b = m.boot(TempDir::new("signin-several-scratch"), &ab, &[]);
    let r = b.call("swx_sign_in", json!({"reason": "sign in", "userAuthorized": true}));
    check!(
        b,
        "two windows, no choice: sign_in_window_ambiguous listing both, nothing typed",
        r["errorCode"] == "sign_in_window_ambiguous" && r["loginWindows"].as_array().map(|a| a.len() == 2).unwrap_or(false)
            && ab.calls_to("pup_credential_reveal").is_empty(),
        s(&r)
    );
    let r = std::thread::scope(|sc| {
        sc.spawn(|| close_window_on_submit(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "sign in", "userAuthorized": true, "window": "solidworks"}))
    });
    check!(b, "window:\"solidworks\" picks the launcher's", r["signedIn"] == true && r["window"]["hwnd"] == 1002, s(&r));
}

#[test]
fn a_cookie_banner_over_the_field_is_never_clicked_through() {
    let m = Machine::new("signin-covered");
    // P2: after a move, the banner COVERS the email box (and resizing does not help here).
    let mut t = page_one();
    fn put(n: &mut Value) {
        match n {
            Value::Array(a) => a.iter_mut().for_each(put),
            Value::Object(o) => {
                if o.get("name") == Some(&json!("Accept All")) {
                    o.insert("rect".into(), json!({"x": 380, "y": 445, "w": 420, "h": 60}));
                }
                o.values_mut().for_each(put);
            }
            _ => {}
        }
    }
    put(&mut t);
    let ab = keystroke_ab();
    ab.set_response("desktop_ui_tree", t);
    let b = m.boot(TempDir::new("signin-covered-scratch"), &ab, &[]);
    b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    wait_for_sign_in_stage(&b);
    let r = b.call("swx_sign_in", json!({"reason": "sign in"}));
    check!(
        b,
        "covered: the window was made taller, the field still covered -> sign_in_needs_user (the banner is the user's); no click, no typing",
        r["errorCode"] == "sign_in_needs_user" && r["cookieBanner"] == true
            && ab.calls_to("desktop_set_window_bounds").iter().any(|c| c["args"]["w"] == 1100 && c["args"]["h"] == 1450 && c["args"]["hwnd"].is_number())
            && ab.calls_to("desktop_ui_window").is_empty()
            && ab.calls_to("desktop_click").is_empty() && ab.calls_to("desktop_type").is_empty(),
        s(&r)
    );
}

// ── 0.9.23: the THIRD host — SWXDesktopUpdate.exe, found by CONTENT ─────────
//
// MEASURED 2026-10-12: title "3DEXPERIENCE" (no "ID", no "login"), class
// WindowsForms10.Window.8.app.0.30495d1_r6_ad1, owned by SWXDesktopUpdate.exe
// (from %TEMP%). desktop_ui_tree returns only the frame (14 nodes);
// desktop_find_control reaches the page by name / automation id.

const UPDATE_HWND: i64 = 857684;
const UPDATE_CLASS: &str = "WindowsForms10.Window.8.app.0.30495d1_r6_ad1";

fn update_machine(tag: &str) -> Machine {
    let m = Machine::new(tag);
    std::fs::write(m.dir.join("preexisting.json"), json!([{"pid": 61000, "image": "SWXDesktopUpdate.exe", "ageSeconds": 30}]).to_string()).unwrap();
    std::fs::write(
        m.dir.join("sign-in.json"),
        json!({"hwnd": UPDATE_HWND, "ownerPid": 61000, "className": UPDATE_CLASS, "title": "3DEXPERIENCE"}).to_string(),
    )
    .unwrap();
    m
}

/// The measured tree: the frame only, even after a wake.
fn frame_tree() -> Value {
    json!({"success": true, "status": "ok", "hwnd": UPDATE_HWND, "maxNodes": 600, "nodeCount": 14, "truncated": false,
           "window": {"title": "3DEXPERIENCE", "rect": {"x": 828, "y": 273, "w": 1316, "h": 1076}},
           "root": {"role": "Window", "name": "3DEXPERIENCE", "automationId": "SWXDesktopUpdateForm", "invokable": false, "settable": false, "children": [
               {"role": "TitleBar", "name": "", "automationId": "", "children": [
                   {"role": "MenuBar", "name": "System Menu Bar", "automationId": ""},
                   {"role": "Button", "name": "Minimize", "automationId": "", "invokable": true},
                   {"role": "Button", "name": "Maximize", "automationId": "", "invokable": true},
                   {"role": "Button", "name": "Close", "automationId": "", "invokable": true}
               ]},
               {"role": "Pane", "name": "", "automationId": ""}
           ]}})
}

fn ctl(role: &str, name: &str, aid: &str, cx: i64, cy: i64, settable: bool, invokable: bool) -> Value {
    json!({"success": true, "data": {"found": true, "control": {"role": role, "name": name, "automationId": aid,
           "invokable": invokable, "settable": settable,
           "rect": {"x": cx - 200, "y": cy - 20, "w": 400, "h": 40, "centerX": cx, "centerY": cy}}}})
}

fn not_found() -> Value {
    json!({"success": false, "error": "no control matched"})
}

const FIND: &str = "desktop_find_control";

/// Page one, as measured: the user-name edit (by automation id or exact
/// name), Continue. A query with a role ALONE answers the measured wrong match.
fn update_page_one(ab: &common::AbStub) {
    ab.clear_rules(FIND);
    let user = ctl("Edit", "Email or username", "username", 1486, 844, true, false);
    ab.when(FIND, json!({"automationId": "username"}), user.clone());
    ab.when(FIND, json!({"role": "Edit", "name": "Email or username"}), user);
    ab.when(FIND, json!({"role": "Button", "name": "Continue"}), ctl("Button", "Continue", "", 1486, 940, false, true));
    ab.when(FIND, json!({"name": "Manage cookies"}), ctl("Hyperlink", "Manage cookies", "", 2034, 1264, false, true));
    ab.set_response(FIND, not_found());
}

/// Page two, as measured: the password edit's NAME carries an icon glyph, so
/// only `{automationId:"password"}` finds it; "Remember me" is a Text.
fn update_page_two(ab: &common::AbStub) {
    ab.clear_rules(FIND);
    let user = ctl("Edit", "Email or username", "username", 1486, 727, true, false);
    ab.when(FIND, json!({"automationId": "username"}), user.clone());
    ab.when(FIND, json!({"role": "Edit", "name": "Email or username"}), user);
    ab.when(FIND, json!({"automationId": "password"}), ctl("Edit", "Password \u{e9a0}", "password", 1486, 856, true, false));
    ab.when(FIND, json!({"role": "Text", "name": "Remember me"}), ctl("Text", "Remember me", "", 1255, 923, false, false));
    ab.when(FIND, json!({"role": "Button", "name": "Log in"}), ctl("Button", "Log in", "", 1486, 1006, false, true));
    ab.when(FIND, json!({"role": "Button", "name": "Back"}), ctl("Button", "Back", "", 1486, 1093, false, true));
    ab.set_response(FIND, not_found());
}

fn update_ab() -> common::AbStub {
    let ab = ab_for(frame_tree(), vault_ok());
    ab.set_response("desktop_raise_window", json!({"success": true}));
    ab.set_response("desktop_click", json!({"success": true}));
    update_page_one(&ab);
    ab
}

/// Continue → page two; Log in → the window closes.
fn drive_update(m: &Machine, ab: &common::AbStub) {
    let deadline = Instant::now() + Duration::from_secs(25);
    let mut state = 0;
    while Instant::now() < deadline {
        let clicks = ab.calls_to("desktop_ui_click");
        let clicked = |n: &str| clicks.iter().any(|c| c["args"]["name"] == n);
        match state {
            0 if clicked("Continue") => {
                update_page_two(ab);
                state = 1;
            }
            1 if clicked("Log in") => {
                let _ = std::fs::remove_file(m.dir.join("sign-in.json"));
                return;
            }
            _ => {}
        }
        std::thread::sleep(Duration::from_millis(15));
    }
}

#[test]
fn the_desktop_update_login_is_found_by_its_content_and_signed_in() {
    assert!(
        !swx_bridge::launch::is_sign_in_window(UPDATE_CLASS, "3DEXPERIENCE"),
        "the fast path does NOT know this window (its title is just \"3DEXPERIENCE\") — content must"
    );
    assert!(swx_bridge::launchos::WATCHED_IMAGES.contains(&"SWXDesktopUpdate.exe"), "the census must see the updater");
    let m = update_machine("signin-update");
    std::fs::write(m.dir.join("foreground.json"), json!({"hwnd": UPDATE_HWND, "pid": 61000, "title": "3DEXPERIENCE", "className": UPDATE_CLASS}).to_string()).unwrap();
    let ab = update_ab();
    let b = m.boot(TempDir::new("signin-update-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20"), ("SWX_SIGN_IN_VERIFY_SECS", "8")]);
    let st = b.call("swx_status", json!({}));
    check!(
        b,
        "swx_status: waitingOn userSignIn, signInWindow {hwnd, title, ownerImage, host:desktopUpdate}, recognized by content",
        st["waitingOn"] == "userSignIn" && st["signInWindow"]["hwnd"] == UPDATE_HWND && st["signInWindow"]["title"] == "3DEXPERIENCE"
            && st["signInWindow"]["ownerImage"] == "SWXDesktopUpdate.exe" && st["signInWindow"]["host"] == "desktopUpdate"
            && st["signInWindow"]["recognizedBy"] == "content",
        s(&st)
    );
    let r = std::thread::scope(|sc| {
        sc.spawn(|| drive_update(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": UPDATE_HWND}))
    });
    check!(
        b,
        "signed in over both pages, located by desktop_find_control, verified by the window closing",
        r["signedIn"] == true && r["twoPage"] == true && r["window"]["host"] == "desktopUpdate" && !m.sign_in_up()
            && r["pages"][0]["located"]["source"] == "desktop_find_control" && r["pages"][1]["located"]["source"] == "desktop_find_control",
        s(&r)
    );
    let sets = ab.calls_to("desktop_ui_set");
    let set = |i: usize| (sets.get(i).map(|c| c["args"]["automationId"].clone()), sets.get(i).map(|c| c["args"]["text"].clone()));
    check!(
        b,
        "CLEAR THEN SET, by automation id: username \"\" then the user name; password \"\" then the password",
        sets.len() == 4
            && set(0) == (Some(json!("username")), Some(json!(""))) && set(1) == (Some(json!("username")), Some(json!(USER)))
            && set(2) == (Some(json!("password")), Some(json!(""))) && set(3) == (Some(json!("password")), Some(json!(PW))),
        format!("{sets:?}")
    );
    check!(
        b,
        "no find_control query ever went out with a role alone (measured: it matched the browser's address bar)",
        ab.calls_to(FIND).iter().all(|c| c["args"].get("name").is_some() || c["args"].get("contains").is_some() || c["args"].get("automationId").is_some()),
        format!("{:?}", ab.calls_to(FIND))
    );
    check!(
        b,
        "Remember me: the label was clicked, its state is unreadable, rememberTicked is null",
        r["rememberTicked"].is_null()
            && ab.calls_to("desktop_click").iter().any(|c| c["args"]["x"] == 1255 && c["args"]["y"] == 923),
        s(&r["steps"])
    );
    let all = everything_but_the_fill(&b, &ab, &[st, r]);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());
}

#[test]
fn a_named_window_is_judged_by_its_content_never_refused_on_census_grounds() {
    // The window is NOT in any census (no sign-in.json): only ab sees it.
    let m = Machine::new("signin-named");
    let _ = std::fs::remove_file(m.dir.join("sign-in.json"));
    let ab = update_ab();
    ab.set_response("pup_credential_reveal", json!({"success": false, "error": "no credential stored for that host", "errorCode": "not_found"}));
    ab.set_response("pup_credentials", json!({"success": true, "data": {"credentials": []}}));
    let b = m.boot(TempDir::new("signin-named-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20")]);
    let r = b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": UPDATE_HWND}));
    check!(
        b,
        "a named hwnd with the user-name field is TAKEN (it gets as far as the vault), not refused sign_in_not_waiting",
        r["errorCode"] != "sign_in_not_waiting" && r["errorCode"] != "sign_in_window_not_a_login"
            && ab.calls_to(FIND).iter().any(|c| c["args"]["hwnd"] == UPDATE_HWND),
        s(&r)
    );
    ab.clear_rules(FIND);
    let r = b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": 4242}));
    check!(
        b,
        "a named hwnd with neither field: sign_in_window_not_a_login, with what WAS looked for and found; nothing typed",
        r["errorCode"] == "sign_in_window_not_a_login" && r["found"]["Edit \"Email or username\""]["found"] == false
            && r["found"]["Edit \"Password\""]["found"] == false && ab.calls_to("desktop_ui_set").is_empty(),
        s(&r)
    );
}

#[test]
fn a_launch_parked_behind_the_updater_login_says_so() {
    // SOLIDWORKS: the launch pauses (never killed) and swx_status names the updater's login.
    let m = update_machine("signin-update-launch");
    let ab = update_ab();
    let b = m.boot(TempDir::new("signin-update-launch-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20")]);
    let r = b.call("swx_start", json!({"waitSeconds": 0, "reason": "start"}));
    let st = wait_for_sign_in_stage(&b);
    check!(
        b,
        "swx_start / swx_status: waitingOn userSignIn with the updater's window; the launch reached its sign-in stage",
        st["waitingOn"] == "userSignIn" && st["signInWindow"]["host"] == "desktopUpdate" && st["signInMode"] == "wait"
            && r["stage"] != "launching",
        format!("{}\n{}", s(&r), s(&st))
    );
}

// ── 0.9.25: ab's policy keeps the password; the bridge prepares the page ────

/// MEASURED 2026-10-12 (hosted 0.9.24), ab's exact refusal.
const ORIGIN_POLICY: &str = "pup_credential_reveal requires a VERIFIED local transport (ab 2.0.96+ X-Adom-Verified-Origin: local); this request arrived with origin \"bridge\"";

#[test]
fn the_reveal_refused_by_ab_policy_leaves_the_page_prepared_for_the_user() {
    let m = update_machine("signin-policy");
    let ab = update_ab();
    ab.set_response("pup_credential_reveal", json!({"success": false, "error": ORIGIN_POLICY}));
    let b = m.boot(TempDir::new("signin-policy-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20"), ("SWX_SIGN_IN_VERIFY_SECS", "8"), ("SWX_SIGN_IN_CONSENT_SECS", "3")]);
    let r = std::thread::scope(|sc| {
        // Continue → page two. (No Log in is ever expected here.)
        sc.spawn(|| {
            let deadline = Instant::now() + Duration::from_secs(15);
            while Instant::now() < deadline {
                if ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["name"] == "Continue") {
                    update_page_two(&ab);
                    return;
                }
                std::thread::sleep(Duration::from_millis(15));
            }
        });
        b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": UPDATE_HWND}))
    });
    check!(
        b,
        "sign_in_vault_policy (not vault_unavailable), ab's exact words as abSaid, the page prepared: page 2, user name filled",
        r["errorCode"] == "sign_in_vault_policy" && r["abSaid"] == ORIGIN_POLICY
            && r["pagePrepared"]["page"] == 2 && r["pagePrepared"]["userNameFilled"] == true,
        s(&r)
    );
    let ways = r["waysOut"].as_array().cloned().unwrap_or_default();
    check!(
        b,
        "waysOut: the user types the password; ab issue #237. The hint says the same",
        ways.len() == 2 && ways[0].as_str().unwrap_or("").contains("user types the password") && ways[1].as_str().unwrap_or("").contains("#237")
            && r["_hint"].as_str().unwrap_or("").contains("#237"),
        s(&r)
    );
    let calls = ab.calls();
    let at = |pred: &dyn Fn(&Value) -> bool| calls.iter().position(|c| pred(c));
    let continued = at(&|c| c["command"] == "desktop_ui_click" && c["args"]["name"] == "Continue");
    let revealed = at(&|c| c["command"] == "pup_credential_reveal");
    check!(
        b,
        "THE PAGE FIRST: the user name was set and Continue pressed BEFORE the reveal was asked",
        matches!((continued, revealed), (Some(c), Some(r)) if c < r)
            && ab.calls_to("desktop_ui_set").iter().any(|c| c["args"]["automationId"] == "username" && c["args"]["text"] == USER),
        format!("{calls:?}")
    );
    check!(
        b,
        "asked ONCE, never retried; no password typed; Log in never pressed; the window is still up for the user",
        ab.calls_to("pup_credential_reveal").len() == 1
            && !ab.calls_to("desktop_ui_set").iter().any(|c| c["args"]["automationId"] == "password" && c["args"]["text"] != "")
            && !ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["name"] == "Log in")
            && m.sign_in_up(),
        format!("{:?}", ab.calls_to("desktop_ui_set"))
    );
    let all = everything_but_the_fill(&b, &ab, &[r]);
    check!(b, "the user name appears in no reply, log or other request", !all.contains(USER), String::new());
}

// ── 0.9.26: the AWAKE page 2 on the updater host, measured 2026-10-12 ───────

/// 37 nodes once probed (the cold tree is the 14-node frame): both edits — the
/// user name prefilled and READ-ONLY — a real CheckBox "Remember me ", Log in,
/// Back, and hyperlinks including "Manage cookies". NO cookie banner.
fn awake_page_two() -> Value {
    json!({"success": true, "status": "ok", "hwnd": UPDATE_HWND, "nodeCount": 37, "truncated": false,
           "window": {"title": "3DEXPERIENCE", "rect": {"x": 828, "y": 273, "w": 1316, "h": 1076}},
           "root": {"role": "Window", "name": "3DEXPERIENCE", "automationId": "SWXDesktopUpdateForm", "children": [
               {"role": "Document", "name": "Login | 3DEXPERIENCE ID | Dassault Systèmes", "automationId": "RootWebArea", "children": [
                   {"role": "Edit", "name": "Email or username", "automationId": "username", "invokable": false, "settable": true,
                    "rect": {"x": 1286, "y": 707, "w": 400, "h": 40, "centerX": 1486, "centerY": 727}},
                   {"role": "Edit", "name": "Password ", "automationId": "password", "invokable": false, "settable": true,
                    "rect": {"x": 1286, "y": 836, "w": 400, "h": 40, "centerX": 1486, "centerY": 856}},
                   {"role": "CheckBox", "name": "Remember me ", "automationId": "", "invokable": false, "settable": true, "toggleState": "Off",
                    "rect": {"x": 1176, "y": 915, "w": 16, "h": 16, "centerX": 1184, "centerY": 923}},
                   {"role": "Button", "name": "Log in", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Button", "name": "Back", "automationId": "", "invokable": true, "settable": false},
                   {"role": "Hyperlink", "name": "Forgot your password?", "automationId": "", "invokable": true},
                   {"role": "Hyperlink", "name": "English", "automationId": "", "invokable": true},
                   {"role": "Hyperlink", "name": "Help", "automationId": "", "invokable": true},
                   {"role": "Hyperlink", "name": "Manage cookies", "automationId": "", "invokable": true}
               ]}
           ]}})
}

const READ_ONLY: &str = "UIA SetValue failed: Cannot call SetValue on read only value";

#[test]
fn the_awake_page_two_skips_the_prefilled_name_and_a_cookie_link_is_not_a_banner() {
    let m = update_machine("signin-awake-two");
    let ab = update_ab();
    update_page_two(&ab);
    ab.set_response("desktop_ui_tree", awake_page_two());
    ab.when("desktop_ui_set", json!({"name": "Email or username"}), json!({"success": false, "error": READ_ONLY}));
    ab.set_response("pup_credential_reveal", json!({"success": false, "error": ORIGIN_POLICY}));
    let b = m.boot(TempDir::new("signin-awake-two-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20"), ("SWX_SIGN_IN_CONSENT_SECS", "3")]);
    let r = b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": UPDATE_HWND}));
    check!(
        b,
        "PAGE 2 (it has the password edit, whatever else it holds): the read-only name is 'prefilled', the reveal asked ONCE, sign_in_vault_policy with pagePrepared {page:2, userNameFilled:true}",
        r["errorCode"] == "sign_in_vault_policy" && r["pages"][0]["page"] == 2 && r["pages"][0]["located"]["source"] == "tree"
            && r["pagePrepared"]["page"] == 2 && r["pagePrepared"]["userNameFilled"] == true
            && r["steps"].as_array().map(|a| a.iter().any(|st| st["nameField"] == "prefilled")).unwrap_or(false)
            && ab.calls_to("pup_credential_reveal").len() == 1,
        s(&r)
    );
    check!(
        b,
        "the 'Manage cookies' LINK is not a banner: cookieBanner false, and no toast about cookies",
        r["cookieBanner"] == false && r["pages"][0]["cookieBanner"] == false
            && !ab.calls_to("notify_user").iter().any(|c| c["args"]["message"].as_str().unwrap_or("").to_lowercase().contains("cookie")),
        s(&r)
    );

    // The same page with the vault answering, and "Log in" failing: a failed
    // fill with NO banner never tells the user to answer a banner.
    let m = update_machine("signin-awake-two-fail");
    let ab = update_ab();
    update_page_two(&ab);
    ab.set_response("desktop_ui_tree", awake_page_two());
    ab.when("desktop_ui_set", json!({"name": "Email or username"}), json!({"success": false, "error": READ_ONLY}));
    ab.when("desktop_ui_click", json!({"name": "Log in"}), json!({"success": false, "error": "Invoke failed"}));
    let b = m.boot(TempDir::new("signin-awake-two-fail-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20")]);
    let r = b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": UPDATE_HWND}));
    check!(
        b,
        "no banner, a failed press: sign_in_fill_failed and NO cookie toast; the password went in by automation id",
        r["errorCode"] == "sign_in_fill_failed"
            && !ab.calls_to("notify_user").iter().any(|c| c["args"]["message"].as_str().unwrap_or("").to_lowercase().contains("cookie"))
            && ab.calls_to("desktop_ui_set").iter().any(|c| c["args"]["text"] == PW),
        s(&r)
    );
    let all = everything_but_the_fill(&b, &ab, &[r]);
    check!(b, "the password appears nowhere", !all.contains(PW), String::new());

    // The vault answers and Log in closes the window. "Remember me" is a real
    // on-screen CheckBox: toggled in the background — and a toggle ab calls
    // done while the box still reads Off is NOT a tick.
    let m = update_machine("signin-awake-two-ok");
    let ab = update_ab();
    update_page_two(&ab);
    ab.set_response("desktop_ui_tree", awake_page_two());
    ab.when("desktop_ui_set", json!({"name": "Email or username"}), json!({"success": false, "error": READ_ONLY}));
    let b = m.boot(TempDir::new("signin-awake-two-ok-scratch"), &ab, &[("SWX_SIGN_IN_WAKE_MS", "20"), ("SWX_SIGN_IN_VERIFY_SECS", "8")]);
    let r = std::thread::scope(|sc| {
        sc.spawn(|| drive_update_login(&m, &ab));
        b.call("swx_sign_in", json!({"reason": "Drew: sign me in", "userAuthorized": true, "window": UPDATE_HWND}))
    });
    check!(
        b,
        "signed in; the toggle was tried in the background, and the box reading Off afterwards is reported as NOT ticked",
        r["signedIn"] == true && !ab.calls_to("desktop_ui_toggle").is_empty() && r["rememberTicked"]["ticked"] == false,
        s(&r)
    );
}

/// Log in → the window closes.
fn drive_update_login(m: &Machine, ab: &common::AbStub) {
    let deadline = Instant::now() + Duration::from_secs(20);
    while Instant::now() < deadline {
        if ab.calls_to("desktop_ui_click").iter().any(|c| c["args"]["name"] == "Log in") {
            let _ = std::fs::remove_file(m.dir.join("sign-in.json"));
            return;
        }
        std::thread::sleep(Duration::from_millis(15));
    }
}