Files
next-ai-draw-io/packages/mcp-server/tests/custom-instructions.test.ts
T
Dayuan Jiang c774a39c33 feat(mcp-server): drawing guide hints and the user's own drawing rules (#980)
* 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
2026-10-11 20:04:59 +09:00

100 lines
3.6 KiB
TypeScript

/**
* Tests for the user's instructions.md appended to the drawing guide
* (src/custom-instructions.ts).
*/
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"
import { homedir, tmpdir } from "node:os"
import { join } from "node:path"
import { afterEach, describe, expect, it } from "vitest"
import {
CUSTOM_INSTRUCTIONS_FILE,
customInstructionsDir,
guideWithCustomInstructions,
readCustomInstructions,
} from "../src/custom-instructions.ts"
import { DRAWING_GUIDE } from "../src/drawing-guide.ts"
const GUIDE = "# Drawing guide\n\nKeep edges orthogonal."
const tempDir = () => mkdtempSync(join(tmpdir(), "mcp-instructions-"))
const writeRules = (dir: string, text: string) =>
writeFileSync(join(dir, CUSTOM_INSTRUCTIONS_FILE), text)
describe("readCustomInstructions", () => {
it("gives an empty string when there is no file", () => {
const dir = tempDir()
expect(readCustomInstructions(dir)).toBe("")
expect(guideWithCustomInstructions(GUIDE, dir)).toBe(GUIDE)
})
it("appends the file's content under its own heading", () => {
const dir = tempDir()
writeRules(dir, "\nAlways draw in minimal style.\n\n")
expect(readCustomInstructions(dir)).toBe(
"Always draw in minimal style.",
)
expect(guideWithCustomInstructions(GUIDE, dir)).toBe(
`${GUIDE}\n\n## Custom Instructions\nAlways draw in minimal style.`,
)
})
it("cuts the content at 5000 characters", () => {
const dir = tempDir()
writeRules(dir, "x".repeat(6000))
expect(readCustomInstructions(dir)).toBe("x".repeat(5000))
})
it("gives an empty string when the file cannot be read", () => {
const dir = tempDir()
// A folder in place of the file: readFileSync fails with EISDIR
mkdirSync(join(dir, CUSTOM_INSTRUCTIONS_FILE))
expect(readCustomInstructions(dir)).toBe("")
expect(guideWithCustomInstructions(GUIDE, dir)).toBe(GUIDE)
})
it("reads the file again on every call", () => {
const dir = tempDir()
writeRules(dir, "First rule.")
expect(readCustomInstructions(dir)).toBe("First rule.")
writeRules(dir, "Second rule.")
expect(readCustomInstructions(dir)).toBe("Second rule.")
})
})
describe("customInstructionsDir", () => {
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("honours DRAWIO_DATA_DIR", () => {
const dir = tempDir()
writeRules(dir, "Use blue fill #dae8fc.")
process.env.DRAWIO_DATA_DIR = dir
expect(customInstructionsDir()).toBe(dir)
expect(guideWithCustomInstructions(GUIDE)).toBe(
`${GUIDE}\n\n## Custom Instructions\nUse blue fill #dae8fc.`,
)
})
it("expands ~, which JSON configs pass on as it is", () => {
process.env.DRAWIO_DATA_DIR = "~/drawio-saves"
expect(customInstructionsDir()).toBe(join(homedir(), "drawio-saves"))
})
it("falls back to the home folder when DRAWIO_DATA_DIR is off or unset", () => {
const home = join(homedir(), ".next-ai-drawio")
process.env.DRAWIO_DATA_DIR = "off"
expect(customInstructionsDir()).toBe(home)
delete process.env.DRAWIO_DATA_DIR
expect(customInstructionsDir()).toBe(home)
})
})
describe("DRAWING_GUIDE", () => {
it("stays within its own budget; instructions.md adds up to 5000 more", () => {
expect(DRAWING_GUIDE.length).toBeLessThanOrEqual(15000)
})
})