Files
deepseek-harness/packages/ui/app-boot/tests/personal-config.spec.ts
T
Turtle 2464d6169f feat(app-boot): move personal config to the Harness home (~/.dsh)
Squashes feat/personal-config-dsh-home: personal config.yaml and .env move
from ~/.config/dsh to the Harness home (~/.dsh), plus the module-graph,
lockfile, and i18n pairing regeneration that followed.
2026-07-22 14:29:52 +08:00

142 lines
5.3 KiB
TypeScript

/**
* Personal-config behavior of `dsh-app-boot`: the Harness home (`~/.dsh`)
* `config.yaml` overlay loader and `boot()` applying the personal overlay over
* a real Loader tree.
*/
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import type { Context } from 'cordis'
import {
boot,
loadPersonalPatches,
PERSONAL_CONFIG_FILENAME,
} from '../src/index.ts'
const NAME = 'dsh-test-bin'
const tmp = (): string => mkdtempSync(join(tmpdir(), 'dsh-personal-config-'))
describe('loadPersonalPatches', () => {
afterEach(() => {
delete process.env.DSH_HOME
})
it('returns undefined when no personal patches file exists', () => {
expect(loadPersonalPatches(NAME, tmp())).toBeUndefined()
})
it('parses a patch list and preserves !!js expressions as loader expression nodes', () => {
const dir = tmp()
writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), [
'- id: tui-agent',
" name: '@deepseek-ai/dsh-tui-demo'",
' config:',
' model: !!js process.env.DSH_SPEC_MODEL',
'- insert:',
' - id: llm',
" name: '@deepseek-ai/dsh-llm-pi-ai'",
'',
].join('\n'))
const patches = loadPersonalPatches(NAME, dir)
expect(patches).toHaveLength(2)
expect(patches?.[0]).toMatchObject({
id: 'tui-agent',
config: { model: { __jsExpr: 'process.env.DSH_SPEC_MODEL' } },
})
expect(patches?.[1]?.insert).toHaveLength(1)
})
it('defaults its directory to the Harness home ($DSH_HOME)', () => {
const dir = tmp()
writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), '- id: x\n config:\n a: 1\n')
process.env.DSH_HOME = dir
expect(loadPersonalPatches(NAME)).toHaveLength(1)
})
it('fails loud on an unreadable file (a present personal config is never skipped)', () => {
const dir = tmp()
mkdirSync(join(dir, PERSONAL_CONFIG_FILENAME)) // a directory: present, unreadable as a file
expect(() => loadPersonalPatches(NAME, dir))
.toThrow(new RegExp(`^${NAME}: failed to read personal patches `))
})
it('fails loud on unparsable YAML and on a !!js tag with no expression body', () => {
const dir = tmp()
writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), 'invalid: [unclosed\n')
expect(() => loadPersonalPatches(NAME, dir))
.toThrow(new RegExp(`^${NAME}: failed to parse personal patches `))
writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), '- id: x\n config:\n a: !!js\n')
expect(() => loadPersonalPatches(NAME, dir))
.toThrow(new RegExp(`^${NAME}: failed to parse personal patches `))
})
it('fails loud when the file is not a top-level array or an entry is not an object', () => {
const dir = tmp()
writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), 'id: not-a-list\n')
expect(() => loadPersonalPatches(NAME, dir))
.toThrow('must be a top-level YAML array of loader patch entries')
writeFileSync(join(dir, PERSONAL_CONFIG_FILENAME), '- just-a-string\n')
expect(() => loadPersonalPatches(NAME, dir))
.toThrow(`${NAME}: personal patches entry 1 in`)
})
})
describe('boot with personal patches', () => {
function writeTree(dir: string): string {
writeFileSync(join(dir, 'noop.mjs'), 'export const name = "noop"\nexport function apply() {}\n')
writeFileSync(join(dir, 'cordis.yml'), '- id: noop\n name: ./noop.mjs\n config:\n value: base\n')
return join(dir, 'cordis.yml')
}
function entryConfig(ctx: Context, id: string): unknown {
return [...ctx.loader.entries()].find(entry => entry.options.id === id)?.options.config
}
it('applies id-targeted overrides, inserts, and interpolates !!js from the environment', async () => {
const dir = tmp()
const personal = tmp()
writeFileSync(join(personal, PERSONAL_CONFIG_FILENAME), [
'- id: noop',
' name: ./noop.mjs',
' config:',
' value: !!js process.env.DSH_APP_BOOT_PERSONAL_SPEC',
'- insert:',
' - id: personal-extra',
' name: ./noop.mjs',
'',
].join('\n'))
process.env['DSH_APP_BOOT_PERSONAL_SPEC'] = 'personal-value'
const ctx = await boot(NAME, writeTree(dir), loadPersonalPatches(NAME, personal))
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)
} finally {
await ctx.fiber.dispose()
delete process.env['DSH_APP_BOOT_PERSONAL_SPEC']
}
})
it('mounts no patch layer for an absent or empty personal overlay', async () => {
const dir = tmp()
const ctx = await boot(NAME, writeTree(dir), loadPersonalPatches(NAME, tmp()))
try {
expect(entryConfig(ctx, 'noop')).toEqual({ value: 'base' })
} finally {
await ctx.fiber.dispose()
}
const empty = tmp()
writeFileSync(join(empty, PERSONAL_CONFIG_FILENAME), '[]\n')
const ctxEmpty = await boot(NAME, writeTree(tmp()), loadPersonalPatches(NAME, empty))
try {
expect(entryConfig(ctxEmpty, 'noop')).toEqual({ value: 'base' })
} finally {
await ctxEmpty.fiber.dispose()
}
})
})