mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-07 10:17:47 +08:00
Found by the second PR review: - After a streamed edit, an older render of the stream stored the edit's original diagram again, and the next failed request (no quota, a lost connection) put that old diagram back on the canvas. The tool handler now marks its call as handled, so the preview code leaves it alone. - An edit applied before the UI showed an earlier broken edit's error was erased when that error undid its preview, or was built on that preview. The handler now starts from the diagram before all unhandled previews, and reads the diagram state that updates at once. - A failed or stopped display_diagram left its half drawn diagram on the canvas. Its preview is undone now, like an edit's. - "New chat" cleared a chat that could not be saved (storage full). - The settings dialog showed a model list, a fetch error or a test result on the provider that was opened after the request started, and marked a model id changed during the test as tested. - A tool call with broken JSON was shown as cut off by the output limit. - History entries and session thumbnails could pair with a later diagram when draw.io answered an export late. - A server model saved before non-ASCII provider names got into the id was reset to the default model.
178 lines
6.2 KiB
TypeScript
178 lines
6.2 KiB
TypeScript
import { TRUNCATED_XML } from "./fixtures/diagrams"
|
|
import {
|
|
createErrorMock,
|
|
expect,
|
|
getChatInput,
|
|
getIframe,
|
|
sendMessage,
|
|
test,
|
|
} from "./lib/fixtures"
|
|
|
|
test.describe("Error Handling", () => {
|
|
test("displays error message when API returns 500", async ({ page }) => {
|
|
await page.route(
|
|
"**/api/chat",
|
|
createErrorMock(500, "Internal server error"),
|
|
)
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await sendMessage(page, "Draw a cat")
|
|
|
|
// Should show error indication
|
|
const errorIndicator = page
|
|
.locator('[role="alert"]')
|
|
.or(page.locator("[data-sonner-toast]"))
|
|
.or(page.locator("text=/error|failed|something went wrong/i"))
|
|
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
|
|
|
// User should be able to type again
|
|
const chatInput = getChatInput(page)
|
|
await chatInput.fill("Retry message")
|
|
await expect(chatInput).toHaveValue("Retry message")
|
|
})
|
|
|
|
test("displays error message when API returns 429 rate limit", async ({
|
|
page,
|
|
}) => {
|
|
await page.route(
|
|
"**/api/chat",
|
|
createErrorMock(429, "Rate limit exceeded"),
|
|
)
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await sendMessage(page, "Draw a cat")
|
|
|
|
// Should show error indication for rate limit
|
|
const errorIndicator = page
|
|
.locator('[role="alert"]')
|
|
.or(page.locator("[data-sonner-toast]"))
|
|
.or(page.locator("text=/rate limit|too many|try again/i"))
|
|
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
|
|
|
// User should be able to type again
|
|
const chatInput = getChatInput(page)
|
|
await chatInput.fill("Retry after rate limit")
|
|
await expect(chatInput).toHaveValue("Retry after rate limit")
|
|
})
|
|
|
|
test("handles network timeout gracefully", async ({ page }) => {
|
|
await page.route("**/api/chat", async (route) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
await route.abort("timedout")
|
|
})
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await sendMessage(page, "Draw a cat")
|
|
|
|
// Should show error indication for network failure
|
|
const errorIndicator = page
|
|
.locator('[role="alert"]')
|
|
.or(page.locator("[data-sonner-toast]"))
|
|
.or(page.locator("text=/error|failed|network|timeout/i"))
|
|
await expect(errorIndicator.first()).toBeVisible({ timeout: 10000 })
|
|
|
|
// After timeout, user should be able to type again
|
|
const chatInput = getChatInput(page)
|
|
await chatInput.fill("Try again after timeout")
|
|
await expect(chatInput).toHaveValue("Try again after timeout")
|
|
})
|
|
|
|
test("shows truncated badge for incomplete XML", async ({ page }) => {
|
|
const toolCallId = `call_${Date.now()}`
|
|
const textId = `text_${Date.now()}`
|
|
const messageId = `msg_${Date.now()}`
|
|
|
|
const events = [
|
|
{ type: "start", messageId },
|
|
{ type: "text-start", id: textId },
|
|
{ type: "text-delta", id: textId, delta: "Creating diagram..." },
|
|
{ type: "text-end", id: textId },
|
|
{
|
|
type: "tool-input-start",
|
|
toolCallId,
|
|
toolName: "display_diagram",
|
|
},
|
|
{
|
|
type: "tool-input-available",
|
|
toolCallId,
|
|
toolName: "display_diagram",
|
|
input: { xml: TRUNCATED_XML },
|
|
},
|
|
{
|
|
type: "tool-output-error",
|
|
toolCallId,
|
|
error: "XML validation failed",
|
|
},
|
|
{ type: "finish" },
|
|
]
|
|
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body:
|
|
events
|
|
.map((e) => `data: ${JSON.stringify(e)}\n\n`)
|
|
.join("") + "data: [DONE]\n\n",
|
|
})
|
|
})
|
|
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
|
|
await sendMessage(page, "Draw something")
|
|
|
|
// Should show truncated badge
|
|
await expect(page.locator('text="Truncated"')).toBeVisible({
|
|
timeout: 15000,
|
|
})
|
|
})
|
|
|
|
test("shows the error of a tool call with broken input", async ({
|
|
page,
|
|
}) => {
|
|
// Invalid JSON that the server could not repair: not a length limit
|
|
const toolCallId = `call_${Date.now()}`
|
|
const events = [
|
|
{ type: "start", messageId: `msg_${Date.now()}` },
|
|
{
|
|
type: "tool-input-start",
|
|
toolCallId,
|
|
toolName: "display_diagram",
|
|
},
|
|
{
|
|
type: "tool-input-error",
|
|
toolCallId,
|
|
toolName: "display_diagram",
|
|
input: '{"xml": "<mxCell value="a"/>"}',
|
|
errorText: "Invalid input for tool display_diagram",
|
|
},
|
|
{ type: "finish" },
|
|
]
|
|
await page.route("**/api/chat", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "text/event-stream",
|
|
body:
|
|
events
|
|
.map((e) => `data: ${JSON.stringify(e)}\n\n`)
|
|
.join("") + "data: [DONE]\n\n",
|
|
})
|
|
})
|
|
await page.goto("/", { waitUntil: "networkidle" })
|
|
await getIframe(page).waitFor({ state: "visible", timeout: 30000 })
|
|
await sendMessage(page, "Draw something")
|
|
|
|
await expect(
|
|
page.getByText("Invalid input for tool display_diagram").first(),
|
|
).toBeVisible({ timeout: 15000 })
|
|
await expect(page.locator('text="Truncated"')).toHaveCount(0)
|
|
})
|
|
})
|