fix(boot): normalize absolute plugin paths in patches

This commit is contained in:
Tianyi Cui committed 2026-09-05 20:14:43 +08:00
1 parent 4aa3cb7818
commit 15e444e0d7
6 files changed
+65 -8

No files matched your search

+19 -4
View File
@@ -424,9 +424,20 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
}
}, SPAWN_TIMEOUT_MS + 30_000)
it('serves the SDK protocol through the sdk profile and exits after shutdown', async () => {
it('serves the SDK protocol with an absolute-path overlay plugin and exits after shutdown', async () => {
const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-'))
const child = execa(process.execPath, [dshBin, '--profile', 'sdk'], {
const pluginPath = join(home, 'plugin #100%.mjs')
const marker = join(home, 'plugin-loaded')
writeFileSync(pluginPath, [
"import { writeFileSync } from 'node:fs'",
'export function apply(ctx, config) { writeFileSync(config.marker, "loaded") }',
'',
].join('\n'))
const patch = join(home, 'absolute.patch.yml')
writeFileSync(patch, JSON.stringify([{ insert: [
{ id: 'absolute-plugin', name: pluginPath, config: { marker } },
] }]))
const child = execa(process.execPath, [dshBin, '--profile', 'sdk', '--patch', patch], {
cwd: home,
reject: false,
timeout: SPAWN_TIMEOUT_MS,
@@ -462,7 +473,8 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
method: 'initialize',
params: { cwd: home, provider: 'deepseek-official', model: 'deepseek-v4-flash' },
})}\n`)
expect(await response(1)).toMatchObject({
const initialized = await response(1)
expect(initialized, `${JSON.stringify(initialized)}\n${stderr}`).toMatchObject({
jsonrpc: '2.0',
id: 1,
result: { serverInfo: { name: 'deepseek-harness-sdk-runtime' } },
@@ -470,8 +482,11 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'shutdown' })}\n`)
expect(await response(2)).toEqual({ jsonrpc: '2.0', id: 2, result: {} })
const result = await child
expect(result.exitCode, `signal=${String(result.signal)}; stderr=${stderr}`).toBe(0)
expect(result.timedOut, stderr).toBe(false)
expect(result.signal, stderr).toBeUndefined()
expect(result.exitCode, stderr).toBe(0)
expect(stderr).toBe('')
expect(readFileSync(marker, 'utf8')).toBe('loaded')
} finally {
child.kill('SIGKILL')
await child
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/boot/app-boot/README.md
README.md: ddbcc900fcbb38e83b35f517f9d38cf0e04927f3
README.zh.md: 184e1a086d09bf6807fd8fb2d17f7157ba8deaa6
README.md: 3f0f5fe369eb8965dbd9d606f5eaf4065e4e450a
README.zh.md: 35a507d57aec1a189128a8be9666ebec74976e4c
+2
View File
@@ -54,6 +54,8 @@ Your machine-local preferences also live in the Harness home:
Profiles with `patchReload: live` watch both user patch files: a valid edit recomposes without restart, while a rejected edit leaves the last good app running. A `startup` profile installs neither those watchers nor the launcher's watch-only HMR fallback.
Inserted plugin names may be absolute filesystem paths, file URLs, or package specifiers. Patch loading converts absolute paths and patch-relative `./` or `../` paths to file URLs, including nested group entries; existing-entry name assertions remain literal.
### Previewing the effective configuration
Before you boot, you can print the exact configuration the app will mount: the dump shows the composed entry list with `!!js` expressions verbatim, grouped under comments naming each source file and the patch layers that changed it, as one loadable YAML document. Patches that match no row are reported with their layer label; a missing, unparsable, or invalid config fails the dump.
+2
View File
@@ -54,6 +54,8 @@ profile 是同一套 dsh 安装提供不同应用界面的方式:`web`、`head
带 `patchReload: live` 的 profile 会监视两份用户 patch 文件:有效编辑无需重启即可重新组合,被拒绝的编辑则让最后一个可用应用继续运行。`startup` profile 既不安装这些监视器,也不安装 launcher 的仅监视 HMR 回退。
插入条目的插件名可以是绝对文件系统路径、文件 URL 或包标识符。patch 加载会把绝对路径以及相对于 patch 文件的 `./` 或 `../` 路径转换为文件 URL,嵌套分组条目也适用;对已有条目名称的断言保持原样。
### 预览生效配置
启动前,你可以打印应用将挂载的确切配置:dump 会以 `!!js` 表达式原样展示组合后的条目列表,并按注释分组标明每个源文件及其 patch 层,输出是一份可加载的 YAML 文档。未匹配到任何行的 patch 会连同其层标签一起报告;配置缺失、无法解析或字段无效都会使 dump 失败。
+2 -2
View File
@@ -325,11 +325,11 @@ export function loadOverlayPatches(binName: string, file: string): PatchOptions[
return parsePatchList(binName, file, content, 'overlay')
}
/** Resolve relative plugin paths in one patch file's `insert` rows without changing assertion names. */
/** Convert inserted filesystem paths to file URLs, anchoring relative paths beside the patch; keep assertion names literal. */
function anchorInsertedPluginNames(patches: PatchOptions[], file: string): PatchOptions[] {
const base = dirname(resolve(file))
const visit = (entry: EntryOptions): void => {
if (typeof entry.name === 'string' && (entry.name.startsWith('./') || entry.name.startsWith('../'))) {
if (typeof entry.name === 'string' && (isAbsolute(entry.name) || entry.name.startsWith('./') || entry.name.startsWith('../'))) {
entry.name = pathToFileURL(resolve(base, entry.name)).href
}
if (entry.group && Array.isArray(entry.config)) entry.config.forEach(visit)
@@ -17,6 +17,7 @@ import Timer from '@deepseek-ai/cordis-plugin-timer'
import {
boot,
loadOptionalPatches,
loadOverlayPatches,
PROFILE_PATCH_FILENAME,
watchUserPatches,
} from '../src/index.ts'
@@ -74,6 +75,43 @@ describe('loadOptionalPatches', () => {
expect(patches?.[1]?.insert).toHaveLength(1)
})
it.each([
{ label: 'optional', load: loadOptionalPatches },
{ label: 'overlay', load: loadOverlayPatches },
])('loads absolute plugin paths from patch files as file URLs ($label)', async ({ load }) => {
const dir = tmp()
const pluginPath = join(dir, 'absolute #100%.mjs')
const pluginUrl = pathToFileURL(pluginPath).href
writeFileSync(pluginPath, 'export function apply(ctx) { ctx.provide("absolutePatchLoaded", true) }\n')
const patchPath = join(dir, PROFILE_PATCH_FILENAME)
writeFileSync(patchPath, JSON.stringify([
{ id: 'existing', name: pluginPath },
{ insert: [
{ id: 'absolute', name: pluginPath },
{ id: 'url', name: pluginUrl },
{ id: 'bare', name: '@deepseek-ai/dsh-system-prompt' },
{ id: 'nested', name: 'cordis:group', group: true, config: [
{ id: 'child', name: pluginPath },
] },
] },
]))
const patches = load(NAME, patchPath)!
expect(patches[0]?.name).toBe(pluginPath)
expect(patches[1]?.insert?.map(entry => entry.name)).toEqual([
pluginUrl, pluginUrl, '@deepseek-ai/dsh-system-prompt', 'cordis:group',
])
expect((patches[1]?.insert?.[3]?.config as { name: string }[])[0]?.name).toBe(pluginUrl)
const configPath = join(dir, 'cordis.yml')
writeFileSync(configPath, '[]\n')
const ctx = await boot(NAME, configPath, [{ insert: [patches[1]!.insert![0]!] }])
try {
expect(ctx.get('absolutePatchLoaded')).toBe(true)
} finally {
await ctx.fiber.dispose()
}
})
it('anchors inserted relative plugins to the patch file and keeps assertion names literal', () => {
const dir = tmp()
const patchPath = join(dir, PROFILE_PATCH_FILENAME)