Files
next-ai-draw-io/packages/mcp-server/tests/persistence.test.ts
T
dayuan.jiang 899924ba98 fix(mcp-server): fix duplicate page exports and auto-save deleting user files
- Preview page: keep an MCP export open until the server has its result.
  A poll answered before that still saw the request and started the same
  export again, so a parallel page export could write the previous
  page's image into its file
- Auto-save only removes its own mcp-*.drawio files, so a DRAWIO_DATA_DIR
  that also holds the user's diagrams keeps them
- screenshot_diagram captures a page that has no id attribute by loading
  just that page, like export_diagram
- An empty <Array as="points"/> no longer hides orphan mxPoints that
  come after it
- POST /api/state refuses a push without xml, which used to wipe the
  stored diagram
- Clear exportOptions when an export ends, reuse hasCells for the empty
  diagram check, and reword two log lines
2026-10-04 07:38:17 +09:00

92 lines
3.3 KiB
TypeScript

/**
* Tests for the per-session .drawio auto-save (src/persistence.ts).
*/
import {
existsSync,
mkdtempSync,
readdirSync,
readFileSync,
utimesSync,
writeFileSync,
} from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import { Autosaver, defaultDataDir } from "../src/persistence.js"
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$/)
})
})