mirror of
https://git.openapi.site/https://github.com/desirecore/registry.git
synced 2026-09-05 16:03:36 +08:00
* feat: 建立 Registry 目录元数据验证基础 * feat: 迁移 Registry 应用目录元数据 * test: 解耦目录迁移进度断言 * feat: 迁移 Registry 服务目录元数据 * fix(catalog): 强制 Registry 目录元数据 * fix(catalog): 兼容 Registry v4 外部集成
416 lines
15 KiB
JavaScript
416 lines
15 KiB
JavaScript
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'))
|
||
})
|