Files
next-ai-draw-io/packages/mcp-server/tests/persistence.test.ts
T
dayuan.jiang 504d2fa812 fix(mcp-server): keep both pages when get_diagram meets a page export, and more review fixes
Found by the PR review, each with a test that failed first:
- get_diagram during a page export returned the one-page projection on
  screen as the whole document (6 of 6 times when timed so). The preview
  page no longer answers a sync while a projection shows, and syncs after
  reloading, so the poll that restores the real document exports it.
- Exports are numbered on the server too: a late result of an export that
  timed out was saved as the next export's file.
- In Chrome, a new_xml with a syntax error counted the <parsererror>
  element as a second cell, so the web app rejected edits that auto-fix
  repairs ("must contain exactly one cell").
- hasCells missed single-quoted ids, so screenshot_diagram called such a
  diagram empty and auto-save never created its file.
- A literal \n directly under a <diagram> that has a model passed
  validation; only text-only pages are compressed data.
- A wrapped mxCell repeating its UserObject's id took the wrapper's place
  in edits, so delete and update left an empty or nested wrapper.
- Bare cells with a shape or edge id of "0" or "1" are rejected with a
  clear message instead of being renamed, which broke their edges.
- DRAWIO_DATA_DIR expands ~, which JSON configs pass on as it is.
2026-10-04 23:04:21 +09:00

97 lines
3.5 KiB
TypeScript

/**
* Tests for the per-session .drawio auto-save (src/persistence.ts).
*/
import {
existsSync,
mkdtempSync,
readdirSync,
readFileSync,
utimesSync,
writeFileSync,
} from "node:fs"
import { homedir, tmpdir } from "node:os"
import { join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { Autosaver, defaultDataDir } from "../src/persistence.ts"
const DIAGRAM = `<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="a" vertex="1" parent="1"/></root></mxGraphModel></diagram></mxfile>`
const BLANK = `<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/></root></mxGraphModel></diagram></mxfile>`
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const tempDir = () => mkdtempSync(join(tmpdir(), "mcp-autosave-"))
describe("Autosaver", () => {
it("writes the latest XML once changes settle", async () => {
const dir = tempDir()
const saver = new Autosaver(dir, 30)
saver.schedule("mcp-a", DIAGRAM.replace('id="a"', 'id="first"'))
saver.schedule("mcp-a", DIAGRAM)
const path = saver.pathFor("mcp-a") as string
expect(existsSync(path)).toBe(false)
await sleep(80)
expect(readFileSync(path, "utf-8")).toBe(DIAGRAM)
expect(readdirSync(dir)).toEqual(["mcp-a.drawio"])
})
it("skips a blank page that was never saved, but saves clearing a diagram", async () => {
const dir = tempDir()
const saver = new Autosaver(dir, 10)
saver.schedule("mcp-blank", BLANK)
await sleep(40)
expect(existsSync(saver.pathFor("mcp-blank") as string)).toBe(false)
saver.schedule("mcp-b", DIAGRAM)
saver.flush()
saver.schedule("mcp-b", BLANK)
saver.flush()
expect(readFileSync(saver.pathFor("mcp-b") as string, "utf-8")).toBe(
BLANK,
)
})
it("keeps only the newest session files and never touches other files", () => {
const dir = tempDir()
const saver = new Autosaver(dir, 10, 2)
for (const [i, id] of ["mine", "mcp-old", "mcp-mid"].entries()) {
writeFileSync(join(dir, `${id}.drawio`), DIAGRAM)
utimesSync(join(dir, `${id}.drawio`), 1000 + i, 1000 + i)
}
saver.schedule("mcp-new", DIAGRAM)
saver.flush()
expect(readdirSync(dir).sort()).toEqual([
"mcp-mid.drawio",
"mcp-new.drawio",
"mine.drawio",
])
})
it("does nothing when saving is off", () => {
const saver = new Autosaver(null)
expect(saver.pathFor("mcp-x")).toBeNull()
saver.schedule("mcp-x", DIAGRAM)
saver.flush()
})
})
describe("defaultDataDir", () => {
const original = process.env.DRAWIO_DATA_DIR
afterEach(() => {
if (original === undefined) delete process.env.DRAWIO_DATA_DIR
else process.env.DRAWIO_DATA_DIR = original
})
it("reads DRAWIO_DATA_DIR, where off disables saving", () => {
process.env.DRAWIO_DATA_DIR = "off"
expect(defaultDataDir()).toBeNull()
process.env.DRAWIO_DATA_DIR = "/tmp/x"
expect(defaultDataDir()).toBe("/tmp/x")
delete process.env.DRAWIO_DATA_DIR
expect(defaultDataDir()).toMatch(/\.next-ai-drawio$/)
})
it("expands ~, which JSON configs pass on as it is", () => {
process.env.DRAWIO_DATA_DIR = "~/drawio-saves"
expect(defaultDataDir()).toBe(join(homedir(), "drawio-saves"))
})
})