From fe30936e2f6aae4b55643ade31f209026209f7dc Mon Sep 17 00:00:00 2001 From: agent_coder Date: Sun, 12 Jul 2026 04:38:38 +0300 Subject: [PATCH] =?UTF-8?q?fix(mcp):=20drawio=20=E2=80=94=20=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE-=D1=81=D1=82=D1=80=D0=B8=D0=BF=20XML-=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20lint-=D0=BE=D1=88=D0=B8?= =?UTF-8?q?=D0=B1=D0=BA=D0=B8=20(#505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Модель органически вставляет `` в mxGraph-XML вопреки запрету в описании тула (природа LLM, промптингом не лечится). Жёсткая ошибка линтера [no-comments] заставляла её ПОЛНОСТЬЮ перегенерировать диаграмму — впустую потраченный tool-call на почти каждой первой генерации. Теперь в общем prepare-пути (`prepareModel`, через который идут drawioCreate/drawioUpdate/drawioEditCells) комментарии срезаются ДО линта: `xml.replace(//g, "")`. Комментарии не несут семантики, так что стрип всегда безопасен. Число срезанных комментариев уходит в `warnings[]` как `stripped N XML comment(s)` (только при N > 0) — модель это видит, но НЕ ретраит. Стрип ГЕЙТИТСЯ на well-formedness: сначала парсим модель, и режем комментарии только если XML корректен. На malformed-входе (например сырой неэкранированный `` into the diagram XML despite the prohibition in the tool + // description; a hard [no-comments] lint failure would force it to regenerate + // the whole diagram (a wasted tool call). Comments carry no diagram semantics, + // so stripping them is always safe. The linter's no-comments rule stays as a + // defense-in-depth backstop for any path that reaches it without this strip. + // + // The strip is GATED on well-formedness: the regex only reliably targets real + // comment nodes once the XML is known to be well-formed, because well-formed + // XML forbids a bare `<` inside an attribute value (it must be entity-escaped, + // e.g. `<!--`) and drawio XML uses no CDATA sections, so nothing but a + // comment node can hold the `` sequence. On MALFORMED input (e.g. + // a raw unescaped `/g) ?? []) : []; + const commentCount = commentMatches.length; + const strippedModel = + commentCount > 0 ? rawModel.replace(//g, "") : rawModel; + const stripWarnings = + commentCount > 0 ? [`stripped ${commentCount} XML comment(s)`] : []; + const { cells, warnings } = lintModel(strippedModel); + const modelXml = normalizeXml(strippedModel); const bbox = computeBBox(cells); const cellCount = cells.filter((c) => c.id !== "0" && c.id !== "1").length; // Geometry quality warnings (non-blocking) are appended to any structural @@ -1065,7 +1090,7 @@ export function prepareModel(inputXml: string): PreparedModel { cells, bbox, cellCount, - warnings: [...warnings, ...quality], + warnings: [...stripWarnings, ...warnings, ...quality], hash: mxHash(modelXml), }; } diff --git a/packages/mcp/test/unit/drawio-xml.test.mjs b/packages/mcp/test/unit/drawio-xml.test.mjs index 8d6e83ec..89e352ec 100644 --- a/packages/mcp/test/unit/drawio-xml.test.mjs +++ b/packages/mcp/test/unit/drawio-xml.test.mjs @@ -232,6 +232,85 @@ test("prepareModel: returns bbox, cellCount, hash and lints", () => { assert.equal(p.hash, mxHash(normalizeXml(VALID_MODEL))); }); +// --- comment auto-strip (issue #505) --------------------------------------- + +// The model organically inserts `` into diagram XML. prepareModel +// (the shared path for drawioCreate/drawioUpdate/drawioEditCells) strips them +// BEFORE the lint runs and reports the count as a non-blocking warning, so the +// model never hits a hard [no-comments] error and never regenerates the diagram. +test("prepareModel: strips XML comments, lint passes, warns with the count", () => { + const m = + '' + + '' + + '' + + '' + + '' + + '' + + ''; + // Would fail [no-comments] if not stripped first. + const p = prepareModel(m); + // Comments are gone from the canonical model. + assert.ok(!p.modelXml.includes(" b"); +}); + +test("prepareModel: malformed input with a raw b" vertex="1" parent="1">' + + '' + + ''; + const issues = issuesOf(() => prepareModel(m)); + // Rejected (threw DrawioLintError) rather than silently accepted. + assert.ok(issues, "expected prepareModel to reject malformed input"); + // By a real content rule, not swallowed by the strip. + assert.ok( + hasRule(issues, "value-escaping", "2") || hasRule(issues, "well-formed-xml"), + `expected value-escaping/well-formed-xml, got ${JSON.stringify(issues)}`, + ); +}); + +test("no-comments rule still fires when a comment reaches the linter directly", () => { + // Defense-in-depth: prepareModel strips first, but lintModel itself (the + // backstop) must still reject a raw comment-bearing model. + const m = + '' + + '' + + ''; + const issues = issuesOf(() => lintModel(m)); + assert.ok(hasRule(issues, "no-comments")); +}); + // --- decode chain: plain ----------------------------------------------------- test("decode chain (plain): buildDrawioSvg -> decodeDrawioSvg round-trips byte-stable", () => {