mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 12:39:52 +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:
@@ -120,34 +120,17 @@ describe("otherVersions", () => {
|
||||
})
|
||||
|
||||
describe("entries", () => {
|
||||
it("record who made them, when, and their page count", () => {
|
||||
it("record who made them", () => {
|
||||
const id = "history-fields"
|
||||
clearHistory(id)
|
||||
const before = Date.now()
|
||||
addHistory(id, A)
|
||||
addHistory(id, B, "<svg/>", "user")
|
||||
addHistory(
|
||||
id,
|
||||
`<mxfile><diagram id="p" name="P"><mxGraphModel><root><mxCell id="0"/></root></mxGraphModel></diagram><diagram id="q" name="Q"><mxGraphModel><root><mxCell id="0"/></root></mxGraphModel></diagram></mxfile>`,
|
||||
"",
|
||||
"restore",
|
||||
)
|
||||
addHistory(id, C, "", "restore")
|
||||
const [first, second, third] = getHistory(id)
|
||||
expect(first.source).toBeUndefined()
|
||||
expect(first.time).toBeGreaterThanOrEqual(before)
|
||||
expect(first.pages).toBe(1)
|
||||
expect(second.source).toBe("user")
|
||||
expect(second.svg).toBe("<svg/>")
|
||||
expect(third.source).toBe("restore")
|
||||
expect(third.pages).toBe(2)
|
||||
// A bare model is one page; text that is no diagram has none
|
||||
addHistory(id, "<mxGraphModel><root/></mxGraphModel>")
|
||||
addHistory(id, "not a diagram")
|
||||
expect(
|
||||
getHistory(id)
|
||||
.slice(-2)
|
||||
.map((e) => e.pages),
|
||||
).toEqual([1, 0])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -171,6 +154,31 @@ describe("currentEntryId and firstCopyIds", () => {
|
||||
])
|
||||
})
|
||||
|
||||
it("group a bare model with the first named page it matches, never a later one", () => {
|
||||
// A bare model (the browser's sync can send one) has no page name:
|
||||
// it is the same as "Arch" and as "Overview", which differ from each
|
||||
// other. Each entry names a first copy with its own content, so a
|
||||
// card never shows one document and restores another
|
||||
const id = "history-bare-groups"
|
||||
const named = (name: string) => A.replace('name="P"', `name="${name}"`)
|
||||
const bare = `<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="a" vertex="1" parent="1"/></root></mxGraphModel>`
|
||||
fill(id, [named("Arch"), bare, named("Overview")])
|
||||
const [arch, model, overview] = getHistory(id)
|
||||
const firsts = firstCopyIds(id)
|
||||
expect([...firsts.entries()]).toEqual([
|
||||
[arch.id, arch.id],
|
||||
[model.id, arch.id],
|
||||
[overview.id, overview.id],
|
||||
])
|
||||
// The entry named as first has the content of every entry in its
|
||||
// group (otherVersions of that content never lists it)
|
||||
for (const entry of getHistory(id)) {
|
||||
expect(otherVersions(id, entry.xml).map((e) => e.id)).not.toContain(
|
||||
firsts.get(entry.id),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it("agree with otherVersions on a page setting that only the canvas shows", () => {
|
||||
// The same cells with another background: one version to the MCP
|
||||
// core (the model's edits stay valid when the user changes a page
|
||||
|
||||
@@ -1197,8 +1197,6 @@ describe("GET /api/history for the version cards", () => {
|
||||
svg: "",
|
||||
xml: page("a"),
|
||||
source: null,
|
||||
time: a.time,
|
||||
pages: 1,
|
||||
firstId: a.id,
|
||||
})
|
||||
expect(d.entries[1]).toMatchObject({
|
||||
@@ -1209,6 +1207,49 @@ describe("GET /api/history for the version cards", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it("gives the state a History key, which a hand edit changes once and a further one not at all", async () => {
|
||||
// The shell reads History (every entry's XML and picture) again
|
||||
// only when the key changes
|
||||
const id = "mcp-history-key"
|
||||
setState(id, page("a"))
|
||||
addHistory(id, page("a"))
|
||||
const [a] = getHistory(id)
|
||||
const first = JSON.parse(
|
||||
(await request(`/api/state?sessionId=${id}`)).body,
|
||||
)
|
||||
expect(first.historyKey).toBe(`1:${a.id}:${a.id}`)
|
||||
// A hand edit: the canvas is at no entry now, History is the same
|
||||
const push = await postJson("/api/state", {
|
||||
sessionId: id,
|
||||
xml: page("b"),
|
||||
baseVersion: first.version,
|
||||
stateId: first.stateId,
|
||||
})
|
||||
expect(push.status).toBe(200)
|
||||
expect(JSON.parse(push.body).historyKey).toBe(`1:${a.id}:null`)
|
||||
const again = await postJson("/api/state", {
|
||||
sessionId: id,
|
||||
xml: page("c"),
|
||||
baseVersion: first.version + 1,
|
||||
stateId: first.stateId,
|
||||
})
|
||||
expect(JSON.parse(again.body).historyKey).toBe(`1:${a.id}:null`)
|
||||
const polled = JSON.parse(
|
||||
(await request(`/api/state?sessionId=${id}`)).body,
|
||||
)
|
||||
expect(polled.historyKey).toBe(`1:${a.id}:null`)
|
||||
// A write keeps the edit in History: the key names the new entry
|
||||
keepInHistory(id, page("c"))
|
||||
setState(id, page("d"))
|
||||
addHistory(id, page("d"))
|
||||
const [, c, d] = getHistory(id)
|
||||
expect(c.xml).toBe(page("c"))
|
||||
const after = JSON.parse(
|
||||
(await request(`/api/state?sessionId=${id}`)).body,
|
||||
)
|
||||
expect(after.historyKey).toBe(`3:${d.id}:${d.id}`)
|
||||
})
|
||||
|
||||
it("names the entry a restored copy stands for, and marks the copy as a restore", async () => {
|
||||
const id = "mcp-history-cards-restore"
|
||||
addHistory(id, page("a"))
|
||||
|
||||
Reference in New Issue
Block a user