← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.64\ No newline at end of file+1.13.65\ No newline at end of file
CHANGELOG.md+12
@@ -3,6 +3,18 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge · GitHub backup: `adom-inc/fusion-bridge` +## 1.13.65 (2026-11-03)++From AI Flow's run on a board an older bridge converted:++`fusion_set_layer_count` at the same count now also fixes vias Fusion marks `extent: invalid`, drawn while the stack had no through via. They stayed invalid after the stack was repaired, and only RIPUP by net name reaches them, so it lands those nets again (`netsLandedAgain`), reads the copper back and undoes it if it differs. `fusion_routing_state` names such nets (`invalidViaNets`).++The notice "The board file has errors and will be opened with warnings" has its own category, `board_opened_with_warnings`. It was read as an unsaved-changes prompt.++`fusion_dismiss_blocking_dialogs`: `closed` now means the window is gone from the screen, and the verb reports `success: false` and `stillUp` if one isn't. It used to report a window closed when the close was only accepted, and that notice ignores WM_CLOSE: it now clicks OK.++Dialog resolutions and hints no longer name the retired `fusion_send_key`.+ ## 1.13.64 (2026-11-03)  `fusion_schematic_run` with a long list (#137): the call waits about 100 s, then answers `stillRunning` with a `runId`, and `fusion_schematic_run {runId}` polls it. Adom Bridge gives a call to another machine 120 s, so a long list used to read as `timed_out` with Fusion still working. A list that finishes in time still answers as one call.
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.64",+  "version": "1.13.65",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.64"+version = "1.13.65" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.64"+version = "1.13.65" edition = "2021"  [dependencies]
handlers/dialog_classify.py+2−2
@@ -81,11 +81,11 @@ _RULES = [     (("save changes", "save document", "do you want to save", "unsaved changes", "save"),      "save_changes",      "Unsaved-changes prompt. Use fusion_close_document (closes without the save modal), "-     "or fusion_send_key {\"key\":\"tab\"} then {\"key\":\"enter\"}."),+     "or fusion_dismiss_blocking_dialogs, which answers No."),      (("what do you want to design", "what to design"),      "what_to_design",-     "Fusion's start picker. fusion_send_key {\"key\":\"escape\"} to dismiss."),+     "Fusion's start picker. fusion_dismiss_blocking_dialogs closes it."),      # ── Launch / licensing / setup dialogs (owned by AdskIdentityManager /     #    FusionLauncher, so classify_launch_dialogs enumerates ALL top-level
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.13.64"+DASH_VERSION="1.13.65" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.64",+  "version": "1.13.65",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+2−2
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.13.64",+  "version": "1.13.65",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -152,4 +152,4 @@   "confirmed_user_skills": [     "fusion-design-efficiency"   ]-}\ No newline at end of file+}
rust/Cargo.lock+5−5
@@ -19,7 +19,7 @@ dependencies = [  [[package]] name = "adom-fusion-bridge"-version = "1.13.64"+version = "1.13.65" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.64"+version = "1.13.65" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.64"+version = "1.13.65" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.64"+version = "1.13.65" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.64"+version = "1.13.65" dependencies = [  "fusion-core",  "windows-sys 0.59.0",
rust/Cargo.toml+1−1
@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"]  # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.64"+version = "1.13.65" edition = "2021" license = "MIT" 
rust/bridge/src/addin.rs+2−2
@@ -193,7 +193,7 @@ fn busy_gate(cmd: &str) -> Option<Value> {         return Some(json!({"success": false,             "error": format!("Fusion main thread busy — {bc} has been running for {elapsed}s. Your command '{cmd}' cannot execute until it finishes."),             "errorCode": "main_thread_busy", "busyCommand": bc, "elapsedSeconds": elapsed, "progress": progress,-            "_hint": format!("A cloud search ({bc}) is in progress{}, running for {elapsed}s. Do NOT retry add-in commands (get_app_state, document_info, etc.) — they will all be rejected until the search finishes. Commands that still work right now: fusion_window_info, fusion_screenshot_fusion, fusion_click_fusion, fusion_send_key, fusion_close_window. Wait for the search to complete, then retry.", pct.map(|p| format!(" (~{p}% done)")).unwrap_or_default())}));+            "_hint": format!("A cloud search ({bc}) is in progress{}, running for {elapsed}s. Do NOT retry add-in commands (get_app_state, document_info, etc.) — they will all be rejected until the search finishes. Commands that still work right now: fusion_window_info, fusion_screenshot_fusion, fusion_close_window. Wait for the search to complete, then retry.", pct.map(|p| format!(" (~{p}% done)")).unwrap_or_default())}));     }     if let Some(mut st) = addin_status_probe(Duration::from_secs(3)) {         let deadline = Instant::now() + SHORT_READ_WAIT;@@ -216,7 +216,7 @@ fn busy_gate(cmd: &str) -> Option<Value> {             let mut resp = json!({"success": false,                 "error": format!("Fusion main thread busy — {bc} running for {elapsed}s (from another bridge/session). Your command '{cmd}' cannot execute until it finishes."),                 "errorCode": "main_thread_busy", "busyCommand": bc, "elapsedSeconds": elapsed,-                "_hint": format!("A long-running command ({bc}) is in progress from another session. Do NOT retry add-in commands — they will all be rejected until it finishes. Do NOT press Escape — the add-in is working, not stuck on a dialog. Commands that still work: fusion_window_info, fusion_screenshot_fusion, fusion_click_fusion, fusion_send_key, fusion_close_window.")});+                "_hint": format!("A long-running command ({bc}) is in progress from another session. Do NOT retry add-in commands — they will all be rejected until it finishes. Do NOT press Escape — the add-in is working, not stuck on a dialog. Commands that still work: fusion_window_info, fusion_screenshot_fusion, fusion_close_window.")});             if let Some(w) = st.get("walkProgress").filter(|w| !w.is_null()) { resp["progress"] = w.clone(); }             return Some(resp);         }
rust/bridge/src/etiquette.rs+1−1
@@ -339,7 +339,7 @@ pub fn post(state_dir: &Path, caller: &Caller, cmd: &str, args: &Value, mut res:                 match cat {                     "upload_in_progress" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "No"}), caller, 12); if res.is_object() { res["uploadCloseDeclined"] = json!(true); } eprintln!("[fusion-bridge] answered the Hub upload-close confirm with No (work-preserving; close retried after jobs drain)"); }                     "create_confirm" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "No"}), caller, 12); if res.is_object() { res["createPromptDeclined"] = json!(true); } eprintln!("[fusion-bridge] declined a standing Create-new prompt at the chokepoint (raised by a raw EDIT; Yes would fabricate an empty artifact)"); }-                    "launch_error" | "lbr_open_error" | "fusion_unexpected_condition" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12); eprintln!("[fusion-bridge] dismissed a standing {cat} dialog at the chokepoint"); }+                    "launch_error" | "lbr_open_error" | "fusion_unexpected_condition" | "board_opened_with_warnings" => { let _ = ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK"}), caller, 12); eprintln!("[fusion-bridge] dismissed a standing {cat} dialog at the chokepoint"); }                     _ => {}                 }             }
rust/bridge/src/lifecycle.rs+3−3
@@ -487,7 +487,7 @@ pub fn post_open_screenshot(ctx: &Ctx, settle_secs: f64) -> (Value, bool) {         }     }     let n = shots["screenshots"].as_array().map(|a| a.len()).unwrap_or(0);-    shots["message"] = json!(format!("Auto-captured {n} Fusion window(s) after open. READ EACH screenshot file to check for blocking dialogs ('What to design?', 'PCB out of date', 'Save changes?'). PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file. Dismiss with fusion_send_key {{\"key\": \"escape\"}} if needed."));+    shots["message"] = json!(format!("Auto-captured {n} Fusion window(s) after open. READ EACH screenshot file to check for blocking dialogs ('What to design?', 'PCB out of date', 'Save changes?'). PCB/ELECTRONICS NAV RULE: a board is NOT a standalone file - the schematic, 2D board and 3D board are VIEWS inside ONE electronics DESIGN. Open the DESIGN document first (fusion_open_cloud_file or fusion_open_by_urn), THEN switch to its schematic and its 2D board (fusion_show_2d_board) and 3D board (fusion_show_3d_board). A Select-Electronics-Design-File picker IS that design list of its schematic + board. An EMPTY 2D board usually means you opened a derivative or the wrong file, or it is out of sync - reopen the parent electronics DESIGN and switch to its board view; do NOT open a .brd or .sch as a standalone file. Dismiss with fusion_dismiss_blocking_dialogs if needed."));     if blocked { shots["dialogBlocking"] = json!(true); shots["message"] = json!(format!("WARNING: A modal dialog is blocking Fusion. {}", shots["message"].as_str().unwrap_or(""))); }     (shots, blocked) }@@ -515,7 +515,7 @@ pub fn diagnose_addin_timeout(ctx: &Ctx, command: &str, original: &Value) -> Val             if main_thread == "blocked" || pending > 0 {                 if let Some(st) = crate::addin::addin_status_probe(Duration::from_secs(3)).filter(|s| s["busy"].as_bool() == Some(true)) {                     let mut resp = json!({"success": false, "error": format!("Fusion main thread busy — {} running for {}s.", st["busyCommand"].as_str().unwrap_or("unknown"), st["elapsedSeconds"]), "errorCode": "main_thread_busy", "busyCommand": st["busyCommand"], "elapsedSeconds": st["elapsedSeconds"],-                        "_hint": "Add-in is busy with a long-running command. Do NOT retry add-in commands. Do NOT press Escape — the add-in is working, not stuck on a dialog. Commands that still work: fusion_window_info, fusion_screenshot_fusion, fusion_click_fusion, fusion_send_key, fusion_close_window."});+                        "_hint": "Add-in is busy with a long-running command. Do NOT retry add-in commands. Do NOT press Escape — the add-in is working, not stuck on a dialog. Commands that still work: fusion_window_info, fusion_screenshot_fusion, fusion_close_window."});                     if !st["walkProgress"].is_null() { resp["progress"] = st["walkProgress"].clone(); }                     return resp;                 }@@ -539,7 +539,7 @@ pub fn diagnose_addin_timeout(ctx: &Ctx, command: &str, original: &Value) -> Val                     let mut seen = Vec::new(); for d in &blocking { let r = d["resolution"].as_str().unwrap_or("").to_string(); if !seen.contains(&r) { seen.push(r); } }                     format!("The AdomBridge add-in is alive but its main thread is blocked by a modal dialog: {titles}.{auto_note} {}", seen.join(" "))                 } else {-                    format!("The AdomBridge add-in is alive but its main thread is not responding (status: {main_thread}, pending: {pending}). Fusion 360 likely has a modal dialog open (Document Recovery, error, or update prompt) that is blocking execution. READ the screenshots to identify the dialog, then dismiss with fusion_send_key {{\"key\": \"escape\"}} or fusion_send_key {{\"key\": \"tab\"}} + {{\"key\": \"enter\"}}.")+                    format!("The AdomBridge add-in is alive but its main thread is not responding (status: {main_thread}, pending: {pending}). Fusion 360 likely has a modal dialog open (Document Recovery, error, or update prompt) that is blocking execution. READ the screenshots to identify the dialog, then fusion_dismiss_blocking_dialogs answers it in the background.")                 };                 return json!({"success": false, "error": "addin_main_thread_blocked", "message": message, "blockingDialogs": blocking, "data": {"command": command, "health": h, "blockingDialogs": blocking, "postOpenScreenshot": shots}});             }
rust/bridge/src/routing_script.py+58−4
@@ -207,7 +207,8 @@ def state(board, meta):     # objects() and board_model() walk the signals in the same order, so vias pair by index     via_rows=[{**v,**w} for v,w in zip(vias,model['vias'])] if len(vias)==len(model['vias']) else model['vias']     airwires=sum(n['airwireCount'] for n in rows)-    return {**meta,**model,'pads':pad_rows,'vias':via_rows,'netRows':rows,'segments':wires,'elements':elements,+    invalid_vias=_invalid_via_nets(board)+    return {**meta,**model,**({'invalidViaNets':invalid_vias,'_hintInvalidVias':'These nets have vias Fusion marks invalid, drawn when the board\'s stack had no through via. Call fusion_set_layer_count at the same count: it repairs the stack and lands them again.'} if invalid_vias else {}),'pads':pad_rows,'vias':via_rows,'netRows':rows,'segments':wires,'elements':elements,             'segmentCount':len(wires),'viaCount':len(vias),'airwireCount':airwires,'fusionAirwires':airwires,             'remainingNets':[n['name'] for n in rows if n['airwireCount']],             'connectivitySource':'unconnected and splitNets are computed from the copper (pads, tracks, arcs, vias and Fusion\'s poured fill joined where they touch); fusionAirwires is Fusion\'s own ratsnest from the export',@@ -3118,6 +3119,46 @@ def stack_has_broken_viadef(board):     return False  +def _invalid_via_nets(board):+    """Nets with a via drawn while the stack had no through via: Fusion writes its extent as 'invalid', and+    it stays that way after the stack is repaired."""+    return sorted(s_.get('name') for s_ in board.findall('signals/signal') if any(not re.fullmatch(r'[0-9]+-[0-9]+', v.get('extent') or '1-16') for v in s_.findall('via')))+++def _reland_invalid_via_nets(app, args, board):+    """Land those nets again, as nothing but RIPUP by name reaches such a via (live: CHANGE VIA, CHANGE EXTENT,+    MOVE, DELETE and RIPUP at a point all leave it). Same tracks and vias, each via now through. Read back; a+    net that doesn't come back as it was is undone."""+    nets = _invalid_via_nets(board)+    _, _, w0, v0 = objects(board)+    before = copper_signature(w0 + v0)+    want = copper_signature(w0 + [dict(v, extent='1-16') if v['net'] in nets else v for v in v0])+    cmds = []+    for n_ in nets:+        s_ = board.find("signals/signal[@name='%s']" % identifier(n_)); n = identifier(n_)+        cmds.append("RIPUP '%s';" % n)+        for wv in s_.findall('wire'):+            if wv.get('layer') == '19':+                continue+            c = _f(wv, 'curve')+            cmds.append("SET WIRE_BEND 2; LAYER %s; WIRE '%s' %.6fmm (%.6fmm %.6fmm) %s(%.6fmm %.6fmm);" % (wv.get('layer'), n, _f(wv, 'width'), _f(wv, 'x1'), _f(wv, 'y1'), ('%+g ' % c) if c else '', _f(wv, 'x2'), _f(wv, 'y2')))+        for v in s_.findall('via'):+            shape = {'square': 'SQUARE', 'octagon': 'OCTAGON'}.get(v.get('shape', 'round'), 'ROUND')+            dia = _f(v, 'diameter') or 0+            cmds.append("CHANGE DRILL %.6fmm; VIA '%s' %s%s 1-16 (%.6fmm %.6fmm);" % (_f(v, 'drill'), n, ('%.6fmm ' % dia) if dia else '', shape, _f(v, 'x'), _f(v, 'y')))+    for c in cmds:+        app.executeTextCommand('Electron.run ' + c)+    # RIPUP by name also hides the net's pour fills, and RATSNEST will not refill a hidden pour+    app.executeTextCommand('Commands.Start Electron::PolyShowFillAll')+    app.executeTextCommand('Electron.run RATSNEST;')+    _, after, meta2 = snapshot(app, args)+    _, _, w1, v1 = objects(after)+    if copper_signature(w1 + v1) != want:+        undone, _, _ = undo_route(app, args, cmds, before)+        fail('readback_mismatch', 'Landing %s again did not bring the same copper back; %s' % (', '.join(nets), 'it was undone' if undone else 'the undo did not restore the copper exactly: read fusion_routing_state'))+    return nets, after, meta2++ def set_layer_count(app, root, board, meta, args):     _guard(meta, args)     try:@@ -3132,11 +3173,13 @@ def set_layer_count(app, root, board, meta, args):     stack = layer_stack(board)     plan = {'from': [L.name(n) for n in now], 'to': ['F.Cu'] + ['In%d.Cu' % i for i in range(1, count - 1)] + ['B.Cu'], 'layerSetup': LAYER_SETUP[count],             'thicknessMm': round((stack.get('totalUm') or 1600) / 1000.0, 4)}-    repair = now == want and stack_has_broken_viadef(board)-    if now == want and not repair:+    broken, bad_nets = now == want and stack_has_broken_viadef(board), _invalid_via_nets(board)+    if now == want and not broken and not bad_nets:         return {'success': True, 'mutated': False, **meta, **plan, 'note': 'the board already has %d copper layers' % count}-    if repair:+    if broken:         plan['repairedViaDefinition'] = True+    if bad_nets:+        plan['netsLandedAgain'] = bad_nets     gone = [n for n in now if n not in want]     used = layers_used(board, gone)     if used:@@ -3151,8 +3194,13 @@ def set_layer_count(app, root, board, meta, args):     plan['stackup'] = [{'type': d.get('type'), 'name': d.get('name'), 'thickness': d.find('material').get('thickness')} for d in new_stack.findall('layerdef')]     if args.get('dryRun'):         return {'success': True, 'mutated': False, **meta, **plan}+    after, meta2 = board, meta+    got = now+    st2 = stack     with tempfile.TemporaryDirectory(prefix='adom-layers-') as d:         orig, new = Path(d) / 'before.edru', Path(d) / 'after.edru'+        if now == want and not broken:+            return _layer_count_reply(app, args, after, meta2, plan, got, st2, bad_nets)         app.executeTextCommand("Electron.run DRC SAVE '%s';" % orig.as_posix())         if not orig.exists():             fail('rules_unreadable', 'Fusion did not save the board\'s design rules, so the layer count was not changed')@@ -3174,6 +3222,12 @@ def set_layer_count(app, root, board, meta, args):         if got != want or not same_parts or stack_has_broken_viadef(after):             app.executeTextCommand("Electron.run SET CONFIRM NO; DRC LOAD '%s'; SET CONFIRM OFF;" % orig.as_posix())             fail('readback_mismatch', 'Fusion read back copper layers %s (asked %s); the board\'s own rules were loaded back' % ([Layers(after).name(n) for n in got], plan['to']))+    return _layer_count_reply(app, args, after, meta2, plan, got, st2, bad_nets)+++def _layer_count_reply(app, args, after, meta2, plan, got, st2, bad_nets):+    if bad_nets:+        _, after, meta2 = _reland_invalid_via_nets(app, args, after)     return {'success': True, 'mutated': True, **meta2, **plan, 'layers': [Layers(after).name(n) for n in got], 'stackupRead': st2,             '_hint': 'New inner layers are signal layers with generic prepreg and core (Dk 4.5); the board thickness is kept. Copper stays on its Fusion layer, so the standard names move with the count: Route15 is In2.Cu on 4 layers and In4.Cu on 6. Re-check an impedance-controlled stack with fusion_layer_stack. Net classes and other design rules are unchanged.'} 
rust/bridge/src/small_verbs.rs+23−3
@@ -521,19 +521,39 @@ pub fn dismiss_blocking_dialogs(state_dir: &Path, args: &Value, caller: &fusion_         }         let mut safe = "No/Cancel (WM_CLOSE, background)";         if cat == "upload_in_progress" { upload_active = true; safe = "No - declined close, Hub upload left to drain (NEVER Yes)"; }-        let ok = d["hwnd"].as_i64().map(|h| ab.call("desktop_close_window", json!({"hwnd": h}), caller, 15).map(|r| r["ok"].as_bool().or(r["success"].as_bool()).unwrap_or(true)).unwrap_or(false)).unwrap_or(false);+        // closed means it is gone from the screen, not that the close was accepted: this notice ignores+        // WM_CLOSE, and the verb used to report it closed while it was still up+        let mut click_error = Value::Null;+        if cat == "board_opened_with_warnings" {+            safe = "OK (the only answer: it's a notice)";+            if let Some(h) = d["hwnd"].as_i64() {+                match ab.call("desktop_ui_click", json!({"hwnd": h, "name": "OK", "role": "Button"}), caller, 15) {+                    Ok(v) if v["success"].as_bool() == Some(false) => { click_error = json!(v["errorCode"].as_str().or(v["error"].as_str()).unwrap_or("refused")); }+                    Err(e) => { click_error = json!(format!("{e}")); }+                    _ => {}+                }+            }+        }+        let gone_within = |h: i64, secs: u64| -> bool { for _ in 0..(secs * 2) { std::thread::sleep(std::time::Duration::from_millis(500)); if !window_present(&ab, caller, h) { return true; } } false };+        let ok = d["hwnd"].as_i64().map(|h| {+            if cat == "board_opened_with_warnings" && gone_within(h, 3) { return true; }+            let _ = ab.call("desktop_close_window", json!({"hwnd": h}), caller, 15);+            gone_within(h, 3)+        }).unwrap_or(false);         // what was closed is evidence (2026-09-03: a post-drill dismiss closed an 'unknown' window         // and the add-in went silent; nothing had recorded which window it was)         eprintln!("[fusion-bridge] dismiss_blocking_dialogs: {} {:?} category={cat} closed={ok}", d["hwnd"], d["title"].as_str().unwrap_or(""));         crate::fusion_prefs::action_ledger(state_dir, "dialog_dismissed", json!({"title": d["title"], "category": cat, "hwnd": d["hwnd"], "closed": ok, "how": "WM_CLOSE"}));-        answered.push(json!({"title": d["title"], "category": cat, "answeredWith": safe, "closed": ok, "resolution": d["resolution"]}));+        answered.push(json!({"title": d["title"], "category": cat, "answeredWith": safe, "closed": ok, "clickError": click_error, "resolution": d["resolution"]}));     }     let mut hint = format!("Answered {} blocking dialog(s) in the background (No/Cancel - never Yes, never focus-stolen).", answered.len());     if upload_active { hint.push_str(" One was a Hub-UPLOAD 'lose these changes?' prompt: it was declined (No), but the upload is STILL in flight - poll app.hasActiveJobs (fusion_state) until it drains before any close/saveAs, or you WILL lose the 3D package."); }     taskbar(if upload_active { Some(1.0) } else { None });     if !left_open.is_empty() { hint.push_str(&format!(" LEFT OPEN {} window(s) no rule knows that do not look like a dialog (see leftOpen[].why): WM_CLOSE on a main, document or EAGLE editor window closes documents and can exit Fusion. fusion_check_dialogs shows their text; decide from that.", left_open.len())); }     answered.extend(native_answered);-    json!({"success": true, "count": answered.len(), "dismissed": answered, "leftOpen": left_open, "uploadStillActive": upload_active, "_hint": hint})+    let still_up: Vec<Value> = answered.iter().filter(|d| d["closed"] != true).map(|d| d["title"].clone()).collect();+    if !still_up.is_empty() { hint.push_str(&format!(" STILL UP: {} did not close (closed:false above), so Fusion is still blocked.", still_up.len())); }+    json!({"success": still_up.is_empty(), "count": answered.len(), "dismissed": answered, "stillUp": still_up, "leftOpen": left_open, "uploadStillActive": upload_active, "_hint": hint}) }  fn screenshot_hwnd(ab: &dyn fusion_core::ab::AbClient, caller: &fusion_core::ab::Caller, hwnd: i64) -> Option<String> {
rust/core/src/dialog_rules.json+11−3
@@ -16,6 +16,14 @@    "category": "fusion_unexpected_condition",    "resolution": "Fusion Electronics asserted inside its own router (seen when a LINE completes a net's last airwire on some boxes, pusher.cpp inLocator). OK is the only button and is informational; the copper already landed. Click OK, then CHECKPOINT the board (fusion_export_eagle_source) before doing anything else, and verify the copper with fusion_routing_state; do not assume the edit was lost or applied."   },+  {+   "needles": [+    "board file has errors",+    "opened with warnings"+   ],+   "category": "board_opened_with_warnings",+   "resolution": "Fusion opened a board file that has errors, and OK is the only answer: it's a notice, not a question, and nothing is saved or lost. fusion_dismiss_blocking_dialogs clicks OK. Read the board before trusting it. A board converted by a bridge before 1.13.61 can have a stack with no through via: fusion_routing_state lists any invalidViaNets, and fusion_set_layer_count at the same count repairs them."+  },   {    "needles": [     "has errors and cannot be opened",@@ -60,7 +68,7 @@     "linked to multiple"    ],    "category": "linked_design_picker",-   "resolution": "This file links to multiple Electronics designs. The chooser is a CEF/web modal whose list is NOT keyboard- or Win32-navigable (SendInput reaches only the native Cancel/OK), so it can't be resolved headlessly today \u2014 select the design on the desktop. (Headless fix tracked: have the add-in open the specific linked design via the Fusion API, avoiding the picker entirely.)"+   "resolution": "This file links to multiple Electronics designs. The chooser is a CEF/web modal whose list is NOT keyboard- or Win32-navigable (SendInput reaches only the native Cancel/OK), so it can't be resolved headlessly today — select the design on the desktop. (Headless fix tracked: have the add-in open the specific linked design via the Fusion API, avoiding the picker entirely.)"   },   {    "needles": [@@ -80,7 +88,7 @@     "save"    ],    "category": "save_changes",-   "resolution": "Unsaved-changes prompt. Use fusion_close_document (closes without the save modal), or fusion_send_key {\"key\":\"tab\"} then {\"key\":\"enter\"}."+   "resolution": "Unsaved-changes prompt. Use fusion_close_document (closes without the save modal), or fusion_dismiss_blocking_dialogs, which answers No."   },   {    "needles": [@@ -88,7 +96,7 @@     "what to design"    ],    "category": "what_to_design",-   "resolution": "Fusion's start picker. fusion_send_key {\"key\":\"escape\"} to dismiss."+   "resolution": "Fusion's start picker. fusion_dismiss_blocking_dialogs closes it."   },   {    "needles": [
rust/core/tests/golden/dialog_titles.json+183−176
@@ -1,178 +1,185 @@ [- {-  "title": "Fusion",-  "result": [-   "generic_modal_read_screenshot",-   "Generic Fusion modal - its body is not Win32-readable, so a screenshot is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. If it mentions uploading / 'lose these changes' / 'sure you want to close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). If it is the 'Select Electronics Design File' chooser during an open, its list is not keyboard/Win32-navigable; open the specific design by URN or select on the desktop."-  ]- },- {-  "title": "fusion360",-  "result": [-   "generic_modal_read_screenshot",-   "Generic Fusion modal - its body is not Win32-readable, so a screenshot is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. If it mentions uploading / 'lose these changes' / 'sure you want to close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). If it is the 'Select Electronics Design File' chooser during an open, its list is not keyboard/Win32-navigable; open the specific design by URN or select on the desktop."-  ]- },- {-  "title": "Autodesk Fusion 360",-  "result": [-   "generic_modal_read_screenshot",-   "Generic Fusion modal - its body is not Win32-readable, so a screenshot is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. If it mentions uploading / 'lose these changes' / 'sure you want to close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). If it is the 'Select Electronics Design File' chooser during an open, its list is not keyboard/Win32-navigable; open the specific design by URN or select on the desktop."-  ]- },- {-  "title": "Save changes to Demo_Board?",-  "result": [-   "save_changes",-   "Unsaved-changes prompt. Use fusion_close_document (closes without the save modal), or fusion_send_key {\"key\":\"tab\"} then {\"key\":\"enter\"}."-  ]- },- {-  "title": "Document Recovery",-  "result": [-   "recovery",-   "Document Recovery prompt. Call fusion_dismiss_recovery, or fusion_relocate_recovery BEFORE fusion_start to prevent it."-  ]- },- {-  "title": "Active Sessions Exceeded",-  "result": [-   "session_conflict_decision",-   "DECISION DIALOG - do NOT auto-pick. This Autodesk seat is already active on another machine; the options SUSPEND or SHUT DOWN Fusion THERE (risking that machine's unsaved work). fire a notify_user and ASK the user which option they want, then click it. (Suspend = pausable/resumable; Shut down = saves the other machine's work to a recovery file.)"-  ]- },- {-  "title": "Suspend Remote Session",-  "result": [-   "session_suspend_confirm",-   "Confirmation of a chosen 'suspend the OTHER machine's Fusion'. Only click Continue if the user ALREADY chose suspend; otherwise Go Back and ASK. Never auto-confirm a seat action."-  ]- },- {-  "title": "Fusion is already open",-  "result": [-   "already_open",-   "Fusion is ALREADY running - a second launch was attempted. Click CANCEL (never Launch): Fusion is a singleton and the running instance is the one to drive. This usually means a prior fusion_stop/kill did not fully clear the old process before relaunch."-  ]- },- {-  "title": "Signing in - Autodesk Fusion",-  "result": [-   "signin_required",-   "Autodesk sign-in needed (first run). NOTIFY the user, drive the 'Sign In' button (desktop_ui_click), fetch any email OTP via adom-google, and after the browser shows \"You're signed in\" click its 'Open Product' button - but NEVER auto-enter the password/2FA. See fusion-aps-signin."-  ]- },- {-  "title": "Error launching streamed application",-  "result": [-   "launch_error",-   "Launch error from an INCOMPLETE webdeploy folder (missing FusionLauncher.exe.ini). Safe to dismiss (OK/close) - it is only an acknowledgement. Launch the COMPLETE production folder (the hash dir that HAS FusionLauncher.exe.ini); detect_fusion now prefers it."-  ]- },- {-  "title": "Create new footprint 'X'?",-  "result": [-   "create_confirm",-   "Fusion is asking whether to CREATE an artifact we asked to open, which means the name does not exist in the library. Answering 'Yes' invents an empty artifact and hides the real problem, so the bridge answers NO and reports the missing name. Fix the NAME (fusion_show_* refuses up front with `available` listing what the .lbr actually holds)."-  ]- },- {-  "title": "Grid",-  "result": [-   "settings_dialog_cancel",-   "A Fusion electronics SETTINGS dialog (e.g. Grid) is open and blocking. It carries no data - Cancel it (WM_CLOSE / Cancel). Nothing we drive opens these deliberately; if it keeps recurring, something is injecting a stray GRID/settings command or keystroke."-  ]- },- {-  "title": "A software problem has caused Fusion to close unexpectedly",-  "result": [-   "crash_report",-   "Fusion CRASHED. The bridge closes this and relaunches Fusion - the add-in only loads at launch, so a relaunch is the only way back to drivable."-  ]- },- {-  "title": "Packages are being uploaded to your Fusion Hub",-  "result": [-   "upload_in_progress",-   "DATA LOSS RISK. Fusion is still uploading 3D package(s) to the Hub. Do NOT click Yes / do NOT close - you will LOSE the uploaded packages and any unsaved bindings. Click No, then WAIT and poll until the upload finishes before closing or saveAs. See the fusion-cloud-save skill."-  ]- },- {-  "title": "Cannot be saved while packages upload",-  "result": [-   "save_blocked_by_upload",-   "saveAs was refused because 3D packages are still uploading to the Hub (this also throws InternalValidationError). Not fatal: wait for the upload to drain, then retry the saveAs and poll past it. See the fusion-cloud-save skill."-  ]- },- {-  "title": "A software update is available",-  "result": [-   "update_nag",-   "Fusion is prompting to update itself. Do NOT auto-confirm (it can start an update mid-automation). Dismiss it on the desktop or let Fusion update, then retry. A pending update is also a common cause of AdomBridge add-in crashes (add-in/host version drift), so clearing it often fixes those too."-  ]- },- {-  "title": "Select Electronics Design",-  "result": [-   "linked_design_picker",-   "This file links to multiple Electronics designs. The chooser is a CEF/web modal whose list is NOT keyboard- or Win32-navigable (SendInput reaches only the native Cancel/OK), so it can't be resolved headlessly today \u2014 select the design on the desktop. (Headless fix tracked: have the add-in open the specific linked design via the Fusion API, avoiding the picker entirely.)"-  ]- },- {-  "title": "Session suspended by: other",-  "result": [-   "session_suspended_regrab",-   "This box's Autodesk session was SUSPENDED by another machine taking the seat. The bridge clicks 'Check again' to regrab it (background UIA). Never click 'Shut Down' - that signs this box out."-  ]- },- {-  "title": "Library.lbr has errors and cannot be opened",-  "result": [-   "lbr_open_error",-   "Fusion could not parse the library file. Click OK, close the leftover empty Untitled design WITHOUT saving, and fix the FILE - as of 1.9.128 fusion_open_lbr refuses unparseable libraries up front (lbr_invalid) so this modal should never appear for a bridge-issued open; a raw caller-staged file can still trip it."-  ]- },- {-  "title": "Could not open file",-  "result": [-   "operation_error",-   "The last operation FAILED - Fusion is showing an Error dialog (the API call may still have returned ok because it opened an EMPTY design). READ the dialog screenshot for the exact message. Common case: '<file>.lbr has errors and cannot be opened' = the library is malformed (re-lint/regenerate; a frequent cause is a single UNNAMED symbol pin - EAGLE <pin name=''> + <connect pin=''> - name the pin from the KiCad pin NUMBER). The bridge already flipped success to False and dismissed the empty design; FIX the source file and retry. Do NOT report success."-  ]- },- {-  "title": "Untitled",-  "result": [-   "unknown",-   "Unrecognized modal - a screenshot is attached: READ it and ANALYZE before acting. Do NOT blindly dismiss (some modals lose work if confirmed wrong). Identify it from the screenshot, then take the work-preserving action."-  ]- },- {-  "title": "",-  "result": null- },- {-  "title": "   ",-  "result": null- },- {-  "title": "WELCOME TO FUSION",-  "result": [-   "signin_required",-   "Autodesk sign-in needed (first run). NOTIFY the user, drive the 'Sign In' button (desktop_ui_click), fetch any email OTP via adom-google, and after the browser shows \"You're signed in\" click its 'Open Product' button - but NEVER auto-enter the password/2FA. See fusion-aps-signin."-  ]- },- {-  "title": "What do you want to design?",-  "result": [-   "what_to_design",-   "Fusion's start picker. fusion_send_key {\"key\":\"escape\"} to dismiss."-  ]- },- {-  "title": "Session Suspended",-  "result": [-   "session_suspended_regrab",-   "This box's Autodesk session was SUSPENDED by another machine taking the seat. The bridge clicks 'Check again' to regrab it (background UIA). Never click 'Shut Down' - that signs this box out."-  ]- }+  {+    "title": "Fusion",+    "result": [+      "generic_modal_read_screenshot",+      "Generic Fusion modal - its body is not Win32-readable, so a screenshot is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. If it mentions uploading / 'lose these changes' / 'sure you want to close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). If it is the 'Select Electronics Design File' chooser during an open, its list is not keyboard/Win32-navigable; open the specific design by URN or select on the desktop."+    ]+  },+  {+    "title": "fusion360",+    "result": [+      "generic_modal_read_screenshot",+      "Generic Fusion modal - its body is not Win32-readable, so a screenshot is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. If it mentions uploading / 'lose these changes' / 'sure you want to close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). If it is the 'Select Electronics Design File' chooser during an open, its list is not keyboard/Win32-navigable; open the specific design by URN or select on the desktop."+    ]+  },+  {+    "title": "Autodesk Fusion 360",+    "result": [+      "generic_modal_read_screenshot",+      "Generic Fusion modal - its body is not Win32-readable, so a screenshot is attached: READ it and ANALYZE before acting. Do NOT blind-dismiss. If it mentions uploading / 'lose these changes' / 'sure you want to close' -> click No and wait for the Hub upload to drain (fusion-cloud-save). If it is the 'Select Electronics Design File' chooser during an open, its list is not keyboard/Win32-navigable; open the specific design by URN or select on the desktop."+    ]+  },+  {+    "title": "Save changes to Demo_Board?",+    "result": [+      "save_changes",+      "Unsaved-changes prompt. Use fusion_close_document (closes without the save modal), or fusion_dismiss_blocking_dialogs, which answers No."+    ]+  },+  {+    "title": "Document Recovery",+    "result": [+      "recovery",+      "Document Recovery prompt. Call fusion_dismiss_recovery, or fusion_relocate_recovery BEFORE fusion_start to prevent it."+    ]+  },+  {+    "title": "Active Sessions Exceeded",+    "result": [+      "session_conflict_decision",+      "DECISION DIALOG - do NOT auto-pick. This Autodesk seat is already active on another machine; the options SUSPEND or SHUT DOWN Fusion THERE (risking that machine's unsaved work). fire a notify_user and ASK the user which option they want, then click it. (Suspend = pausable/resumable; Shut down = saves the other machine's work to a recovery file.)"+    ]+  },+  {+    "title": "Suspend Remote Session",+    "result": [+      "session_suspend_confirm",+      "Confirmation of a chosen 'suspend the OTHER machine's Fusion'. Only click Continue if the user ALREADY chose suspend; otherwise Go Back and ASK. Never auto-confirm a seat action."+    ]+  },+  {+    "title": "Fusion is already open",+    "result": [+      "already_open",+      "Fusion is ALREADY running - a second launch was attempted. Click CANCEL (never Launch): Fusion is a singleton and the running instance is the one to drive. This usually means a prior fusion_stop/kill did not fully clear the old process before relaunch."+    ]+  },+  {+    "title": "Signing in - Autodesk Fusion",+    "result": [+      "signin_required",+      "Autodesk sign-in needed (first run). NOTIFY the user, drive the 'Sign In' button (desktop_ui_click), fetch any email OTP via adom-google, and after the browser shows \"You're signed in\" click its 'Open Product' button - but NEVER auto-enter the password/2FA. See fusion-aps-signin."+    ]+  },+  {+    "title": "Error launching streamed application",+    "result": [+      "launch_error",+      "Launch error from an INCOMPLETE webdeploy folder (missing FusionLauncher.exe.ini). Safe to dismiss (OK/close) - it is only an acknowledgement. Launch the COMPLETE production folder (the hash dir that HAS FusionLauncher.exe.ini); detect_fusion now prefers it."+    ]+  },+  {+    "title": "Create new footprint 'X'?",+    "result": [+      "create_confirm",+      "Fusion is asking whether to CREATE an artifact we asked to open, which means the name does not exist in the library. Answering 'Yes' invents an empty artifact and hides the real problem, so the bridge answers NO and reports the missing name. Fix the NAME (fusion_show_* refuses up front with `available` listing what the .lbr actually holds)."+    ]+  },+  {+    "title": "Grid",+    "result": [+      "settings_dialog_cancel",+      "A Fusion electronics SETTINGS dialog (e.g. Grid) is open and blocking. It carries no data - Cancel it (WM_CLOSE / Cancel). Nothing we drive opens these deliberately; if it keeps recurring, something is injecting a stray GRID/settings command or keystroke."+    ]+  },+  {+    "title": "A software problem has caused Fusion to close unexpectedly",+    "result": [+      "crash_report",+      "Fusion CRASHED. The bridge closes this and relaunches Fusion - the add-in only loads at launch, so a relaunch is the only way back to drivable."+    ]+  },+  {+    "title": "Packages are being uploaded to your Fusion Hub",+    "result": [+      "upload_in_progress",+      "DATA LOSS RISK. Fusion is still uploading 3D package(s) to the Hub. Do NOT click Yes / do NOT close - you will LOSE the uploaded packages and any unsaved bindings. Click No, then WAIT and poll until the upload finishes before closing or saveAs. See the fusion-cloud-save skill."+    ]+  },+  {+    "title": "Cannot be saved while packages upload",+    "result": [+      "save_blocked_by_upload",+      "saveAs was refused because 3D packages are still uploading to the Hub (this also throws InternalValidationError). Not fatal: wait for the upload to drain, then retry the saveAs and poll past it. See the fusion-cloud-save skill."+    ]+  },+  {+    "title": "A software update is available",+    "result": [+      "update_nag",+      "Fusion is prompting to update itself. Do NOT auto-confirm (it can start an update mid-automation). Dismiss it on the desktop or let Fusion update, then retry. A pending update is also a common cause of AdomBridge add-in crashes (add-in/host version drift), so clearing it often fixes those too."+    ]+  },+  {+    "title": "Select Electronics Design",+    "result": [+      "linked_design_picker",+      "This file links to multiple Electronics designs. The chooser is a CEF/web modal whose list is NOT keyboard- or Win32-navigable (SendInput reaches only the native Cancel/OK), so it can't be resolved headlessly today — select the design on the desktop. (Headless fix tracked: have the add-in open the specific linked design via the Fusion API, avoiding the picker entirely.)"+    ]+  },+  {+    "title": "Session suspended by: other",+    "result": [+      "session_suspended_regrab",+      "This box's Autodesk session was SUSPENDED by another machine taking the seat. The bridge clicks 'Check again' to regrab it (background UIA). Never click 'Shut Down' - that signs this box out."+    ]+  },+  {+    "title": "Library.lbr has errors and cannot be opened",+    "result": [+      "lbr_open_error",+      "Fusion could not parse the library file. Click OK, close the leftover empty Untitled design WITHOUT saving, and fix the FILE - as of 1.9.128 fusion_open_lbr refuses unparseable libraries up front (lbr_invalid) so this modal should never appear for a bridge-issued open; a raw caller-staged file can still trip it."+    ]+  },+  {+    "title": "Could not open file",+    "result": [+      "operation_error",+      "The last operation FAILED - Fusion is showing an Error dialog (the API call may still have returned ok because it opened an EMPTY design). READ the dialog screenshot for the exact message. Common case: '<file>.lbr has errors and cannot be opened' = the library is malformed (re-lint/regenerate; a frequent cause is a single UNNAMED symbol pin - EAGLE <pin name=''> + <connect pin=''> - name the pin from the KiCad pin NUMBER). The bridge already flipped success to False and dismissed the empty design; FIX the source file and retry. Do NOT report success."+    ]+  },+  {+    "title": "Untitled",+    "result": [+      "unknown",+      "Unrecognized modal - a screenshot is attached: READ it and ANALYZE before acting. Do NOT blindly dismiss (some modals lose work if confirmed wrong). Identify it from the screenshot, then take the work-preserving action."+    ]+  },+  {+    "title": "",+    "result": null+  },+  {+    "title": "   ",+    "result": null+  },+  {+    "title": "WELCOME TO FUSION",+    "result": [+      "signin_required",+      "Autodesk sign-in needed (first run). NOTIFY the user, drive the 'Sign In' button (desktop_ui_click), fetch any email OTP via adom-google, and after the browser shows \"You're signed in\" click its 'Open Product' button - but NEVER auto-enter the password/2FA. See fusion-aps-signin."+    ]+  },+  {+    "title": "What do you want to design?",+    "result": [+      "what_to_design",+      "Fusion's start picker. fusion_dismiss_blocking_dialogs closes it."+    ]+  },+  {+    "title": "Session Suspended",+    "result": [+      "session_suspended_regrab",+      "This box's Autodesk session was SUSPENDED by another machine taking the seat. The bridge clicks 'Check again' to regrab it (background UIA). Never click 'Shut Down' - that signs this box out."+    ]+  },+  {+    "title": "The board file has errors and will be opened with warnings",+    "result": [+      "board_opened_with_warnings",+      "Fusion opened a board file that has errors, and OK is the only answer: it's a notice, not a question, and nothing is saved or lost. fusion_dismiss_blocking_dialogs clicks OK. Read the board before trusting it. A board converted by a bridge before 1.13.61 can have a stack with no through via: fusion_routing_state lists any invalidViaNets, and fusion_set_layer_count at the same count repairs them."+    ]+  } ]\ No newline at end of file
tests/layer_drill.py+6−1
@@ -78,7 +78,12 @@ if broken:     check('a route that does not land comes off', r.get('errorCode') == 'readback_mismatch' and r.get('undone') and len(st(doc).get('tracks') or []) == b, {k: r.get(k) for k in ('errorCode', 'undone', 'error')}) elif r.get('success'):     call('fusion_execute_text_command', {'command': 'Electron.run UNDO;'})-r = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('set_layer_count repairs it', r.get('success') and r.get('repairedViaDefinition') == True or not broken, {k: r.get(k) for k in ('errorCode', 'error', 'note', 'mutated', 'repairedViaDefinition')}); time.sleep(2)+call('fusion_execute_text_command', {'command': "Electron.run CHANGE DRILL 0.4mm; VIA 'MID' 0.8mm ROUND (14mm 6mm);"})+stuck = [v for v in st(doc).get('vias') or [] if v.get('extent') == 'invalid']; check('a via drawn now is marked invalid', stuck, st(doc).get('vias'))+check('routing_state names its net', st(doc).get('invalidViaNets') == ['MID'], st(doc).get('invalidViaNets'))+r = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('it lands the net again', r.get('netsLandedAgain') == ['MID'], {k: r.get(k) for k in ('errorCode', 'error', 'netsLandedAgain')})+check('no via is invalid now, and it is still there', not [v for v in st(doc).get('vias') or [] if v.get('extent') == 'invalid'] and any(abs(v['at'][0] - 14) < .01 for v in st(doc).get('vias') or []), st(doc).get('vias'))+check("set_layer_count repairs it", r.get("success") and r.get("repairedViaDefinition") == True or not broken, {k: r.get(k) for k in ('errorCode', 'error', 'note', 'mutated', 'repairedViaDefinition')}); time.sleep(2) print('   Fusion now holds:', (call('fusion_run_modeling_script', {'script': READ}).get('data') or {}).get('result')) via_lands(doc, "CHANGE DRILL 0.3mm; VIA 'MID' 0.6mm ROUND 1-16 (10mm 5mm);", 'via 1-16 lands after the repair') r2 = call('fusion_set_layer_count', {'expectDocument': doc, 'count': 4}); check('a second call has nothing to repair', r2.get('success') and not r2.get('mutated'), r2.get('mutated'))
tests/test_routing.py+40
@@ -684,6 +684,46 @@ class LayerCountTests(unittest.TestCase):         with self.assertRaises(r.RoutingError):             r.stackup_for(8, 0.8, {}, None) +    def test_a_via_marked_invalid_is_landed_again_with_its_net(self):+        root = ET.parse(ROOT/'demo/routing/fixture/fusion-routing.brd').getroot(); b = root.find('drawing/board')+        sig = b.find('signals/signal'); name = sig.get('name')+        ET.SubElement(sig, 'wire', x1='1', y1='1', x2='5', y2='1', width='0.3', layer='1')+        ET.SubElement(sig, 'via', x='5', y='1', extent='invalid', drill='0.4', diameter='0.8')+        self.assertEqual([name], r._invalid_via_nets(b))+        import copy+        fixed = copy.deepcopy(b); fixed.find('signals/signal').find('via').set('extent', '1-16')+        self.assertEqual([], r._invalid_via_nets(fixed))+        sent = []+        class App:+            def executeTextCommand(self, cmd): sent.append(cmd)+        saved = r.snapshot; r.snapshot = lambda app, args: (root, fixed, {'boardId': 'b', 'revision': 'r2'})+        try:+            nets, after, _ = r._reland_invalid_via_nets(App(), {}, b)+        finally:+            r.snapshot = saved+        self.assertEqual([name], nets)+        cmds = [c[len('Electron.run '):] for c in sent if c.startswith('Electron.run ')]+        self.assertEqual("RIPUP '%s';" % name, cmds[0])+        self.assertTrue(any("WIRE '%s' 0.300000mm" % name in c for c in cmds)); self.assertTrue(any(' 1-16 (5.000000mm 1.000000mm)' in c for c in cmds))+    def test_landing_again_undoes_when_the_copper_differs(self):+        root = ET.parse(ROOT/'demo/routing/fixture/fusion-routing.brd').getroot(); b = root.find('drawing/board')+        sig = b.find('signals/signal')+        ET.SubElement(sig, 'wire', x1='1', y1='1', x2='5', y2='1', width='0.3', layer='1')+        ET.SubElement(sig, 'via', x='5', y='1', extent='invalid', drill='0.4', diameter='0.8')+        import copy+        lost = copy.deepcopy(b); s2 = lost.find('signals/signal'); s2.remove(s2.find('via'))+        sent = []+        class App:+            def executeTextCommand(self, cmd): sent.append(cmd)+        saved = r.snapshot+        r.snapshot = lambda app, args: (root, b if 'Electron.run UNDO;' in sent else lost, {'boardId': 'b', 'revision': 'r2'})+        try:+            with self.assertRaises(r.RoutingError) as e:+                r._reland_invalid_via_nets(App(), {}, b)+        finally:+            r.snapshot = saved+        self.assertEqual('readback_mismatch', e.exception.code); self.assertIn('undone', str(e.exception)); self.assertIn('Electron.run UNDO;', sent)+     def test_stackup_has_the_shape_fusion_writes(self):         # against a 4-layer stack Fusion wrote itself: same elements and attributes, including the via definition         real = ET.parse(ROOT/'addin/AdomBridge/resources/dru/Adom-3rdPartyFab-4-Layer.edru').getroot().find('.//layerstackup')