Files
next-ai-draw-io/tests/unit/providers.test.ts
T
dayuan.jiang 717f7e9445 feat(settings): providers that can list their models fetch the list instead of a hand-written one
Step 7 of the provider refactor plan, an intended UI behavior change. The
hand-written SUGGESTED_MODELS table (179 ids for 19 providers) is gone. The
six providers that cannot list their models online (Bedrock, Vertex AI,
Azure, EdgeOne, Doubao, MiniMax) keep a short list on their record, still
get its first model when added, and still show quick-add chips and the admin
panel's dropdown. For the other 18 the settings dialog fetches the provider's
own list once the user has entered credentials on the page while it has no
model yet (600 ms after the typing pauses), and opens the picker with the
result; the 'Get all models' button and the model id input work as before.
Credentials a provider already had when its page opened fetch nothing by
themselves. The admin panel shows the 'suggested' dropdown only for the six.

Two e2e tests follow the change: the first-provider flow mocks the list
endpoint and picks the model from the list; the empty-list test checks that a
new key fetches again and opens the list once it has models.
2026-10-12 21:17:51 +09:00

160 lines
5.6 KiB
TypeScript

/**
* The rules the provider record (lib/providers.ts) must keep, and the parts
* of the UI that follow its order. The record replaced thirteen tables keyed
* by provider name; while they were being replaced, a fixture of their old
* values checked the derived ones, and the behavior snapshot
* (provider-matrix.test.ts) still holds every scenario's answer.
*/
import { readFileSync } from "node:fs"
import path from "node:path"
import { describe, expect, it, vi } from "vitest"
import { envVarsOf } from "@/lib/ai-providers"
import { i18n } from "@/lib/i18n/config"
import { POPULAR_PROVIDERS, PROVIDER_GROUPS } from "@/lib/provider-setup"
import { PROVIDER_NAMES, PROVIDERS, publicDefaultUrl } from "@/lib/providers"
import { PROVIDER_INFO } from "@/lib/types/model-config"
vi.mock("@/lib/admin/settings", () => ({
loadSettings: () => ({}),
getEnvFallback: (key: string) => process.env[key] ?? null,
}))
const sorted = (list: Iterable<string>) => [...list].sort()
describe("the provider record", () => {
it("is what the rest of the app imports as PROVIDER_INFO", () => {
expect(PROVIDER_INFO).toBe(PROVIDERS)
expect(PROVIDER_NAMES).toHaveLength(24)
})
it("has 21 providers taking part in server key auto-detection", () => {
// credential apiKey or vertexKey: the providers whose <P>_API_KEY the
// server must have; aws, optionalKey and none need no key
const detected = PROVIDER_NAMES.filter((p) =>
["apiKey", "vertexKey"].includes(PROVIDERS[p].credential),
)
expect(detected).toHaveLength(21)
// Ollama has a key variable too, an optional one
const withKeyVar = PROVIDER_NAMES.filter((p) => !!envVarsOf(p).apiKey)
expect(sorted(detected)).toEqual(
sorted(withKeyVar.filter((p) => p !== "ollama")),
)
})
it("writes every note and hint in every UI language", () => {
for (const p of PROVIDER_NAMES) {
for (const copy of [PROVIDERS[p].note, PROVIDERS[p].baseUrlHint]) {
if (!copy) continue
expect(sorted(Object.keys(copy))).toEqual(sorted(i18n.locales))
for (const text of Object.values(copy)) {
expect(text.trim()).not.toBe("")
}
}
}
})
it("only calls https defaults on its own", () => {
expect(publicDefaultUrl("sglang")).toBeUndefined()
expect(publicDefaultUrl("openai")).toBe("https://api.openai.com/v1")
expect(publicDefaultUrl("bedrock")).toBeUndefined()
})
it("gives the no-key and fixed-credential providers no key variable to detect", () => {
for (const p of PROVIDER_NAMES) {
const { credential, envPrefix, fixedCredentials } = PROVIDERS[p]
if (credential === "none") expect([p, envPrefix]).toEqual([p, null])
if (credential === "aws") expect([p, envPrefix]).toEqual([p, null])
if (fixedCredentials) {
expect(["aws", "vertexKey", "optionalKey"]).toContain(
credential,
)
}
}
const fixed = PROVIDER_NAMES.filter(
(p) => PROVIDERS[p].fixedCredentials,
)
expect(fixed).toEqual(["bedrock", "vertexai", "ollama"])
})
it("keeps a short model list exactly for the providers that cannot list models", () => {
for (const p of PROVIDER_NAMES) {
const { modelList, models } = PROVIDERS[p]
if (modelList) {
expect([p, models]).toEqual([p, undefined])
} else {
expect([p, models?.length ?? 0]).not.toEqual([p, 0])
}
}
})
it("names every server variable in env.example", () => {
// env.example is written by hand; a new provider adds its two lines
const example = readFileSync(
path.join(process.cwd(), "env.example"),
"utf8",
)
for (const p of PROVIDER_NAMES) {
const { apiKey, baseUrl } = envVarsOf(p)
for (const name of [apiKey, baseUrl]) {
if (!name) continue
expect([p, name, example.includes(name)]).toEqual([
p,
name,
true,
])
}
}
})
})
describe("the picker's order", () => {
it("groups every provider once, in record order", () => {
expect(PROVIDER_GROUPS).toEqual([
{
id: "vendors",
providers: [
"openai",
"anthropic",
"google",
"deepseek",
"qwen",
"doubao",
"glm",
"kimi",
"minimax",
"mimo",
],
},
{
id: "aggregators",
providers: [
"openrouter",
"aihubmix",
"siliconflow",
"modelscope",
"novita",
"atlascloud",
"qiniu",
"gateway",
],
},
{
id: "cloud",
providers: ["bedrock", "vertexai", "azure", "edgeone"],
},
{ id: "selfHosted", providers: ["ollama", "sglang"] },
])
})
it("offers the same popular providers", () => {
expect(POPULAR_PROVIDERS).toEqual([
"openai",
"anthropic",
"google",
"deepseek",
"openrouter",
"qwen",
])
})
})