mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-11 03:59:58 +08:00
fix(mcp-server): make edit_diagram all-or-nothing and fix preview sync races
- edit_diagram applies nothing when any operation fails, rejects invalid or multi-cell new_xml, validates only the target page, and returns the current page XML on every rejection (including stale edits) - Fix get_diagram reading the old diagram right after an AI write: the preview pushed its sync reply with a newer version than it was taken at - Keep a user edit that loses the race with an AI write in history and tell the user in the preview - Autofix removes only exact foreign tags (a stray <mxGraph/> deleted <mxGraphModel>), fixes tag case, drops orphan <mxPoint>s, and rejects unknown element names in model XML - Edit empty and compressed pages; PNG exports use the page on screen; tag download exports; reload from the server after a page export - Expand ~ in paths, tell the model when the browser sync timed out, use registerPrompt, require SDK ^1.31.0
This commit is contained in:
@@ -8,12 +8,14 @@
|
||||
|
||||
import http from "node:http"
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import { addHistory } from "../src/history.js"
|
||||
import { addHistory, getHistory } from "../src/history.js"
|
||||
import {
|
||||
getState,
|
||||
requestSync,
|
||||
setState,
|
||||
shutdown,
|
||||
startHttpServer,
|
||||
waitForSync,
|
||||
} from "../src/http-server.js"
|
||||
|
||||
let port = 0
|
||||
@@ -189,6 +191,55 @@ describe("POST /api/state", () => {
|
||||
expect(getState(id)?.xml).toBe(xml)
|
||||
}
|
||||
})
|
||||
|
||||
it("keeps a rejected user edit in history", async () => {
|
||||
const id = "mcp-conflict-history"
|
||||
setState(id, "<mxfile>user v1</mxfile>", undefined, true)
|
||||
const aiVersion = setState(id, "<mxfile>AI edit</mxfile>")
|
||||
const before = getHistory(id).length
|
||||
|
||||
const stale = await postJson("/api/state", {
|
||||
sessionId: id,
|
||||
xml: "<mxfile>lost user edit</mxfile>",
|
||||
baseVersion: aiVersion - 1,
|
||||
})
|
||||
expect(stale.status).toBe(409)
|
||||
expect(JSON.parse(stale.body).savedToHistory).toBe(true)
|
||||
const history = getHistory(id)
|
||||
expect(history).toHaveLength(before + 1)
|
||||
expect(history.at(-1)?.xml).toBe("<mxfile>lost user edit</mxfile>")
|
||||
})
|
||||
|
||||
it("ends a pending sync when the sync reply is older than an AI write", async () => {
|
||||
const id = "mcp-stale-sync"
|
||||
setState(id, "<mxfile>before</mxfile>", undefined, true)
|
||||
const aiVersion = setState(id, "<mxfile>AI edit</mxfile>")
|
||||
requestSync(id)
|
||||
const before = getHistory(id).length
|
||||
|
||||
// The browser exported its old diagram, then loaded the AI write
|
||||
const stale = await postJson("/api/state", {
|
||||
sessionId: id,
|
||||
xml: "<mxfile>before</mxfile>",
|
||||
baseVersion: aiVersion - 1,
|
||||
source: "sync",
|
||||
})
|
||||
expect(stale.status).toBe(409)
|
||||
expect(JSON.parse(stale.body).savedToHistory).toBe(false)
|
||||
expect(getState(id)?.xml).toBe("<mxfile>AI edit</mxfile>")
|
||||
expect(getState(id)?.syncRequested).toBeUndefined()
|
||||
expect(getHistory(id)).toHaveLength(before)
|
||||
expect(await waitForSync(id, 200)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("preview page", () => {
|
||||
it("serves a script that parses", async () => {
|
||||
const res = await request("/?mcp=mcp-test-script")
|
||||
const script = res.body.match(/<script>([\s\S]*)<\/script>/)?.[1]
|
||||
expect(script).toBeTruthy()
|
||||
expect(() => new Function(script as string)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("history restore", () => {
|
||||
|
||||
Reference in New Issue
Block a user