mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 04:29:51 +08:00
* feat(mcp-server): add source-reading and clear-canvas hints to the drawing guide Carry the missing web-prompt sentences into the MCP drawing guide: draw from a document, image or web page by reading it yourself first, compare image replications with screenshot_diagram, keep replies short after a successful draw, compose artistic requests from standard shapes, and clear the canvas by sending only the two root cells. Replace the "If the diagram is large" bullet with guidance on choosing create_new_diagram vs edit_diagram. The server INSTRUCTIONS mention reading files yourself and opening .drawio files with load_diagram; the create_new_diagram description documents the clear-canvas call, and its handler reports "Canvas cleared" when the prepared XML holds only the root cells. Add tests for both and the matching README lines in the mcp-server and claude-plugin packages. * feat(mcp-server): append the user's instructions.md to the drawing guide Read <DRAWIO_DATA_DIR>/instructions.md (default ~/.next-ai-drawio) on every call and append its first 5000 characters to the drawing guide under a "## Custom Instructions" heading, for start_session, get_drawing_guide and the diagram-workflow prompt. start_session now tells the model where the file lives. Document the feature in both READMEs and cover it with unit and server-wiring tests. * fix(mcp-server): review fixes for drawing guide text and custom instructions file * fix(mcp-server): Codex review fixes for the guide and custom instructions - Report "Canvas cleared" only for a one-page document; several empty pages get the page summary - Build the instructions.md path with path.join (Windows separators) - Keep the 15000-character guide budget in the wiring test - Plugin README: DRAWIO_DATA_DIR also holds instructions.md
192 lines
7.2 KiB
TypeScript
192 lines
7.2 KiB
TypeScript
/**
|
|
* Tests for wrapCellsInModel: the model may send only the mxCell elements of
|
|
* a page, like in the web app, and the server adds the wrapper and root cells.
|
|
*/
|
|
|
|
import { beforeAll, describe, expect, it } from "vitest"
|
|
import { installDomPolyfill } from "../src/dom.ts"
|
|
|
|
beforeAll(() => {
|
|
installDomPolyfill()
|
|
})
|
|
|
|
import { prepareNewDiagram, reservedIdError } from "../src/new-diagram.ts"
|
|
import { hasCells, wrapCellsInModel } from "../src/pages.ts"
|
|
import { validateAndFixXml } from "../src/xml-validation.ts"
|
|
|
|
const A = `<mxCell id="2" value="A" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>`
|
|
const B = `<mxCell id="3" value="B" vertex="1" parent="1"><mxGeometry x="200" y="0" width="80" height="40" as="geometry"/></mxCell>`
|
|
const ROOTS = `<mxCell id="0"/><mxCell id="1" parent="0"/>`
|
|
|
|
describe("wrapCellsInModel", () => {
|
|
it("wraps sibling cells so they pass validation", () => {
|
|
expect(validateAndFixXml(A + B).valid).toBe(false)
|
|
const wrapped = wrapCellsInModel(A + B)
|
|
expect(wrapped).toBe(
|
|
`<mxGraphModel><root>${ROOTS}${A}${B}</root></mxGraphModel>`,
|
|
)
|
|
expect(validateAndFixXml(wrapped).valid).toBe(true)
|
|
})
|
|
|
|
it("replaces root cells the model wrote itself", () => {
|
|
const wrapped = wrapCellsInModel(
|
|
`<mxCell id="0"></mxCell><mxCell id="1" parent="0"/>${A}`,
|
|
)
|
|
expect(wrapped.match(/id="0"/g)).toHaveLength(1)
|
|
expect(wrapped.match(/id="1"/g)).toHaveLength(1)
|
|
expect(wrapped).toContain(A)
|
|
})
|
|
|
|
it("unwraps a <root> and drops trailing provider tags", () => {
|
|
const wrapped = wrapCellsInModel(
|
|
`<root>${A}</root></invoke></function_calls>`,
|
|
)
|
|
expect(wrapped).toBe(
|
|
`<mxGraphModel><root>${ROOTS}${A}</root></mxGraphModel>`,
|
|
)
|
|
})
|
|
|
|
it("drops comments and text before the first cell", () => {
|
|
const expected = `<mxGraphModel><root>${ROOTS}${A}</root></mxGraphModel>`
|
|
expect(wrapCellsInModel(`Here is the diagram: ${A}`)).toBe(expected)
|
|
expect(wrapCellsInModel(`<!-- boxes -->\n${A}`)).toBe(expected)
|
|
})
|
|
|
|
it("leaves <mxGraphModel> and <mxfile> input unchanged", () => {
|
|
const model = `<mxGraphModel><root>${ROOTS}${A}</root></mxGraphModel>`
|
|
const file = `<mxfile><diagram id="p" name="P">${model}</diagram></mxfile>`
|
|
expect(wrapCellsInModel(model)).toBe(model)
|
|
expect(wrapCellsInModel(file)).toBe(file)
|
|
})
|
|
|
|
it("replaces root cells written over two lines", () => {
|
|
const wrapped = wrapCellsInModel(
|
|
`<mxCell id="0">\n</mxCell>\n<mxCell id="1" parent="0">\n</mxCell>\n${A}`,
|
|
)
|
|
expect(wrapped).toBe(
|
|
`<mxGraphModel><root>${ROOTS}${A}</root></mxGraphModel>`,
|
|
)
|
|
})
|
|
|
|
it("keeps a UserObject cell at the end", () => {
|
|
const wrapped = `<UserObject id="u" label="U"><mxCell vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell></UserObject>`
|
|
expect(wrapCellsInModel(A + wrapped)).toContain(wrapped)
|
|
expect(wrapCellsInModel(`${wrapped}</invoke>`)).toBe(
|
|
`<mxGraphModel><root>${ROOTS}${wrapped}</root></mxGraphModel>`,
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("hasCells", () => {
|
|
it("counts cells written with single quotes", () => {
|
|
expect(hasCells(`<mxCell id='2' vertex='1' parent='1'/>`)).toBe(true)
|
|
expect(hasCells(`<mxCell id='0'/><mxCell id='1' parent='0'/>`)).toBe(
|
|
false,
|
|
)
|
|
})
|
|
|
|
it("counts a compressed page as having cells", () => {
|
|
// draw.io's compressed format: the page's model is text
|
|
expect(
|
|
hasCells(
|
|
`<mxfile><diagram id="p" name="P">dZHBDoIwDIafhjtsGPWM6MkTB8/LVmBxrGQMQZ/eLRuIUS/bv/VfmybF</diagram></mxfile>`,
|
|
),
|
|
).toBe(true)
|
|
})
|
|
|
|
it("counts cells with spaces around the =", () => {
|
|
expect(hasCells(`<mxCell id = "a" vertex="1" parent="1"/>`)).toBe(true)
|
|
expect(
|
|
hasCells(`<mxCell id = "0"/><mxCell id = "1" parent="0"/>`),
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("reservedIdError", () => {
|
|
it("finds a shape that uses a root cell id", () => {
|
|
expect(reservedIdError(A)).toBeNull()
|
|
expect(reservedIdError(ROOTS + A)).toBeNull()
|
|
expect(
|
|
reservedIdError(`<mxCell id = "1" vertex="1" parent="1"/>`),
|
|
).toMatch(/"0" and "1"/)
|
|
// A linked or labelled shape has its id on the wrapper
|
|
expect(
|
|
reservedIdError(
|
|
`<UserObject id="1" label="Docs"><mxCell vertex="1" parent="1"><mxGeometry as="geometry"/></mxCell></UserObject>`,
|
|
),
|
|
).toMatch(/"0" and "1"/)
|
|
// Other attributes ending in id, and id text inside a label
|
|
expect(
|
|
reservedIdError(
|
|
`<UserObject label="Server" rack-id="1" id="7"><mxCell vertex="1" parent="1"/></UserObject>`,
|
|
),
|
|
).toBeNull()
|
|
expect(
|
|
reservedIdError(
|
|
`<UserObject id="u" label="Example: id='1'"><mxCell vertex="1" parent="1"/></UserObject>`,
|
|
),
|
|
).toBeNull()
|
|
expect(
|
|
reservedIdError(
|
|
`<mxCell id="5" data-id="0" vertex="1" parent="1"/>`,
|
|
),
|
|
).toBeNull()
|
|
expect(
|
|
reservedIdError(
|
|
`<mxCell id="5" value="id="1" vertex="1"" vertex="1" parent="1"/>`,
|
|
),
|
|
).toBeNull()
|
|
// A whole model has its own root cells
|
|
expect(
|
|
reservedIdError(
|
|
`<mxGraphModel><root>${ROOTS}</root></mxGraphModel>`,
|
|
),
|
|
).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("prepareNewDiagram", () => {
|
|
it("rejects a shape that uses a root cell id", () => {
|
|
// It would clash with the added root cell "1" and be renamed,
|
|
// which breaks the edges that point to it
|
|
const cells =
|
|
`<mxCell id="1" value="Start" vertex="1" parent="1"><mxGeometry x="0" y="0" width="80" height="40" as="geometry"/></mxCell>` +
|
|
`<mxCell id="3" edge="1" parent="1" source="1" target="2"><mxGeometry relative="1" as="geometry"/></mxCell>`
|
|
const out = prepareNewDiagram(cells)
|
|
expect(out.ok).toBe(false)
|
|
if (out.ok) return
|
|
expect(out.error).toMatch(/"0" and "1"/)
|
|
})
|
|
|
|
it("still accepts the root cells sent along with the shapes", () => {
|
|
const out = prepareNewDiagram(
|
|
`<mxCell id="0"/><mxCell id="1" parent="0"/>${A}`,
|
|
)
|
|
expect(out.ok).toBe(true)
|
|
})
|
|
|
|
it("accepts the root cells alone as a blank page", () => {
|
|
const out = prepareNewDiagram(
|
|
`<mxCell id="0"/><mxCell id="1" parent="0"/>`,
|
|
)
|
|
expect(out.ok).toBe(true)
|
|
if (!out.ok) return
|
|
expect(hasCells(out.xml)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe("labels that look like attributes", () => {
|
|
const layer = `<mxCell id="5" value="Move to id='1'" vertex="1" parent="1"/>`
|
|
|
|
it("keep their cell when the root cells are stripped", () => {
|
|
const wrapped = wrapCellsInModel(ROOTS + layer)
|
|
expect(wrapped).toBe(
|
|
`<mxGraphModel><root>${ROOTS}${layer}</root></mxGraphModel>`,
|
|
)
|
|
})
|
|
|
|
it("are not taken for a reserved id", () => {
|
|
expect(reservedIdError(layer)).toBeNull()
|
|
})
|
|
})
|