← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.15+0.9.16
Cargo.lock+1−1
@@ -107,7 +107,7 @@ dependencies = [  [[package]] name = "swx-bridge"-version = "0.9.15"+version = "0.9.16" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "swx-bridge"-version = "0.9.15"+version = "0.9.16" edition = "2021" rust-version = "1.80" description = "Adom SOLIDWORKS bridge — swx_* verbs over the SOLIDWORKS COM API, as a single native binary"
README.md+11−1
@@ -461,7 +461,17 @@ not.  ## Roadmap -- v0.9.15 (this): **the real login window, as measured.** The first real+- v0.9.16 (this): **any login window, typed where it really is.** Measured on+  the Visualize login window: page 2 (its "Remember me" checkbox is off-screen,+  so its visible label is what gets clicked), the page re-lays when the window+  moves (clicks now land at the field's own fresh centre, and a banner over the+  field is never clicked through), and a click only takes focus after the window+  is raised. Typing a password from the cloud is refused by policy, so the+  bridge — which reads the vault and types on the desktop — now answers ANY+  3DEXPERIENCE ID login window on the user's say-so (`userAuthorized:true`,+  `window:"visualize"|"solidworks"|<hwnd>`), naming the process that owns it.+  536 tests.+- v0.9.15: **the real login window, as measured.** The first real   3DEXPERIENCE ID window (raised by Visualize) was measured on 2026-10-02: its   accessibility tree is lazy (13 nodes until a point is queried, then 38), the   form is two pages (user name + Continue, then the password), a cookie banner
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "solidworks",   "displayName": "SOLIDWORKS",-  "version": "0.9.15",+  "version": "0.9.16",   "description": "Bridge for SOLIDWORKS desktop CAD \u2014 create and open documents, a modeling DSL, file management (find/open/save/export/Pack and Go), custom properties, BOM generation, design analysis (mass properties, bodies, parameters, interference, rebuild health) and background window/dialog etiquette, over the SOLIDWORKS COM API. Single native binary: no interpreter, no PowerShell, ~10ms cold start.",   "author": "Adom Inc.",   "license": "MIT",
docs/LAUNCH-RESILIENCE.md+10
@@ -1346,3 +1346,13 @@ render has worked by hand, and that waits on his sign-in. **The dependent skill.** `skills/solidworks-sign-in/SKILL.md` ships in the package as a second user skill: the measured flow, each line marked measured or unmeasured, and NO credentials — only where they come from in general terms.++**MEASURED 2026-10-02 on the Visualize login window, and 0.9.16.**++| | Measured | 0.9.16 |+|---|---|---|+| P1 — page 2 | Edit "Email or username" (prefilled), Edit "Password " (automationId `password`), CheckBox "Remember me " (toggleState off, rect OFF-SCREEN: centerX -14493 — the native input is hidden; the visible control is the Text "Remember me…"), Button "Log in", Button "Back", the cookie banner still present | Page 2 is now a measured fixture. "Remember me": already on → nothing; an on-screen invokable box → a background Invoke; an off-screen one → a real click on its visible LABEL (only while the user is idle, with the window raised and in front), then the toggle state read again; a tick that cannot be made safely is skipped and said, never failing the sign-in |+| P2 — geometry moves | moving the window re-laid the page; the cookie banner then COVERED the email box; a click at the earlier point hit the banner, select-all selected page text, the typing went nowhere | Before every real click the tree is RE-READ and the click lands at the field's own `rect.centerX/centerY` in screen px (`desktop_click {x, y}`). A field a cookie-banner element overlaps → the window is made 1100x1450 (`desktop_ui_window {action:"resize"}` — arguments UNMEASURED) and re-read; still covered → `sign_in_needs_user` (the banner is the user's) |+| P3 — focus | a plain click does not take focus while the user's app is active (the login thread's focus was 0); `desktop_raise_window {hwnd, reason}` brings it forward and then the click lands; `desktop_type {text, hwnd:<Chrome_RenderWidgetHostHWND child>}` types; reading the user name back works | Raise, confirm the login window is the foreground window, click, then select-all and type to the render widget when the tree names its handle (else the window) |+| P4 — the vault | now holds the login (host iam.3ds.com, stored with `credential_set`) | The reveal asks the host the vault lists on the 3ds.com domain (`iam.*` first) |+| P5 — policy | typing the password through ab FROM THE CLOUD is refused by the auto-mode classifier ("Credential Leakage") — rightly | The only path is the bridge reading the vault LOCALLY and typing locally. So `swx_sign_in {userAuthorized:true}` (the user's say-so) answers ANY 3DEXPERIENCE ID login window on the machine — our launch chain's, SOLIDWORKS Visualize's (`SLDWORKSVisualize.exe` is now in the census), any Dassault process's — and reports which process owns it; `window:"visualize"\|"solidworks"\|<hwnd>` picks one when several are up (`sign_in_window_ambiguous` otherwise). Without the authorization, only the bridge's own launch's window. This wires the unwired half of T4 |
install.sh

  
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "solidworks-bridge",-  "version": "0.9.15",+  "version": "0.9.16",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the SOLIDWORKS bridge (swx_* verbs). The bridge runtime itself is the release zip — a single native swx-bridge.exe; Adom Bridge loads that.",   "tags": [
references/launch-and-sign-in.md+5
@@ -228,3 +228,8 @@ Read this before starting SOLIDWORKS, while waiting through a launch or a 3DEXPE - **The measured login flow** (two pages, a lazy tree, a cookie banner, real   keystrokes only while the user is idle) is in the **solidworks-sign-in**   skill, which ships with this one.+- **Any login window, on the user's say-so:** `swx_sign_in {reason,+  userAuthorized:true}` also answers a 3DEXPERIENCE ID login window the+  bridge's launch did not raise — SOLIDWORKS Visualize's, for one. Add+  `window:"visualize"` / `"solidworks"` / an hwnd when several are open; the+  reply's `window` names the owning process.
skills/solidworks-sign-in/SKILL.md+32−4
@@ -46,12 +46,36 @@ Every line below is marked **[measured]** (seen on a real machine, 2026-10-02) o    (a failed attempt leaves the box dirty), then `desktop_type`. Each one needs a    `reason`, and each takes focus (the login window comes to the front). 4. [measured] `desktop_ui_click {name: "Continue"}` works in the background.-5. [unmeasured] Page 2 should hold the password box and a sign-in button. Read-   its inventory first; do not assume its names.+5. [measured] Page 2 (after a good page-1 fill and Continue): Edit "Email or+   username" (prefilled), Edit "Password " (automationId `password`), CheckBox+   "Remember me " (off), Button "Log in", Button "Back", and the cookie banner+   still there.+   - [measured] The "Remember me" checkbox's own input is OFF-SCREEN (its rect+     centre is far left of the screen). The visible control is its Text label+     "Remember me…": click the LABEL to tick it, then read the toggle state.+   - [unmeasured] What the page shows after "Log in" (a wrong password, a second+     step). Read it; never retry. 6. [measured] A cookie banner sits on the page ("Accept All", "Manage my    preferences"). Never touch it: that choice belongs to the user. In the    measured layout it does not cover the form. +## Geometry and focus++- [measured] Moving the window RE-LAYS the page: the cookie banner then covered+  the email box, a click at the old point hit the banner, select-all selected+  page text, and the typing went nowhere. After any move or resize, re-read the+  tree and click at the control's OWN `rect.centerX/centerY` (screen pixels).+  A window of about 1100x1450 kept the box clear of the banner.+- [measured] A plain click does not take focus while the user's own app is+  active. `desktop_raise_window {hwnd, reason}` brings the login window+  forward; confirm it IS the foreground window before typing.+- [measured] Typing lands when sent to the page's render widget (a+  `Chrome_RenderWidgetHostHWND` child window). Reading the user name back from+  the tree works.+- [measured] Typing a password through ab from a cloud container is refused by+  policy ("Credential Leakage"), and rightly. The only acceptable path is the+  bridge reading the vault on the desktop and typing there: `swx_sign_in`.+ ## Rules that never bend  - [measured] NEVER send keystrokes while the user is typing. On 10-02 keystrokes@@ -84,5 +108,9 @@ own secret store on their desktop, in general terms: An agent must never write a password into this skill, the bridge repository, a wiki page, a log line or a chat message. -Once `swx_sign_in` has succeeded on a real window, it is the preferred path: it-does every step above, in order, and fails closed with a reason when it cannot.+`swx_sign_in` is the path: it does every step above, in order, and fails closed+with a reason when it cannot. For a login window the bridge's own launch did not+raise (SOLIDWORKS Visualize's, or one already up), pass `userAuthorized:true` —+only on the user's say-so — and `window:"visualize"` or `"solidworks"` (or the+window handle) when more than one is open; the reply names the process that+owns the window.
src/launch.rs+5−1
@@ -541,7 +541,7 @@ fn on_sign_in_window(hwnd: i64) {             update(|a| a.sign_in_auto_tried = true);             crate::signin::sign_in(                 "swx_start {signIn:\"auto\"}: the launch reached the 3DEXPERIENCE sign-in",-                false,+                crate::signin::Opts::default(),                 crate::caller::self_caller(),             );         }@@ -1915,6 +1915,10 @@ fn choose_our_log( }  /// The pids currently recorded as our chain.+pub fn our_chain_pids() -> Vec<u32> {+    chain_pids_now()+}+ fn chain_pids_now() -> Vec<u32> {     attempt().map(|a| a.chain.iter().map(|p| p.pid).collect()).unwrap_or_default() }
src/launchos.rs+12−2
@@ -53,6 +53,9 @@ pub const WATCHED_IMAGES: &[&str] = &[     "swCefSubProc.exe",     "ENOPLMCSAClient.exe",     "EdmServerV6.exe",+    // SOLIDWORKS Visualize: it raises its OWN 3DEXPERIENCE ID login (measured+    // 2026-10-02), which swx_sign_in answers under the user's authorization.+    "SLDWORKSVisualize.exe",     "SWXDesktopHelper.exe",     "sldBgDwld.exe",     // PARENT LINKS ONLY — never terminable (see `launch::NEVER_TERMINATE`).@@ -1284,8 +1287,15 @@ impl LaunchOs for FakeOs {         // owns it. `sign-in.json` is {"ownerPid":N,...} and the file is DELETED         // to model the user answering it — which is what lets a test prove that         // the paused deadlines resume.-        let si = self.read_json("sign-in.json");-        if si.is_object() {+        // `sign-in.json` may hold ONE login window or an ARRAY of them (0.9.16:+        // the Visualize host and the launcher's at once).+        let si_all = self.read_json("sign-in.json");+        let rows: Vec<Value> = match &si_all {+            Value::Array(a) => a.clone(),+            v if v.is_object() => vec![v.clone()],+            _ => Vec::new(),+        };+        for si in &rows {             let owner = si.get("ownerPid").and_then(|x| x.as_u64()).map(|p| p as u32);             // No owner named: it belongs to whichever launcher pid asks, which is             // how a scenario says "our chain is showing it".
src/signin.rs+305−54
@@ -149,6 +149,17 @@ pub struct Element {     pub settable: Option<bool>,     /// Screen rectangle (x, y, w, h), when ab reports one — for a real click.     pub rect: Option<[i64; 4]>,+    /// Screen centre — ab's own `rect.centerX/centerY` when it gives them+    /// (MEASURED 2026-10-02: click there, in screen px).+    pub center: Option<(i64, i64)>,+}++impl Element {+    /// OFF-SCREEN (MEASURED 2026-10-02: the "Remember me" checkbox's native+    /// input sits at centerX -14493; the visible control is its Text label).+    pub fn offscreen(&self) -> bool {+        self.center.map(|(x, y)| x < -1000 || y < -1000).unwrap_or(false)+    } }  impl Element {@@ -208,6 +219,7 @@ pub fn parse_inventory(reply: &Value) -> Vec<Element> {                         invokable: m.get("invokable").and_then(|x| x.as_bool()),                         settable: m.get("settable").and_then(|x| x.as_bool()),                         rect: m.get("rect").and_then(rect_xywh),+                        center: m.get("rect").and_then(rect_center),                     });                     // THE PASSWORD FIELD (MEASURED 2026-10-02: ab's nodes carry no                     // `isPassword` key): an explicit password flag, OR an edit@@ -266,6 +278,18 @@ fn rect_xywh(r: &Value) -> Option<[i64; 4]> {     Some([x as i64, y as i64, w as i64, h as i64]) } +fn rect_center(r: &Value) -> Option<(i64, i64)> {+    let n = |k: &str| r.get(k).and_then(|v| v.as_f64());+    if let (Some(x), Some(y)) = (n("centerX"), n("centerY")) {+        return Some((x as i64, y as i64));+    }+    rect_xywh(r).map(|[x, y, w, h]| (x + w / 2, y + h / 2))+}++fn overlaps(a: [i64; 4], b: [i64; 4]) -> bool {+    a[0] < b[0] + b[2] && b[0] < a[0] + a[2] && a[1] < b[1] + b[3] && b[1] < a[1] + a[3]+}+ /// One read: the elements, ab's node count, and the window's centre if known. fn read_once(hwnd: i64) -> Result<(Vec<Element>, u64, Option<(i64, i64)>), String> {     let r = crate::caller::ad_callback("desktop_ui_tree", json!({"hwnd": hwnd, "maxDepth": 30, "maxNodes": 400}), AB_TIMEOUT);@@ -647,9 +671,20 @@ fn tell_user(what: &str) { }  /// SIGN IN, unattended. `who` is the caller's identity for the ledger.-pub fn sign_in(reason: &str, user_confirmed_new_password: bool, who: Value) -> Value {+/// Which login window, and on whose authority.+#[derive(Clone, Debug, Default)]+pub struct Opts {+    pub user_confirmed_new_password: bool,+    /// The user authorized signing in to ANY 3DEXPERIENCE ID login window on+    /// this machine (0.9.16) — not only one a launch of this bridge raised.+    pub user_authorized: bool,+    /// "visualize" | "solidworks" | an hwnd, when several are up.+    pub window: Option<String>,+}++pub fn sign_in(reason: &str, opts: Opts, who: Value) -> Value {     let started = std::time::Instant::now();-    let out = sign_in_inner(reason, user_confirmed_new_password);+    let out = sign_in_inner(reason, &opts);     let outcome = out.get("errorCode").and_then(|c| c.as_str()).unwrap_or(if out.get("signedIn") == Some(&json!(true)) { "signedIn" } else { "unknown" }).to_string();     crate::ledger::record(         "solidworks_sign_in",@@ -660,29 +695,136 @@ pub fn sign_in(reason: &str, user_confirmed_new_password: bool, who: Value) -> V             "elapsedSec": started.elapsed().as_secs(),             "consent": out.pointer("/vault/consent").cloned().unwrap_or(Value::Null),             "rememberTicked": out.get("rememberTicked").cloned().unwrap_or(Value::Null),+            "window": out.get("window").cloned().unwrap_or(Value::Null),+            "userAuthorized": opts.user_authorized,         }),     );     out } -fn sign_in_inner(reason: &str, user_confirmed_new_password: bool) -> Value {+/// A 3DEXPERIENCE ID login window, and the process that owns it.+#[derive(Clone, Debug)]+pub struct LoginWindow {+    pub hwnd: i64,+    pub title: String,+    pub owner_pid: u32,+    pub owner_image: String,+    /// Owned by a process of THIS bridge's launch chain.+    pub ours: bool,+}++impl LoginWindow {+    pub fn to_json(&self) -> Value {+        json!({"hwnd": self.hwnd, "title": self.title, "ownerPid": self.owner_pid,+               "ownerImage": self.owner_image, "ownedByThisBridgesLaunch": self.ours,+               "host": if self.owner_image.eq_ignore_ascii_case("SLDWORKSVisualize.exe") { "visualize" } else { "solidworks" }})+    }+}++const SHELLS: &[&str] = &["explorer.exe", "cmd.exe", "powershell.exe", "pwsh.exe"];++/// Every 3DEXPERIENCE ID login window: those of OUR launch chain, and — with+/// `all` — those of every Dassault process in the census (SOLIDWORKS, its+/// launcher, SOLIDWORKS Visualize…). `Err` = nothing could be read.+pub fn login_windows(os: &dyn crate::launchos::LaunchOs, all: bool) -> Result<Vec<LoginWindow>, String> {+    let ours = crate::launch::our_chain_pids();+    let census = os.processes();+    let mut candidates: Vec<(u32, String, bool)> = Vec::new();+    let image_of = |pid: u32| -> String {+        census.as_ref().ok().and_then(|c| c.iter().find(|p| p.pid == pid).map(|p| p.image.clone())).unwrap_or_default()+    };+    for p in &ours {+        candidates.push((*p, image_of(*p), true));+    }+    if all {+        match &census {+            Ok(c) => {+                for p in c.iter().filter(|p| !SHELLS.iter().any(|s| p.image.eq_ignore_ascii_case(s))) {+                    if !candidates.iter().any(|(q, _, _)| *q == p.pid) {+                        candidates.push((p.pid, p.image.clone(), false));+                    }+                }+            }+            Err(e) if candidates.is_empty() => return Err(format!("the process census could not be read: {e}")),+            Err(_) => {}+        }+    }+    let mut out: Vec<LoginWindow> = Vec::new();+    let mut read_any = false;+    for (pid, image, is_ours) in candidates {+        if let Ok(ws) = os.windows_of_pid(pid) {+            read_any = true;+            for w in ws.iter().filter(|w| w.visible && crate::launch::is_sign_in_window(&w.class_name, &w.title)) {+                if !out.iter().any(|o| o.hwnd == w.hwnd) {+                    out.push(LoginWindow { hwnd: w.hwnd, title: w.title.clone(), owner_pid: pid, owner_image: image.clone(), ours: is_ours });+                }+            }+        }+    }+    if !read_any && !out.is_empty() {+        return Ok(out);+    }+    if !read_any {+        return Err("no candidate process's windows could be read".into());+    }+    Ok(out)+}++/// Is that login window still up? `None` = cannot tell.+fn still_up(os: &dyn crate::launchos::LaunchOs, lw: &LoginWindow) -> Option<bool> {+    os.windows_of_pid(lw.owner_pid).ok().map(|ws| ws.iter().any(|w| w.hwnd == lw.hwnd && w.visible))+}++fn sign_in_inner(reason: &str, opts: &Opts) -> Value {     let os = crate::launchos::os();-    // ONLY OUR LAUNCH'S WINDOW: a launch this bridge started, its sign-in window-    // owned by a process of OUR chain.-    let Some(a) = crate::launch::attempt() else {-        return err("sign_in_not_waiting", "no launch by this bridge is recorded", "swx_start begins one; swx_sign_in answers only the sign-in window of a launch this bridge started.", json!({}));+    let user_confirmed_new_password = opts.user_confirmed_new_password;+    let attempt = crate::launch::attempt();+    // WHICH WINDOW. Without the user's authorization: only the sign-in window+    // of a launch THIS bridge started, owned by its own chain. With it+    // (0.9.16): any 3DEXPERIENCE ID login window on the machine — the+    // launcher's, SOLIDWORKS Visualize's (which asks separately, measured+    // 2026-10-02) — and `window` picks one when several are up.+    if !opts.user_authorized && attempt.is_none() {+        return err("sign_in_not_waiting", "no launch by this bridge is recorded", "swx_start begins one; swx_sign_in answers only the sign-in window of a launch this bridge started — or, with userAuthorized:true (the user's say-so), any 3DEXPERIENCE ID login window on this machine.", json!({}));+    }+    let found = match login_windows(os.as_ref(), opts.user_authorized) {+        Ok(f) => f,+        Err(e) => return err("sign_in_not_waiting", format!("the login windows could not be read: {e}"), "Nothing was typed anywhere.", json!({})),     };-    let hwnd = match crate::launch::sign_in_now(os.as_ref()) {-        Some(Some((h, _))) => h,-        Some(None) => {-            return err("sign_in_not_waiting", "no 3DEXPERIENCE sign-in window is open for this bridge's launch", "Nothing to do: swx_status says what the launch is waiting on.", json!({"launchStage": a.stage.name()}))+    let pool: Vec<LoginWindow> = if opts.user_authorized { found } else { found.into_iter().filter(|w| w.ours).collect() };+    let picked: Vec<LoginWindow> = match opts.window.as_deref().map(|w| w.trim().to_lowercase()) {+        None => pool.clone(),+        Some(w) if w == "visualize" => pool.iter().filter(|l| l.owner_image.eq_ignore_ascii_case("SLDWORKSVisualize.exe")).cloned().collect(),+        Some(w) if w == "solidworks" => pool.iter().filter(|l| !l.owner_image.eq_ignore_ascii_case("SLDWORKSVisualize.exe")).cloned().collect(),+        Some(w) => match w.parse::<i64>() {+            Ok(h) => pool.iter().filter(|l| l.hwnd == h).cloned().collect(),+            Err(_) => return err("sign_in_bad_window", format!("window {w:?} is not \"visualize\", \"solidworks\" or an hwnd"), "Pass window:\"visualize\", \"solidworks\" or a window handle.", json!({})),+        },+    };+    let listed = json!(pool.iter().map(|l| l.to_json()).collect::<Vec<_>>());+    let lw = match picked.as_slice() {+        [one] => one.clone(),+        [] => {+            return err(+                "sign_in_not_waiting",+                if opts.user_authorized { "no 3DEXPERIENCE ID login window matching the request is open on this machine" } else { "no 3DEXPERIENCE sign-in window is open for this bridge's launch" },+                "Nothing to do. swx_status says what a launch is waiting on; `loginWindows` lists what is open.",+                json!({"loginWindows": listed, "launchStage": attempt.as_ref().map(|a| a.stage.name())}),+            )         }-        None => {-            return err("sign_in_not_waiting", "the windows of this launch's processes could not be read, so no sign-in window can be attributed to it", "Nothing was typed anywhere.", json!({}))+        many => {+            return err(+                "sign_in_window_ambiguous",+                format!("{} 3DEXPERIENCE ID login windows are open", many.len()),+                "Pick one: window:\"visualize\", window:\"solidworks\" or its hwnd. Nothing was typed.",+                json!({"loginWindows": listed}),+            )         }     };+    let hwnd = lw.hwnd;+    let window_j = lw.to_json();     // NEVER RETRIED after a rejection (lockout risk).-    if a.sign_in_rejected || (rejected_before().is_some() && !user_confirmed_new_password) {+    if attempt.as_ref().map(|a| a.sign_in_rejected).unwrap_or(false) || (rejected_before().is_some() && !user_confirmed_new_password) {         return err(             "sign_in_rejected_before",             "a sign-in with the saved password was REJECTED before, and it is never retried (an account lockout is worse than a manual sign-in)",@@ -739,7 +881,7 @@ fn sign_in_inner(reason: &str, user_confirmed_new_password: bool) -> Value {     let mut pages: Vec<Value> = vec![json!({"page": 1, "inventory": inv_j, "wake": wake_1, "cookieBanner": banner})];     let mut remember_ticked = Value::Null;     // 3. PAGE ONE (and, on a one-page form, the whole sign-in).-    if let Err(fe) = fill_page(hwnd, read.window_rect, &form, &secret, &mut steps, &mut remember_ticked) {+    if let Err(fe) = fill_page(hwnd, &read.elements, &form, &secret, &mut steps, &mut remember_ticked) {         drop(secret);         return scrub(&fill_failure(fe, banner, &pages, &steps, &vault), &needles);     }@@ -752,9 +894,9 @@ fn sign_in_inner(reason: &str, user_confirmed_new_password: bool) -> Value {         let deadline = std::time::Instant::now() + Duration::from_secs(verify_secs());         let mut last: Vec<Element> = Vec::new();         let mut page2: Option<Form> = None;-        let mut page2_rect = read.window_rect;+        let mut page2_els: Vec<Element> = Vec::new();         loop {-            if let Some(None) = crate::launch::sign_in_now(os.as_ref()) {+            if still_up(os.as_ref(), &lw) == Some(false) {                 break; // the window closed after the user name alone             }             if let Ok(r2) = inventory(hwnd) {@@ -764,7 +906,7 @@ fn sign_in_inner(reason: &str, user_confirmed_new_password: bool) -> Value {                         pages.push(json!({"page": 2, "inventory": inventory_json(&r2.elements), "wake": r2.wake_json(),                                           "cookieBanner": cookie_banner(&r2.elements)}));                         crate::launch::note_sign_in_inventory(inventory_json(&r2.elements));-                        page2_rect = r2.window_rect.or(page2_rect);+                        page2_els = r2.elements.clone();                         page2 = Some(f2);                         break;                     }@@ -782,12 +924,12 @@ fn sign_in_inner(reason: &str, user_confirmed_new_password: bool) -> Value {             Some(f2) => {                 // The user name was typed on page one; page two takes the password.                 let f2 = Form { user: None, ..f2 };-                if let Err(fe) = fill_page(hwnd, page2_rect, &f2, &secret, &mut steps, &mut remember_ticked) {+                if let Err(fe) = fill_page(hwnd, &page2_els, &f2, &secret, &mut steps, &mut remember_ticked) {                     drop(secret);                     return scrub(&fill_failure(fe, banner, &pages, &steps, &vault), &needles);                 }             }-            None if crate::launch::sign_in_now(os.as_ref()) == Some(None) => {}+            None if still_up(os.as_ref(), &lw) == Some(false) => {}             None => {                 drop(secret);                 let last_j = inventory_json(&last);@@ -810,10 +952,11 @@ fn sign_in_inner(reason: &str, user_confirmed_new_password: bool) -> Value {     // 5. VERIFY BY EFFECT: the window goes away.     let deadline = std::time::Instant::now() + Duration::from_secs(verify_secs());     loop {-        match crate::launch::sign_in_now(os.as_ref()) {-            Some(None) => {+        match still_up(os.as_ref(), &lw) {+            Some(false) => {                 let out = crate::verbs::ok(json!({                     "signedIn": true,+                    "window": window_j,                     "verifiedBy": "the sign-in window closed after the submit",                     "twoPage": two_page,                     "cookieBanner": banner,@@ -853,24 +996,18 @@ const FILL_REASON: &str = "signing in to 3DEXPERIENCE for a SOLIDWORKS launch th /// it has that one), "Remember me" (if present and off), then press its submit /// as a background Invoke (MEASURED 2026-10-02: `desktop_ui_click {name: /// "Continue"}` works without focus). Never touches a cookie-consent button.-fn fill_page(hwnd: i64, origin: Option<[i64; 4]>, form: &Form, secret: &Secret, steps: &mut Vec<Value>, remember_ticked: &mut Value) -> Result<(), FillErr> {+fn fill_page(hwnd: i64, page: &[Element], form: &Form, secret: &Secret, steps: &mut Vec<Value>, remember_ticked: &mut Value) -> Result<(), FillErr> {     if let Some(u_el) = &form.user {         let Some(u) = &secret.user else {             return Err(FillErr::Failed { step: "user name".into(), why: "the saved login has no user name to type".into(), observed: None, may_be_banner: false });         };-        fill_field(hwnd, origin, u_el, u, true, steps)?;+        fill_field(hwnd, u_el, u, true, steps)?;     }     if let Some(p_el) = &form.password {-        fill_field(hwnd, origin, p_el, &secret.pass, false, steps)?;+        fill_field(hwnd, p_el, &secret.pass, false, steps)?;     }     if let Some(rm) = &form.remember {-        let on = rm.toggle_state.as_deref().map(|t| t.to_lowercase()).map(|t| t == "on" || t == "true" || t == "1" || t == "checked");-        if on == Some(true) {-            *remember_ticked = json!("alreadyOn");-        } else {-            let r = crate::caller::ad_callback("desktop_ui_click", merge(target(rm, "checkbox"), json!({"hwnd": hwnd, "reason": FILL_REASON})), AB_TIMEOUT);-            *remember_ticked = json!(ab_ok(&r));-        }+        *remember_ticked = tick_remember(hwnd, page, rm);     }     let r = crate::caller::ad_callback("desktop_ui_click", merge(target(&form.submit, "button"), json!({"hwnd": hwnd, "reason": FILL_REASON})), AB_TIMEOUT);     steps.push(json!({"step": format!("press \"{}\"", form.submit.name), "ok": ab_ok(&r), "via": "uiaInvoke"}));@@ -880,6 +1017,60 @@ fn fill_page(hwnd: i64, origin: Option<[i64; 4]>, form: &Form, secret: &Secret,     Ok(()) } +/// "REMEMBER ME". MEASURED 2026-10-02: the CheckBox "Remember me " reports+/// toggleState off and an OFF-SCREEN rect (centerX -14493) — the native input is+/// hidden; the visible control is its Text label "Remember me…". So: already+/// on → nothing; an on-screen, invokable checkbox → a background Invoke; an+/// off-screen one → a real click on its LABEL, only while the user is idle and+/// with the login window in front, then the toggle state read again. A tick that+/// cannot be made safely is skipped and said — it never fails the sign-in.+fn tick_remember(hwnd: i64, page: &[Element], rm: &Element) -> Value {+    let is_on = |t: Option<&str>| t.map(|t| t.to_lowercase()).map(|t| t == "on" || t == "true" || t == "1" || t == "checked") == Some(true);+    if is_on(rm.toggle_state.as_deref()) {+        return json!("alreadyOn");+    }+    if !rm.offscreen() && rm.invokable != Some(false) {+        let r = crate::caller::ad_callback("desktop_ui_click", merge(target(rm, "checkbox"), json!({"hwnd": hwnd, "reason": FILL_REASON})), AB_TIMEOUT);+        return json!({"ticked": ab_ok(&r), "via": "uiaInvoke"});+    }+    let stem = rm.name.trim().trim_end_matches(['.', '…']).to_lowercase();+    let Some(label) = page.iter().find(|e| {+        !e.is("checkbox") && !e.offscreen() && e.center.is_some() && {+            let n = e.name.trim().trim_end_matches(['.', '…']).to_lowercase();+            !stem.is_empty() && n.starts_with(&stem)+        }+    }) else {+        return json!({"ticked": false, "why": "the checkbox is off-screen and no visible label for it was found"});+    };+    if let Err(why) = user_idle() {+        return json!({"ticked": false, "why": format!("skipped: {why} (a real click is needed for the label)")});+    }+    let _ = crate::caller::ad_callback("desktop_raise_window", json!({"hwnd": hwnd, "reason": FILL_REASON}), AB_TIMEOUT);+    let settle = std::time::Instant::now() + Duration::from_millis(1500);+    while login_in_front(hwnd).is_err() && std::time::Instant::now() < settle {+        std::thread::sleep(Duration::from_millis(100));+    }+    if let Err(why) = login_in_front(hwnd) {+        return json!({"ticked": false, "why": format!("skipped: {why}")});+    }+    let (x, y) = label.center.unwrap_or_default();+    let r = crate::caller::ad_callback("desktop_click", json!({"x": x, "y": y, "hwnd": hwnd, "reason": FILL_REASON}), AB_TIMEOUT);+    // READ THE TOGGLE AGAIN: the label click is evidence only if the box says so.+    let read_toggle = || {+        inventory(hwnd)+            .ok()+            .and_then(|r| r.elements.into_iter().find(|e| e.is("checkbox") && e.name == rm.name))+            .and_then(|e| e.toggle_state)+    };+    let until = std::time::Instant::now() + Duration::from_millis(1500);+    let mut after = read_toggle();+    while !is_on(after.as_deref()) && std::time::Instant::now() < until {+        std::thread::sleep(Duration::from_millis(150));+        after = read_toggle();+    }+    json!({"ticked": ab_ok(&r) && is_on(after.as_deref()), "via": "labelClick", "label": label.name, "toggleStateAfter": after})+}+ pub(crate) enum FillErr {     /// The user is typing (or the idle time cannot be read): no keystroke sent.     NeedsIdle(String),@@ -941,6 +1132,51 @@ fn field_value(hwnd: i64, el: &Element) -> Option<String> {     walk(&unwrap_ab(&r), el) } +/// Where a field is NOW: its centre (ab's own), whether a cookie-banner+/// element overlaps it, and the page's render widget if the tree names one.+struct Spot {+    center: (i64, i64),+    covered: bool,+    render_widget: Option<i64>,+}++fn locate(hwnd: i64, el: &Element) -> Option<Spot> {+    let r = crate::caller::ad_callback("desktop_ui_tree", json!({"hwnd": hwnd, "maxDepth": 30, "maxNodes": 400}), AB_TIMEOUT);+    if !ab_ok(&r) {+        return None;+    }+    let els = parse_inventory(&r);+    let field = els.iter().find(|e| e.is("edit") && e.name == el.name && e.automation_id == el.automation_id)?;+    let center = field.center?;+    let covered = match field.rect {+        Some(fr) => els+            .iter()+            .filter(|e| {+                let n = e.name.trim().to_lowercase();+                COOKIE_CHOICES.contains(&n.as_str()) || n.contains("cookie")+            })+            .filter_map(|e| e.rect)+            .any(|br| overlaps(fr, br)),+        None => false,+    };+    fn widget(v: &Value) -> Option<i64> {+        match v {+            Value::Array(a) => a.iter().find_map(widget),+            Value::Object(m) => {+                let class = ["className", "class", "class_name"].iter().find_map(|k| m.get(*k).and_then(|c| c.as_str())).unwrap_or("");+                if class == "Chrome_RenderWidgetHostHWND" {+                    if let Some(h) = ["hwnd", "nativeWindowHandle", "handle"].iter().find_map(|k| m.get(*k).and_then(|h| h.as_i64())) {+                        return Some(h);+                    }+                }+                m.values().find_map(widget)+            }+            _ => None,+        }+    }+    Some(Spot { center, covered, render_widget: widget(&unwrap_ab(&r)) })+}+ /// FILL ONE FIELD. First the background UIA ValuePattern set; MEASURED /// 2026-10-02 the 3DEXPERIENCE ID page refuses it ("control found but has NO /// ValuePattern … try a foregrounded desktop_type") even on an edit the tree@@ -955,7 +1191,7 @@ fn field_value(hwnd: i64, el: &Element) -> Option<String> { ///   * the user-name field READ BACK and compared before anything continues; a ///     mismatch stops with what was observed. The password is never read back. /// Enter is never pressed: the submit is a background Invoke.-fn fill_field(hwnd: i64, origin: Option<[i64; 4]>, el: &Element, text: &[u8], read_back: bool, steps: &mut Vec<Value>) -> Result<(), FillErr> {+fn fill_field(hwnd: i64, el: &Element, text: &[u8], read_back: bool, steps: &mut Vec<Value>) -> Result<(), FillErr> {     let label = if read_back { "user name" } else { "password" };     let typed = String::from_utf8_lossy(text).to_string();     let r = crate::caller::ad_callback(@@ -975,27 +1211,33 @@ fn fill_field(hwnd: i64, origin: Option<[i64; 4]>, el: &Element, text: &[u8], re     }     // ── REAL KEYSTROKES ──────────────────────────────────────────────────────     user_idle().map_err(FillErr::NeedsIdle)?;-    // The click lands on the field through a screenshot of the window-    // (`desktop_click {space:"image", shotId, x, y}`).-    let (Some([ex, ey, ew, eh]), Some([wx, wy, _, _])) = (el.rect, origin) else {-        return Err(FillErr::Failed { step: format!("{label} (keystrokes)"), why: "the field's or the window's position is unknown, so the field cannot be clicked".into(), observed: None, may_be_banner: false });-    };-    let shot = crate::caller::ad_callback("desktop_screenshot_window", json!({"hwnd": hwnd, "reason": FILL_REASON}), AB_TIMEOUT);-    let shot_d = unwrap_ab(&shot);-    let Some(shot_id) = ["shotId", "shot_id", "id"].iter().find_map(|k| shot_d.get(*k).or_else(|| shot.get(*k)).and_then(|v| v.as_str()).map(String::from)) else {-        return Err(FillErr::Failed { step: format!("{label} (keystrokes)"), why: format!("the window screenshot gave no shotId: {}", ab_error(&shot)), observed: None, may_be_banner: false });+    // FRESH GEOMETRY (MEASURED 2026-10-02: moving the window re-laid the page+    // and the cookie banner then COVERED the email box; a click at the old+    // point hit the banner and the typing went nowhere). Re-read the tree and+    // click at the field's OWN centre, in screen px.+    let mut spot = locate(hwnd, el);+    if let Some(sp) = &spot {+        if sp.covered {+            // Make the window tall enough that the field clears the banner+            // (MEASURED: 1100x1450 worked). The resize arguments are UNMEASURED.+            let _ = crate::caller::ad_callback(+                "desktop_ui_window",+                json!({"action": "resize", "hwnd": hwnd, "width": 1100, "height": 1450, "reason": FILL_REASON}),+                AB_TIMEOUT,+            );+            std::thread::sleep(Duration::from_millis(wake_ms()));+            spot = locate(hwnd, el);+        }+    }+    let Some(sp) = spot else {+        return Err(FillErr::Failed { step: format!("{label} (keystrokes)"), why: "the field's position could not be read from the window's tree".into(), observed: None, may_be_banner: false });     };-    let (x, y) = (ex + ew / 2 - wx, ey + eh / 2 - wy);-    let r = crate::caller::ad_callback(-        "desktop_click",-        json!({"space": "image", "shotId": shot_id, "x": x, "y": y, "hwnd": hwnd, "reason": FILL_REASON}),-        AB_TIMEOUT,-    );-    if !ab_ok(&r) {-        return Err(FillErr::Failed { step: format!("click on the {label} field"), why: ab_error(&r), observed: None, may_be_banner: true });+    if sp.covered {+        return Err(FillErr::Failed { step: format!("{label} (keystrokes)"), why: "the cookie banner covers the field, even after making the window taller".into(), observed: None, may_be_banner: true });     }-    // THE CLICK TAKES FOCUS — asynchronously: give the window a moment to come-    // to the front before the first check.+    // FOCUS (MEASURED: a plain click does not take focus while the user's app is+    // active): raise the login window, then confirm IT is in front.+    let _ = crate::caller::ad_callback("desktop_raise_window", json!({"hwnd": hwnd, "reason": FILL_REASON}), AB_TIMEOUT);     let settle = std::time::Instant::now() + Duration::from_millis(1500);     while login_in_front(hwnd).is_err() && std::time::Instant::now() < settle {         std::thread::sleep(Duration::from_millis(100));@@ -1004,13 +1246,22 @@ fn fill_field(hwnd: i64, origin: Option<[i64; 4]>, el: &Element, text: &[u8], re         user_idle().map_err(FillErr::NeedsIdle)?;         login_in_front(hwnd).map_err(|e| FillErr::Failed { step: format!("{label}: {what}"), why: e, observed: None, may_be_banner: false })     };+    guard("before the click")?;+    let (x, y) = sp.center;+    let r = crate::caller::ad_callback("desktop_click", json!({"x": x, "y": y, "hwnd": hwnd, "reason": FILL_REASON}), AB_TIMEOUT);+    if !ab_ok(&r) {+        return Err(FillErr::Failed { step: format!("click on the {label} field"), why: ab_error(&r), observed: None, may_be_banner: true });+    }+    // Keystrokes go to the page's render widget when the tree names it+    // (MEASURED: desktop_type {text, hwnd:<Chrome_RenderWidgetHostHWND child>}).+    let target_hwnd = sp.render_widget.unwrap_or(hwnd);     guard("before select-all")?;-    let r = crate::caller::ad_callback("desktop_press_key", json!({"keys": ["ctrl+a"], "hwnd": hwnd, "reason": FILL_REASON}), AB_TIMEOUT);+    let r = crate::caller::ad_callback("desktop_press_key", json!({"keys": ["ctrl+a"], "hwnd": target_hwnd, "reason": FILL_REASON}), AB_TIMEOUT);     if !ab_ok(&r) {         return Err(FillErr::Failed { step: format!("{label}: select-all"), why: ab_error(&r), observed: None, may_be_banner: false });     }     guard("before typing")?;-    let r = crate::caller::ad_callback("desktop_type", json!({"text": typed, "hwnd": hwnd, "sensitive": true, "reason": FILL_REASON}), AB_TIMEOUT);+    let r = crate::caller::ad_callback("desktop_type", json!({"text": typed, "hwnd": target_hwnd, "sensitive": true, "reason": FILL_REASON}), AB_TIMEOUT);     if !ab_ok(&r) {         return Err(FillErr::Failed { step: format!("{label}: typing"), why: ab_error(&r), observed: None, may_be_banner: false });     }
src/verbs.rs+7−2
@@ -3007,7 +3007,7 @@ pub const CATALOG: &[(&str, &str, &str)] = &[     ("swx_close_all_safely", "exec", "save EVERY open document into a recovery folder, then close it — nothing closes before it is on disk; needs only a reason"),     ("swx_shutdown", "exec", "close_all_safely, then quit SOLIDWORKS; idempotent, and the quit is skipped unless the session really emptied"),     ("swx_recover_list", "read", "what previous sweeps preserved, and where — newest first"),-    ("swx_sign_in", "exec", "sign in to 3DEXPERIENCE for a launch this bridge started, from the user's local vault (the password never leaves the laptop or reaches a reply); fails closed and never retries a rejected password"),+    ("swx_sign_in", "exec", "sign in to 3DEXPERIENCE for a launch this bridge started (or, with userAuthorized:true, any 3DEXPERIENCE ID login window, e.g. Visualize's; window picks one), from the user's local vault (the password never leaves the laptop or reaches a reply); fails closed and never retries a rejected password"),     ("swx_user_presence", "read", "is the user working in this SOLIDWORKS? foreground + last input + documents the bridge did not open (the guard every quit and sweep passes)"),     ("swx_kill", "process", "force-kill SOLIDWORKS (reason + confirm; discards unsaved work)"),     ("swx_window_info", "read", "SOLIDWORKS' windows and its main hwnd (read-only, nothing raised)"),@@ -3054,7 +3054,12 @@ fn swx_sign_in(_ctx: &Ctx, args: &Value) -> Value {     let who = crate::caller::current()         .map(|c| json!({"aiThread": c.thread, "containerName": c.container}))         .unwrap_or(Value::Null);-    crate::signin::sign_in(reason, arg_bool(args, "userConfirmedNewPassword", false), who)+    let opts = crate::signin::Opts {+        user_confirmed_new_password: arg_bool(args, "userConfirmedNewPassword", false),+        user_authorized: arg_bool(args, "userAuthorized", false),+        window: args.get("window").and_then(|w| w.as_str().map(String::from).or_else(|| w.as_i64().map(|h| h.to_string()))),+    };+    crate::signin::sign_in(reason, opts, who) }  /// `swx_user_presence` — the presence measurement every quit and sweep is
tests/common/mod.rs+20−1
@@ -355,6 +355,10 @@ pub struct AbStub {     /// like a crash reporter" and "we clicked a button on it", so a stub that     /// answered every verb with a window list could not tell those apart.     replies: std::sync::Arc<Mutex<std::collections::BTreeMap<String, Value>>>,+    /// One-shot triggers: when `.0` arrives, `.1` answers `.2` from then on —+    /// swapped BEFORE `.0` is answered, so a test can model "the page changed as+    /// a result of this call" without racing the bridge.+    triggers: std::sync::Arc<Mutex<Vec<(String, String, Value)>>>,     _thread: std::thread::JoinHandle<()>, } @@ -370,6 +374,8 @@ impl AbStub {         let replies: std::sync::Arc<Mutex<std::collections::BTreeMap<String, Value>>> =             std::sync::Arc::new(Mutex::new(std::collections::BTreeMap::new()));         let reps = std::sync::Arc::clone(&replies);+        let triggers: std::sync::Arc<Mutex<Vec<(String, String, Value)>>> = std::sync::Arc::new(Mutex::new(Vec::new()));+        let trig = std::sync::Arc::clone(&triggers);         let thread = std::thread::spawn(move || {             for stream in listener.incoming() {                 let Ok(mut s) = stream else { break };@@ -407,6 +413,14 @@ impl AbStub {                         }                     }                 }+                {+                    let mut t = trig.lock().unwrap();+                    let fired: Vec<(String, String, Value)> = t.iter().filter(|(c, _, _)| *c == command).cloned().collect();+                    t.retain(|(c, _, _)| *c != command);+                    for (_, target, payload) in fired {+                        reps.lock().unwrap().insert(target, payload);+                    }+                }                 let payload = match reps.lock().unwrap().get(&command) {                     Some(custom) => custom.clone(),                     None => json!({@@ -426,7 +440,12 @@ impl AbStub {                 );             }         });-        AbStub { url: format!("http://127.0.0.1:{port}"), calls, windows, replies, _thread: thread }+        AbStub { url: format!("http://127.0.0.1:{port}"), calls, windows, replies, triggers, _thread: thread }+    }++    /// When `trigger` arrives (once), `target` answers `payload` from then on.+    pub fn on_call_set(&self, trigger: &str, target: &str, payload: Value) {+        self.triggers.lock().unwrap().push((trigger.to_string(), target.to_string(), payload));     }      /// Answer one ab verb with a specific payload.
tests/signin.rs+161−14
@@ -175,7 +175,7 @@ fn the_bridge_signs_in_from_the_vault_and_the_password_appears_nowhere() {     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"] == true+        r["success"] == true && r["signedIn"] == true && r["rememberTicked"]["ticked"] == true             && r["vault"]["consent"] == "perUse" && !m.sign_in_up(),         s(&r)     );@@ -580,7 +580,7 @@ fn page_one() -> Value {            "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}},+                   {"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},@@ -596,18 +596,37 @@ fn page_one() -> Value {            ]}}) } -/// Page TWO — UNMEASURED (nobody has seen it): a password edit and a submit.-fn page_two() -> Value {-    json!({"success": true, "status": "ok", "hwnd": 777001, "nodeCount": 30, "truncated": 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": "Password", "automationId": "password", "invokable": false, "settable": true, "value": "", "rect": {"x": 384, "y": 450, "w": 400, "h": 40}},-               {"role": "CheckBox", "name": "Remember me", "automationId": "", "invokable": true, "settable": false, "toggleState": "Off"},+               {"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.@@ -694,7 +713,7 @@ fn the_measured_two_page_lazy_login_is_one_flow_with_one_consent() {         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,+            && sets[1]["args"]["name"] == "Password " && sets[1]["args"]["text"] == PW,         format!("{sets:?}")     );     let pages = r["pages"].as_array().cloned().unwrap_or_default();@@ -707,7 +726,7 @@ fn the_measured_two_page_lazy_login_is_one_flow_with_one_consent() {         "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),+            && 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);@@ -780,7 +799,7 @@ fn with_username_value(v: &str) -> Value { 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_screenshot_window", json!({"success": true, "data": {"shotId": "shot-1"}}));+    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}));@@ -796,7 +815,7 @@ fn drive_keystrokes(m: &Machine, ab: &common::AbStub, typed_lands: Option<String     while Instant::now() < deadline {         let clicks = ab.calls_to("desktop_ui_click");         let clicked = |n: &str| clicks.iter().any(|c| c["args"]["name"] == n);-        if focus_follows && !ab.calls_to("desktop_click").is_empty() {+        if focus_follows && !ab.calls_to("desktop_raise_window").is_empty() {             std::fs::write(m.dir.join("foreground.json"),                 json!({"hwnd": 777001, "pid": 1, "title": "Login - 3DEXPERIENCE ID", "className": "x"}).to_string()).unwrap();         }@@ -811,7 +830,12 @@ fn drive_keystrokes(m: &Machine, ab: &common::AbStub, typed_lands: Option<String                 ab.set_response("desktop_ui_tree", page_two());                 state = 2;             }-            2 if clicked("Log in") => {+            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;             }@@ -828,11 +852,29 @@ fn the_page_that_refuses_a_background_set_is_typed_safely_and_read_back() {     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);     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,@@ -846,8 +888,10 @@ fn the_page_that_refuses_a_background_set_is_typed_safely_and_read_back() {         "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())-            && ab.calls_to("desktop_click").iter().all(|c| c["args"]["space"] == "image" && c["args"]["shotId"] == "shot-1"-                && c["args"]["x"] == 484 && c["args"]["y"] == 420),+            // 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);@@ -935,3 +979,106 @@ fn a_missing_login_names_every_way_to_grow_the_vault() {         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_ui_window").iter().any(|c| c["args"]["action"] == "resize" && c["args"]["height"] == 1450)+            && ab.calls_to("desktop_click").is_empty() && ab.calls_to("desktop_type").is_empty(),+        s(&r)+    );+}
uninstall.sh