From 3ea378185cd5ba619e09aa25b3007ec7847d87e1 Mon Sep 17 00:00:00 2001 From: Raylan LIN Date: Fri, 2 Oct 2026 07:01:42 +0000 Subject: [PATCH] fix: P134 set_material verifies (zh/en names), run_macro binds swApp/Part and reports where it stopped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set_material: SetMaterialPropertyName2 returns nothing and silently ignores an unknown name (a Chinese install has 合金钢, not "Alloy Steel"), so the tool reported success while the tree said 材质 <未指定> and masses came out at water density. Each candidate (given name, then its zh/en alias, both database spellings) is now applied and verified by reading the density back; nothing taking is an error. run_macro: the description promised swApp and Part were bound, but nothing assigned them, and a runtime error inside the entry Sub reached the runner with Err clear, so broken macros "completed". The entry Sub now binds swApp/Part/swModel when used, records line number + text before each statement (skipping continuation lines and the first line after Select Case) and sets a completion flag at its end and on every Exit Sub; a macro that did not reach the end fails with the line it stopped on. Connection note: first result only, and distinguishes not-running / background instance shown / second instance started. 201 JS + 67 Python tests pass; typecheck, lint, ruff, compileall OK. --- CHANGELOG.md | 46 ++++++++++++++ README.md | 8 +-- README.zh-CN.md | 8 +-- package.json | 2 +- sidecar/sw_agent/bridge.py | 6 +- sidecar/sw_agent/server.py | 3 +- sidecar/sw_agent/tools/document.py | 87 ++++++++++++++++++++++++++- sidecar/tests/test_p133_visibility.py | 73 +++++++++++++++++++++- src/main/agent/agent-loop-sidecar.ts | 5 +- src/main/scripts/vba-macro-writer.ts | 74 ++++++++++++++++++++++- tests/vba-macro-writer.test.mjs | 33 ++++++++++ 11 files changed, 328 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7fb9dd..c69be07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,52 @@ ## [Unreleased] +## [0.2.133] - 2026-10-02 + +### Fixed (P134 — material that never took, macros that could not fail) + +Seen in a real session: the part was built correctly and was visible. But the tree still +said 材质 <未指定> after `set_material` "succeeded", every mass came out at water density, and +the agent then spent about ten `run_macro` calls on macros that all "completed" without +doing anything. + +**`set_material` reported success when nothing was applied.** +- **Why:** `SetMaterialPropertyName2` returns nothing, and SolidWorks silently ignores a name + it does not know. A Chinese install has 「合金钢」, not "Alloy Steel". +- **Fix:** the tool now applies each candidate — the given name, then its zh/en alias (Alloy + Steel ↔ 合金钢, Plain Carbon Steel ↔ 普通碳钢, 6061 Alloy ↔ 6061 合金, …; "steel" / + "钢" / "aluminum" / "铝" map to a standard grade) — in both database spellings, and + verifies each one by reading the density back. +- **Result:** if nothing takes, the tool raises an error naming what it tried. If the + density is unchanged on a part that already has a material, it returns `verified:false` + with a note instead of claiming success. + +**`run_macro` bound nothing.** The tool description promised that `swApp` and `Part` are +bound, but nothing assigned them, so every macro touching SolidWorks died on its first +statement. The entry `Sub` now binds `swApp` / `Part` / `swModel` (only when the macro uses +them). + +**`run_macro` reported success after a runtime error.** An error inside the entry `Sub` +reached the runner with `Err` already clear, so a macro that stopped on its first broken +line came back "脚本执行完成". +- **Fix:** the entry `Sub` is instrumented. Before each statement it records the line number + and text (no marker after a ` _` continuation or between `Select Case` and its first + `Case`), and it sets a completion flag on its last line and on every `Exit Sub`. +- **Result:** a macro that did not reach the end is reported as failed, with the line it + stopped on. + +**Connection note:** it is now attached to the first result only (it was repeated on every +result), and it distinguishes "SolidWorks was not running" from "a background instance +without a window was shown" from "a second instance was started". + +### Tests +- **`tests/vba-macro-writer.test.mjs`:** binding, completion flag and line reporting, no + marker where VBScript forbids a statement, Functions-only scripts. +- **`sidecar/tests/test_p133_visibility.py`:** `set_material` falls back to the Chinese + name and verifies; a material that never takes is an error; an ambiguous case is not + claimed; the background-instance note. +- **Totals:** 201 JS + 67 Python tests. + ## [0.2.132] - 2026-10-02 ### Fixed (P133 — tools built the part in a SolidWorks the user could not see) diff --git a/README.md b/README.md index edb69ca..1d75fe7 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@

- version + version electron react typescript python - tests + tests license

@@ -86,7 +86,7 @@ Millwright: - **Agentic tool loop.** Observe → reason → act. The model chains multiple tool calls, reads structured JSON back from each one, and recovers from errors instead of failing silently. - **Visual understanding.** Reorient, rotate, screenshot, and analyze the model — via a multimodal main model or a dedicated vision model. - **Resident execution engine.** A persistent Python sidecar holds one COM connection open across an entire multi-step task. -- **Developer-friendly.** 197 TypeScript/Node tests plus a Python suite (`pytest sidecar/tests`) for the sidecar, a typed IPC boundary, and a `SKIP_SW_CONNECT` mode for UI-only development without SolidWorks installed. +- **Developer-friendly.** 201 TypeScript/Node tests plus a Python suite (`pytest sidecar/tests`) for the sidecar, a typed IPC boundary, and a `SKIP_SW_CONNECT` mode for UI-only development without SolidWorks installed. ## Cross-version compatibility @@ -248,7 +248,7 @@ Contributions welcome — see [CONTRIBUTING.md](docs/CONTRIBUTING.md). We especi - [x] **v0.1** — MVP: Electron shell, LLM adapters, COM bridge, first tool set - [x] **v0.2** — Python sidecar, agentic tool loop, dual-engine fallback, vision feedback, confirmation cards, Apache-2.0 open source -- [x] **v0.2.4 → v0.2.132** — Extensive hardening against real SolidWorks installs ← *current*: the sketch → feature → cut → visual-verification loop now runs end to end on real hardware +- [x] **v0.2.4 → v0.2.133** — Extensive hardening against real SolidWorks installs ← *current*: the sketch → feature → cut → visual-verification loop now runs end to end on real hardware - [ ] **v0.3** — Streaming tool calls, sketching on model faces (not just reference planes), hole wizard, sheet metal, drawing annotations, remaining `#VERIFY` parameters confirmed - [ ] **v1.0** — MCP server, multi-CAD support diff --git a/README.zh-CN.md b/README.zh-CN.md index ee73d31..0cfe97c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -27,12 +27,12 @@

- version + version electron react typescript python - tests + tests license

@@ -85,7 +85,7 @@ Millwright: - **Agent 工具循环。** 观察 → 推理 → 执行。模型串联多次工具调用,读取每次返回的结构化 JSON,出错能自愈而不是静默失败。 - **视觉理解。** 可翻转、旋转、截屏,再做分析——既支持多模态主模型,也支持独立视觉模型。 - **常驻执行引擎。** 常驻 Python 边车在一整个多步任务中复用同一条 COM 连接。 -- **开发者友好。** 197 个 TS/Node 单元测试,另有独立的 Python 测试套件(`pytest sidecar/tests`),类型化 IPC 边界,`SKIP_SW_CONNECT` 纯 UI 开发模式(无需 SolidWorks)。 +- **开发者友好。** 201 个 TS/Node 单元测试,另有独立的 Python 测试套件(`pytest sidecar/tests`),类型化 IPC 边界,`SKIP_SW_CONNECT` 纯 UI 开发模式(无需 SolidWorks)。 ## 跨版本兼容 @@ -247,7 +247,7 @@ SolidWorks - [x] **v0.1** — MVP:Electron 骨架、LLM 适配器、COM 桥接、首批工具 - [x] **v0.2** — Python 边车、agent 工具循环、双引擎降级、视觉反馈、确认卡片,Apache-2.0 开源 -- [x] **v0.2.4 → v0.2.132** — 大量真机加固 ← *当前*:草图 → 特征 → 切除 → 视觉核验的完整闭环已在真机上端到端跑通 +- [x] **v0.2.4 → v0.2.133** — 大量真机加固 ← *当前*:草图 → 特征 → 切除 → 视觉核验的完整闭环已在真机上端到端跑通 - [ ] **v0.3** — 流式工具调用、在模型面上画草图(而非仅基准面)、孔向导、钣金、工程图标注、剩余 `# VERIFY` 参数完成核验 - [ ] **v1.0** — MCP server、多 CAD 支持 diff --git a/package.json b/package.json index b461744..5fc97d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "millwright", - "version": "0.2.132", + "version": "0.2.133", "description": "Open-source AI automation for SolidWorks — talk to your CAD.", "keywords": [ "solidworks", diff --git a/sidecar/sw_agent/bridge.py b/sidecar/sw_agent/bridge.py index 183b998..5fe4bab 100644 --- a/sidecar/sw_agent/bridge.py +++ b/sidecar/sw_agent/bridge.py @@ -145,7 +145,11 @@ def ensure_visible(app, pids_before: set[int] | None) -> str | None: after = _sw_pids() second = bool(pids_before) and after is not None and bool(after - pids_before) if not second: - return ("SolidWorks 之前没有可见窗口,已启动/显示 SolidWorks;之后的建模都在这个窗口里进行。") + if pids_before: # attached to a SolidWorks already running in the background + return ("连接到的是一个在后台运行、没有窗口的 SolidWorks(可能是之前残留的进程),已把它显示出来," + "之后的建模都在这个窗口里。如果你还开着另一个 SolidWorks 窗口,请以这个新显示的为准," + "或关掉多余的那个。") + return "SolidWorks 之前没有运行,已为你启动并显示,之后的建模都在这个窗口里。" note = ("你已打开的 SolidWorks 无法被连接,系统因此新启动了一个 SolidWorks 实例(现已显示出来)," "模型会建在这个新窗口里,而不是你原来的窗口。通常原因是两者权限级别不同——其中一个是" "「以管理员身份运行」的。") diff --git a/sidecar/sw_agent/server.py b/sidecar/sw_agent/server.py index 00d2b11..1dd8428 100644 --- a/sidecar/sw_agent/server.py +++ b/sidecar/sw_agent/server.py @@ -207,9 +207,10 @@ def work(): except Exception: # noqa: BLE001 pass # P133: we are driving a SolidWorks the user did not open (see - # bridge.ensure_visible) — say so on every result until the session reconnects + # bridge.ensure_visible) — say so ONCE, on the first result after connecting if getattr(ctx, "connect_note", None): data["_connection"] = ctx.connect_note + ctx.connect_note = None if mutating: adv = _advisory(ctx) if adv: diff --git a/sidecar/sw_agent/tools/document.py b/sidecar/sw_agent/tools/document.py index b0297ad..6fb077d 100644 --- a/sidecar/sw_agent/tools/document.py +++ b/sidecar/sw_agent/tools/document.py @@ -249,10 +249,91 @@ def save_as(ctx: Context, path: str): category="document", ) def set_material(ctx: Context, material: str, database: str = ""): + """P133: SetMaterialPropertyName2 returns nothing, and an unknown name is silently + ignored — a Chinese SolidWorks has 「合金钢」, not "Alloy Steel". The tool used to report + success while the tree still said 材质 <未指定> and every mass came out at water + density. Now each candidate (the given name, then its zh/en alias) is applied and + VERIFIED by reading the density back; nothing taking is an error, not a success.""" part = ctx.require(DOC_PART, "part") - db = database or "SOLIDWORKS Materials" - part.SetMaterialPropertyName2("", db, material) - return {"material": material, "database": db} + dbs = [database] if database else ["SOLIDWORKS Materials", "solidworks materials"] + names = _material_candidates(material) + before = _density(ctx) + tried = [] + for db in dbs: + for name in names: + tried.append(f"{db}/{name}") + try: + part.SetMaterialPropertyName2("", db, name) + except Exception: # noqa: BLE001 — try the next spelling + continue + try: + ctx.model.ForceRebuild3(False) + except Exception: # noqa: BLE001 + pass + after = _density(ctx) + if after is None: # cannot read back on this install — report, do not claim + return {"material": name, "database": db, "verified": False} + if _changed(before, after): + return {"material": name, "database": db, "verified": True, + "density_kg_m3": round(after, 3)} + if before is not None and not _changed(_NO_MATERIAL_DENSITY, before): + continue # still the no-material density: the name did not take + # the part already had a material of this density — re-applying the same one, + # or a name SolidWorks ignored; the density cannot tell which + return {"material": name, "database": db, "verified": False, + "density_kg_m3": round(after, 3), + "note": "density unchanged — either this material was already applied, or " + "the name was not recognised; check the material in the feature tree"} + raise SWError( + f"material '{material}' was not applied — SolidWorks did not recognise the name " + f"(tried {', '.join(tried)}; density still {before} kg/m³). Use the exact name from " + "SolidWorks' material dialog — a Chinese install names them e.g. 合金钢 / 普通碳钢 / 6061 合金.") + + +# P133: English ↔ Chinese names of SolidWorks' standard materials (+ generic words) +_MATERIAL_ALIASES: list[tuple[str, ...]] = [ + ("Alloy Steel", "合金钢"), + ("Plain Carbon Steel", "普通碳钢"), + ("Cast Carbon Steel", "铸造碳钢"), + ("Cast Alloy Steel", "铸造合金钢"), + ("1060 Alloy", "1060 合金"), + ("6061 Alloy", "6061 合金"), + ("Plain Carbon Steel", "普通碳钢", "steel", "钢"), + ("6061 Alloy", "6061 合金", "aluminum", "aluminium", "铝", "铝合金"), +] + +_NO_MATERIAL_DENSITY = 1000.0 # SolidWorks' density for a part with no material assigned + + +def _material_candidates(material: str) -> list[str]: + m = material.strip() + low = m.lower() + out = [m] + for group in _MATERIAL_ALIASES: + if low in (g.lower() for g in group): + out += [g for g in group[:2] if g not in out] + return out + + +def _density(ctx) -> float | None: + """kg/m³ from IMassProperty, or None when it cannot be read.""" + try: + ext = ctx.model.Extension + for maker in ("CreateMassProperty", "CreateMassProperty2"): + try: + mp = sw_get(ext, maker) + except Exception: # noqa: BLE001 + continue + if mp is not None: + return float(sw_get(mp, "Density")) + except Exception: # noqa: BLE001 + pass + return None + + +def _changed(before: float | None, after: float) -> bool: + ref = _NO_MATERIAL_DENSITY if before is None else before + return abs(after - ref) > 1e-6 @tool("rebuild_model", "Force a full model rebuild, i.e. Ctrl+Q (= Rebuild; ForceRebuild3)", params={}, category="document") diff --git a/sidecar/tests/test_p133_visibility.py b/sidecar/tests/test_p133_visibility.py index b26d266..31ea059 100644 --- a/sidecar/tests/test_p133_visibility.py +++ b/sidecar/tests/test_p133_visibility.py @@ -64,4 +64,75 @@ def test_hidden_only_instance_is_shown(monkeypatch): app = _App(False) note = bridge.ensure_visible(app, set()) # SolidWorks was not running before assert app.Visible is True - assert "新启动" not in note + assert "没有运行" in note + + +def test_hidden_background_instance_is_shown(monkeypatch): + monkeypatch.setattr(bridge, "_sw_pids", lambda: {100}) + app = _App(False) + note = bridge.ensure_visible(app, {100}) # same process, it just had no window + assert app.Visible is True + assert "后台" in note + + +# ---- set_material: verified by density, zh/en aliases ---- + +_DENSITY = {"合金钢": 7700.0, "普通碳钢": 7800.0, "6061 合金": 2700.0} + + +class _MatPart: + """A Chinese SolidWorks: only Chinese names exist; unknown names are silently ignored.""" + + def __init__(self, material=None): + self.material = material + self.calls = [] + + def SetMaterialPropertyName2(self, cfg, db, name): + self.calls.append(name) + if name in _DENSITY: + self.material = name + + def ForceRebuild3(self, _): + return True + + +def _mat_ctx(part): + class Mp: + _oleobj_ = object() + + @property + def Density(self): + return _DENSITY.get(part.material, 1000.0) + + class Ext: + _oleobj_ = object() + CreateMassProperty = Mp() + + class Ctx: + model = part + def require(self, *_): + return part + part.Extension = Ext() + return Ctx() + + +def test_set_material_falls_back_to_the_chinese_name_and_verifies(): + from sw_agent.tools import document + part = _MatPart() + out = document.set_material(_mat_ctx(part), "Alloy Steel") + assert part.material == "合金钢" + assert out["material"] == "合金钢" and out["verified"] is True and out["density_kg_m3"] == 7700.0 + + +def test_set_material_that_never_takes_is_an_error(): + import pytest + from sw_agent.bridge import SWError + from sw_agent.tools import document + with pytest.raises(SWError, match="not applied"): + document.set_material(_mat_ctx(_MatPart()), "Unobtainium") + + +def test_set_material_unknown_name_on_a_part_with_material_is_not_claimed(): + from sw_agent.tools import document + out = document.set_material(_mat_ctx(_MatPart("普通碳钢")), "Unobtainium") + assert out["verified"] is False and "unchanged" in out["note"] diff --git a/src/main/agent/agent-loop-sidecar.ts b/src/main/agent/agent-loop-sidecar.ts index eaa3417..a6b3637 100644 --- a/src/main/agent/agent-loop-sidecar.ts +++ b/src/main/agent/agent-loop-sidecar.ts @@ -136,7 +136,10 @@ const VIRTUAL_TOOLS = [ + 'accepts, pick faces and edges by meaning, and report the REAL error instead of ' + 'silently doing nothing. A static check runs first and REFUSES code that passes ' + 'millimetres to metre-based APIs or opens with On Error Resume Next. ' - + 'Write the body only — swApp and Part are already bound, no Sub main() wrapper.', + + 'Write the body only — no Sub main() wrapper. swApp (the SolidWorks application), ' + + 'Part and swModel (both = the active document) are bound for you. A runtime error ' + + 'stops the macro and is reported with the line it stopped on; a macro has no other ' + + 'way to return data than WScript.Echo, which comes back as the tool output.', parameters: { type: 'object', properties: { diff --git a/src/main/scripts/vba-macro-writer.ts b/src/main/scripts/vba-macro-writer.ts index 5dfcb96..a4411ea 100644 --- a/src/main/scripts/vba-macro-writer.ts +++ b/src/main/scripts/vba-macro-writer.ts @@ -136,6 +136,11 @@ export function vbaToVbs(vbaCode: string, opts?: { resultFilePath?: string }): s } // 只有 Function 没有 Sub 的罕见情形: 顶层代码原样执行,entry 留空 + // 9b. P133: 给入口 Sub 打桩 —— 预绑定 swApp/Part/swModel,逐行记录位置,末尾置完成标志。 + const usesSw = /\b(swApp|Part|swModel)\b/i.test(vbaCode); + const instrumented = entry ? instrumentEntry(code, entry, usesSw) : null; + if (instrumented) code = instrumented; + // 10. 组装完整 VBS const resultPath = opts?.resultFilePath ?? ''; // VBS 字符串没有反斜杠转义,路径直接嵌入;只需防御性处理双引号 @@ -148,6 +153,10 @@ export function vbaToVbs(vbaCode: string, opts?: { resultFilePath?: string }): s Dim SWCP_RESULT_PATH SWCP_RESULT_PATH = "${resultPathLiteral}" Dim SWCP_APP +Dim SWCP_LN, SWCP_AT, SWCP_DONE +SWCP_LN = 0 +SWCP_AT = "" +SWCP_DONE = ${instrumented ? 'False' : 'True'} `; const runner = entry @@ -160,7 +169,13 @@ If Err.Number <> 0 Then Dim SWCP_ERRDESC SWCP_ERRDESC = Err.Description If SWCP_ERRDESC = "" Then SWCP_ERRDESC = "未知错误 (代码 " & Err.Number & ")" - SWCP_Fail "脚本执行出错: " & SWCP_ERRDESC + SWCP_Fail "脚本执行出错" & SWCP_Where() & ": " & SWCP_ERRDESC +End If +' P133: a runtime error inside the entry Sub can reach here with Err already clear — the +' macro then "succeeded" while it had stopped at its first broken line. The completion +' flag set by the instrumented last line is the evidence that it ran to the end. +If Not SWCP_DONE Then + SWCP_Fail "脚本没有执行到最后一行" & SWCP_Where() & "。这是 VBScript 运行时错误(常见原因:对象为空/未绑定、方法或属性不存在、参数个数不对)。" End If On Error GoTo 0 SWCP_WriteResult True, "脚本执行完成" @@ -200,6 +215,15 @@ Function SWCP_ConnectSW() Set SWCP_ConnectSW = SWCP_APP End Function +' P133: 出错位置(入口 Sub 内第几行 + 该行代码) +Function SWCP_Where() + If SWCP_LN > 0 Then + SWCP_Where = ",停在第 " & SWCP_LN & " 行: " & SWCP_AT + Else + SWCP_Where = "" + End If +End Function + ' 写执行结果 JSON。Unicode=True → UTF-16LE+BOM,中文消息不乱码。 Sub SWCP_WriteResult(ok, msg) On Error Resume Next @@ -246,6 +270,54 @@ ${code} ${runner}${supportLib}`; } +/** + * P133: instrument the entry Sub. + * - swApp / Part / swModel are bound first when the macro uses them — the run_macro tool + * promised they were, but nothing assigned them, so every macro touching SolidWorks + * died on its first statement. + * - Before each statement: SWCP_LN / SWCP_AT record where we are, so a failure names the + * line. Skipped where VBScript allows no statement: continuation lines (previous line + * ends in " _") and the first line after "Select Case". + * - Last line (and every Exit Sub): SWCP_DONE = True — proof the body ran to the end. + * Returns null when the entry Sub cannot be located (the runner then trusts Err alone). + */ +function instrumentEntry(code: string, entry: string, bindSw: boolean): string | null { + const lines = code.split('\n'); + const head = new RegExp(`^\\s*Sub\\s+${entry}\\s*\\(`, 'i'); + const start = lines.findIndex((l) => head.test(l)); + if (start < 0) return null; + let end = -1; + for (let i = start + 1; i < lines.length; i++) { + if (/^\s*End\s+Sub\b/i.test(lines[i])) { end = i; break; } + } + if (end < 0) return null; + + const out: string[] = []; + if (bindSw) { + out.push(' Set swApp = SWCP_ConnectSW()', ' Set Part = swApp.ActiveDoc', ' Set swModel = Part'); + } + let n = 0; + let prev = ''; + for (const line of lines.slice(start + 1, end)) { + const t = line.trim(); + const significant = t !== '' && !t.startsWith("'") && !/^rem\b/i.test(t); + if (significant) { + n++; + const afterContinuation = /\s_\s*$/.test(prev); + const afterSelect = /^\s*Select\s+Case\b/i.test(prev); + if (!afterContinuation && !afterSelect) { + const indent = /^\s*/.exec(line)![0]; + const text = t.length > 120 ? t.slice(0, 117) + '...' : t; + out.push(`${indent}SWCP_LN = ${n}: SWCP_AT = "${text.replace(/"/g, '""')}"`); + } + prev = line; + } + out.push(line.replace(/\bExit\s+Sub\b/gi, 'SWCP_DONE = True: Exit Sub')); + } + out.push(' SWCP_DONE = True'); + return [...lines.slice(0, start + 1), ...out, ...lines.slice(end)].join('\n'); +} + /** * 检查 VBA 代码中无法转换为 VBScript 的语法。 * 返回问题列表(空 = 兼容)。engine 在执行前调用, diff --git a/tests/vba-macro-writer.test.mjs b/tests/vba-macro-writer.test.mjs index 4a84032..88f3287 100644 --- a/tests/vba-macro-writer.test.mjs +++ b/tests/vba-macro-writer.test.mjs @@ -168,3 +168,36 @@ test('checkVbsCompatibility 合法代码无误报', () => { const issues = checkVbsCompatibility(WRAPPED_VBA); assert.equal(issues.length, 0, `不应有兼容性问题: ${issues.join(', ')}`); }); + +// P133: run_macro promised "swApp and Part are already bound" — nothing bound them, and a +// runtime error inside the entry Sub still ended as "脚本执行完成". +test('P133: swApp / Part / swModel are bound when the macro uses them', () => { + const vbs = vbaToVbs('Debug = Part.GetTitle', { resultFilePath: 'C:\\r.json' }); + assert.match(vbs, /Set swApp = SWCP_ConnectSW\(\)\n\s*Set Part = swApp\.ActiveDoc\n\s*Set swModel = Part/); + const plain = vbaToVbs('x = 1 + 1', { resultFilePath: 'C:\\r.json' }); + assert.doesNotMatch(plain, /Set Part = /, 'no SolidWorks connection for a macro that does not touch it'); +}); + +test('P133: completion flag + failing line are reported', () => { + const vbs = vbaToVbs('a = 1\nb = Part.GetTitle', { resultFilePath: 'C:\\r.json' }); + assert.match(vbs, /SWCP_DONE = False/); + assert.match(vbs, /SWCP_LN = 2: SWCP_AT = "b = Part\.GetTitle"\n\s*b = Part\.GetTitle/); + assert.match(vbs, /SWCP_DONE = True\nEnd Sub/); + assert.match(vbs, /If Not SWCP_DONE Then\n\s*SWCP_Fail/); +}); + +test('P133: no marker where VBScript allows no statement; Exit Sub still completes', () => { + const vbs = vbaToVbs( + 'Select Case k\n Case 1\n y = 1\nEnd Select\ns = "a" & _\n "b"\nIf z Then Exit Sub', + { resultFilePath: 'C:\\r.json' }, + ); + assert.match(vbs, /Select Case k\n\s*Case 1/, 'nothing between Select Case and the first Case'); + assert.match(vbs, /s = "a" & _\n\s*"b"/, 'a continued line is not split'); + assert.match(vbs, /If z Then SWCP_DONE = True: Exit Sub/); +}); + +test('P133: a script with only Functions is not marked as unfinished', () => { + const vbs = vbaToVbs('Function f()\n f = 1\nEnd Function', { resultFilePath: 'C:\\r.json' }); + assert.match(vbs, /SWCP_DONE = True\n/); + assert.doesNotMatch(vbs, /SWCP_DONE = False/); +});