mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 04:29:51 +08:00
fix(mcp-server): review fixes for the version cards and History
The shell's version cards (shell/node-versions-source.ts): - a version's change and undo target are the state it replaced, the History entry right before its first copy, not the card before it: after a restore those differ, and undo went to the wrong version (and not where restore_version steps_back=1 goes) - a card restores the newest copy of its content, as restore_version does, so page settings the user changed (a "user" copy) are kept - a blank page after a drawing is a clear of the canvas, a version of its own; only the blank page before any drawing is hidden - numbers and changes are keyed by content, not by the first copy's id, so a version keeps them when its first copy drops out of the server's 20-entry buffer; the caches start over for another server state (the process restarted: entry ids name other content) The server's History (src/history.ts): - firstCopyIds compares each entry with the first of every group only: a bare model matches any page name, so "same content" is not transitive, and a card could show one document and restore another - the time and pages fields had no reader; pages parsed every XML once more on every write Reading History (src/http-server.ts, shell/mcp-sync-core.ts): - GET /api/state and a push's answer carry a History key (entry count, newest id, the entry on the canvas); the shell reads History again only when it changes, so a hand edit no longer downloads every entry's XML and thumbnail - a failed History read is told again at the next poll - a History list from a state the poll has not seen yet is dropped
This commit is contained in:
@@ -1079,6 +1079,18 @@ describe("MCP sync History", () => {
|
||||
expect(t.notices).toEqual(["restoreFailed"])
|
||||
})
|
||||
|
||||
it("drops a History list of a state the poll has not seen yet", async () => {
|
||||
// Another process answers on the port; its History has other ids.
|
||||
// Its state is seen at the next poll, which tells the cards again
|
||||
const t = await inStep()
|
||||
const history = t.sync.fetchHistory()
|
||||
t.next("GET", "/api/history").answer({
|
||||
...historyList,
|
||||
body: { ...historyList.body, stateId: "S2" },
|
||||
})
|
||||
expect(await history).toBeNull()
|
||||
})
|
||||
|
||||
it("passes on the entry the server says is on the canvas", async () => {
|
||||
const t = await inStep()
|
||||
const history = t.sync.fetchHistory()
|
||||
@@ -1092,18 +1104,24 @@ describe("MCP sync History", () => {
|
||||
|
||||
// The version cards read History again when the server changed
|
||||
describe("MCP sync server change listener", () => {
|
||||
it("tells once per server version: the first poll, a write, the tab's own edit", async () => {
|
||||
it("tells once per History key: the first poll, a write, the tab's first edit, not its next", async () => {
|
||||
// The server's key names History (count, newest id) and the entry
|
||||
// on the canvas; the version cards read History only when it changes
|
||||
const t = open()
|
||||
let changes = 0
|
||||
const stop = t.sync.onServerChange(() => changes++)
|
||||
t.sync.setReady(true)
|
||||
let poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 2, "<mxfile>A</mxfile>"))
|
||||
t.next("GET").answer(
|
||||
state("S1", 2, "<mxfile>A</mxfile>", { historyKey: "1:0:0" }),
|
||||
)
|
||||
await poll
|
||||
expect(changes).toBe(1)
|
||||
// The same version again: nothing new
|
||||
// The same key again: nothing new
|
||||
poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 2, "<mxfile>A</mxfile>"))
|
||||
t.next("GET").answer(
|
||||
state("S1", 2, "<mxfile>A</mxfile>", { historyKey: "1:0:0" }),
|
||||
)
|
||||
await poll
|
||||
expect(changes).toBe(1)
|
||||
// The thumbnail of the write is stored: its entry has a picture now
|
||||
@@ -1115,29 +1133,82 @@ describe("MCP sync server change listener", () => {
|
||||
expect(changes).toBe(2)
|
||||
// An AI write
|
||||
poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 3, "<mxfile>B</mxfile>"))
|
||||
t.next("GET").answer(
|
||||
state("S1", 3, "<mxfile>B</mxfile>", { historyKey: "2:1:1" }),
|
||||
)
|
||||
await poll
|
||||
expect(changes).toBe(3)
|
||||
// The user's edit is saved: the canvas is at no version now
|
||||
await edit(t, "<mxfile>C</mxfile>")
|
||||
t.next("POST", "/api/state").answer({
|
||||
status: 200,
|
||||
body: { success: true, version: 4 },
|
||||
body: { success: true, version: 4, historyKey: "2:1:null" },
|
||||
})
|
||||
await t.settle()
|
||||
expect(changes).toBe(4)
|
||||
// The next poll sees the version the push already told about
|
||||
// The next poll sees the key the push already told about
|
||||
poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 4, "<mxfile>C</mxfile>"))
|
||||
t.next("GET").answer(
|
||||
state("S1", 4, "<mxfile>C</mxfile>", { historyKey: "2:1:null" }),
|
||||
)
|
||||
await poll
|
||||
expect(changes).toBe(4)
|
||||
// A further edit changes History not at all: no download of it
|
||||
await edit(t, "<mxfile>C2</mxfile>")
|
||||
t.next("POST", "/api/state").answer({
|
||||
status: 200,
|
||||
body: { success: true, version: 5, historyKey: "2:1:null" },
|
||||
})
|
||||
await t.settle()
|
||||
expect(changes).toBe(4)
|
||||
stop()
|
||||
poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 5, "<mxfile>D</mxfile>"))
|
||||
t.next("GET").answer(
|
||||
state("S1", 6, "<mxfile>D</mxfile>", { historyKey: "3:2:2" }),
|
||||
)
|
||||
await poll
|
||||
expect(changes).toBe(4)
|
||||
})
|
||||
|
||||
it("tells once per server version when the server sends no key", async () => {
|
||||
const t = open()
|
||||
let changes = 0
|
||||
t.sync.onServerChange(() => changes++)
|
||||
let poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 2, "<mxfile>A</mxfile>"))
|
||||
await poll
|
||||
poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 2, "<mxfile>A</mxfile>"))
|
||||
await poll
|
||||
expect(changes).toBe(1)
|
||||
poll = t.sync.poll()
|
||||
t.next("GET").answer(state("S1", 3, "<mxfile>B</mxfile>"))
|
||||
await poll
|
||||
expect(changes).toBe(2)
|
||||
})
|
||||
|
||||
it("tells again at the next poll when a History read failed", async () => {
|
||||
// The cards would otherwise keep the old list until the next write
|
||||
const t = await inStep()
|
||||
let changes = 0
|
||||
t.sync.onServerChange(() => changes++)
|
||||
const history = t.sync.fetchHistory()
|
||||
t.next("GET", "/api/history").answer({ status: 500, body: {} })
|
||||
expect(await history).toBeNull()
|
||||
const poll = t.sync.poll()
|
||||
t.next("GET", "/api/state").answer(state("S1", 2, "<mxfile>A</mxfile>"))
|
||||
await poll
|
||||
expect(changes).toBe(1)
|
||||
// Also when the request itself fails
|
||||
const again = t.sync.fetchHistory()
|
||||
t.next("GET", "/api/history").fail()
|
||||
expect(await again).toBeNull()
|
||||
const next = t.sync.poll()
|
||||
t.next("GET", "/api/state").answer(state("S1", 2, "<mxfile>A</mxfile>"))
|
||||
await next
|
||||
expect(changes).toBe(2)
|
||||
})
|
||||
|
||||
it("tells when a rejected edit was kept in History, and when the state was recreated", async () => {
|
||||
const t = await inStep()
|
||||
let changes = 0
|
||||
|
||||
Reference in New Issue
Block a user