Files
next-ai-draw-io/tests/unit/chat-route-edgeone.test.ts
T
dayuan.jiang b8b851fc5b fix: what the whole-PR review and Copilot found
- A redirect followed for a custom base URL also drops the key headers of
  providers that do not use Authorization (x-api-key, x-goog-api-key,
  api-key) when it goes to another origin.
- A second Enter or click while a message is being prepared (attachments
  read, diagram exported) no longer sends it twice.
- The admin Test on the deployment's own endpoints (EdgeOne, the server's
  keyless Ollama, an address on the server's network) counts toward the
  quota like a chat; the chat and the Test share one rule for it. The
  Test of an Azure entry set up by AZURE_RESOURCE_NAME only goes where chat
  goes.
- EdgeOne's function is called at the site root again, as on main: EdgeOne
  serves edge functions there, outside Next's base path.
- MCP History: the state before a write is kept unless the browser saved
  no change of the user's since the last server write (draw.io's sync copy
  of it adds no entry), and the dedupe compares the exact text again, so a
  change of page size or other settings only is its own version.
- MCP: an edit keeps untouched labels as draw.io shows them (a literal line
  break in an attribute is a space); a new document of empty pages the
  user named is auto-saved; load_diagram reads only regular files, so a
  pipe cannot hold up the other write tools; the preview does not load
  back its own push still on its way (an undo made meanwhile is saved).
- Two overlapping saves of a new chat no longer reload the canvas from the
  older copy.
- At most three screenshot checks per user turn, passed or failed, as
  documented.
- Desktop: the main window navigates only within the app (draw.io stays in
  its frame); a presets file that is not JSON and cannot be moved aside is
  not overwritten.
- A last self-closing cell with a raw "<" in a value is not taken for cut
  off output.
- README: Material Design shapes load their icons from fonts.gstatic.com.
2026-10-05 21:19:32 +09:00

140 lines
4.7 KiB
TypeScript

// @vitest-environment node
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
vi.mock("@/lib/dynamo-quota-manager", () => ({
isQuotaEnabled: () => false,
checkAndIncrementRequest: async () => ({ allowed: true }),
recordTokenUsage: async () => {},
}))
import { POST as chat } from "@/app/api/chat/route"
const ENV = ["AI_MODELS_CONFIG", "AI_PROVIDER", "AI_MODEL"]
const saved: Record<string, string | undefined> = {}
const calls: Array<{ url: string; headers: Headers }> = []
beforeEach(() => {
for (const k of ENV) saved[k] = process.env[k]
delete process.env.AI_PROVIDER
delete process.env.AI_MODEL
calls.length = 0
vi.stubGlobal(
"fetch",
vi.fn(async (url: string, init: RequestInit) => {
calls.push({ url: String(url), headers: new Headers(init.headers) })
throw new Error("no network in tests")
}),
)
})
afterEach(() => {
for (const k of ENV) {
if (saved[k] === undefined) delete process.env[k]
else process.env[k] = saved[k]
}
vi.unstubAllGlobals()
})
describe("EdgeOne as a server model", () => {
it("calls the site's Edge AI function with the cookies", async () => {
// An admin panel or ai-models.json provider: the client sends the
// provider name's slug, not "edgeone"
process.env.AI_MODELS_CONFIG = JSON.stringify({
providers: [
{
name: "Edge Pages",
provider: "edgeone",
models: ["@tx/deepseek-ai/deepseek-v3-0324"],
},
],
})
const res = await chat(
new Request("http://localhost/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-ai-provider": "edge-pages",
"x-selected-model-id":
"server:edge-pages:@tx/deepseek-ai/deepseek-v3-0324",
cookie: "eo_token=t; eo_time=1",
},
body: JSON.stringify({
messages: [
{
id: "u1",
role: "user",
parts: [{ type: "text", text: "Draw two boxes" }],
},
],
xml: "",
}),
}),
)
await res.text()
expect(calls[0]?.url).toBe(
"http://localhost/api/edgeai/chat/completions",
)
expect(calls[0]?.headers.get("cookie")).toBe("eo_token=t; eo_time=1")
})
})
const send = (headers: Record<string, string>) =>
chat(
new Request("http://localhost/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json", ...headers },
body: JSON.stringify({
messages: [
{
id: "u1",
role: "user",
parts: [{ type: "text", text: "Draw two boxes" }],
},
],
xml: "",
}),
}),
).then((r) => r.text())
describe("EdgeOne endpoints", () => {
it("works when the deployment names EdgeOne only in AI_PROVIDER", async () => {
process.env.AI_PROVIDER = "edgeone"
process.env.AI_MODEL = "@tx/deepseek-ai/deepseek-v3-0324"
await send({})
expect(calls[0]?.url).toBe(
"http://localhost/api/edgeai/chat/completions",
)
})
it("always calls the site's own function, whatever URL the request names", async () => {
// Another host would get the user's EdgeOne cookies
await send({
"x-ai-provider": "edgeone",
"x-ai-model": "@tx/deepseek-ai/deepseek-v3-0324",
"x-ai-base-url": "https://elsewhere.example/api/edgeai",
cookie: "eo_token=t",
})
expect(calls[0]?.url).toBe(
"http://localhost/api/edgeai/chat/completions",
)
})
it("calls the function at the site root, also with a base path", async () => {
const savedPath = process.env.NEXT_PUBLIC_BASE_PATH
process.env.NEXT_PUBLIC_BASE_PATH = "/draw"
try {
await send({
"x-ai-provider": "edgeone",
"x-ai-model": "@tx/deepseek-ai/deepseek-v3-0324",
})
expect(calls[0]?.url).toBe(
"http://localhost/api/edgeai/chat/completions",
)
} finally {
if (savedPath === undefined)
delete process.env.NEXT_PUBLIC_BASE_PATH
else process.env.NEXT_PUBLIC_BASE_PATH = savedPath
}
})
})