mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-09 11:17:04 +08:00
- A call the server runs (get_shape_library) still reaches the browser's tool handler, and it dropped the stored diagram of an earlier broken edit, whose preview then stayed. Only the tools that draw take it now. - A display_diagram whose final XML fails the checks loads the diagram from before its preview again, as a failed edit does. - After Stop, a tool result that arrives later (a screenshot check still running) no longer sends a new request; Stop also skips calls the tool handler already took. - New chat and opening another chat kept nothing of a diagram drawn without messages when it could not be saved; now they stay on it. - The settings dialog drops a model list or test result whose provider credentials changed meanwhile, also in another tab. - A saved provider this version does not know crashed the whole page on load; it is skipped. - The input emptied a moment after the message showed in the chat, so it briefly appeared twice (seen as a flaky e2e test). - The desktop app's preset switch on the same port refetches the server models instead of reloading the page, which lost unsent attachments.
226 lines
7.4 KiB
TypeScript
226 lines
7.4 KiB
TypeScript
import { act, cleanup, renderHook, waitFor } from "@testing-library/react"
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
import { useModelConfig } from "@/hooks/use-model-config"
|
|
import type { FlattenedServerModel } from "@/lib/server-model-config"
|
|
import { STORAGE_KEYS } from "@/lib/storage"
|
|
import type { MultiModelConfig } from "@/lib/types/model-config"
|
|
|
|
const SERVER_MODELS: FlattenedServerModel[] = [
|
|
{
|
|
id: "server:openai-main:gpt-4o-mini",
|
|
modelId: "gpt-4o-mini",
|
|
provider: "openai",
|
|
providerLabel: "OpenAI Main",
|
|
isDefault: false,
|
|
},
|
|
{
|
|
id: "server:openai-main:gpt-4o",
|
|
modelId: "gpt-4o",
|
|
provider: "openai",
|
|
providerLabel: "OpenAI Main",
|
|
isDefault: true,
|
|
},
|
|
]
|
|
|
|
const USER_CONFIG: MultiModelConfig = {
|
|
version: 1,
|
|
providers: [
|
|
{
|
|
id: "p1",
|
|
provider: "openai",
|
|
apiKey: "sk-test",
|
|
models: [{ id: "m1", modelId: "gpt-4o" }],
|
|
},
|
|
],
|
|
}
|
|
|
|
function storeConfig(config: MultiModelConfig) {
|
|
localStorage.setItem(STORAGE_KEYS.modelConfigs, JSON.stringify(config))
|
|
}
|
|
|
|
async function renderLoaded() {
|
|
const hook = renderHook(() => useModelConfig())
|
|
await waitFor(() => expect(hook.result.current.isLoaded).toBe(true))
|
|
return hook
|
|
}
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear()
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => ({
|
|
ok: true,
|
|
json: async () => ({ models: SERVER_MODELS }),
|
|
})),
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe("useModelConfig server model selection", () => {
|
|
it("replaces a saved server model that no longer exists", async () => {
|
|
storeConfig({
|
|
...USER_CONFIG,
|
|
selectedModelId: "server:openai-production:gpt-4o",
|
|
})
|
|
const { result } = await renderLoaded()
|
|
expect(result.current.selectedModelId).toBe("server:openai-main:gpt-4o")
|
|
})
|
|
|
|
it("keeps a server model saved under its old id", async () => {
|
|
// Before non-ASCII provider names got their own slug, "主力 OpenAI"
|
|
// became "openai"
|
|
const renamed: FlattenedServerModel = {
|
|
id: "server:4e3b-529b-openai:gpt-4o-mini",
|
|
modelId: "gpt-4o-mini",
|
|
provider: "openai",
|
|
providerLabel: "主力 OpenAI",
|
|
isDefault: false,
|
|
}
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => ({
|
|
ok: true,
|
|
json: async () => ({ models: [...SERVER_MODELS, renamed] }),
|
|
})),
|
|
)
|
|
storeConfig({
|
|
...USER_CONFIG,
|
|
selectedModelId: "server:openai:gpt-4o-mini",
|
|
})
|
|
const { result } = await renderLoaded()
|
|
expect(result.current.selectedModelId).toBe(renamed.id)
|
|
})
|
|
|
|
it("keeps a saved server model that still exists", async () => {
|
|
storeConfig({
|
|
...USER_CONFIG,
|
|
selectedModelId: "server:openai-main:gpt-4o-mini",
|
|
})
|
|
const { result } = await renderLoaded()
|
|
expect(result.current.selectedModelId).toBe(
|
|
"server:openai-main:gpt-4o-mini",
|
|
)
|
|
})
|
|
|
|
it("skips a saved provider this version does not know", async () => {
|
|
// Saved by another version, or edited by hand: it used to crash the
|
|
// whole page on load
|
|
storeConfig({
|
|
...USER_CONFIG,
|
|
providers: [
|
|
...USER_CONFIG.providers,
|
|
{
|
|
id: "p9",
|
|
provider: "not-a-provider" as any,
|
|
apiKey: "k",
|
|
models: [{ id: "m9", modelId: "x" }],
|
|
},
|
|
],
|
|
})
|
|
const { result } = await renderLoaded()
|
|
expect(result.current.config.providers.map((p) => p.id)).toEqual(["p1"])
|
|
expect(result.current.models.map((m) => m.id)).toContain("m1")
|
|
})
|
|
|
|
it("keeps a selected user model", async () => {
|
|
storeConfig({ ...USER_CONFIG, selectedModelId: "m1" })
|
|
const { result } = await renderLoaded()
|
|
expect(result.current.selectedModelId).toBe("m1")
|
|
})
|
|
|
|
it("falls back to the default server model when the selected model is deleted", async () => {
|
|
storeConfig({ ...USER_CONFIG, selectedModelId: "m1" })
|
|
const { result } = await renderLoaded()
|
|
act(() => result.current.deleteModel("p1", "m1"))
|
|
expect(result.current.selectedModelId).toBe("server:openai-main:gpt-4o")
|
|
})
|
|
|
|
it("falls back to the default server model when the selected provider is deleted", async () => {
|
|
storeConfig({ ...USER_CONFIG, selectedModelId: "m1" })
|
|
const { result } = await renderLoaded()
|
|
act(() => result.current.deleteProvider("p1"))
|
|
expect(result.current.selectedModelId).toBe("server:openai-main:gpt-4o")
|
|
})
|
|
})
|
|
|
|
describe("useModelConfig in the desktop app", () => {
|
|
it("reloads the server models after a preset switch restarts the server", async () => {
|
|
// The new preset offers other models; the saved one is gone
|
|
let restarted: (() => void) | undefined
|
|
;(window as any).electronAPI = {
|
|
onServerRestarted: (callback: () => void) => {
|
|
restarted = callback
|
|
return () => {
|
|
restarted = undefined
|
|
}
|
|
},
|
|
}
|
|
try {
|
|
storeConfig({
|
|
...USER_CONFIG,
|
|
selectedModelId: "server:openai-main:gpt-4o-mini",
|
|
})
|
|
const { result } = await renderLoaded()
|
|
await waitFor(() => expect(restarted).toBeDefined())
|
|
const nextModels: FlattenedServerModel[] = [
|
|
{
|
|
id: "server:claude:claude-sonnet-5-5",
|
|
modelId: "claude-sonnet-5-5",
|
|
provider: "anthropic",
|
|
providerLabel: "Claude",
|
|
isDefault: true,
|
|
},
|
|
]
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async () => ({
|
|
ok: true,
|
|
json: async () => ({ models: nextModels }),
|
|
})),
|
|
)
|
|
act(() => restarted?.())
|
|
await waitFor(() =>
|
|
expect(result.current.selectedModelId).toBe(
|
|
"server:claude:claude-sonnet-5-5",
|
|
),
|
|
)
|
|
} finally {
|
|
delete (window as any).electronAPI
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("useModelConfig across tabs", () => {
|
|
it("reloads the config when another tab saves it", async () => {
|
|
storeConfig({ ...USER_CONFIG, selectedModelId: "m1" })
|
|
const { result } = await renderLoaded()
|
|
|
|
const fromOtherTab: MultiModelConfig = {
|
|
...USER_CONFIG,
|
|
providers: [
|
|
...USER_CONFIG.providers,
|
|
{
|
|
id: "p2",
|
|
provider: "anthropic",
|
|
apiKey: "sk-ant",
|
|
models: [{ id: "m2", modelId: "claude-sonnet-4-5" }],
|
|
},
|
|
],
|
|
selectedModelId: "m2",
|
|
}
|
|
act(() => {
|
|
storeConfig(fromOtherTab)
|
|
window.dispatchEvent(
|
|
new StorageEvent("storage", { key: STORAGE_KEYS.modelConfigs }),
|
|
)
|
|
})
|
|
|
|
expect(result.current.selectedModelId).toBe("m2")
|
|
expect(result.current.config.providers).toHaveLength(2)
|
|
})
|
|
})
|