Compare commits

..
Author SHA1 Message Date
dayuan.jiang 81516f3984 test(e2e): deselect by clicking a corner away from the shape
The test clicked the canvas at (10, 10) to deselect. When the click on the
shape before it needed retries, Playwright scrolled the shape into the
container's top-left corner, so the next click landed on the shape and the
selection stayed (seen on the main run of cd5352c and on #973). The new
clickEmptyCanvas helper clicks the corner farthest from the shape.
Reproduced locally by scrolling the shape into the corner: the old click
keeps the selection, the helper clears it.
2026-10-10 23:43:54 +09:00
9 changed files with 78 additions and 271 deletions
@@ -1,55 +0,0 @@
# Issues labelled "wait for confrim" are waiting for the reporter. After 15
# days without activity the issue gets a reminder and the "no response"
# label; after 15 more days it is closed. A reply from the reporter removes
# both labels, so the issue shows up as needing the maintainer again.
name: Wait for confirmation
on:
schedule:
- cron: "17 3 * * *"
workflow_dispatch:
issue_comment:
types: [created]
permissions:
issues: write
jobs:
remind-and-close:
if: github.event_name != 'issue_comment'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
only-issue-labels: "wait for confrim"
days-before-issue-stale: 15
days-before-issue-close: 15
days-before-pr-stale: -1
days-before-pr-close: -1
stale-issue-label: "no response"
stale-issue-message: >-
Still there? We asked for more information 15 days ago. A reply
would help; if we hear nothing in another 15 days this issue will
be closed. It can be reopened any time.
close-issue-message: >-
No reply in 30 days, so this issue is closed for now. If the
problem is still there, reply or reopen it.
close-issue-reason: not_planned
remove-stale-when-updated: true
reporter-replied:
if: >-
github.event_name == 'issue_comment' &&
contains(github.event.issue.labels.*.name, 'wait for confrim') &&
github.event.comment.user.type != 'Bot' &&
!contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
steps:
- name: The reporter answered, so the issue is the maintainer's again
run: >-
gh issue edit "$NUMBER" --repo "$REPO"
--remove-label "wait for confrim" --remove-label "no response"
env:
GH_TOKEN: ${{ github.token }}
NUMBER: ${{ github.event.issue.number }}
REPO: ${{ github.repository }}
+36 -36
View File
@@ -75,7 +75,7 @@
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.5.15",
"@biomejs/biome": "2.5.7",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
@@ -2093,9 +2093,9 @@
}
},
"node_modules/@biomejs/biome": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.15.tgz",
"integrity": "sha512-WZTW4slm/pdkh92K6t/3aEN++44JD1PQ7squ7RCMLI1flHGl43DVOIGXvCjHiZgBMO1V4uYxoFNtqrIA4qGuIQ==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.7.tgz",
"integrity": "sha512-zr8K/DcY5tYsQOQwqMJ0AWElo6QgmgNI7idXgXLhevVszlt8RGVpesEJPqx3ThazLaOwjJ5Y8fz3BtH5fGZNsw==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@@ -2109,20 +2109,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
"@biomejs/cli-darwin-arm64": "2.5.15",
"@biomejs/cli-darwin-x64": "2.5.15",
"@biomejs/cli-linux-arm64": "2.5.15",
"@biomejs/cli-linux-arm64-musl": "2.5.15",
"@biomejs/cli-linux-x64": "2.5.15",
"@biomejs/cli-linux-x64-musl": "2.5.15",
"@biomejs/cli-win32-arm64": "2.5.15",
"@biomejs/cli-win32-x64": "2.5.15"
"@biomejs/cli-darwin-arm64": "2.5.7",
"@biomejs/cli-darwin-x64": "2.5.7",
"@biomejs/cli-linux-arm64": "2.5.7",
"@biomejs/cli-linux-arm64-musl": "2.5.7",
"@biomejs/cli-linux-x64": "2.5.7",
"@biomejs/cli-linux-x64-musl": "2.5.7",
"@biomejs/cli-win32-arm64": "2.5.7",
"@biomejs/cli-win32-x64": "2.5.7"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.15.tgz",
"integrity": "sha512-BZVzFhJ/mUvTLMYbc9x6el0o2Uv5zP7bACYyhFb6fSc1giroXm7PL0a5KqMJ9F+/BnkYRjagD/ROSDVOZ6eapg==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.7.tgz",
"integrity": "sha512-vxo/Ls3/PYdQWyLhYYcgMOCzQypAjcY+iihS8M0wW03l16TCLW4zqZzGo75gm1VdCMj38hTVZ31KBWrZ4G9dJw==",
"cpu": [
"arm64"
],
@@ -2137,9 +2137,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.15.tgz",
"integrity": "sha512-V5Kw63V+fVGNFhFrizDxMbGXAzZCh8kYzJpy3GI6gTapWDg7bZK9oYC9CbVgQSEdTnupz0U5Efz8Ev+5vdBBXw==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.7.tgz",
"integrity": "sha512-Cd3Ga61amT/Yl/0x8elP5hhGYaFy4bw6WuysTgf7oo8TA5tJ5A1k+DkVoJ2BHbTVil51gTX9VPzArnrlLJ3Kyg==",
"cpu": [
"x64"
],
@@ -2154,9 +2154,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.15.tgz",
"integrity": "sha512-XaG7P7eeSLYETD3K9grfB0mQmpLQZjygdbsxekWBJAg4am79fGAtRpfRyNnTvXB2fZFNnJZG3nXm7HBat0+VUw==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.7.tgz",
"integrity": "sha512-rR2QE0yF2GYSuYuKIa7pKvODGJqnOH+2eDREAM8wV+mWKSkMQKdAp4zXEZfTaxY8PMoNONnpgSWcBCyLDPDOKg==",
"cpu": [
"arm64"
],
@@ -2174,9 +2174,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.15.tgz",
"integrity": "sha512-tGzZUTcJCV7tj5Adh/Gn6nR4MycqA3iohq2LEcrarAgRBkU0h0OKd2UzCRkOsOYOuUGaUJb4YmqRqyy2feGGSw==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.7.tgz",
"integrity": "sha512-xPI5yB6XlpDbNkS+bm1t42olw5c4l3UrlOmLg7KtLJvjvkNF/1V4tnUgfkylGIeb3u/T+BzMGYqgQhzjAoJzuQ==",
"cpu": [
"arm64"
],
@@ -2194,9 +2194,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.15.tgz",
"integrity": "sha512-xE4iEW/3LqlYj9GFgGrFsFSH51dEEpUbYWBfeOv0q87WkUDxK2o/HhreSb7qMJqck70RVM6Lg96hgedcAYcOkA==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.7.tgz",
"integrity": "sha512-FQgqJhscrqJUFptGaRSUJWlXAExwWcDwLuK49dvKfkQ1bB5SEEyFssnsxQY83Xm6jR0EbbX3+8+D5bfvYqUG2Q==",
"cpu": [
"x64"
],
@@ -2214,9 +2214,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.15.tgz",
"integrity": "sha512-IlxUcyxilVGPsE008x13pWdkTbU3nQpP2i9b5UrbOYj6goBkKsRX1XB7yJLcV+O1H9LBchIIm4adOaVLZBX0ZQ==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.7.tgz",
"integrity": "sha512-rE5VZi+qtmPgQH+l7jVxYoZ18b/TiHEhulhMpjmCZH1PltSbjRcxNWywC3HZ9tYottG7ORkeTtoscBilKSBm0g==",
"cpu": [
"x64"
],
@@ -2234,9 +2234,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.15.tgz",
"integrity": "sha512-2kPKzhNlm8C+Ru3GcO0Me2ODkCBLrVOUNuyi84RJyWDVKAA4+Kjj3jMjL938lF6BRfvltC4vB2nPY9SvN51Ovg==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.7.tgz",
"integrity": "sha512-Oq4x0CCwP4jirrcTywXs5kOGZ4v5vuEP+gWrbtjApOA2CL9F3F9GlIdQIci8AKSCa/zURanMRpX/4wQ7Am6hHg==",
"cpu": [
"arm64"
],
@@ -2251,9 +2251,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
"version": "2.5.15",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.15.tgz",
"integrity": "sha512-yAzh4UqEImV6Hcy0zjUqNfJAsUhoD64FBQCdTG8Md/Z2wQeVP/ZmJ3XRYAs1g8J9fjTFyl2fCtc5FB5til47+g==",
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.7.tgz",
"integrity": "sha512-V+0wu/nrj2S+MhP4EQ0uHNolP0IALEsz45pg0WoKkHfDeh0+ItHwP/p7bX5RPoMOl9NkpHYWdYPhIcy2mACHvQ==",
"cpu": [
"x64"
],
+1 -1
View File
@@ -110,7 +110,7 @@
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
"@biomejs/biome": "2.5.15",
"@biomejs/biome": "2.5.7",
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "^4",
"@tailwindcss/typography": "^0.5.19",
+1 -1
View File
@@ -123,7 +123,7 @@ Use the standard MCP configuration with:
| `get_drawing_guide` | Return the drawing rules again, for example after a long conversation was compacted |
| `get_shape_library` | Return the shapes and icon styles of a library such as `aws4`, `azure2`, or `kubernetes` |
| `create_new_diagram` | Create a new diagram from XML; a plain list of `mxCell` elements is enough |
| `load_diagram` | Load a `.drawio` file into the session, from a `path` on disk or from its `xml` content (handles compressed files) |
| `load_diagram` | Load a `.drawio` file from disk into the session (handles compressed files) |
| `edit_diagram` | Edit diagram by ID-based operations (update/add/delete cells); all or nothing |
| `get_diagram` | Get the current diagram XML, including your edits in the browser |
| `screenshot_diagram` | Return a PNG of a page so the AI can check the rendered diagram |
+1 -1
View File
@@ -18,7 +18,7 @@ import {
export const DRAWING_GUIDE = `# Draw.io drawing guide
## Workflow
- create_new_diagram draws a new diagram and REPLACES the whole document. add_page adds another tab. edit_diagram changes cells of an existing page. load_diagram opens a .drawio file (the server reads the file itself, or takes the file's content as its 'xml' argument when you already have it in hand). get_diagram returns the current XML, including the user's manual edits. export_diagram saves to a file.
- create_new_diagram draws a new diagram and REPLACES the whole document. add_page adds another tab. edit_diagram changes cells of an existing page. load_diagram opens a .drawio file (the server reads the file itself). get_diagram returns the current XML, including the user's manual edits. export_diagram saves to a file.
- Before drawing, describe your layout plan in 2-3 sentences, so shapes do not overlap and edges do not cross shapes.
- Send XML only through tool calls, never in chat text. Never draw a box just to send the user a message.
- Before using any icon library (AWS, Azure, GCP, Kubernetes, Cisco, BPMN, Material Design, web icons...), call get_shape_library and use the exact style names it returns. NEVER guess icon style names. For AWS, use the AWS 2025 icons (library aws4).
+35 -86
View File
@@ -455,54 +455,21 @@ registerWriteTool(
{
title: "Load .drawio file",
description:
"Load a .drawio diagram into the current session, REPLACING the entire diagram (all pages). " +
"Provide ONE of two mutually exclusive sources: 'path' (the server reads the file from disk — you do NOT need to read the file yourself or pass its XML through create_new_diagram) " +
"or 'xml' (the raw file content you already have — from another tool, a repository read, or an API response — so no temporary file needs to be written first). " +
"Both accept plain XML and draw.io's compressed save format.\n\n" +
"After loading from 'path' (or from compressed 'xml'), call get_diagram before edit_diagram — you haven't seen the file's cell IDs yet. " +
"Plain-XML 'xml' content you supplied yourself is already known and can be edited immediately.",
"Load a .drawio file from disk into the current session, REPLACING the entire diagram (all pages). " +
"The server reads the file directly — you do NOT need to read the file yourself or pass its XML through create_new_diagram. " +
"Handles both plain-XML and draw.io's compressed save format.\n\n" +
"After loading, call get_diagram before edit_diagram — you haven't seen the file's cell IDs or structure yet.",
inputSchema: {
path: z
.string()
.optional()
.describe(
"Path to the .drawio file to load (e.g. /Users/me/diagram.drawio or ~/diagram.drawio). Relative paths resolve against the MCP server's working directory, which is often not your project. Mutually exclusive with 'xml'.",
),
xml: z
.string()
.optional()
.describe(
"Raw .drawio file content: a plain <mxfile>/<mxGraphModel>, or draw.io's compressed save format. Use when the content is already in hand (another tool's output, a repository read, an API response). Mutually exclusive with 'path'.",
"Absolute path to the .drawio file to load (e.g. /Users/me/diagram.drawio or ~/diagram.drawio). Relative paths resolve against the MCP server's working directory, which is often not your project.",
),
},
annotations: { openWorldHint: false },
},
async ({ path, xml: inlineXml }) => {
async ({ path }) => {
try {
// Argument validation comes before the session check: a bad
// argument is a caller error and should be reported as such.
if (path !== undefined && inlineXml !== undefined) {
return {
content: [
{
type: "text",
text: "Error: Provide either 'path' or 'xml', not both.",
},
],
isError: true,
}
}
if (path === undefined && inlineXml === undefined) {
return {
content: [
{
type: "text",
text: "Error: Provide either 'path' (a .drawio file to read) or 'xml' (the file's content).",
},
],
isError: true,
}
}
if (!currentSession) {
return {
content: [
@@ -515,37 +482,29 @@ registerWriteTool(
}
}
// Exactly one of path/xml is present (validated above).
let content = ""
let sourceLabel = ""
if (inlineXml !== undefined) {
content = inlineXml
sourceLabel = "inline XML"
} else if (path !== undefined) {
const fs = await import("node:fs/promises")
const nodePath = await import("node:path")
const absolutePath = nodePath.resolve(expandHome(path))
const fs = await import("node:fs/promises")
const nodePath = await import("node:path")
const absolutePath = nodePath.resolve(expandHome(path))
try {
// A pipe or device could be read forever, and the other
// write tools wait for this one
if (!(await fs.stat(absolutePath)).isFile()) {
throw new Error("not a regular file")
}
content = await fs.readFile(absolutePath, "utf-8")
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return {
content: [
{
type: "text",
text: `Error: Cannot read file ${absolutePath}: ${msg}`,
},
],
isError: true,
}
let content: string
try {
// A pipe or device could be read forever, and the other
// write tools wait for this one
if (!(await fs.stat(absolutePath)).isFile()) {
throw new Error("not a regular file")
}
content = await fs.readFile(absolutePath, "utf-8")
} catch (e) {
const msg = e instanceof Error ? e.message : String(e)
return {
content: [
{
type: "text",
text: `Error: Cannot read file ${absolutePath}: ${msg}`,
},
],
isError: true,
}
sourceLabel = absolutePath
}
const loaded = parseDrawioFileContent(content)
@@ -558,7 +517,7 @@ registerWriteTool(
const xml = loaded.xml
log.info(
`Loading diagram from ${sourceLabel} (${xml.length} chars)`,
`Loading diagram from ${absolutePath} (${xml.length} chars)`,
)
// Save the user's current state before replacing (same flow as
@@ -578,17 +537,10 @@ registerWriteTool(
currentSession.xml = xml
currentSession.version++
setState(currentSession.id, xml)
// Edit-gate semantics by source:
// - 'path': the model only supplied a path, so it doesn't know
// the file's cell IDs — keep the gate (one get_diagram first).
// - plain 'xml': the model supplied the exact content, same
// rationale as create_new_diagram — record it as seen.
// - compressed 'xml': the session now holds the decompressed
// form, which the model cannot derive from the compressed
// input — keep the gate.
const markSeen =
inlineXml !== undefined && !loaded.hadCompressedPages
currentSession.lastSeenXml = markSeen ? xml : ""
// Deliberately NOT marking the loaded XML as seen: the model only
// supplied a path, so it doesn't know the file's cell IDs. The
// edit gate will require one get_diagram before edits.
currentSession.lastSeenXml = ""
addHistory(currentSession.id, xml, "")
@@ -599,17 +551,13 @@ registerWriteTool(
? `Pages (${pages.length}): ${pages.map((p) => `[${p.index}] id=${p.id} name="${p.name}" cells=${p.cellCount}`).join(" | ")}`
: "no pages parsed"
log.info(`Diagram loaded (${pageSummary})`)
const gateHint = markSeen
? ""
: "\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet."
log.info(`Diagram loaded from file (${pageSummary})`)
return {
content: [
{
type: "text",
text: `Diagram loaded from ${sourceLabel}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}${gateHint}`,
text: `Diagram loaded from ${absolutePath}!\n\nThe diagram is now visible in your browser.\n\n${pageSummary}\n\nCall get_diagram before edit_diagram — you haven't seen this file's cell IDs yet.`,
},
],
}
@@ -624,6 +572,7 @@ registerWriteTool(
}
},
)
// Tool: edit_diagram
registerWriteTool(
"edit_diagram",
+2 -6
View File
@@ -18,7 +18,7 @@ import {
import { getXmlSyntaxError } from "./xml-syntax.ts"
export type LoadResult =
| { ok: true; xml: string; hadCompressedPages: boolean }
| { ok: true; xml: string }
| { ok: false; error: string }
/**
@@ -101,9 +101,5 @@ export function parseDrawioFileContent(content: string): LoadResult {
decompressedAny = true
}
// Nothing changed — keep the file's own serialisation.
return {
ok: true,
xml: decompressedAny ? serializeMxfile(doc) : trimmed,
hadCompressedPages: decompressedAny,
}
return { ok: true, xml: decompressedAny ? serializeMxfile(doc) : trimmed }
}
+2 -37
View File
@@ -54,11 +54,7 @@ describe("decompressPageContent", () => {
describe("parseDrawioFileContent", () => {
it("passes a plain-XML mxfile through unchanged", () => {
const r = parseDrawioFileContent(PLAIN_MXFILE)
expect(r).toEqual({
ok: true,
xml: PLAIN_MXFILE,
hadCompressedPages: false,
})
expect(r).toEqual({ ok: true, xml: PLAIN_MXFILE })
})
it("wraps a bare mxGraphModel into a one-page mxfile", () => {
@@ -107,11 +103,7 @@ describe("parseDrawioFileContent", () => {
it("keeps empty pages as-is", () => {
const withEmpty = `<mxfile><diagram id="a" name="Page-1">${MODEL_XML}</diagram><diagram id="b" name="Empty"></diagram></mxfile>`
const r = parseDrawioFileContent(withEmpty)
expect(r).toEqual({
ok: true,
xml: withEmpty,
hadCompressedPages: false,
})
expect(r).toEqual({ ok: true, xml: withEmpty })
})
it("rejects empty files", () => {
@@ -132,30 +124,3 @@ describe("parseDrawioFileContent", () => {
if (!r.ok) expect(r.error).toContain('"Broken"')
})
})
describe("hadCompressedPages", () => {
it("is false for a plain-XML mxfile", () => {
const r = parseDrawioFileContent(PLAIN_MXFILE)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(false)
})
it("is false for a bare mxGraphModel", () => {
const r = parseDrawioFileContent(MODEL_XML)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(false)
})
it("is true for a fully compressed mxfile", () => {
const r = parseDrawioFileContent(COMPRESSED_MXFILE)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(true)
})
it("is true for a mixed plain/compressed file", () => {
const mixed = `<mxfile><diagram id="a" name="Plain">${MODEL_XML}</diagram><diagram id="b" name="Squeezed">${drawioCompress(MODEL_XML)}</diagram></mxfile>`
const r = parseDrawioFileContent(mixed)
expect(r.ok).toBe(true)
if (r.ok) expect(r.hadCompressedPages).toBe(true)
})
})
@@ -188,51 +188,3 @@ describe("MCP server wiring", () => {
expect(text.length).toBeLessThanOrEqual(15000)
})
})
describe("load_diagram dual-source arguments", () => {
it("advertises both optional 'path' and 'xml' sources", async () => {
const resp = await send("tools/list", {})
const load = resp.result.tools.find(
(t: { name: string }) => t.name === "load_diagram",
)
const props = load?.inputSchema?.properties ?? {}
expect(props.path).toBeTruthy()
expect(props.xml).toBeTruthy()
const required: string[] = load?.inputSchema?.required ?? []
expect(required).not.toContain("path")
expect(required).not.toContain("xml")
})
it("rejects passing both 'path' and 'xml'", async () => {
// Argument validation fires before the session check: no session
// exists in this harness, so a both-args call must report the
// mutual-exclusion error, not "No active session".
const resp = await send("tools/call", {
name: "load_diagram",
arguments: { path: "/tmp/x.drawio", xml: "<mxfile/>" },
})
expect(resp.error, JSON.stringify(resp.error)).toBeUndefined()
expect(resp.result?.isError).toBe(true)
expect(resp.result?.content?.[0]?.text).toContain("not both")
})
it("rejects passing neither 'path' nor 'xml'", async () => {
const resp = await send("tools/call", {
name: "load_diagram",
arguments: {},
})
expect(resp.result?.isError).toBe(true)
expect(resp.result?.content?.[0]?.text).toContain("either 'path'")
})
it("accepts 'xml' alone as a source (fails only on the missing session)", async () => {
const resp = await send("tools/call", {
name: "load_diagram",
arguments: {
xml: '<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>',
},
})
expect(resp.result?.isError).toBe(true)
expect(resp.result?.content?.[0]?.text).toContain("No active session")
})
})