mirror of
https://git.openapi.site/https://github.com/desirecore/registry.git
synced 2026-10-21 01:13:19 +08:00
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>
This commit is contained in:
147
scripts/plugin-contract.test.mjs
Normal file
147
scripts/plugin-contract.test.mjs
Normal file
@@ -0,0 +1,147 @@
|
||||
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)
|
||||
})
|
||||
Reference in New Issue
Block a user