← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.13.51\ No newline at end of file+1.13.52\ No newline at end of file
CHANGELOG.md+16
@@ -3,6 +3,22 @@ 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.52 (2026-11-02)++Issue #136, the actual cause, reproduced with the reporter's library and bisected live on Fusion 2706.1.14: Fusion raises+its `assetAdded` error on every ADD, whichever part, from a .lbr where a `<package3d>` names a footprint the file does+not have. `adom-lbr assemble` merges parts with identical footprints, and a 3D entry written per original part was left+pointing at the merged-away name, so any library with two parts sharing a footprint hit this. Removing only those+entries makes the same library add; adding one to a clean library breaks it.+- `fusion_schematic_run`, `fusion_sch_place_part` and `fusion_sch_swap_part` remove those entries before USE, when no+  device uses them, and `fusion_schematic_run` names them in `library3dEntriesRemoved`. An entry a device still points at is+  left alone.++Also for #136: the seat dialog. The bridge answers Autodesk's Active Sessions Exceeded dialog with a click, which Adom Bridge+gates behind its approval, and with approval off the refusal was silent, so a run sat at the dialog. It now raises the+approval prompt (at most every 5 minutes) and clicks through on the next pass once approved; `fusion_claim_seat` and+`fusion_resolve_seat_dialog` answer `needs_approval` with `tellUser`.+ ## 1.13.51 (2026-11-02)  Drew Owens, PR #33 (add-in 1.0.32): typed writes recompute at checkpoints, as Fusion does. Every write still reads the
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.13.51",+  "version": "1.13.52",   "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.",
contracts/describe.json+2−1
@@ -22494,6 +22494,7 @@       "output": {         "added": "[name]",         "commands": "int",+        "library3dEntriesRemoved": "[name]",         "libraryLoaded": "path",         "libraryReformatted": "bool",         "nets": "{net:[part.pin]}",@@ -22502,7 +22503,7 @@         "verified": "bool"       },       "statusVerb": "fusion_get_app_state",-      "summary": "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr Fusion 2706 cannot ADD from (no line breaks between elements, a byte-order mark, single-quoted attributes) is rewritten in place first, keeping its element tree (libraryReformatted). The bridge sends USE itself, for the file named in libraryLoaded. Readback: the parts and nets on the sheet from a fresh export.",+      "summary": "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr Fusion 2706 cannot ADD from (no line breaks between elements, a byte-order mark, single-quoted attributes, a 3D package naming a footprint the file does not have) is rewritten in place first, keeping its element tree apart from those dead 3D entries (libraryReformatted, library3dEntriesRemoved). The bridge sends USE itself, for the file named in libraryLoaded. Readback: the parts and nets on the sheet from a fresh export.",       "timeoutSeconds": 120     },     {
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.13.51"+version = "1.13.52" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.51"+version = "1.13.52" edition = "2021"  [dependencies]
describe.py+2−2
@@ -662,9 +662,9 @@ _T = [     ("fusion_ripup_nets", "Take whole nets' tracks and vias back up (RIPUP per net, one undo step each) so they can be routed again, wider or another way. Their pours stay. Readback proves no track or via is left on those nets and no pour was lost.",      {"expectDocument":"required exact board name","boardId":"required","expectedRevision":"required","nets":"required [net name]"},      {"success":"bool","verified":"bool","ripped":"[net]","revision":"str"},120,"fusion_routing_state",False,{}),-    ("fusion_schematic_run", "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr Fusion 2706 cannot ADD from (no line breaks between elements, a byte-order mark, single-quoted attributes) is rewritten in place first, keeping its element tree (libraryReformatted). The bridge sends USE itself, for the file named in libraryLoaded. Readback: the parts and nets on the sheet from a fresh export.",+    ("fusion_schematic_run", "Build a schematic live: a list of complete, non-interactive EAGLE commands run one at a time on the active schematic (ADD 'dev@lib' 'NAME' R0 (x y), VALUE, NET 'name' (pts), LABEL, WIRE, TEXT, LAYER, CHANGE STYLE/SIZE/WIDTH/RATIO, WINDOW FIT), with an optional pause between them so a viewer sees it grow. Anything else is refused before the first command runs. ADD is proven against libraryPath on disk first (the deviceset must be there; the library is USEd by the bridge), so the component picker can never open. A .lbr Fusion 2706 cannot ADD from (no line breaks between elements, a byte-order mark, single-quoted attributes, a 3D package naming a footprint the file does not have) is rewritten in place first, keeping its element tree apart from those dead 3D entries (libraryReformatted, library3dEntriesRemoved). The bridge sends USE itself, for the file named in libraryLoaded. Readback: the parts and nets on the sheet from a fresh export.",      {"expectDocument":"optional exact schematic name","libraryPath":"required for ADD: the .lbr on this machine (its file name is the library name in ADD)","commands":"required [command] (1..400)","pauseMs":"optional 0..3000 between commands"},-     {"success":"bool","verified":"bool","parts":"[name]","added":"[name]","nets":"{net:[part.pin]}","commands":"int","libraryLoaded":"path","libraryReformatted":"bool"},120,"fusion_get_app_state",False,{}),+     {"success":"bool","verified":"bool","parts":"[name]","added":"[name]","nets":"{net:[part.pin]}","commands":"int","libraryLoaded":"path","libraryReformatted":"bool","library3dEntriesRemoved":"[name]"},120,"fusion_get_app_state",False,{}),     ("fusion_board_bom", "The bill of materials from the live board's own part attributes: one line per MPN with designators, quantity, value, package, manufacturer and the Mouser, DigiKey and LCSC numbers, plus the JLCPCB form (Comment, Designator, Footprint, LCSC). Board-only parts (test pads) are listed as such, and any part missing a distributor number is named. Read-only; with outputDir it also writes bom.csv and bom-jlcpcb.csv there.",      {"expectDocument":"required exact board name","outputDir":"optional folder on this machine for bom.csv + bom-jlcpcb.csv"},      {"lines":"[{designators,quantity,value,package,mpn,manufacturer,mouser,digikey,lcsc}]","csv":"str","jlcpcbCsv":"str","files":"{name:path}","boardOnly":"[str]","missingPartNumbers":"{mouser,digikey,lcsc:[str]}"},60,"fusion_routing_state",False,{}),
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.51"+DASH_VERSION="1.13.52" 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.51",+  "version": "1.13.52",   "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.51",+  "version": "1.13.52",   "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.51"+version = "1.13.52" dependencies = [  "adom-eda-checks",  "base64",@@ -299,7 +299,7 @@ dependencies = [  [[package]] name = "fusion-core"-version = "1.13.51"+version = "1.13.52" dependencies = [  "serde",  "serde_json",@@ -310,7 +310,7 @@ dependencies = [  [[package]] name = "fusion-lbr"-version = "1.13.51"+version = "1.13.52" dependencies = [  "regex",  "roxmltree",@@ -322,14 +322,14 @@ dependencies = [  [[package]] name = "fusion-os-mac"-version = "1.13.51"+version = "1.13.52" dependencies = [  "fusion-core", ]  [[package]] name = "fusion-os-win"-version = "1.13.51"+version = "1.13.52" 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.51"+version = "1.13.52" edition = "2021" license = "MIT" 
rust/bridge/src/lbr_use.py+39−10
@@ -6,10 +6,14 @@ # 2026-10-31 and 2026-11-01, on a library that adds cleanly otherwise): #   - elements inside <library> with no line break between them (one is enough); #   - a UTF-8 byte-order mark;-#   - attributes in single quotes.+#   - attributes in single quotes;+#   - a <package3d> whose <packageinstance> names a footprint the file does not have (#136: adom-lbr+#     assemble merges parts with identical footprints, and a 3D entry written per original part was left+#     pointing at the merged-away name). One is enough, whichever part is being added. # Fine as they are: indentation, tabs, CRLF, no XML declaration, a DOCTYPE, comments, non-ASCII # text, apostrophes and escaped HTML in text, the EAGLE version, an unresolvable 3D package URN.-# So such a file is rewritten in place before USE, keeping its element tree exactly. In place,+# So such a file is rewritten in place before USE, keeping its element tree exactly, apart from those+# dead 3D entries, which no device uses and which are named in the reply. In place, # because a copy at another path with the same library name makes Fusion ask which one to take # (its Select Library dialog) once both have been USEd. @@ -18,6 +22,22 @@ def _lbr_shape(root):             for e in root.iter()]  +def _orphan_3d(root):+    """The <package3d> entries Fusion 2706's ADD cannot take and nothing uses: every footprint they name+    is missing from the file, and no device points at them. (package3d element, its parent) pairs."""+    lib = root.find('.//library')+    if lib is None: return []+    packages = {p.get('name') for p in lib.iter('package')}+    used = {i.get('package3d_urn') for i in lib.iter('package3dinstance')}+    holder = lib.find('packages3d')+    out = []+    for q in (list(holder) if holder is not None else []):+        names = [i.get('name') for i in q.iter('packageinstance')]+        if names and not any(n in packages for n in names) and not ({q.get('wip_urn'), q.get('urn')} & used):+            out.append((q, holder))+    return out++ def lbr_needs_rewrite(data):     """True when the file (bytes or text) has anything Fusion 2706's ADD cannot read: a byte-order     mark, single-quoted attributes, or two elements inside <library> with no line break between@@ -29,14 +49,19 @@ def lbr_needs_rewrite(data):     body = _re.sub(rb'^\s*<\?xml[^>]*\?>', b'', data, count=1)     if _re.search(rb"""<[A-Za-z][^<>"']*\s[\w:.-]+\s*=\s*'""", body): return True     i = data.find(b'<library')-    return i >= 0 and _re.search(rb'>[ \t]*<', data[i:]) is not None+    if i >= 0 and _re.search(rb'>[ \t]*<', data[i:]) is not None: return True+    if b'<package3d ' not in data: return False+    import xml.etree.ElementTree as _ET+    try: return bool(_orphan_3d(_ET.fromstring(data)))+    except _ET.ParseError: return False  -def normalize_lbr(path):+def normalize_lbr(path, removed=None):     """Rewrite a .lbr Fusion 2706 cannot ADD from in place: no byte-order mark, double-quoted-    attributes, a line break between elements. Returns True when it rewrote the file. Only the file's-    formatting changes: the element tree (tags, attributes, text, comments) is compared before and-    after, and nothing is written when it differs."""+    attributes, a line break between elements, no dead 3D entries (their names go on `removed`). Returns+    True when it rewrote the file. Apart from those entries only the file's formatting changes: the+    element tree (tags, attributes, text, comments) is compared before and after, and nothing is written+    when it differs."""     import os as _os     import re as _re     import xml.etree.ElementTree as _ET@@ -47,6 +72,9 @@ def normalize_lbr(path):         return False     # parsed from bytes, so the file's own declared encoding is honoured; written back as UTF-8     root = _ET.fromstring(raw, parser=_ET.XMLParser(target=_ET.TreeBuilder(insert_comments=True)))+    for q, holder in _orphan_3d(root):+        holder.remove(q)+        if removed is not None: removed.append(q.get('name'))     before = _lbr_shape(root)     _ET.indent(root, space='')     body = _ET.tostring(root, encoding='unicode')@@ -61,12 +89,13 @@ def normalize_lbr(path):     return True  -def use_library(app, path):+def use_library(app, path, removed=None):     """USE a .lbr on this machine so ADD/REPLACE can take its devices, rewriting it first when-    Fusion 2706 could not read it (see above). Returns True when the file was rewritten."""+    Fusion 2706 could not read it (see above). Returns True when the file was rewritten; the 3D entries+    it dropped go on `removed`."""     from pathlib import Path as _P     try:-        changed = normalize_lbr(path)+        changed = normalize_lbr(path, removed)     except PermissionError:         raise RuntimeError('library_not_writable: %s is formatted in a way Fusion 2706 cannot ADD from (PlacePartWorker assetAdded: '                            'no line breaks between elements, a byte-order mark or single-quoted attributes), and this machine will not let '
rust/bridge/src/readiness_exec.rs+24−6
@@ -19,7 +19,7 @@ fn owned_popup_count(ab: &dyn AbClient, ctx: &Ctx, main_hwnd: i64) -> i64 { /// `_resolve_seat_dialog`: Continue (or Check again) through UIA in the background, verified by /// the parent's owned-popup count. {sawDialog, clicks, verified}. pub fn resolve_seat_dialog(ctx: &Ctx, max_clicks: u32) -> Value {-    let (mut clicks, mut saw, mut verified) = (0u32, false, false);+    let (mut clicks, mut saw, mut verified, mut needs_approval) = (0u32, false, false, false);     let Some(ab) = Loopback::from_env() else { return json!({"sawDialog": false, "clicks": 0, "verified": false}) };     for _ in 0..max_clicks {         let facts = fusion_core::windows_facts::facts_from_ab(&ab, &ctx.caller);@@ -30,11 +30,15 @@ pub fn resolve_seat_dialog(ctx: &Ctx, max_clicks: u32) -> Value {         let mut landed = false;         if hwnd != 0 {             for btn in ["Continue", "Check again"] {-                if let Ok(r) = ab.call("desktop_ui_click", json!({"hwnd": hwnd, "name": btn}), &ctx.caller, 4) {-                    if r["success"].as_bool() == Some(true) || r["status"].as_str() == Some("ok") {-                        if btn == "Check again" { eprintln!("[fusion-bridge] seat: clicked 'Check again' to REGRAB the suspended session (Session Suspended variant)"); }-                        landed = true; break;+                match ab.call("desktop_ui_click", json!({"hwnd": hwnd, "name": btn}), &ctx.caller, 4) {+                    Ok(r) => {+                        if r["success"].as_bool() == Some(true) || r["status"].as_str() == Some("ok") {+                            if btn == "Check again" { eprintln!("[fusion-bridge] seat: clicked 'Check again' to REGRAB the suspended session (Session Suspended variant)"); }+                            landed = true; break;+                        }+                        if r["errorCode"].as_str() == Some("needs_approval") || r["data"]["errorCode"].as_str() == Some("needs_approval") { needs_approval = true; }                     }+                    Err(e) => { if format!("{e}").contains("needs_approval") { needs_approval = true; } }                 }             }         }@@ -42,13 +46,27 @@ pub fn resolve_seat_dialog(ctx: &Ctx, max_clicks: u32) -> Value {         std::thread::sleep(Duration::from_millis(1500));         if parent != 0 && owned_popup_count(&ab, ctx, parent) == 0 { verified = true; break; }     }-    json!({"sawDialog": saw, "clicks": clicks, "verified": verified})+    // ab gates UI clicks behind its shell approval, even for a bridge (#133, #136: with approval off the+    // click was refused and the run sat at the seat dialog with nothing said). Raise the prompt, at most+    // every five minutes; once it is approved the next pass clicks through.+    if needs_approval {+        static ASKED: std::sync::Mutex<Option<std::time::Instant>> = std::sync::Mutex::new(None);+        let due = { let mut a = ASKED.lock().unwrap(); let due = a.map(|t| t.elapsed() > Duration::from_secs(300)).unwrap_or(true); if due { *a = Some(std::time::Instant::now()); } due };+        if due { let _ = ab.call("request_shell_approval", json!({"reason": "Answer Autodesk's Active Sessions Exceeded dialog so this PC takes the Fusion seat"}), &ctx.caller, 10); }+    }+    json!({"sawDialog": saw, "clicks": clicks, "verified": verified, "needsApproval": needs_approval}) }  /// The verb-shaped answer (`fusion_resolve_seat_dialog`). pub fn resolve_seat_dialog_verb(ctx: &Ctx, args: &Value) -> Value {     let r = resolve_seat_dialog(ctx, args["maxClicks"].as_u64().unwrap_or(3).max(1) as u32);     let still = Loopback::from_env().map(|ab| fusion_core::windows_facts::facts_from_ab(&ab, &ctx.caller).licensing_dialog.is_some()).unwrap_or(false);+    if still && r["needsApproval"].as_bool() == Some(true) {+        return json!({"success": false, "errorCode": "needs_approval", "clicks": r["clicks"], "resolved": false, "detail": r,+            "error": "Autodesk's seat dialog is up, and Adom Bridge needs approval before the bridge can answer it. The prompt has been raised.",+            "tellUser": "Adom Bridge needs your OK to answer Autodesk's Active Sessions Exceeded dialog: click Approve (1 hr, 24 hr or Auto) in the Adom Bridge window, on its notification, or in Hydrogen.",+            "_hint": "Once approved, call this again (or fusion_claim_seat). Continue makes this PC the active seat; Fusion on the other machine suspends and keeps its documents."});+    }     json!({"success": true, "clicks": r["clicks"], "resolved": !still, "detail": r, "_hint": "Readiness executor: the seat dialog is clicked Continue (Suspend pre-selected) and verified gone by screenshot; poll fusion_readiness."}) } 
rust/bridge/src/routing_script.py+4−3
@@ -676,10 +676,11 @@ def sch_run(app, args):     except Exception: pass     used = 'use' if lbr else None     reformatted = False+    orphans_removed = []     if lbr:         # a compact .lbr (no line break between elements) is rewritten first: Fusion 2706 asserts         # (PlacePartWorker assetAdded) on every ADD from one (#135; lbr_use.py)-        try: reformatted = use_library(app, lbr)+        try: reformatted = use_library(app, lbr, orphans_removed)         except RuntimeError as ex:             code = 'library_not_writable' if str(ex).startswith('library_not_writable') else 'library_unreadable'             fail(code, str(ex).split(': ', 1)[-1] if code == 'library_not_writable' else str(ex))@@ -702,7 +703,7 @@ def sch_run(app, args):             names = part_names()             if names is not None and want.upper() not in names:   # EAGLE keeps part names in upper case                 return {'success': False, 'mutated': len(done) > 1, 'errorCode': 'add_failed', 'failedAt': len(done) - 1, 'failedCommand': c, 'part': want,-                        'libraryFrom': used, 'libraryLoaded': lbr, 'libraryReformatted': reformatted, 'librariesHeld': sorted(held), 'commands': len(done), 'documentName': name,+                        'libraryFrom': used, 'libraryLoaded': lbr, 'libraryReformatted': reformatted, 'library3dEntriesRemoved': orphans_removed, 'librariesHeld': sorted(held), 'commands': len(done), 'documentName': name,                         'error': 'ADD of %s did not land: Fusion found no such device in %s (%s). The commands before it ran.' % (want, libname,                                  'the design holds that library, so the device is missing from it' if used == 'design' else 'Fusion did not take it from the .lbr loaded with USE'),                         '_hint': 'The bridge loads libraryLoaded with USE itself, so USE is not in the command list. Check the deviceset is in that file and that the schematic in front is the design you meant (documentName); then run the rest of the commands.'}@@ -717,7 +718,7 @@ def sch_run(app, args):     ok = not missing     return {'success': ok, 'mutated': True, 'verified': ok, 'errorCode': None if ok else 'readback_mismatch',             'error': None if ok else 'parts not on the sheet after ADD: %s' % missing, 'documentName': name,-            'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done), 'libraryFrom': used, 'libraryLoaded': lbr, 'libraryReformatted': reformatted}+            'parts': after, 'added': [p for p in after if p not in before], 'nets': nets, 'commands': len(done), 'libraryFrom': used, 'libraryLoaded': lbr, 'libraryReformatted': reformatted, 'library3dEntriesRemoved': orphans_removed}   def board_bom(board, args):
rust/bridge/src/seat.rs+4
@@ -82,6 +82,10 @@ pub fn claim_seat(ctx: &crate::addin::Ctx, args: &Value, pass: &dyn Fn(&str, Val         let facts = ab.as_ref().map(|a| fusion_core::windows_facts::facts_from_ab(a, &ctx.caller)).unwrap_or_default();         if facts.licensing_dialog.is_none() { break; }         resolved = crate::readiness_exec::resolve_seat_dialog_verb(ctx, &json!({}));+        if resolved["errorCode"] == "needs_approval" {+            return json!({"success": false, "errorCode": "needs_approval", "steps": steps, "holdsSeat": false, "error": resolved["error"], "tellUser": resolved["tellUser"],+                "_hint": "The approval prompt is up. When the user has clicked Approve, call fusion_claim_seat again."});+        }         steps.push(json!({"step": "answered the Autodesk seat modal (Continue: this box takes the seat)", "clicks": resolved["clicks"]}));         std::thread::sleep(Duration::from_secs(2));     }
tests/test_lbr_use.py+27
@@ -70,6 +70,33 @@ class LbrUse(unittest.TestCase):         for text in keep:             self.assertFalse(NS['lbr_needs_rewrite'](text), text[:60]) +    def test_orphan_3d_entries_are_removed_and_named(self):+        q = ('<package3d name="%s" urn="" wip_urn="urn:x:%s" type="model">\n<packageinstances>\n'+             '<packageinstance name="%s"/>\n</packageinstances>\n</package3d>\n')+        good, ghost = q % ('R0603', 'a', 'R0603'), q % ('GHOST', 'b', 'GHOST')+        text = BROKEN.replace('<symbols>', '<packages3d>\n' + good + ghost + '</packages3d>\n<symbols>', 1)+        p = self.write(text)+        self.assertTrue(NS['lbr_needs_rewrite'](open(p, 'rb').read()))+        removed = []+        self.assertTrue(NS['normalize_lbr'](p, removed))+        self.assertEqual(removed, ['GHOST'])+        out = open(p, encoding='utf-8').read()+        self.assertNotIn('GHOST', out)+        self.assertIn('name="R0603" urn=""', out)+        self.assertFalse(NS['lbr_needs_rewrite'](open(p, 'rb').read()))+        # a clean 3D entry alone is no reason to touch the file+        q2 = self.write(BROKEN.replace('<symbols>', '<packages3d>\n' + good + '</packages3d>\n<symbols>', 1))+        self.assertFalse(NS['normalize_lbr'](q2))++    def test_a_3d_entry_a_device_uses_is_kept(self):+        q = ('<package3d name="GONE" urn="" wip_urn="urn:x:used" type="model">\n<packageinstances>\n'+             '<packageinstance name="GONE"/>\n</packageinstances>\n</package3d>\n')+        text = BROKEN.replace('<symbols>', '<packages3d>\n' + q + '</packages3d>\n<symbols>', 1).replace(+            '</connects>', '</connects>\n<package3dinstances>\n<package3dinstance package3d_urn="urn:x:used"/>\n</package3dinstances>', 1)+        p = self.write(text)+        self.assertFalse(NS['normalize_lbr'](p))+        self.assertIn('GONE', open(p, encoding='utf-8').read())+     def test_use_library_runs_use_after_the_rewrite(self):         p = self.write(COMPACT)         seen = []