Files
dsh/upstream/container/platform/test/user-plugins.test.mjs
T

184 lines
8.5 KiB
JavaScript

import assert from 'node:assert/strict'
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import test from 'node:test'
import { markUserPluginRestartState, UserPluginInventory } from '../../control-plane/modules/plugin-manager/user-inventory.mjs'
async function fixture({ dependencies, bundles, packages = {}, selection } = {}) {
const root = await mkdtemp(join(tmpdir(), 'dsh-user-plugins-'))
const profile = join(root, 'dsh/profiles/web')
const selectionPath = join(root, 'platform/state/management/user-plugins.json')
await mkdir(join(profile, 'node_modules'), { recursive: true })
await writeFile(join(profile, 'package.json'), JSON.stringify({
name: 'dsh-profile-web',
dependencies: dependencies ?? {},
dsh: { profile: { bundles: bundles ?? [] } },
}))
await writeFile(join(profile, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n')
for (const [name, metadata] of Object.entries(packages)) {
const destination = join(profile, 'node_modules', ...name.split('/'))
if (metadata === 'dangling') {
await mkdir(join(destination, '..'), { recursive: true })
await symlink(join(root, 'missing-package'), destination)
continue
}
await mkdir(destination, { recursive: true })
await writeFile(join(destination, 'package.json'), typeof metadata === 'string' ? metadata : JSON.stringify({
name,
version: '1.2.3',
...metadata,
}))
}
if (selection !== undefined) {
await mkdir(join(selectionPath, '..'), { recursive: true })
await writeFile(selectionPath, JSON.stringify(selection))
}
return { root, profile, selectionPath }
}
test('inventories only profile Bundle dependencies without importing their entrypoints', async () => {
const { root, selectionPath } = await fixture({
dependencies: {
'working-plugin': '^1.2.0',
'plain-library': '2.0.0',
'git-plugin': 'github:example/plugin#main',
'local-plugin': 'file:../local-plugin',
},
bundles: ['@deepseek-ai/dsh-base', 'working-plugin', 'git-plugin'],
packages: {
'working-plugin': { description: 'Adds useful workflow tools.', dsh: { bundle: { patch: './cordis.patch.yml' } }, main: './throw-if-imported.mjs' },
'plain-library': {},
'git-plugin': { dsh: { bundle: { patch: './plugin.patch.yml' } } },
'local-plugin': { dsh: { bundle: { patch: './plugin.patch.yml' } } },
},
selection: { schema: 1, disabled: [{ name: 'local-plugin', index: 2 }] },
})
const inventory = await new UserPluginInventory({ dshHome: join(root, 'dsh'), selectionPath }).read()
assert.match(inventory.revision, /^sha256:[a-f0-9]{64}$/)
assert.deepEqual(inventory.plugins.map(plugin => [plugin.name, plugin.source, plugin.enabled, plugin.previousIndex]), [
['working-plugin', 'registry', true, null],
['git-plugin', 'git', true, null],
['local-plugin', 'file', false, 2],
])
assert.equal(inventory.plugins[0].version, '1.2.3')
assert.equal(inventory.plugins[0].description, 'Adds useful workflow tools.')
assert.equal(inventory.plugins[1].description, null)
assert.equal(inventory.plugins[0].damaged, false)
})
test('marks only user plugins which differ from the state loaded by DSH', () => {
const plugin = Object.freeze({
name: 'alpha', spec: '1.0.0', version: '1.0.0', enabled: true,
damaged: false, metadataError: null, reservedNameConflict: false,
})
const loaded = Object.freeze({ schema: 1, profile: 'web', revision: 'sha256:loaded', plugins: Object.freeze([plugin]) })
const unchanged = markUserPluginRestartState({ ...loaded, revision: 'sha256:lockfile-only' }, loaded)
assert.equal(unchanged.restartRequired, false)
assert.equal(unchanged.plugins[0].pendingRestart, false)
const added = Object.freeze({ ...plugin, name: 'beta' })
const changed = markUserPluginRestartState({
...loaded,
revision: 'sha256:changed',
plugins: Object.freeze([{ ...plugin, version: '1.1.0' }, added]),
}, loaded)
assert.equal(changed.restartRequired, true)
assert.deepEqual(changed.plugins.map(value => [value.name, value.pendingRestart]), [['alpha', true], ['beta', true]])
const removed = markUserPluginRestartState({ ...loaded, revision: 'sha256:removed', plugins: Object.freeze([]) }, loaded)
assert.equal(removed.restartRequired, true)
})
test('keeps damaged and dangling dependency metadata visible for recovery', async () => {
const { root, selectionPath } = await fixture({
dependencies: { broken: 'https://example.invalid/broken.tgz', dangling: 'link:../dangling' },
bundles: ['broken', 'dangling'],
packages: { broken: '{', dangling: 'dangling' },
})
const inventory = await new UserPluginInventory({ dshHome: join(root, 'dsh'), selectionPath }).read()
assert.deepEqual(inventory.plugins.map(plugin => [plugin.name, plugin.source, plugin.enabled, plugin.damaged]), [
['broken', 'url', true, true],
['dangling', 'file', true, true],
])
assert.match(inventory.plugins[0].metadataError, /valid JSON/)
assert.match(inventory.plugins[1].metadataError, /missing/)
})
test('keeps an enabled dependency visible when its Bundle declaration is invalid', async () => {
const { root, selectionPath } = await fixture({
dependencies: { 'startup-breaking-plugin': '1.0.0' },
bundles: ['startup-breaking-plugin'],
packages: { 'startup-breaking-plugin': { dsh: { bundle: { patch: 42 } } } },
})
const inventory = await new UserPluginInventory({ dshHome: join(root, 'dsh'), selectionPath }).read()
assert.equal(inventory.plugins.length, 1)
assert.equal(inventory.plugins[0].name, 'startup-breaking-plugin')
assert.equal(inventory.plugins[0].enabled, true)
assert.equal(inventory.plugins[0].damaged, true)
assert.match(inventory.plugins[0].metadataError, /Bundle metadata/)
})
test('uses verified Environment names for reserved conflicts instead of namespace prefixes', async () => {
const { root, selectionPath } = await fixture({
dependencies: {
'@dsh-docker/fake-external': '1.0.0',
'@dsh-docker/settings-document-editor': '1.0.0',
},
bundles: ['@dsh-docker/fake-external', '@dsh-docker/settings-document-editor'],
packages: {
'@dsh-docker/fake-external': { dsh: { bundle: { patch: './cordis.patch.yml' } } },
'@dsh-docker/settings-document-editor': {},
},
})
const inventory = await new UserPluginInventory({
dshHome: join(root, 'dsh'),
selectionPath,
systemPluginNames: async () => ['@dsh-docker/settings-document-editor'],
}).read()
assert.deepEqual(inventory.plugins.map(plugin => [plugin.name, plugin.reservedNameConflict, plugin.enabled]), [
['@dsh-docker/fake-external', false, true],
['@dsh-docker/settings-document-editor', true, false],
])
})
test('revision covers exact manifest, lockfile, and disabled-order bytes', async () => {
const { root, profile, selectionPath } = await fixture({
dependencies: { plugin: '1.0.0' },
bundles: ['plugin'],
packages: { plugin: { dsh: { bundle: { patch: './cordis.patch.yml' } } } },
})
const manager = new UserPluginInventory({ dshHome: join(root, 'dsh'), selectionPath })
const first = await manager.read()
await writeFile(join(profile, 'pnpm-lock.yaml'), 'lockfileVersion: 9\n# exact byte changed\n')
const second = await manager.read()
await mkdir(join(selectionPath, '..'), { recursive: true })
await writeFile(selectionPath, '{"schema":1,"disabled":[]}\n')
const third = await manager.read()
await rm(selectionPath)
await writeFile(join(profile, 'pnpm-lock.yaml'), '')
const emptyLockfile = await manager.read()
await rm(join(profile, 'pnpm-lock.yaml'))
const missingLockfile = await manager.read()
assert.notEqual(first.revision, second.revision)
assert.notEqual(second.revision, third.revision)
assert.notEqual(emptyLockfile.revision, missingLockfile.revision)
})
test('rejects malformed authoritative profile and selection state', async () => {
const malformed = await fixture({ dependencies: { plugin: 123 } })
await assert.rejects(
new UserPluginInventory({ dshHome: join(malformed.root, 'dsh'), selectionPath: malformed.selectionPath }).read(),
/non-empty string spec/,
)
const state = await fixture({
dependencies: { plugin: '1.0.0' },
packages: { plugin: { dsh: { bundle: { patch: './cordis.patch.yml' } } } },
selection: { schema: 1, disabled: [{ name: '../unsafe', index: 0 }] },
})
await assert.rejects(
new UserPluginInventory({ dshHome: join(state.root, 'dsh'), selectionPath: state.selectionPath }).read(),
/disabled-order entry/,
)
})