Files
config-center/__tests__/validate.test.mjs
Yige ce1a936284 fix: restore provider-based pricing currencies (#56)
Restore MiniMax domestic CNY pricing and enforce provider-based currency classification.
2026-07-13 16:16:03 +08:00

446 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* config-center frozen schema 校验测试
*
* 关键场景:
* - 现有所有数据文件必须通过 frozen schema 校验
* - PR #1 引发死锁的反例defaultTemperature: null必须被拒绝
* - 防御未知字段additionalProperties: false保护老客户端
*
* 运行:
* npm test
* node --test __tests__/
*/
import { describe, it } from 'node:test'
import { strict as assert } from 'node:assert'
import { readFileSync, readdirSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Ajv } from 'ajv'
import addFormats from 'ajv-formats'
import { validateFile, loadSchemas } from '../scripts/validate.mjs'
const validators = loadSchemas()
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = resolve(__dirname, '..')
function loadSchema(name) {
const p = join(ROOT, 'schemas', `${name}.schema.json`)
return JSON.parse(readFileSync(p, 'utf8'))
}
function compile(name) {
const ajv = new Ajv({ allErrors: true, strict: false })
addFormats(ajv)
return ajv.compile(loadSchema(name))
}
// ==================== 真实数据 happy path ====================
describe('真实数据全量校验', () => {
it('所有 compute/providers/*.json 应通过 provider schema', () => {
const dir = join(ROOT, 'compute', 'providers')
const failures = []
for (const file of readdirSync(dir)) {
if (!file.endsWith('.json') || file === '_index.json') continue
const result = validateFile(join(dir, file), validators)
if (!result.ok) failures.push({ file, errors: result.errors })
}
assert.equal(
failures.length,
0,
`${failures.length} provider 文件校验失败:\n${JSON.stringify(failures, null, 2)}`,
)
})
it('所有 compute/coding-plans/*.json 应通过 provider schema', () => {
const dir = join(ROOT, 'compute', 'coding-plans')
const failures = []
for (const file of readdirSync(dir)) {
if (!file.endsWith('.json') || file === '_index.json') continue
const result = validateFile(join(dir, file), validators)
if (!result.ok) failures.push({ file, errors: result.errors })
}
assert.equal(
failures.length,
0,
`${failures.length} coding-plan 文件校验失败:\n${JSON.stringify(failures, null, 2)}`,
)
})
it('manifest.json 应通过 manifest schema', () => {
const result = validateFile(join(ROOT, 'manifest.json'), validators)
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
})
it('compute/pricing.json 应通过 pricing schema', () => {
const result = validateFile(join(ROOT, 'compute', 'pricing.json'), validators)
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
})
it('compute/service-map.json 应通过 service-map schema', () => {
const result = validateFile(join(ROOT, 'compute', 'service-map.json'), validators)
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
})
it('两个 _index.json 应通过 providers-index schema', () => {
const r1 = validateFile(join(ROOT, 'compute', 'providers', '_index.json'), validators)
const r2 = validateFile(join(ROOT, 'compute', 'coding-plans', '_index.json'), validators)
assert.equal(r1.ok, true, JSON.stringify(r1.errors, null, 2))
assert.equal(r2.ok, true, JSON.stringify(r2.errors, null, 2))
})
it('runtimes/recommended.json 应通过 runtime-recommended schema', () => {
const result = validateFile(join(ROOT, 'runtimes', 'recommended.json'), validators)
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
})
it('runtimes/versions-fallback.json 应通过 runtime-versions-fallback schema', () => {
const result = validateFile(join(ROOT, 'runtimes', 'versions-fallback.json'), validators)
assert.equal(result.ok, true, JSON.stringify(result.errors, null, 2))
})
it('DeepSeek V4 Pro/Flash 应与官方的 1M/384K reasoning profile 一致', () => {
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'deepseek.json'), 'utf8'))
const specFile = JSON.parse(readFileSync(join(ROOT, 'compute', 'model-specs', 'deepseek.json'), 'utf8'))
const modelIds = ['deepseek-v4-pro', 'deepseek-v4-flash']
for (const modelId of modelIds) {
const model = provider.models.find((item) => item.modelName === modelId)
assert.ok(model, `provider 缺少 ${modelId}`)
assert.equal(model.contextWindow, 1000000)
assert.equal(model.maxOutputTokens, 384000)
assert.ok(model.capabilities.includes('reasoning'))
assert.ok(model.capabilities.includes('deep_thinking'))
assert.ok(model.serviceType.includes('reasoning'))
assert.equal(model.extra.supportsThinking, true)
assert.equal(model.extra.thinkingDefault, true)
assert.deepEqual(model.extra.reasoningEffort, ['high', 'max'])
const specs = specFile.specs.filter((item) => item.id === modelId)
assert.equal(specs.length, 1, `model-specs 中 ${modelId} 应且仅应有一条规格`)
for (const { spec } of specs) {
assert.equal(spec.contextWindow, 1000000)
assert.equal(spec.maxOutputTokens, 384000)
assert.equal(spec.supportsReasoning, true)
assert.ok(spec.capabilities.includes('reasoning'))
assert.ok(spec.capabilities.includes('deep_thinking'))
assert.ok(spec.serviceType.includes('reasoning'))
assert.equal(spec.extra.supportsThinking, true)
assert.equal(spec.extra.thinkingDefault, true)
assert.deepEqual(spec.extra.reasoningEffort, ['high', 'max'])
}
}
})
it('所有 Provider 应按供应商归属计价,模型来源不覆盖供应商币种', () => {
const expectedCurrencies = {
anthropic: 'USD',
baichuan: 'CNY',
baidu: 'CNY',
cohere: 'USD',
dashscope: 'CNY',
deepseek: 'CNY',
google: 'USD',
kling: 'CNY',
lingyiwanwu: 'CNY',
'local-whisper': 'USD',
minimax: 'CNY',
mistral: 'USD',
moonshot: 'CNY',
ollama: 'USD',
openai: 'USD',
'openai-codex': 'USD',
openrouter: 'USD',
perplexity: 'USD',
siliconflow: 'CNY',
stability: 'USD',
tencent: 'CNY',
volcengine: 'CNY',
xai: 'USD',
xiaomi: 'CNY',
xunfei: 'CNY',
zhipu: 'CNY',
'zhipu-embedding': 'CNY',
}
const dir = join(ROOT, 'compute', 'providers')
const actualProviders = []
for (const file of readdirSync(dir)) {
if (!file.endsWith('.json') || file === '_index.json') continue
const data = JSON.parse(readFileSync(join(dir, file), 'utf8'))
actualProviders.push(data.provider)
assert.ok(
Object.hasOwn(expectedCurrencies, data.provider),
`未给 Provider ${data.provider} 声明计价归属`,
)
assert.equal(
data.priceCurrency,
expectedCurrencies[data.provider],
`${data.label} 应按供应商归属使用 ${expectedCurrencies[data.provider]} 计价`,
)
}
assert.deepEqual(actualProviders.sort(), Object.keys(expectedCurrencies).sort())
})
it('MiniMax 应使用国内开放平台人民币价', () => {
const provider = JSON.parse(readFileSync(join(ROOT, 'compute', 'providers', 'minimax.json'), 'utf8'))
const model = (modelName) => provider.models.find((item) => item.modelName === modelName)
assert.equal(provider.priceCurrency, 'CNY')
assert.deepEqual(
[model('MiniMax-M3').inputPrice, model('MiniMax-M3').outputPrice],
[2.1, 8.4],
)
assert.deepEqual(
[model('MiniMax-M2.7').inputPrice, model('MiniMax-M2.7').outputPrice],
[2.1, 8.4],
)
assert.deepEqual(
[model('MiniMax-M2.7-highspeed').inputPrice, model('MiniMax-M2.7-highspeed').outputPrice],
[4.2, 16.8],
)
assert.equal(model('image-01').extra.pricePerImage, 0.025)
assert.equal(model('speech-2.8-hd').extra.pricePerMillionCharacters, 350)
assert.equal(model('speech-2.8-turbo').extra.pricePerMillionCharacters, 200)
assert.equal(model('music-2.6').extra.pricePerSongUpToFiveMinutes, 1)
})
})
// ==================== Runtime 清单反例 ====================
describe('runtime-recommended schema 反例', () => {
const validate = compile('runtime-recommended')
function makeValidManifest() {
return JSON.parse(readFileSync(join(ROOT, 'runtimes', 'recommended.json'), 'utf8'))
}
it('拒绝缺少平台归档archives 六平台必填)', () => {
const data = makeValidManifest()
delete data.node.archives['win32-arm64']
assert.equal(validate(data), false)
})
it('拒绝非法 sha256长度/字符集不符)', () => {
const data = makeValidManifest()
data.python.sha256['darwin-arm64'] = 'not-a-sha'
assert.equal(validate(data), false)
})
it('拒绝未知字段additionalProperties: false 保护老客户端)', () => {
const data = makeValidManifest()
data.unknownField = true
assert.equal(validate(data), false)
})
})
// ==================== Provider schema 反例 ====================
describe('provider schema 反例(防 PR #1 重演)', () => {
const validate = compile('provider')
function makeValidProvider() {
return {
id: 'provider-test-001',
provider: 'test',
label: 'Test',
baseUrl: 'https://api.test.com',
apiKeyRef: 'test',
apiKeyVerified: false,
enabled: false,
status: 'unconfigured',
services: ['chat'],
models: [
{
modelName: 'm1',
displayName: 'M1',
serviceType: ['chat'],
capabilities: ['chat'],
},
],
}
}
it('合法 minimal provider 通过', () => {
assert.equal(validate(makeValidProvider()), true, JSON.stringify(validate.errors))
})
it('拒绝 defaultTemperature: nullPR #1 死锁元凶)', () => {
const data = makeValidProvider()
data.models[0].defaultTemperature = null
assert.equal(validate(data), false)
assert.ok(
validate.errors.some((e) => e.instancePath.endsWith('/defaultTemperature')),
`应有 defaultTemperature 错误,实际:${JSON.stringify(validate.errors)}`,
)
})
it('拒绝 defaultTopP: null', () => {
const data = makeValidProvider()
data.models[0].defaultTopP = null
assert.equal(validate(data), false)
assert.ok(validate.errors.some((e) => e.instancePath.endsWith('/defaultTopP')))
})
it('拒绝 defaultTemperature: "0.7"string', () => {
const data = makeValidProvider()
data.models[0].defaultTemperature = '0.7'
assert.equal(validate(data), false)
})
it('接受省略 defaultTemperature/defaultTopPreasoning 模型推荐做法)', () => {
const data = makeValidProvider()
// 不设这两个字段
assert.equal(validate(data), true, JSON.stringify(validate.errors))
})
it('接受 defaultTemperature: 0.7(合法 number', () => {
const data = makeValidProvider()
data.models[0].defaultTemperature = 0.7
data.models[0].defaultTopP = 0.95
assert.equal(validate(data), true, JSON.stringify(validate.errors))
})
it('拒绝 defaultTemperature 超出 [0, 2] 范围', () => {
const data = makeValidProvider()
data.models[0].defaultTemperature = 3.0
assert.equal(validate(data), false)
})
it('拒绝 model 顶层未知字段additionalProperties: false 保护老客户端)', () => {
const data = makeValidProvider()
data.models[0].brandNewField = 'oops'
assert.equal(validate(data), false)
assert.ok(
validate.errors.some((e) => e.keyword === 'additionalProperties'),
`应有 additionalProperties 错误,实际:${JSON.stringify(validate.errors)}`,
)
})
it('拒绝 provider 顶层未知字段', () => {
const data = makeValidProvider()
data.unknownTopLevelField = 'oops'
assert.equal(validate(data), false)
assert.ok(validate.errors.some((e) => e.keyword === 'additionalProperties'))
})
it('拒绝缺少必填字段(如 model 缺 modelName', () => {
const data = makeValidProvider()
delete data.models[0].modelName
assert.equal(validate(data), false)
})
it('拒绝非法 status enum', () => {
const data = makeValidProvider()
data.status = 'wrong-status'
assert.equal(validate(data), false)
})
it('拒绝非法 priceCurrency enum仅允许 USD/CNY', () => {
const data = makeValidProvider()
data.priceCurrency = 'EUR'
assert.equal(validate(data), false)
})
})
// ==================== Manifest schema ====================
describe('manifest schema', () => {
const validate = compile('manifest')
it('合法 manifest 通过', () => {
assert.equal(
validate({
version: '1.0.0',
presetDataVersion: 30,
updatedAt: '2026-04-25',
description: 'test',
}),
true,
)
})
it('拒绝 presetDataVersion 为 string', () => {
assert.equal(
validate({
version: '1.0.0',
presetDataVersion: '30',
updatedAt: '2026-04-25',
}),
false,
)
})
it('拒绝非法 updatedAt 格式', () => {
assert.equal(
validate({
version: '1.0.0',
presetDataVersion: 30,
updatedAt: '04/25/2026',
}),
false,
)
})
it('拒绝缺少 presetDataVersion', () => {
assert.equal(
validate({ version: '1.0.0', updatedAt: '2026-04-25' }),
false,
)
})
})
// ==================== Service map schema ====================
describe('service-map schema', () => {
const validate = compile('service-map')
it('合法 service-map 通过', () => {
assert.equal(
validate({
chat: { modelName: 'gpt-5-mini', providerId: 'provider-openai-001' },
}),
true,
)
})
it('拒绝缺少 providerId 的条目', () => {
assert.equal(validate({ chat: { modelName: 'gpt-5-mini' } }), false)
})
it('拒绝条目中含未知字段', () => {
assert.equal(
validate({
chat: {
modelName: 'gpt-5-mini',
providerId: 'provider-openai-001',
extra: 'oops',
},
}),
false,
)
})
})
// ==================== Providers-index schema ====================
describe('providers-index schema', () => {
const validate = compile('providers-index')
it('合法 _index.json 通过', () => {
assert.equal(validate({ description: 'x', order: ['openai', 'anthropic'] }), true)
})
it('拒绝 order 含重复元素', () => {
assert.equal(validate({ order: ['openai', 'openai'] }), false)
})
it('拒绝缺少 order', () => {
assert.equal(validate({ description: 'x' }), false)
})
})