Files
registry/scripts/catalog/validator.test.mjs
Yige 223be1f04b feat: 建立统一目录元数据与获取契约 (#5)
* feat: 建立 Registry 目录元数据验证基础

* feat: 迁移 Registry 应用目录元数据

* test: 解耦目录迁移进度断言

* feat: 迁移 Registry 服务目录元数据

* fix(catalog): 强制 Registry 目录元数据

* fix(catalog): 兼容 Registry v4 外部集成
2026-08-31 04:03:38 -04:00

416 lines
15 KiB
JavaScript
Raw Permalink 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.
import assert from 'node:assert/strict'
import { afterEach, test } from 'node:test'
import { cpSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { validateRegistry } from './validator.mjs'
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
const temporaryRoots = []
afterEach(() => {
while (temporaryRoots.length > 0) rmSync(temporaryRoots.pop(), { recursive: true, force: true })
})
function writeJson(file, value) {
mkdirSync(dirname(file), { recursive: true })
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
}
function legacyApp(overrides = {}) {
return {
id: 'demo-app',
name: 'Demo App',
type: 'docker-app',
version: '1.2.3',
author: 'Demo Org',
description: '一个用于测试的应用',
tags: ['demo'],
icon: 'box',
iconLetter: 'D',
platformSupport: ['macos', 'windows', 'linux'],
category: 'tools',
shortDesc: '一个用于测试的应用',
fullDesc: '这是用于验证 Registry 契约的测试应用。',
install: {
method: 'docker',
requirements: { docker: true, minMemory: '1GB', minDisk: '1GB', ports: [8080] },
configNeeded: ['Docker'],
},
...overrides,
}
}
function legacyMcp(overrides = {}) {
return {
id: 'demo-mcp',
name: 'Demo MCP',
type: 'mcp',
version: '2026.8.31',
author: 'Demo Org',
description: '一个用于测试的 MCP 服务',
tags: ['demo'],
icon: 'terminal',
platformSupport: ['macos', 'windows', 'linux'],
capabilities: ['read'],
toolCount: 1,
install: { method: 'npx', packageName: '@demo/mcp', command: 'npx', args: ['-y', '@demo/mcp@1.0.0'] },
connection: { transport: 'stdio', command: 'npx', args: ['-y', '@demo/mcp@1.0.0'] },
...overrides,
}
}
function legacyHttp(overrides = {}) {
return {
id: 'demo-http',
name: 'Demo HTTP',
type: 'http-api',
version: '3.0',
author: 'Demo Org',
description: '一个用于测试的 HTTP 服务',
tags: ['demo'],
icon: 'globe',
platformSupport: ['macos', 'windows', 'linux'],
endpoint: 'https://api.example.com/v1',
capabilities: ['query'],
...overrides,
}
}
function unknownTimestamps(overrides = {}) {
return {
catalogUpdatedAt: { state: 'unknown' },
releasePublishedAt: { state: 'unknown' },
reviewedAt: { state: 'unknown' },
upstreamObservedAt: { state: 'unknown' },
...overrides,
}
}
function appSidecar(overrides = {}) {
return {
$schema: '../../schemas/catalog-metadata.v1.schema.json',
schemaVersion: 1,
identity: { kind: 'app', id: 'demo-app' },
presentation: {
defaultLocale: 'zh-CN',
i18n: {
'zh-CN': {
name: 'Demo App',
summary: '一个用于测试的应用',
description: '这是用于验证 Registry 契约的测试应用。',
},
},
tags: ['demo'],
},
release: { state: 'known', version: '1.2.3', versionScheme: 'semver' },
timestamps: unknownTimestamps(),
provenance: {},
governance: { availability: 'listing-only', license: { state: 'unknown' }, redistribution: 'verify-package-terms' },
compatibility: { platforms: { state: 'all' } },
spec: { kind: 'app', category: 'tools' },
...overrides,
}
}
function mcpSidecar(overrides = {}) {
return {
$schema: '../../schemas/catalog-metadata.v1.schema.json',
schemaVersion: 1,
identity: { kind: 'service', id: 'demo-mcp' },
presentation: {
defaultLocale: 'zh-CN',
i18n: { 'zh-CN': { name: 'Demo MCP', summary: '一个用于测试的 MCP 服务' } },
tags: ['demo'],
},
release: { state: 'known', version: '2026.8.31', versionScheme: 'calver' },
timestamps: unknownTimestamps(),
provenance: {},
governance: { availability: 'listing-only', license: { state: 'unknown' }, redistribution: 'verify-package-terms' },
compatibility: { platforms: { state: 'known', values: ['macos', 'windows', 'linux'] } },
spec: { kind: 'service', protocol: 'mcp', capabilities: ['read'], toolCount: 1 },
...overrides,
}
}
function httpSidecar(overrides = {}) {
return {
$schema: '../../schemas/catalog-metadata.v1.schema.json',
schemaVersion: 1,
identity: { kind: 'service', id: 'demo-http' },
presentation: {
defaultLocale: 'zh-CN',
i18n: { 'zh-CN': { name: 'Demo HTTP', summary: '一个用于测试的 HTTP 服务' } },
tags: ['demo'],
},
release: { state: 'known', version: '3.0', versionScheme: 'opaque' },
timestamps: unknownTimestamps(),
provenance: {},
governance: { availability: 'listing-only', license: { state: 'unknown' }, redistribution: 'verify-package-terms' },
compatibility: { platforms: { state: 'all' } },
spec: { kind: 'service', protocol: 'http', authType: 'unknown', capabilities: ['query'] },
...overrides,
}
}
function makeRegistry(entries, sidecars = new Map(), rootOverrides = {}) {
const root = mkdtempSync(join(tmpdir(), 'registry-validator-'))
temporaryRoots.push(root)
cpSync(join(repoRoot, 'schemas'), join(root, 'schemas'), { recursive: true })
const counts = {
totalEntries: entries.length,
dockerApps: entries.filter((entry) => entry.type === 'docker-app').length,
mcpServices: entries.filter((entry) => entry.type === 'mcp').length,
httpApis: entries.filter((entry) => entry.type === 'http-api').length,
}
writeFileSync(join(root, 'SCHEMA_VERSION'), '3.1.0\n')
writeJson(join(root, 'manifest.json'), {
$schema: 'http://json-schema.org/draft-07/schema#',
id: 'desirecore-registry-manifest',
version: '3.1.0',
name: 'Test Registry',
description: 'Test Registry',
maintainer: 'Test',
repository: 'https://example.com/registry',
lastUpdated: '2026-08-31',
stats: counts,
dataVersion: '3.1.0',
catalogMetadata: {
version: '1.0.0',
schema: 'schemas/catalog-metadata.v1.schema.json',
sidecarPath: 'entries/<id>/catalog-metadata.v1.json',
required: false,
legacyFallback: true,
},
...rootOverrides,
})
for (const entry of entries) {
const entryDir = join(root, 'entries', entry.id)
writeJson(join(entryDir, 'manifest.json'), entry)
if (sidecars.has(entry.id)) writeJson(join(entryDir, 'catalog-metadata.v1.json'), sidecars.get(entry.id))
}
return root
}
function codes(report) {
return report.errors.map((item) => item.code)
}
test('当前 v4 条目保持可读21 个 App/Service 具备 sidecar', () => {
const report = validateRegistry(repoRoot)
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
assert.deepEqual({
totalEntries: report.counts.totalEntries,
dockerApps: report.counts.dockerApps,
mcpServices: report.counts.mcpServices,
httpApis: report.counts.httpApis,
externalIntegrations: report.counts.externalIntegrations,
}, {
totalEntries: 22,
dockerApps: 8,
mcpServices: 8,
httpApis: 5,
externalIntegrations: 1,
})
assert.equal(report.counts.sidecars + report.counts.legacyOnly, report.counts.totalEntries - report.counts.externalIntegrations)
assert.equal(report.warnings.filter((item) => item.code === 'missing-sidecar').length, report.counts.legacyOnly)
})
test('合法 listing-only App sidecar 通过', () => {
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', appSidecar()]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
assert.equal(report.counts.sidecars, 1)
})
test('合法 MCP sidecar 保留 CalVer、能力和 toolCount', () => {
const report = validateRegistry(makeRegistry([legacyMcp()], new Map([['demo-mcp', mcpSidecar()]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
})
test('合法 HTTP sidecar 保留 opaque 版本、协议、鉴权 unknown 和能力', () => {
const report = validateRegistry(makeRegistry([legacyHttp()], new Map([['demo-http', httpSidecar()]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
})
test('stats 与实际目录不一致时失败', () => {
const root = makeRegistry([legacyApp()], new Map(), { stats: { totalEntries: 99, dockerApps: 8, mcpServices: 0, httpApis: 0 } })
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(codes(report).includes('stats-mismatch'))
})
test('目录名与 manifest.id 不一致时失败', () => {
const root = makeRegistry([legacyApp()])
const source = join(root, 'entries', 'demo-app')
const target = join(root, 'entries', 'wrong-directory')
cpSync(source, target, { recursive: true })
rmSync(source, { recursive: true, force: true })
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(codes(report).includes('directory-id-mismatch'))
})
test('legacy type-specific 必需字段缺失时失败', () => {
const entry = legacyMcp()
delete entry.connection
const report = validateRegistry(makeRegistry([entry]))
assert.equal(report.ok, false)
assert.ok(codes(report).includes('schema'))
})
test('sidecar 严格拒绝额外字段和正文 sourceId', () => {
const sidecar = appSidecar({ sourceId: 'registry:official', unexpected: true })
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.ok(report.errors.filter((item) => item.code === 'schema').length >= 2)
})
test('sidecar 拒绝正文 official 治理声明', () => {
const sidecar = appSidecar({
governance: {
availability: 'listing-only',
stewardship: 'official',
license: { state: 'unknown' },
redistribution: 'verify-package-terms',
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.ok(codes(report).includes('schema'))
})
test('i18n 必须包含默认语言且不能伪造重复翻译', () => {
const missingDefault = appSidecar({
presentation: { defaultLocale: 'en-US', i18n: { 'zh-CN': appSidecar().presentation.i18n['zh-CN'] } },
})
let report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', missingDefault]])))
assert.ok(codes(report).includes('missing-default-locale'))
const duplicated = appSidecar()
duplicated.presentation.i18n['en-US'] = { ...duplicated.presentation.i18n['zh-CN'] }
report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', duplicated]])))
assert.ok(codes(report).includes('duplicate-translation'))
})
test('证据路径拒绝绝对路径和目录穿越', () => {
const sidecar = appSidecar({
provenance: { content: { kind: 'git', url: 'https://example.com/repo.git', ref: 'a'.repeat(40), path: '../secret' } },
governance: {
availability: 'listing-only',
license: { state: 'unknown' },
redistribution: 'verify-package-terms',
compliance: {
licenseEvidencePath: '/tmp/LICENSE',
reviewedRef: 'a'.repeat(40),
reviewedAt: '2026-08-31',
reviewedBy: 'test',
upstreamEndorsed: false,
},
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.equal(codes(report).filter((code) => code === 'unsafe-evidence-path').length, 2)
})
test('mutable source 可 listing-only但不能 installable', () => {
const mutableSource = { kind: 'git', url: 'https://example.com/repo.git', ref: 'main' }
let sidecar = appSidecar({ provenance: { content: mutableSource } })
let report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
sidecar = appSidecar({
provenance: { content: mutableSource },
timestamps: unknownTimestamps({ reviewedAt: { state: 'known', value: '2026-08-31', precision: 'day' } }),
governance: {
availability: 'installable',
stewardship: 'pointer',
license: { state: 'known', value: 'MIT', evidencePath: 'LICENSE' },
redistribution: 'source-pointer-only',
listingMaintainer: { name: 'DesireCore Team', verified: true },
upstreamMaintainer: { name: 'Demo Org', verified: false },
branding: { relationship: 'independent-listing', nameUsage: 'nominative', logoStatus: 'not-used' },
compliance: {
licenseEvidencePath: 'LICENSE',
reviewedRef: 'main',
reviewedAt: '2026-08-31',
reviewedBy: 'catalog-review-v1',
upstreamEndorsed: false,
},
},
})
report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, false)
assert.ok(codes(report).includes('installable-without-evidence'))
})
test('完整不可变来源与审核证据允许 installable', () => {
const ref = 'a'.repeat(40)
const sidecar = appSidecar({
provenance: { content: { kind: 'git', url: 'https://example.com/repo.git', ref } },
timestamps: unknownTimestamps({ reviewedAt: { state: 'known', value: '2026-08-31', precision: 'day' } }),
governance: {
availability: 'installable',
stewardship: 'pointer',
license: { state: 'known', value: 'MIT', evidencePath: 'LICENSE' },
redistribution: 'source-pointer-only',
listingMaintainer: { name: 'DesireCore Team', verified: true },
upstreamMaintainer: { name: 'Demo Org', verified: false },
branding: { relationship: 'independent-listing', nameUsage: 'nominative', logoStatus: 'not-used' },
compliance: {
licenseEvidencePath: 'LICENSE',
reviewedRef: ref,
reviewedAt: '2026-08-31',
reviewedBy: 'catalog-review-v1',
upstreamEndorsed: false,
},
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.equal(report.ok, true, JSON.stringify(report.errors, null, 2))
})
test('审核 ref 和审核时间必须绑定同一来源与 timestamps', () => {
const sidecar = appSidecar({
provenance: { content: { kind: 'git', url: 'https://example.com/repo.git', ref: 'a'.repeat(40) } },
timestamps: unknownTimestamps({ reviewedAt: { state: 'known', value: '2026-08-31', precision: 'day' } }),
governance: {
availability: 'listing-only',
license: { state: 'unknown' },
redistribution: 'verify-package-terms',
compliance: {
licenseEvidencePath: 'LICENSE',
reviewedRef: 'b'.repeat(40),
reviewedAt: '2026-08-30',
reviewedBy: 'catalog-review-v1',
upstreamEndorsed: false,
},
},
})
const report = validateRegistry(makeRegistry([legacyApp()], new Map([['demo-app', sidecar]])))
assert.ok(codes(report).includes('review-ref-mismatch'))
assert.ok(codes(report).includes('review-time-mismatch'))
})
test('--require-sidecars 等价策略会让 legacy-only 失败', () => {
const report = validateRegistry(makeRegistry([legacyApp()]), { requireSidecars: true })
assert.equal(report.ok, false)
assert.ok(codes(report).includes('missing-sidecar'))
})
test('根 manifest 声明 required 时默认拒绝缺失 sidecar', () => {
const root = makeRegistry([legacyApp()], new Map(), {
catalogMetadata: {
version: '1.0.0',
schema: 'schemas/catalog-metadata.v1.schema.json',
sidecarPath: 'entries/<id>/catalog-metadata.v1.json',
required: true,
legacyFallback: true,
},
})
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(codes(report).includes('missing-sidecar'))
})