fix: retire leftovers of the removed $DSH_HOME/config.yaml personal overlay

The profile rework left references to the old entry modes behind. Renames
the user patch-layer API and its spec file (watchPersonalPatches ->
watchUserPatches, personal-config.spec.ts -> user-patches.spec.ts) and
retargets the prose that still named `config.yaml`, `--config`, raw-config
mode, and surface overlays: repository-plugin and mcp-memory READMEs, the
credentials-local anchor into app-boot, vendor manifest items 12-13, the
vendored include/hmr comments, and install.sh.

Restores the boot-failure guard the rework dropped with raw mode: the
built-bin case now boots `--profile web --patch <invalid>` and asserts the
settled diagnostic and exit 1, so the HMR initial-scan deadlock stays
covered; its orphaned raw fixture is renamed and the unused one deleted.
The superseded personal-config Agent Note and its superseding profile note
are now cross-linked.
This commit is contained in:
Turtle
2026-08-06 17:28:51 +08:00
parent 20acfdb71d
commit ef30572e63
21 changed files with 79 additions and 87 deletions
@@ -341,11 +341,11 @@ describe('include refresh with overlay patches', () => {
describe('include patches layered over one base', () => {
it('lets a later patch configure or disable a row an earlier patch inserted', async () => {
// The surface/`--config`/personal composition: `dsh` includes one shared
// base and applies each source as its own patch list at the SAME include
// The bundle/user-layer/`--patch` composition: `dsh` includes one root
// and applies each source as its own patch list at the SAME include
// level, because patches never cross an include boundary. A later layer
// must therefore be able to reach a row an earlier layer inserted, or
// surface-only rows would be invisible to the user's personal config.
// bundle-only rows would be invisible to the user's patch layer.
const dir = mkdtempSync(join(tmpdir(), 'dsh-config-layered-'))
writeFileSync(join(dir, 'noop.mjs'), NOOP_PLUGIN)
writeFileSync(join(dir, 'base.yml'), '- id: shared\n name: ./noop.mjs\n config:\n value: base\n')
@@ -355,30 +355,30 @@ describe('include patches layered over one base', () => {
' config:',
' path: ./base.yml',
' patches:',
// Layer 1 (a surface overlay): patch a base row and add two of its own.
// Layer 1 (a bundle layer): patch a base row and add two of its own.
' - id: shared',
' config:',
' value: surface',
' value: bundle',
' - insert:',
' - id: surface-kept',
' - id: bundle-kept',
' name: ./noop.mjs',
' config:',
' value: surface-default',
' - id: surface-dropped',
' value: bundle-default',
' - id: bundle-dropped',
' name: ./noop.mjs',
// Layer 2 (the user): reconfigure one inserted row and disable the other.
' - id: surface-kept',
' - id: bundle-kept',
' config:',
' value: personal',
' - id: surface-dropped',
' value: user',
' - id: bundle-dropped',
' disabled: true',
'',
].join('\n'))
const ctx = await boot(NAME, join(dir, 'cordis.yml'))
try {
expect(entryConfig(ctx, 'shared')).toEqual({ value: 'surface' })
expect(entryConfig(ctx, 'surface-kept')).toEqual({ value: 'personal' })
const dropped = [...ctx.loader.entries()].find(entry => entry.options.id === 'surface-dropped')
expect(entryConfig(ctx, 'shared')).toEqual({ value: 'bundle' })
expect(entryConfig(ctx, 'bundle-kept')).toEqual({ value: 'user' })
const dropped = [...ctx.loader.entries()].find(entry => entry.options.id === 'bundle-dropped')
expect(dropped?.options.disabled).toBe(true)
expect(dropped?.fiber).toBeUndefined()
} finally {
@@ -17,12 +17,12 @@ import {
boot,
loadOptionalPatches,
PROFILE_PATCH_FILENAME,
watchPersonalPatches,
watchUserPatches,
} from '../src/index.ts'
const NAME = 'dsh-test-bin'
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-personal-config-'))
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-user-patches-'))
async function eventually(test: () => boolean, message: string): Promise<void> {
const deadline = Date.now() + 10_000
@@ -39,15 +39,15 @@ describe('loadOptionalPatches', () => {
delete process.env.DSH_HOME
})
it('returns undefined when no personal patches file exists', () => {
it('returns undefined when no user patch file exists', () => {
expect(loadOptionalPatches(NAME, join(tmp(), PROFILE_PATCH_FILENAME))).toBeUndefined()
})
it('parses a patch list and preserves !!js expressions as loader expression nodes', () => {
const dir = tmp()
writeFileSync(join(dir, PROFILE_PATCH_FILENAME), [
'- id: tui-agent',
" name: '@deepseek-ai/dsh-tui-demo'",
'- id: agent-loop',
" name: '@deepseek-ai/dsh-agent-loop'",
' config:',
' model: !!js process.env.DSH_SPEC_MODEL',
'- insert:',
@@ -58,13 +58,13 @@ describe('loadOptionalPatches', () => {
const patches = loadOptionalPatches(NAME, join(dir, PROFILE_PATCH_FILENAME))
expect(patches).toHaveLength(2)
expect(patches?.[0]).toMatchObject({
id: 'tui-agent',
id: 'agent-loop',
config: { model: { __jsExpr: 'process.env.DSH_SPEC_MODEL' } },
})
expect(patches?.[1]?.insert).toHaveLength(1)
})
it('fails loud on an unreadable file (a present personal config is never skipped)', () => {
it('fails loud on an unreadable file (a present user patch layer is never skipped)', () => {
const dir = tmp()
mkdirSync(join(dir, PROFILE_PATCH_FILENAME)) // a directory: present, unreadable as a file
expect(() => loadOptionalPatches(NAME, join(dir, PROFILE_PATCH_FILENAME)))
@@ -92,7 +92,7 @@ describe('loadOptionalPatches', () => {
})
})
describe('boot with personal patches', () => {
describe('boot with user patches', () => {
function writeTree(dir: string): string {
writeFileSync(join(dir, 'noop.mjs'), [
'export const name = "noop"',
@@ -111,31 +111,31 @@ describe('boot with personal patches', () => {
it('applies id-targeted overrides, inserts, and interpolates !!js from the environment', async () => {
const dir = tmp()
const personal = tmp()
writeFileSync(join(personal, PROFILE_PATCH_FILENAME), [
const userDir = tmp()
writeFileSync(join(userDir, PROFILE_PATCH_FILENAME), [
'- id: noop',
' name: ./noop.mjs',
' config:',
' value: !!js process.env.DSH_APP_BOOT_PERSONAL_SPEC',
' value: !!js process.env.DSH_APP_BOOT_USER_SPEC',
'- insert:',
' - id: personal-extra',
' - id: user-extra',
' name: ./noop.mjs',
'',
].join('\n'))
process.env['DSH_APP_BOOT_PERSONAL_SPEC'] = 'personal-value'
const ctx = await boot(NAME, writeTree(dir), loadOptionalPatches(NAME, join(personal, PROFILE_PATCH_FILENAME)))
process.env['DSH_APP_BOOT_USER_SPEC'] = 'user-value'
const ctx = await boot(NAME, writeTree(dir), loadOptionalPatches(NAME, join(userDir, PROFILE_PATCH_FILENAME)))
try {
const noop = [...ctx.loader.entries()].find(entry => entry.options.id === 'noop')
// The mounted plugin received the interpolated environment value.
expect(noop?.fiber?.config).toEqual({ value: 'personal-value' })
expect([...ctx.loader.entries()].some(entry => entry.options.id === 'personal-extra')).toBe(true)
expect(noop?.fiber?.config).toEqual({ value: 'user-value' })
expect([...ctx.loader.entries()].some(entry => entry.options.id === 'user-extra')).toBe(true)
} finally {
await ctx.fiber.dispose()
delete process.env['DSH_APP_BOOT_PERSONAL_SPEC']
delete process.env['DSH_APP_BOOT_USER_SPEC']
}
})
it('mounts no patch layer for an absent or empty personal overlay', async () => {
it('mounts no patch layer for an absent or empty user layer', async () => {
const dir = tmp()
const ctx = await boot(NAME, writeTree(dir), loadOptionalPatches(NAME, join(tmp(), PROFILE_PATCH_FILENAME)))
try {
@@ -155,8 +155,8 @@ describe('boot with personal patches', () => {
it('watches add, failure, recovery, and removal through transactional HMR', { timeout: 20_000 }, async () => {
const dir = tmp()
const personal = tmp()
const filename = join(personal, PROFILE_PATCH_FILENAME)
const userDir = tmp()
const filename = join(userDir, PROFILE_PATCH_FILENAME)
const basePatches = [{ id: 'noop', config: { value: 'generated' } }]
const ctx = await boot(NAME, writeTree(dir), basePatches)
await ctx.plugin(Timer)
@@ -165,14 +165,14 @@ describe('boot with personal patches', () => {
ctx.on('hmr/config-update-failed', (failedFilename, error) => {
failures.push({ filename: failedFilename, error })
})
const dispose = await watchPersonalPatches(ctx, {
const dispose = await watchUserPatches(ctx, {
binName: NAME,
filename,
compose: personalPatches => [...basePatches, ...personalPatches],
compose: userPatches => [...basePatches, ...userPatches],
})
try {
writeFileSync(filename, '- id: noop\n config:\n value: live\n')
await eventually(() => (entryConfig(ctx, 'noop') as { value?: string }).value === 'live', 'personal config addition was not applied')
await eventually(() => (entryConfig(ctx, 'noop') as { value?: string }).value === 'live', 'user patch addition was not applied')
writeFileSync(filename, '- id: noop\n config:\n fail: true\n')
await eventually(() => failures.length === 1, 'failed candidate was not broadcast')
@@ -192,17 +192,17 @@ describe('boot with personal patches', () => {
await settleChokidarChangeThrottle()
unlinkSync(filename)
await eventually(() => (entryConfig(ctx, 'noop') as { value?: string }).value === 'generated', 'personal config removal did not restore the app-owned patch')
await eventually(() => (entryConfig(ctx, 'noop') as { value?: string }).value === 'generated', 'user patch removal did not restore the app-owned patch')
expect(failures).toHaveLength(2)
await settleChokidarChangeThrottle()
// Default compose: the personal overlay IS the whole patch list, so a
// Default compose: the user layer IS the whole patch list, so a
// fresh generation replaces the app-owned layer instead of stacking on it.
await dispose()
const disposeDefault = await watchPersonalPatches(ctx, { binName: NAME, filename })
const disposeDefault = await watchUserPatches(ctx, { binName: NAME, filename })
try {
writeFileSync(filename, '- id: noop\n config:\n value: identity\n')
await eventually(() => (entryConfig(ctx, 'noop') as { value?: string }).value === 'identity', 'default-compose personal patch was not applied')
await eventually(() => (entryConfig(ctx, 'noop') as { value?: string }).value === 'identity', 'default-compose user patch was not applied')
} finally {
await disposeDefault()
}
@@ -215,7 +215,7 @@ describe('boot with personal patches', () => {
it('fails loud when the exact watcher lacks HMR or a root Include', async () => {
const dir = tmp()
const withoutHmr = await boot(NAME, writeTree(dir))
await expect(watchPersonalPatches(withoutHmr, { binName: NAME, filename: join(tmp(), PROFILE_PATCH_FILENAME) })).rejects.toThrow('requires the Cordis HMR service')
await expect(watchUserPatches(withoutHmr, { binName: NAME, filename: join(tmp(), PROFILE_PATCH_FILENAME) })).rejects.toThrow('requires the Cordis HMR service')
await withoutHmr.fiber.dispose()
const withoutInclude = new Context()
@@ -223,7 +223,7 @@ describe('boot with personal patches', () => {
await withoutInclude.plugin(Loader)
await withoutInclude.plugin(Timer)
await withoutInclude.plugin(Hmr, { root: [], ignored: [], debounce: 0 })
await expect(watchPersonalPatches(withoutInclude, { binName: NAME, filename: join(tmp(), PROFILE_PATCH_FILENAME) })).rejects.toThrow('requires the root Include entry')
await expect(watchUserPatches(withoutInclude, { binName: NAME, filename: join(tmp(), PROFILE_PATCH_FILENAME) })).rejects.toThrow('requires the root Include entry')
await withoutInclude.fiber.dispose()
})
@@ -238,7 +238,7 @@ describe('boot with personal patches', () => {
try {
const teardown = Object.assign(new Error('cannot create effect on inactive context'), { code: 'INACTIVE_EFFECT' })
ctx.provide('hmr', { registerConfig: () => Promise.reject(teardown) })
const dispose = await watchPersonalPatches(ctx, { binName: NAME, filename: join(tmp(), PROFILE_PATCH_FILENAME) })
const dispose = await watchUserPatches(ctx, { binName: NAME, filename: join(tmp(), PROFILE_PATCH_FILENAME) })
await expect(dispose()).resolves.toBeUndefined()
} finally {
await ctx.fiber.dispose()
@@ -252,9 +252,9 @@ describe('boot with personal patches', () => {
try {
await ctx.plugin(Timer)
await ctx.plugin(Hmr, { root: [], ignored: [], debounce: 0 })
const dispose = await watchPersonalPatches(ctx, { binName: NAME, filename })
// Same personal path registered twice: HMR refuses; not a teardown race.
await expect(watchPersonalPatches(ctx, { binName: NAME, filename })).rejects.toThrow('already registered')
const dispose = await watchUserPatches(ctx, { binName: NAME, filename })
// Same user-layer path registered twice: HMR refuses; not a teardown race.
await expect(watchUserPatches(ctx, { binName: NAME, filename })).rejects.toThrow('already registered')
await dispose()
} finally {
await ctx.fiber.dispose()