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, / { assert.match(component, /onClick=\{\(\) => void discoverModels\(selectedProviderId\)\}>获取模型列表<\/Button>/); assert.doesNotMatch(component, /title="获取供应商模型列表"[\s\S]*ReasonActionModal/); assert.match(component, /adminRequestJson\("\/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]* { 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.doesNotMatch(component, /versionAction|submitVersionAction/); assert.match(component, /onClick=\{\(\) => void runVersionAction\("publish", item\)\}>发布<\/Button>/); assert.match(component, /onClick=\{\(\) => void runVersionAction\("rollback", item\)\}>回滚到此版<\/Button>/); assert.doesNotMatch(route, /reason:\s*z\.string/); for (const reason of ["保存模型供应商", "保存模型草稿", "发布模型版本", "删除模型版本", "回滚模型版本"]) { assert.match(route, new RegExp(reason)); } assert.match(route, /action: z\.literal\("delete"\), versionId: z\.string\(\)\.uuid\(\)/); 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_delete_model_version[\s\S]*a\.reason/); assert.match(mutationHandler, /admin_rollback_model[\s\S]*a\.reason/); }); test("published model edits fork a draft and every version can be deleted", () => { const migration = readFileSync( new URL("../supabase/migrations/20261020150000_admin_model_version_draft_and_delete.sql", import.meta.url), "utf8", ); assert.match(component, /item\.status === "draft" \? "编辑草稿" : "修改"/); assert.match(component, /onClick=\{\(\) => confirmRemove\(item\)\}>删除<\/Button>/); assert.match(component, /保存后会生成新版本草稿,当前已发布版本保持不变/); assert.match(component, /action: "delete", versionId: item\.id/); assert.match(component, /已经绑定这个版本的会话将无法继续提问/); assert.match(migration, /v_existing\.status='draft'/); assert.match(migration, /admin_delete_model_version/); assert.match(migration, /model_default_required/); assert.match(migration, /delete from public\.model_publish_events/); assert.match( readFileSync(new URL("../src/lib/admin/admin-error-response.ts", import.meta.url), "utf8"), /请先把另一个已发布模型设为默认,再删除这个模型/, ); }); 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(/]*>/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", () => { // 原值: padding: 0 12px // 新值: padding: 0 var(--space-3) // 原因: 7ee7f825 把间距收敛到 token;合同测试跟随 CSS,不改业务代码。 assert.match( globals, /input:not\(\[type="radio"\]\):not\(\[type="checkbox"\]\):not\(\[class\^="ant-"\]\):not\(\[class\*=" ant-"\]\):not\(\.ant-picker input\), select[^\{]+\{ width: 100%; min-height: 44px; padding: 0 var\(--space-3\);/, ); assert.match( globals, /input:not\(\[type="radio"\]\):not\(\[type="checkbox"\]\):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;/); });