Second review round on the contained-group composition:
- Ownership scanned only `insert` rows, so an external bundle could set a
built-in row's id as a child through `{ id: own, config: [...] }` and the
Loader re-parented that row under the bundle while `originOf` still named
the built-in layer. The rows an id-targeted patch sets as a group's config
now count as the layer's own: an external bundle that sets an id another
layer owns is left out whole, a built-in layer that takes another layer's
id fails loud, and a layer restating its own child is not a repeat.
- `recompose` read the committed profile before awaiting the update, so a
watcher firing while a bundle was being enabled composed the tree before
the enable and applied it after; the plugin vanished while `layers` kept
listing it. Recompositions now queue, each reading what the previous one
committed, and a rejection stays its own caller's outcome.
- A patch that replaced a built-in target's config removed the wrapper the
bundle had put there, yet later inserts still targeted the detached
wrapper and vanished. The composition forgets the wrapper on such an
override and creates a new one under the same id.
- Generated wrapper ids bypassed the duplicate check, so a bundle declaring
a row spelled like a wrapper produced two nodes with one id. Declared and
generated ids now go through one registration.
- Failure records outlived their rows when a bundle update dropped a failed
row without unmounting the group, or when a pending row sat in a plain
nested group whose id the record named. A contained group now drops the
records of rows its configuration no longer names after each update, and
the audit records every row under the contained group that isolates it.
- `applyEntryPatches` assigned override values by reference, so applying a
patch list carried a later insert's rows back into the patch object; the
vendored include now clones object-valued overrides as it does inserts.
168 lines
9.3 KiB
TypeScript
168 lines
9.3 KiB
TypeScript
/**
|
|
* Tree-wide row-id ownership across the profile stack: built-in layers claim
|
|
* first and fail loud on a duplicate, an external bundle that collides is left
|
|
* out and recorded, a bundle that repeats one of its own ids is left out the
|
|
* same way, a user insert of a taken id is dropped, and every conflict
|
|
* carries its message.
|
|
*/
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
|
|
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
|
import { claimLayerIds, composeProfileStack, CONTAINED_GROUP_MODULE, formatRowConflict, type ProfileLayer } from '../src/index.ts'
|
|
|
|
const NAME = 'dsh-test-bin'
|
|
|
|
function layer(
|
|
packageName: string, trust: ProfileLayer['trust'], patches: PatchOptions[], stage: ProfileLayer['stage'] = 'runtime',
|
|
): ProfileLayer {
|
|
return { packageName, version: '1.0.0', packageDir: '/nowhere', patchPath: '/nowhere/cordis.patch.yml', trust, stage, patches }
|
|
}
|
|
|
|
const base = layer('@deepseek-ai/dsh-base', 'builtin', [{ insert: [
|
|
{ id: 'settings', name: 'settings' },
|
|
{ id: 'tools', name: 'cordis:group', group: true, config: [{ id: 'tool-bash', name: 'bash' }] },
|
|
] }])
|
|
|
|
describe('claimLayerIds', () => {
|
|
it('lets built-in layers own their ids, including group children, before any external layer', () => {
|
|
const ext = layer('ext', 'external', [{ insert: [{ id: 'tool-bash', name: 'ext' }] }])
|
|
const { owners, skipped } = claimLayerIds([ext, base])
|
|
expect(owners.get('tool-bash')?.packageName).toBe('@deepseek-ai/dsh-base')
|
|
expect(owners.get('tools')?.packageName).toBe('@deepseek-ai/dsh-base')
|
|
expect(skipped.get('ext')).toEqual([
|
|
{
|
|
rowId: 'tool-bash', moduleName: 'ext', layer: 'ext', packageName: 'ext', declaredBy: '@deepseek-ai/dsh-base',
|
|
message: 'row "tool-bash" is already declared by @deepseek-ai/dsh-base',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('throws when two built-in or boot-staged layers declare one id, or one declares it twice', () => {
|
|
const twin = layer('twin', 'external', [{ insert: [{ id: 'settings', name: 'twin' }] }], 'boot')
|
|
expect(() => claimLayerIds([base, twin])).toThrow(/row "settings" is declared by both @deepseek-ai\/dsh-base and twin/)
|
|
const stutter = layer('stutter', 'builtin', [{ insert: [{ id: 'x', name: 'a' }] }, { insert: [{ id: 'x', name: 'b' }] }])
|
|
expect(() => claimLayerIds([stutter])).toThrow(/row "x" is declared twice by stutter/)
|
|
})
|
|
|
|
it('leaves out a bundle whose config override sets a row another layer owns', () => {
|
|
const hijack = layer('hijack', 'external', [
|
|
{ insert: [{ id: 'own', name: 'cordis:group', group: true, config: [] }] },
|
|
{ id: 'own', config: [{ id: 'settings', name: 'hijack/impostor' }] },
|
|
])
|
|
const { skipped } = claimLayerIds([base, hijack])
|
|
expect(skipped.get('hijack')).toEqual([
|
|
{
|
|
rowId: 'settings', moduleName: 'hijack/impostor', layer: 'hijack', packageName: 'hijack', declaredBy: '@deepseek-ai/dsh-base',
|
|
message: 'row "settings" is already declared by @deepseek-ai/dsh-base',
|
|
},
|
|
])
|
|
const stack = composeProfileStack(NAME, [base, hijack], [])
|
|
expect(stack.skippedBundles).toEqual(['hijack'])
|
|
expect(stack.owners.get('settings')?.packageName).toBe('@deepseek-ai/dsh-base')
|
|
})
|
|
|
|
it('lets a built-in layer restate its own rows through a config override, and rejects another layer\'s', () => {
|
|
const restating = layer('restating', 'builtin', [
|
|
{ insert: [{ id: 'g', name: 'cordis:group', group: true, config: [{ id: 'a', name: 'a' }] }] },
|
|
{ id: 'g', config: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }] },
|
|
])
|
|
expect([...claimLayerIds([restating]).owners.keys()]).toEqual(['g', 'a', 'b'])
|
|
const taking = layer('taking', 'builtin', [
|
|
{ insert: [{ id: 'h', name: 'cordis:group', group: true, config: [] }] },
|
|
{ id: 'h', config: [{ id: 'settings', name: 'taking/impostor' }] },
|
|
])
|
|
expect(() => claimLayerIds([base, taking])).toThrow(/row "settings" is declared by both @deepseek-ai\/dsh-base and taking/)
|
|
})
|
|
|
|
it('leaves out a bundle that declares one of its own ids twice and composes each mounted bundle once', () => {
|
|
const stutter = layer('stutter', 'external', [{ insert: [{ id: 'x', name: 'stutter/a' }, { id: 'x', name: 'stutter/b' }] }])
|
|
const clean = layer('clean', 'external', [{ insert: [{ id: 'y', name: 'clean' }] }])
|
|
const { owners, skipped, composed } = claimLayerIds([base, stutter, clean])
|
|
expect(skipped.get('stutter')).toEqual([
|
|
{ rowId: 'x', moduleName: 'stutter/b', layer: 'stutter', packageName: 'stutter', declaredBy: 'stutter', message: 'row "x" is declared twice by stutter' },
|
|
])
|
|
expect(owners.has('x')).toBe(false)
|
|
expect(owners.get('y')?.packageName).toBe('clean')
|
|
expect([...composed.keys()]).toEqual(['clean'])
|
|
expect(composed.get('clean')?.patches[1]).toEqual({ id: 'bundle/clean', insert: [{ id: 'y', name: 'clean' }] })
|
|
})
|
|
|
|
it('gives the earlier external bundle the id and leaves the later one out whole', () => {
|
|
const first = layer('first', 'external', [{ insert: [{ id: 'hello', name: 'first' }, { id: 'only-first', name: 'first/x' }] }])
|
|
const second = layer('second', 'external', [{ insert: [{ id: 'hello', name: 'second' }, { id: 'only-second', name: 'second/x' }] }])
|
|
const { owners, skipped } = claimLayerIds([base, first, second])
|
|
expect(owners.get('hello')?.packageName).toBe('first')
|
|
expect(owners.get('bundle/first')?.packageName).toBe('first')
|
|
expect(owners.has('only-second')).toBe(false)
|
|
expect(skipped.get('second')?.map(conflict => conflict.rowId)).toEqual(['hello'])
|
|
})
|
|
})
|
|
|
|
describe('composeProfileStack', () => {
|
|
it('mounts owning layers in manifest order and drops a user insert of a taken id', () => {
|
|
const ext = layer('ext', 'external', [{ insert: [{ id: 'ext-tool', name: 'ext' }] }])
|
|
const stack = composeProfileStack(NAME, [base, ext], [
|
|
{ label: '/p/cordis.patch.yml', patches: [
|
|
{ id: 'settings', config: { path: '/x' } },
|
|
{ insert: [{ id: 'mine', name: 'mine' }, { id: 'ext-tool', name: 'clash' }] },
|
|
{ insert: [{ id: 'g', name: 'cordis:group', group: true, config: [{ id: 'tool-bash', name: 'nested-clash' }] }] },
|
|
] },
|
|
{ label: '/home/cordis.patch.yml', patches: [
|
|
{ insert: [{ id: 'mine', name: 'twice' }] },
|
|
{ insert: [{ id: 'clean', name: 'clean' }, { name: 'anonymous' } as EntryOptions] },
|
|
] },
|
|
])
|
|
expect(stack.layers.map(current => current.label)).toEqual(['@deepseek-ai/dsh-base', 'ext', '/p/cordis.patch.yml', '/home/cordis.patch.yml'])
|
|
expect(stack.layers[1]?.patches[0]?.insert?.[0]).toMatchObject({ id: 'bundle/ext', name: CONTAINED_GROUP_MODULE })
|
|
expect([...stack.owners.keys()]).toEqual(['settings', 'tools', 'tool-bash', 'ext-tool', 'bundle/ext'])
|
|
expect(stack.layers[2]?.patches).toEqual([
|
|
{ id: 'settings', config: { path: '/x' } },
|
|
{ insert: [{ id: 'mine', name: 'mine' }] },
|
|
])
|
|
// An insert with no conflict passes through as the same object; an anonymous row claims nothing.
|
|
expect(stack.layers[3]?.patches).toEqual([{ insert: [{ id: 'clean', name: 'clean' }, { name: 'anonymous' }] }])
|
|
expect(stack.patches).toEqual(stack.layers.flatMap(current => current.patches))
|
|
expect(stack.skippedBundles).toEqual([])
|
|
expect(stack.conflicts).toEqual([
|
|
{ rowId: 'ext-tool', moduleName: 'clash', layer: '/p/cordis.patch.yml', declaredBy: 'ext', message: 'row "ext-tool" is already declared by ext' },
|
|
{
|
|
rowId: 'tool-bash', moduleName: 'cordis:group', layer: '/p/cordis.patch.yml', declaredBy: '@deepseek-ai/dsh-base',
|
|
message: 'row "tool-bash" is already declared by @deepseek-ai/dsh-base',
|
|
},
|
|
{
|
|
rowId: 'mine', moduleName: 'twice', layer: '/home/cordis.patch.yml', declaredBy: '/p/cordis.patch.yml',
|
|
message: 'row "mine" is already declared by /p/cordis.patch.yml',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('leaves a colliding external bundle out of the stack and reports it', () => {
|
|
const clash = layer('clash', 'external', [{ insert: [{ id: 'settings', name: 'clash' }] }])
|
|
const stack = composeProfileStack(NAME, [base, clash], [])
|
|
expect(stack.layers.map(current => current.label)).toEqual(['@deepseek-ai/dsh-base'])
|
|
expect(stack.skippedBundles).toEqual(['clash'])
|
|
expect(stack.conflicts).toEqual([
|
|
{
|
|
rowId: 'settings', moduleName: 'clash', layer: 'clash', packageName: 'clash', declaredBy: '@deepseek-ai/dsh-base',
|
|
message: 'row "settings" is already declared by @deepseek-ai/dsh-base',
|
|
},
|
|
])
|
|
})
|
|
|
|
it('prefixes a built-in duplicate with the binary name', () => {
|
|
const twin = layer('twin', 'builtin', [{ insert: [{ id: 'settings', name: 'twin' }] }])
|
|
expect(() => composeProfileStack(NAME, [base, twin], [])).toThrow(/^dsh-test-bin: row "settings" is declared by both/)
|
|
})
|
|
})
|
|
|
|
describe('formatRowConflict', () => {
|
|
it('names the bundle left out, or the user layer whose insert was skipped', () => {
|
|
const message = 'row "x" is already declared by base'
|
|
expect(formatRowConflict({ rowId: 'x', moduleName: 'm', layer: 'pkg', packageName: 'pkg', declaredBy: 'base', message }))
|
|
.toBe('bundle pkg left out — row "x" is already declared by base')
|
|
expect(formatRowConflict({ rowId: 'x', moduleName: 'm', layer: '/p/cordis.patch.yml', declaredBy: 'base', message }))
|
|
.toBe('/p/cordis.patch.yml: insert of m skipped — row "x" is already declared by base')
|
|
})
|
|
})
|