Files
Jyotisha/frontend/tests/admin-model-management-ui-contract.test.ts
Jesse_Chen fc47939ba9
Staging Backend Quality Gate / validate (push) Successful in 14m16s
Staging Backend Quality Gate / publish (push) Successful in 1h2m51s
fix(admin): repair model discovery and simplify model setup
2026-08-08 17:29:17 +08:00

126 lines
6.9 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
const component = readFileSync(
new URL("../src/components/admin/model-management.tsx", import.meta.url),
"utf8",
);
const globals = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const route = readFileSync(new URL("../src/app/api/admin/models/route.ts", import.meta.url), "utf8");
const mutationHandler = readFileSync(
new URL("../src/lib/admin/model-mutation-handler.ts", import.meta.url),
"utf8",
);
test("provider form keeps codes server-owned and API keys write-only", () => {
assert.doesNotMatch(component, /name="code"|code:\s*values\.code/);
assert.match(component, /代码预览(服务端生成)/);
assert.match(component, /<Input readOnly value=\{editingProvider\?\.code \?\? "保存后由服务端自动生成"\}/);
assert.match(component, /name="apiKey"[\s\S]*required: !editingProvider[\s\S]*<Input\.Password/);
assert.match(component, /\.\.\.\(apiKey \? \{ apiKey \} : \{\}\)/);
assert.match(component, /留空表示保留现有 API Key/);
assert.match(component, /anthropic: "Anthropic 官方"/);
});
test("model discovery is the only model picker and generates system fields", () => {
assert.match(component, /onClick=\{\(\) => void discoverModels\(selectedProviderId\)\}>获取模型列表<\/Button>/);
assert.doesNotMatch(component, /title="获取供应商模型列表"[\s\S]*ReasonActionModal/);
assert.match(component, /adminRequestJson<DiscoveredModelsPayload>\("\/api\/admin\/models\/discover"/);
assert.match(component, /body: JSON\.stringify\(\{ providerId \}\)/);
assert.match(component, /未发现可用模型,请检查供应商配置后重试/);
assert.doesNotMatch(component, /可继续手工输入/);
assert.match(component, /name="providerModel" label="模型"[\s\S]*<Select[\s\S]*showSearch[\s\S]*optionFilterProp="label"/);
assert.doesNotMatch(component, /供应商模型名(可手工输入)|name="modelId"|name="label" label="显示名称"/);
assert.match(component, /import \{ createDiscoveredModelId \} from "\.\/model-id"/);
assert.match(component, /const modelId = editingModel\?\.modelId \?\? await createDiscoveredModelId\(values\.providerId, values\.providerModel\)/);
assert.doesNotMatch(component, /providerModel\.toLowerCase\(\)\.replace/);
assert.match(component, /label: editingModel\?\.label \?\? \(discovered\?\.label\?\.trim\(\) \|\| values\.providerModel\)\.slice\(0, 60\)/);
});
test("model form exposes only essential switches and credit cost", () => {
assert.match(component, /name="creditCost" label="单次点数"/);
assert.match(component, /name="enabled" label="启用"/);
assert.match(component, /name="isDefault" label="默认模型"/);
for (const field of [
"description",
"modelTier",
"contextWindow",
"inputCostMicrousdPerMillion",
"outputCostMicrousdPerMillion",
"fallbackModelId",
"settingsJson",
]) {
assert.doesNotMatch(component, new RegExp(`name=["']${field}["']`));
}
assert.doesNotMatch(component, /模型档位|上下文窗口|输入成本(微美元\/百万 Token)|输出成本(微美元\/百万 Token)|回退模型 ID|设置 JSON/);
});
test("hidden model fields preserve draft values or use database-compatible defaults", () => {
assert.match(component, /description: editingModel\?\.description \?\? ""/);
assert.match(component, /modelTier: editingModel\?\.modelTier \?\? "standard"/);
assert.match(component, /contextWindow: editingModel\?\.contextWindow \?\? null/);
assert.match(component, /inputCostMicrousdPerMillion: editingModel\?\.inputCostMicrousdPerMillion \?\? 0/);
assert.match(component, /outputCostMicrousdPerMillion: editingModel\?\.outputCostMicrousdPerMillion \?\? 0/);
assert.match(component, /fallbackModelId: editingModel\?\.fallbackModelId \?\? null/);
assert.match(component, /settings: editingModel\?\.settings \?\? \{\}/);
});
test("model mutations omit client reasons while the server keeps fixed audit reasons", () => {
assert.doesNotMatch(component, /ReasonActionModal|name="reason"|reason:\s*(?:values\.reason|reason)/);
assert.doesNotMatch(component, /reauthPermission=[^\n]*models\.|验证并(?:保存|获取)/);
assert.match(component, /open=\{providerOpen\} okText="保存"[\s\S]*onFinish=\{saveProvider\}/);
assert.doesNotMatch(component, /继续验证/);
assert.match(component, /<Modal[\s\S]*open=\{Boolean\(versionAction\)\}[\s\S]*onOk=\{\(\) => void submitVersionAction\(\)\}/);
assert.doesNotMatch(route, /reason:\s*z\.string/);
for (const reason of ["保存模型供应商", "保存模型草稿", "发布模型版本", "回滚模型版本"]) {
assert.match(route, new RegExp(reason));
}
assert.match(route, /reason:\s*modelMutationAuditReasons\[body\.data\.action\]/);
assert.match(mutationHandler, /admin_save_model_provider[\s\S]*a\.reason/);
assert.match(mutationHandler, /admin_save_model_draft[\s\S]*a\.reason/);
assert.match(mutationHandler, /admin_publish_model[\s\S]*a\.reason/);
assert.match(mutationHandler, /admin_rollback_model[\s\S]*a\.reason/);
});
test("provider rows expose a direct add-model entry with the provider preselected", () => {
assert.match(component, /function openModel\(model\?: ModelVersion, providerId\?: string\)/);
assert.match(component, /providerId: providerId \?\? providers\[0\]\?\.id/);
assert.match(component, /onClick=\{\(\) => openModel\(undefined, item\.id\)\}>添加模型<\/Button>/);
assert.match(component, />新增模型<\/Button>/);
});
test("model management input numbers fill their columns", () => {
const inputNumbers = [...component.matchAll(/<InputNumber\b[^>]*>/g)].map(([source]) => source);
assert.ok(inputNumbers.length > 0);
for (const inputNumber of inputNumbers) {
assert.match(inputNumber, /style=\{\{ width: "100%" \}\}/);
}
});
test("model management renders localized labels without changing stored enum values", () => {
assert.match(component, /openai: "OpenAI 官方"/);
assert.match(component, /anthropic: "Anthropic 官方"/);
assert.match(component, /"openai-compatible": "OpenAI 兼容接口"/);
assert.match(component, /standard: "标准"/);
assert.match(component, /premium: "高级"/);
assert.match(component, /internal: "内部"/);
assert.match(component, /draft: "草稿"/);
assert.match(component, /published: "已发布"/);
assert.match(component, /retired: "已下线"/);
});
test("global native input sizing excludes Ant Design picker internals", () => {
assert.match(
globals,
/input:not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\), select[^\{]+\{ width: 100%; min-height: 44px; padding: 0 12px;/,
);
assert.match(
globals,
/input:not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\):disabled/,
);
assert.doesNotMatch(globals, /(?:^|\n)input, select \{ width: 100%; min-height: 44px; padding: 0 12px;/);
});