Files

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,
})
})