330 lines
15 KiB
JavaScript
330 lines
15 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { cp, lstat, mkdtemp, mkdir, readFile, readlink, rm, symlink, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { spawnSync } from 'node:child_process'
|
|
import test from 'node:test'
|
|
import {
|
|
discardSystemPluginSelection,
|
|
linkSystemPluginScope,
|
|
listBundledSystemPlugins,
|
|
listManagedSystemPlugins,
|
|
materializeSystemPluginSelection,
|
|
rebuildBundledSystemPluginView,
|
|
reconcileSystemPlugins,
|
|
SystemPluginSelectionStore,
|
|
} from '../../control-plane/modules/plugin-manager/system.mjs'
|
|
import { createHash } from 'node:crypto'
|
|
import { hashTree } from '../lib/tree-hash.mjs'
|
|
|
|
test('Bootstrap lists System Plugins from the selected runtime view without rehashing the Deployment', async () => {
|
|
const source = await readFile(new URL('../bootstrap/index.mjs', import.meta.url), 'utf8')
|
|
const list = source.slice(source.indexOf('const systemPlugins = {'), source.indexOf(' mutate:', source.indexOf('const systemPlugins = {')))
|
|
assert.match(list, /realpath\(paths\.deploymentView\)/)
|
|
assert.match(list, /environmentRoot: join\(deploymentRoot, 'environment'\)/)
|
|
assert.match(list, /sourceRoot: join\(deploymentRoot, 'system-plugins'\)/)
|
|
assert.doesNotMatch(list, /deployments\.selected\(\)/)
|
|
})
|
|
|
|
async function archive(root, id, patch) {
|
|
const source = join(root, `source-${id}`, 'package')
|
|
await mkdir(source, { recursive: true })
|
|
await writeFile(join(source, 'package.json'), JSON.stringify({
|
|
name: `@dsh-docker/${id}`,
|
|
type: 'module',
|
|
main: 'plugin.mjs',
|
|
dshDocker: { description: { zh: `${id} 功能`, en: `${id} feature` } },
|
|
}))
|
|
await writeFile(join(source, 'cordis.patch.json'), JSON.stringify(patch))
|
|
await writeFile(join(source, 'plugin.mjs'), `export const id = ${JSON.stringify(id)}\n`)
|
|
const path = join(root, `${id}.tgz`)
|
|
const result = spawnSync('tar', ['-czf', path, '-C', dirname(source), 'package'], { encoding: 'utf8' })
|
|
assert.equal(result.status, 0, result.stderr)
|
|
return path
|
|
}
|
|
|
|
function dirname(path) {
|
|
return path.slice(0, path.lastIndexOf('/'))
|
|
}
|
|
|
|
test('reconciles only declared System Plugins into a separate immutable overlay', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-system-plugins-'))
|
|
const userHome = join(root, 'user-home')
|
|
await mkdir(join(userHome, 'profiles/web'), { recursive: true })
|
|
await writeFile(join(userHome, 'cordis.patch.yml'), '[{"id":"user-owned"}]\n')
|
|
await writeFile(join(userHome, 'profiles/web/package.json'), '{"dependencies":{"third-party":"1.0.0"}}\n')
|
|
const first = await archive(root, 'update-ui', [{ insert: [{ id: 'dsh-docker.update-ui.host', name: '@dsh-docker/update-ui' }] }])
|
|
const second = await archive(root, 'diagnostics', [{ insert: [{ id: 'dsh-docker.diagnostics.host', name: '@dsh-docker/diagnostics' }] }])
|
|
const artifacts = new Map([['update-ui', first], ['diagnostics', second]])
|
|
await reconcileSystemPlugins({
|
|
root: join(root, 'managed'),
|
|
environmentVersion: '1.0.0',
|
|
plugins: [
|
|
{ id: 'update-ui', sha256: '1'.repeat(64) },
|
|
{ id: 'diagnostics', sha256: '2'.repeat(64) },
|
|
],
|
|
artifactPath: reference => artifacts.get(reference.id),
|
|
})
|
|
const overlay = JSON.parse(await readFile(join(root, 'managed/current/cordis.patch.yml'), 'utf8'))
|
|
assert.deepEqual(overlay.flatMap(entry => entry.insert).map(entry => entry.id), ['dsh-docker.update-ui.host', 'dsh-docker.diagnostics.host'])
|
|
assert.deepEqual(overlay.flatMap(entry => entry.insert).map(entry => entry.name), [
|
|
'@dsh-docker/update-ui',
|
|
'@dsh-docker/diagnostics',
|
|
])
|
|
assert.equal(await readFile(join(userHome, 'cordis.patch.yml'), 'utf8'), '[{"id":"user-owned"}]\n')
|
|
assert.match(await readFile(join(userHome, 'profiles/web/package.json'), 'utf8'), /third-party/)
|
|
})
|
|
|
|
test('rejects a System Plugin which claims another namespace', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-system-plugin-invalid-'))
|
|
const artifact = await archive(root, 'update-ui', [{ insert: [{ id: 'unowned-row', name: '@dsh-docker/update-ui' }] }])
|
|
await assert.rejects(reconcileSystemPlugins({
|
|
root: join(root, 'managed'),
|
|
environmentVersion: 'invalid',
|
|
plugins: [{ id: 'update-ui', sha256: '1'.repeat(64) }],
|
|
artifactPath: () => artifact,
|
|
}), /namespace/)
|
|
})
|
|
|
|
test('rejects a System Plugin which patches an existing DSH row', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-system-plugin-mutation-'))
|
|
const artifact = await archive(root, 'update-ui', [{
|
|
id: 'dsh-docker.update-ui.host',
|
|
name: '@dsh-docker/update-ui',
|
|
}])
|
|
await assert.rejects(reconcileSystemPlugins({
|
|
root: join(root, 'managed'),
|
|
environmentVersion: 'invalid',
|
|
plugins: [{ id: 'update-ui', sha256: '1'.repeat(64) }],
|
|
artifactPath: () => artifact,
|
|
}), /only a non-empty insert list/)
|
|
})
|
|
|
|
test('retargets only the reserved System Plugin scope without changing persistent Profile data', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-system-plugin-scope-'))
|
|
const dshHome = join(root, 'dsh')
|
|
const profileRoot = join(dshHome, 'profiles', 'web')
|
|
const modulesRoot = join(dshHome, 'profiles', 'node_modules')
|
|
const userPackageRoot = join(modulesRoot, '@example', 'persistent-plugin')
|
|
const firstView = join(root, 'run', 'system-plugins-v1')
|
|
const secondView = join(root, 'run', 'system-plugins-v2')
|
|
await mkdir(join(profileRoot, 'node_modules'), { recursive: true })
|
|
await mkdir(userPackageRoot, { recursive: true })
|
|
await mkdir(join(firstView, 'packages'), { recursive: true })
|
|
await mkdir(join(secondView, 'packages'), { recursive: true })
|
|
await writeFile(join(profileRoot, 'package.json'), '{"dependencies":{"persistent-plugin":"1.0.0"}}\n')
|
|
await writeFile(join(profileRoot, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
|
|
await writeFile(join(userPackageRoot, 'package.json'), '{"name":"@example/persistent-plugin"}\n')
|
|
await symlink(userPackageRoot, join(profileRoot, 'node_modules', 'persistent-plugin'), 'dir')
|
|
const profileBefore = await hashTree(profileRoot)
|
|
|
|
const link = await linkSystemPluginScope({ dshHome, viewRoot: firstView })
|
|
assert.equal(await readlink(link), join(firstView, 'packages'))
|
|
assert.equal(await linkSystemPluginScope({ dshHome, viewRoot: firstView }), link)
|
|
assert.equal(await linkSystemPluginScope({ dshHome, viewRoot: secondView }), link)
|
|
assert.equal(await readlink(link), join(secondView, 'packages'))
|
|
assert.equal(await hashTree(profileRoot), profileBefore)
|
|
assert.equal(await readFile(join(userPackageRoot, 'package.json'), 'utf8'), '{"name":"@example/persistent-plugin"}\n')
|
|
})
|
|
|
|
test('rebuilds a bundled System Plugin view only from the current Environment artifacts', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-system-plugin-repair-'))
|
|
const environment = join(root, 'environment')
|
|
const artifact = await archive(root, 'platform-management', [{
|
|
insert: [{ id: 'dsh-docker.platform-management.plugin', name: '@dsh-docker/platform-management' }],
|
|
}])
|
|
const bytes = await readFile(artifact)
|
|
const sha256 = createHash('sha256').update(bytes).digest('hex')
|
|
await mkdir(join(environment, 'artifacts'), { recursive: true })
|
|
await cp(artifact, join(environment, 'artifacts', 'system-plugin-platform-management'))
|
|
await writeFile(join(environment, 'environment.manifest.json'), JSON.stringify({
|
|
schema: 1,
|
|
manifestType: 'environment',
|
|
version: '1.0.0',
|
|
keyringGeneration: 1,
|
|
targetSequence: 1,
|
|
issuedAt: '2026-08-20T00:00:00.000Z',
|
|
artifacts: [{
|
|
id: 'system-plugin-platform-management',
|
|
mediaType: 'application/vnd.dsh-platform.system-plugin.v1+tar+gzip',
|
|
sha256,
|
|
size: bytes.byteLength,
|
|
url: 'https://example.invalid/system-plugin-platform-management',
|
|
}],
|
|
bootstrapApi: 1,
|
|
components: [],
|
|
patches: [],
|
|
systemPlugins: [{ id: 'platform-management', sha256 }],
|
|
}))
|
|
const initialRoot = join(root, 'initial')
|
|
const initial = await reconcileSystemPlugins({
|
|
root: initialRoot,
|
|
environmentVersion: 'fixture',
|
|
plugins: [{ id: 'platform-management', sha256 }],
|
|
artifactPath: () => artifact,
|
|
})
|
|
const expectedSha256 = await hashTree(initial)
|
|
const rebuilt = await rebuildBundledSystemPluginView({
|
|
environmentRoot: environment,
|
|
outputRoot: join(root, 'views'),
|
|
expectedSha256,
|
|
requestedPluginId: 'platform-management',
|
|
})
|
|
assert.equal(await hashTree(rebuilt), expectedSha256)
|
|
assert.deepEqual(await listBundledSystemPlugins({ environmentRoot: environment, viewRoot: rebuilt }), [{
|
|
id: 'platform-management',
|
|
artifactId: 'system-plugin-platform-management',
|
|
sha256,
|
|
installed: true,
|
|
reason: null,
|
|
}])
|
|
await rm(join(rebuilt, 'packages', 'platform-management'), { recursive: true })
|
|
const repairedAgain = await rebuildBundledSystemPluginView({
|
|
environmentRoot: environment,
|
|
outputRoot: join(root, 'views'),
|
|
expectedSha256,
|
|
requestedPluginId: 'platform-management',
|
|
})
|
|
assert.equal(await hashTree(repairedAgain), expectedSha256)
|
|
await assert.rejects(rebuildBundledSystemPluginView({
|
|
environmentRoot: environment,
|
|
outputRoot: join(root, 'views'),
|
|
expectedSha256,
|
|
requestedPluginId: 'unknown',
|
|
}), /not bundled/)
|
|
})
|
|
|
|
test('persists install and enable selection while protecting Platform Management', async () => {
|
|
const root = await mkdtemp(join(tmpdir(), 'dsh-system-plugin-selection-'))
|
|
const environment = join(root, 'environment')
|
|
const definitions = [
|
|
['platform-management', '1'],
|
|
['diagnostics', '2'],
|
|
]
|
|
const artifacts = []
|
|
await mkdir(join(environment, 'artifacts'), { recursive: true })
|
|
for (const [id] of definitions) {
|
|
const artifact = await archive(root, id, [{
|
|
insert: [{ id: `dsh-docker.${id}.plugin`, name: `@dsh-docker/${id}` }],
|
|
}])
|
|
const bytes = await readFile(artifact)
|
|
const sha256 = createHash('sha256').update(bytes).digest('hex')
|
|
const artifactId = `system-plugin-${id}`
|
|
await cp(artifact, join(environment, 'artifacts', artifactId))
|
|
artifacts.push({ id: artifactId, sha256, size: bytes.byteLength, pluginId: id, path: artifact })
|
|
}
|
|
await writeFile(join(environment, 'environment.manifest.json'), JSON.stringify({
|
|
schema: 1,
|
|
manifestType: 'environment',
|
|
version: '1.0.0',
|
|
keyringGeneration: 1,
|
|
targetSequence: 1,
|
|
issuedAt: '2026-08-20T00:00:00.000Z',
|
|
artifacts: artifacts.map(artifact => ({
|
|
id: artifact.id,
|
|
mediaType: 'application/vnd.dsh-platform.system-plugin.v1+tar+gzip',
|
|
sha256: artifact.sha256,
|
|
size: artifact.size,
|
|
url: `https://example.invalid/${artifact.id}`,
|
|
})),
|
|
bootstrapApi: 1,
|
|
components: [],
|
|
patches: [],
|
|
systemPlugins: artifacts.map(artifact => ({ id: artifact.pluginId, sha256: artifact.sha256 })),
|
|
}))
|
|
const source = await reconcileSystemPlugins({
|
|
root: join(root, 'source'),
|
|
environmentVersion: 'fixture',
|
|
plugins: artifacts.map(artifact => ({ id: artifact.pluginId, sha256: artifact.sha256 })),
|
|
artifactPath: reference => artifacts.find(artifact => artifact.pluginId === reference.id).path,
|
|
})
|
|
const store = new SystemPluginSelectionStore(join(root, 'state', 'system-plugins.json'))
|
|
const pluginIds = artifacts.map(artifact => artifact.pluginId)
|
|
const initial = await listManagedSystemPlugins({ environmentRoot: environment, sourceRoot: source, selectionStore: store })
|
|
assert.deepEqual(initial.map(plugin => [plugin.id, plugin.description, plugin.installed, plugin.enabled, plugin.protected]), [
|
|
['platform-management', { zh: 'platform-management 功能', en: 'platform-management feature' }, true, true, true],
|
|
['diagnostics', { zh: 'diagnostics 功能', en: 'diagnostics feature' }, true, true, false],
|
|
])
|
|
const active = await materializeSystemPluginSelection({
|
|
environmentRoot: environment,
|
|
sourceRoot: source,
|
|
outputRoot: join(root, 'views'),
|
|
selectionStore: store,
|
|
})
|
|
|
|
await assert.rejects(store.configure(pluginIds, 'platform-management', 'uninstall'), /managed by the platform/)
|
|
await assert.rejects(store.recover(pluginIds, 'diagnostics', 'uninstall'), /not a platform recovery target/)
|
|
await store.recover(pluginIds, 'platform-management', 'uninstall')
|
|
assert.deepEqual(await store.read(pluginIds).then(value => value['platform-management']), {
|
|
installed: false, enabled: false, protected: true,
|
|
})
|
|
await store.recover(pluginIds, 'platform-management', 'install')
|
|
await store.configure(pluginIds, 'diagnostics', 'disable')
|
|
const pending = await listManagedSystemPlugins({
|
|
environmentRoot: environment,
|
|
sourceRoot: source,
|
|
selectionStore: store,
|
|
activeRoot: active.path,
|
|
})
|
|
assert.deepEqual(pending.find(plugin => plugin.id === 'diagnostics'), {
|
|
id: 'diagnostics',
|
|
artifactId: 'system-plugin-diagnostics',
|
|
sha256: artifacts.find(artifact => artifact.pluginId === 'diagnostics').sha256,
|
|
description: { zh: 'diagnostics 功能', en: 'diagnostics feature' },
|
|
installed: true,
|
|
enabled: false,
|
|
activeInstalled: true,
|
|
activeEnabled: true,
|
|
pendingRestart: true,
|
|
protected: false,
|
|
reason: null,
|
|
})
|
|
const discarded = await discardSystemPluginSelection({
|
|
environmentRoot: environment,
|
|
sourceRoot: source,
|
|
selectionStore: store,
|
|
activeRoot: active.path,
|
|
})
|
|
assert.equal(discarded.find(plugin => plugin.id === 'diagnostics').pendingRestart, false)
|
|
assert.deepEqual((await store.read(pluginIds)).diagnostics, {
|
|
installed: true, enabled: true, protected: false,
|
|
})
|
|
await store.configure(pluginIds, 'diagnostics', 'disable')
|
|
const disabled = await materializeSystemPluginSelection({
|
|
environmentRoot: environment,
|
|
sourceRoot: source,
|
|
outputRoot: join(root, 'views'),
|
|
selectionStore: store,
|
|
})
|
|
assert.equal((await lstat(join(disabled.path, 'packages', 'diagnostics'))).isSymbolicLink(), true)
|
|
assert.deepEqual(JSON.parse(await readFile(join(disabled.path, 'cordis.patch.yml'), 'utf8'))
|
|
.flatMap(entry => entry.insert).map(row => row.name), ['@dsh-docker/platform-management'])
|
|
assert.equal((await listManagedSystemPlugins({
|
|
environmentRoot: environment,
|
|
sourceRoot: source,
|
|
selectionStore: store,
|
|
activeRoot: disabled.path,
|
|
})).find(plugin => plugin.id === 'diagnostics').pendingRestart, false)
|
|
|
|
await store.configure(pluginIds, 'diagnostics', 'uninstall')
|
|
const deleted = await materializeSystemPluginSelection({
|
|
environmentRoot: environment,
|
|
sourceRoot: source,
|
|
outputRoot: join(root, 'views'),
|
|
selectionStore: store,
|
|
})
|
|
await assert.rejects(lstat(join(deleted.path, 'packages', 'diagnostics')), error => error.code === 'ENOENT')
|
|
assert.deepEqual((await listManagedSystemPlugins({ environmentRoot: environment, sourceRoot: source, selectionStore: store }))
|
|
.map(plugin => [plugin.id, plugin.installed, plugin.enabled]), [
|
|
['platform-management', true, true],
|
|
['diagnostics', false, false],
|
|
])
|
|
|
|
await store.configure(pluginIds, 'diagnostics', 'install')
|
|
assert.deepEqual((await new SystemPluginSelectionStore(join(root, 'state', 'system-plugins.json')).read(pluginIds)).diagnostics, {
|
|
installed: true,
|
|
enabled: true,
|
|
protected: false,
|
|
})
|
|
})
|