Files
registry/scripts/plugin-contract.test.mjs
Yige 3828aa6800 feat(registry): 插件与应用关系契约4.2 / plugin and application contract (#10)
* feat(registry): 增加插件贡献与应用依赖的4.2契约

* fix(registry): preserve legacy App manifests for published clients

---------

Co-authored-by: yi-ge <6657330+yi-ge@users.noreply.github.com>
2026-10-07 19:08:18 -07:00

148 lines
8.1 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.
import assert from 'node:assert/strict'
import { test, afterEach } from 'node:test'
import { cpSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs'
import { join, dirname } from 'node:path'
import { tmpdir } from 'node:os'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
import { validateRegistry } from './catalog/validator.mjs'
const source = join(dirname(fileURLToPath(import.meta.url)), '..')
const temporary = []
afterEach(() => { for (const root of temporary.splice(0)) rmSync(root, { recursive: true, force: true }) })
const contribution = { id: 'help', point: 'session.panels', contractVersion: 1, definitionRef: { path: 'panel.json', sha256: 'a'.repeat(64) } }
const requires = [{ sourceId: 'official', appId: 'capability-provider', contributionId: 'help', contractVersion: 1, optional: false }]
const extension = { apiVersion: '1.0.0', hosts: ['agent-service'], runtime: { kind: 'declarative' }, contributes: [contribution] }
function fixture(type = 'artifact') {
const root = mkdtempSync(join(tmpdir(), 'registry-plugin-'))
temporary.push(root)
cpSync(join(source, 'schemas'), join(root, 'schemas'), { recursive: true })
cpSync(join(source, 'entries/desirecore-control'), join(root, 'entries/desirecore-control'), { recursive: true })
const manifest = JSON.parse(readFileSync(join(source, 'manifest.json'), 'utf8'))
manifest.stats = { totalEntries: 1, dockerApps: 0, nativeApps: type === 'native-app' ? 1 : 0, artifactApps: type === 'artifact' ? 1 : 0, mcpServices: 0, httpApis: 0, externalIntegrations: 0 }
writeFileSync(join(root, 'manifest.json'), JSON.stringify(manifest))
writeFileSync(join(root, 'SCHEMA_VERSION'), manifest.version + '\n')
mutate(root, 'manifest.json', entry => {
entry.type = type
entry.productKind = 'plugin'
entry.entrypoints = []
entry.extension = structuredClone(extension)
if (type === 'artifact') entry.install = { method: 'artifact', requirements: { docker: false, ports: [] } }
})
mutate(root, 'catalog-metadata.v1.json', sidecar => {
sidecar.spec.productKind = 'plugin'
sidecar.spec.entrypointKinds = []
sidecar.spec.extension = structuredClone(extension)
})
return root
}
function mutate(root, file, change) {
const path = join(root, 'entries/desirecore-control', file)
const data = JSON.parse(readFileSync(path, 'utf8'))
change(data)
writeFileSync(path, JSON.stringify(data))
}
function result(root) {
const cli = spawnSync(process.execPath, [join(source, 'scripts/validate-registry.mjs'), '--root', root], { encoding: 'utf8' })
return { cli, offline: validateRegistry(root) }
}
function expectValid(root) {
const report = result(root)
assert.equal(report.cli.status, 0, report.cli.stderr)
assert.equal(report.offline.ok, true, JSON.stringify(report.offline.errors))
return report
}
function expectInvalid(root) {
const report = result(root)
assert.equal(report.cli.status, 1, report.cli.stdout + report.cli.stderr)
assert.equal(report.offline.ok, false, JSON.stringify(report.offline))
}
test('无 UI artifact 插件仍为 App,无端口与执行进程', () => {
const report = expectValid(fixture())
assert.equal(report.offline.counts.artifactApps, 1)
assert.equal(report.offline.counts.mcpServices, 0)
})
test('普通原生应用可携带宿主贡献', () => {
const root = fixture('native-app')
mutate(root, 'manifest.json', data => { data.productKind = 'application'; data.entrypoints = [{ id: 'manage', name: '维护', kind: 'agent' }] })
mutate(root, 'catalog-metadata.v1.json', data => { data.spec.productKind = 'application'; data.spec.entrypointKinds = ['agent'] })
expectValid(root)
})
test('旧普通应用省略主要归属时受信归一 application,保留显式维护入口', () => {
const root = fixture('native-app')
mutate(root, 'manifest.json', data => {
delete data.productKind
delete data.extension
data.entrypoints = [{ id: 'manage', name: '维护', kind: 'agent' }]
})
mutate(root, 'catalog-metadata.v1.json', data => {
data.spec.productKind = 'application'
delete data.spec.extension
data.spec.entrypointKinds = ['agent']
})
expectValid(root)
mutate(root, 'catalog-metadata.v1.json', data => { data.spec.productKind = 'plugin' })
const report = result(root)
assert.equal(report.offline.ok, false)
assert.ok(report.offline.errors.some(error => error.code === 'product-kind-mismatch'))
})
test('省略归属不能把无入口插件声明冒充旧普通应用', () => {
const root = fixture('native-app')
mutate(root, 'manifest.json', data => { delete data.productKind })
expectInvalid(root)
})
test('原生应用的真实服务 runtime 由固定 descriptorRef 定位', () => {
const root = fixture('native-app')
const runtime = { kind: 'service', protocol: 'http', descriptorRef: { path: 'runtime.json', sha256: 'c'.repeat(64) } }
mutate(root, 'manifest.json', data => { data.extension.runtime = structuredClone(runtime) })
mutate(root, 'catalog-metadata.v1.json', data => { data.spec.extension.runtime = structuredClone(runtime) })
expectValid(root)
})
test('普通应用可只依赖能力,依赖 sourceId 不冒充目录身份', () => {
const root = fixture('native-app')
mutate(root, 'manifest.json', data => { data.productKind = 'application'; data.entrypoints = [{ id: 'manage', name: '维护', kind: 'agent' }]; data.extension.contributes = []; data.extension.requires = structuredClone(requires) })
mutate(root, 'catalog-metadata.v1.json', data => { data.spec.productKind = 'application'; data.spec.entrypointKinds = ['agent']; data.spec.extension.contributes = []; data.spec.extension.requires = structuredClone(requires) })
expectValid(root)
})
for (const [name, change] of [
['插件无贡献', data => { data.extension.contributes = []; data.extension.requires = structuredClone(requires) }],
['贡献与依赖均为空', data => { data.extension.contributes = [] }],
['非法贡献点', data => { data.extension.contributes[0].point = 'arbitrary.code' }],
['材料相对路径穿越', data => { data.extension.contributes[0].definitionRef.path = '../panel.json' }],
['材料缺摘要', data => { delete data.extension.contributes[0].definitionRef.sha256 }],
['artifact 声明端口', data => { data.install.requirements.ports = [9333] }],
['artifact 伪造执行服务', data => { data.extension.runtime = { kind: 'service', protocol: 'http', descriptorRef: { path: 'runtime.json', sha256: 'c'.repeat(64) } } }],
['伪造条目来源', data => { data.sourceId = 'official' }],
['其他层级伪造来源', data => { data.extension.runtime.sourceId = 'official' }],
['重复贡献 ID', data => { data.extension.contributes.push(structuredClone(contribution)) }],
['重复入口 ID', data => { data.entrypoints = [{ id: 'x', name: '一', kind: 'agent' }, { id: 'x', name: '二', kind: 'agent' }] }],
['第三方伪造 builtin', data => { data.entrypoints = [{ id: 'x', name: '一', kind: 'builtin', route: '/settings' }] }],
['第三方指定 reserved port', data => { data.entrypoints = [{ id: 'x', name: '一', kind: 'external', serviceRef: 'native-port:9333' }] }],
['结构错误 entrypoints', data => { data.entrypoints = {} }],
['结构错误贡献数组', data => { data.extension.contributes = [null] }],
]) test('CLI 与离线校验都拒绝 ' + name, () => {
const root = fixture()
mutate(root, 'manifest.json', change)
expectInvalid(root)
})
test('同一产品侧录不能改变 productKind 或关系声明', () => {
const root = fixture()
mutate(root, 'catalog-metadata.v1.json', data => { data.spec.productKind = 'application' })
const report = validateRegistry(root)
assert.equal(report.ok, false)
assert.ok(report.errors.some(error => error.code === 'product-kind-mismatch'))
})
test('artifact 统计必须对应真实文件,来源摘要不可漂移', () => {
const root = fixture()
mutate(root, 'catalog-metadata.v1.json', data => { data.provenance.content.sha256 = 'b'.repeat(64) })
assert.equal(validateRegistry(root).ok, false)
const manifestPath = join(root, 'manifest.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
manifest.stats.artifactApps = 0
writeFileSync(manifestPath, JSON.stringify(manifest))
expectInvalid(root)
})