Files
next-ai-draw-io/tests/unit/mcp-shell-config.test.ts
T
dayuan.jiang a2ca229830 feat(mcp-server): the shell syncs with the server like the classic page
shell/mcp-sync-core.ts ports preview.js's protocol without the page or
React: polling GET /api/state every 2 s with the state id and version,
pushes of the user's edits with their base version and state, 409 and 413
handling, recovery of a recreated session (the tab's copy goes to History),
the token refresh after a 403, sync and export requests (PNG by page id,
SVG of another page through a transient one-page projection with autosave
ignored, the 600 ms wait for icons), thumbnails for History, and History
reads and restores that name the state the list belongs to.

A server write goes on the canvas as one undo step (loadDiagram commit,
with the change marked) when decideLoad finds it changes only the page on
screen, keeps the file variables and needs no load-only setting; the other
pages are compared with the MCP core's contentFingerprint. draw.io's own
re-serialized copy of such a write is recognized with isSameDocument and
not pushed back as an edit.

shell/use-mcp-sync.ts mounts the sync once inside DiagramProvider, reads
draw.io's autosave and load messages, and reports the connection state,
which the shell's status bar shows; notices come as toasts from the
dictionaries.
2026-10-11 20:56:07 +09:00

124 lines
4.5 KiB
TypeScript

/**
* The MCP's canvas shell takes its settings from the config the server
* writes into the page, and picks its language and theme in the browser.
*/
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { describe, expect, it } from "vitest"
import { i18n } from "@/lib/i18n/config"
import {
getShellDictionary,
matchLocale,
pickLocale,
} from "@/packages/mcp-server/shell/dictionary"
import { readShellConfig } from "@/packages/mcp-server/shell/runtime-config"
import {
isDarkPreferred,
THEME_STORAGE_KEY,
} from "@/packages/mcp-server/shell/theme"
const shellDir = join(process.cwd(), "packages/mcp-server/shell")
describe("shell runtime config", () => {
it("takes the server's values", () => {
const config = readShellConfig({
sessionId: "mcp-abc",
token: "0123",
apiBase: "/api",
drawioBaseUrl: "https://embed.diagrams.net/",
drawioUi: "sketch",
drawioLang: "ja",
drawioDark: "dark",
lang: "ja",
})
expect(config.sessionId).toBe("mcp-abc")
expect(config.drawioBaseUrl).toBe("https://embed.diagrams.net/")
expect(config.drawioUi).toBe("sketch")
expect(config.drawioDark).toBe("dark")
})
it("fills in defaults for what is missing or malformed", () => {
expect(readShellConfig(undefined)).toEqual({
sessionId: "",
token: "",
apiBase: "/api",
drawioBaseUrl: "/drawio/index.html",
drawioUi: "",
drawioLang: "",
drawioDark: "auto",
lang: "",
})
const odd = readShellConfig({
sessionId: 42,
drawioDark: "sometimes",
token: "t",
})
expect(odd.sessionId).toBe("")
expect(odd.drawioDark).toBe("auto")
expect(odd.token).toBe("t")
})
})
describe("shell language", () => {
it("maps language tags to the dictionaries we have", () => {
expect(matchLocale("en-US")).toBe("en")
expect(matchLocale("zh")).toBe("zh")
expect(matchLocale("zh-CN")).toBe("zh")
expect(matchLocale("zh-Hans-CN")).toBe("zh")
expect(matchLocale("zh-TW")).toBe("zh-Hant")
expect(matchLocale("zh-Hant")).toBe("zh-Hant")
expect(matchLocale("ZH-HK")).toBe("zh-Hant")
expect(matchLocale("ja-JP")).toBe("ja")
expect(matchLocale("de")).toBeNull()
expect(matchLocale("")).toBeNull()
})
it("prefers the server's language, then the browser's, then English", () => {
expect(pickLocale("zh-tw", ["en-US"])).toBe("zh-Hant")
expect(pickLocale("", ["de-DE", "ja-JP", "en"])).toBe("ja")
expect(pickLocale("de", ["fr"])).toBe("en")
expect(pickLocale("", [])).toBe("en")
})
it("has every dictionary, with the shell's own texts", () => {
for (const locale of i18n.locales) {
const dict = getShellDictionary(locale)
expect(Object.keys(dict.shell).sort(), locale).toEqual([
"aiChanged",
"connected",
"historyChanged",
"offline",
"restoreFailed",
"restoredFromFile",
"toggleTheme",
"tooLarge",
"unreachable",
"waiting",
])
expect(dict.shell.waiting, locale).toBeTruthy()
expect(dict.workspace.canvasLoading, locale).toBeTruthy()
}
})
})
describe("shell theme", () => {
it("follows the stored choice, then the server, then the system", () => {
expect(isDarkPreferred("true", "light", false)).toBe(true)
expect(isDarkPreferred("false", "dark", true)).toBe(false)
expect(isDarkPreferred(null, "dark", false)).toBe(true)
expect(isDarkPreferred(null, "light", true)).toBe(false)
expect(isDarkPreferred(null, "auto", true)).toBe(true)
expect(isDarkPreferred(null, "auto", false)).toBe(false)
expect(isDarkPreferred("maybe", "auto", true)).toBe(true)
})
it("is applied before the first paint by the page's own script", () => {
const html = readFileSync(join(shellDir, "index.html"), "utf8")
expect(THEME_STORAGE_KEY.startsWith("mcp:")).toBe(true)
expect(html).toContain(`localStorage.getItem("${THEME_STORAGE_KEY}")`)
expect(html).toContain("window.__MCP_CONFIG__ = {{CONFIG_JSON}};")
expect(html).toContain('src="/shell/shell.js"')
expect(html).toContain('href="/shell/shell.css"')
})
})