dsh 0.2.0-rc.2: in-container update (experimental channel), path-agnostic compose, upstream patches adapted to 0.1.x/0.2.x
This commit is contained in:
commit
e6dcef9bf5
302 files changed
+75178
No files matched your search
@@ -0,0 +1,183 @@
|
||||
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/,
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user