fix(app-boot): record user-disabled bundles in the profile manifest

`disableBundle` now moves a package from `dsh.profile.bundles` to
`dsh.profile.disabledBundles`, and `reconcileInstalledBundles` enables only
a bundle named in neither list, so an unrelated pnpm run never re-enables a
bundle the user turned off while an update that gives a dependency
`dsh.bundle` still activates it (the built-bin e2e scenario the previous
"new dependencies only" rule broke). The record goes when its dependency
does.

Also: `visitIdentifiedRows` in patch-rows owns the per-patch restatement
tracking that compose-stack and external-bundles had cloned (duplication
gate), and the probe spec's flood fixture retries short and EAGAIN writes
because the child's stderr pipe is non-blocking once tsx touched
process.stderr (the test raced the parent's drain).
This commit is contained in:
Yichen Jiang committed 2026-09-08 17:29:35 +08:00
1 parent 5a9e1760fd
commit 07878ae1d3
12 files changed
+136 -74

No files matched your search

@@ -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 .agents/notes/implemented/architecture/2026-09-04-boot-scoped-fail-loud-and-package-probe.md
2026-09-04-boot-scoped-fail-loud-and-package-probe.md: 2b892cacf14a9f62921b0fce015f14970a2b5a06
2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md: f7004f83f417282670fb90effadf257716066e67
2026-09-04-boot-scoped-fail-loud-and-package-probe.md: 015d822cfaf9e933654f19cf68fb16b95a7625d8
2026-09-04-boot-scoped-fail-loud-and-package-probe.zh.md: ce7b2ca0ddd25e600f605d348c2c355f062ccec3
@@ -18,6 +18,8 @@ Separately, nothing could say what an installed package was without importing it
**The probe runs the package where it cannot hurt.** `probePackage` reads the installed package's manifest in the host — kind from `dsh.bundle`, the rows and overrides of its patch, `dsh.plugins` declarations, `engines.dsh`, title and description — and spawns one Node child — `probe-child.ts`, its own module beside the probe, run through tsx under a source launch and as `lib/probe-child.js` when built — that resolves `@deepseek-ai/cordis` from the package, imports the main export and every declared addable module, and sends one report over an IPC channel. stdout and stderr stay the imported modules' own, so a package that prints at import still reports, and the child is killed once the report arrived, so a package that keeps a timer alive costs nothing more. The report and the cached record are validated field by field as the process and file boundaries they cross: a message that does not echo the run's token is the package's own, not the report, and an unrecognized record is probed again. The child gets the parent environment minus credential-shaped names, removes the token and `process.send` before importing, keeps at most 16 KiB of stderr for the failure text, and is awaited to `close` after the kill so nothing of it outlives the call. A child that throws, exits, or hangs yields `ok: false` with the reason or a rejection naming the timeout; `ok` states only that the main export imported and cordis is not a second copy, and `kind` with `addable[].ok` decide what can be enabled or added. Records are cached under the profile's `.dsh-plugins/` and invalidated by version.
**A disabled bundle is a manifest fact.** `disableBundle` moves the package from `dsh.profile.bundles` to `dsh.profile.disabledBundles`, and `reconcileInstalledBundles` enables only a bundle named in neither list — one the run installed, or a dependency whose update declared `dsh.bundle` — so an unrelated pnpm run never re-enables what the user turned off, and the record goes when its dependency does. Comparing the manifest before and after the run was rejected: the installed state alone cannot tell a bundle the user disabled from one that became a bundle after it was installed.
## Alternatives considered
**Attribute a runtime rejection to the plugin that produced it and mark that plugin failed.** The right end state, but a promise carries no fiber, and cordis's effect wrappers cover only what plugins register through them. Deferred: the guard reports and lets the process continue; attribution needs an async-context seam.
@@ -18,6 +18,8 @@ Status: implemented
**探针在伤不到宿主的地方运行包。** `probePackage` 在宿主里读取已安装包的 manifest——从 `dsh.bundle` 得到种类、其 patch 的行与覆盖、`dsh.plugins` 声明、`engines.dsh`、标题与描述——并生成一个 Node 子进程——`probe-child.ts`,探针旁边的独立模块,源码启动时经 tsx 运行,构建后是 `lib/probe-child.js`——从该包解析 `@deepseek-ai/cordis`,import 主导出与每个声明为可添加的模块,经 IPC 通道发出一份报告。stdout 与 stderr 仍归被 import 的模块自己,所以在 import 时打印的包照样能报告;报告一到子进程就被杀掉,因此让定时器一直活着的包不再多花任何代价。报告与缓存记录按各自跨越的进程边界与文件边界逐字段校验:没有回显本次 token 的消息是包自己的、不算报告,无法识别的记录重新探测。子进程拿到的是剔除了密钥形态变量的父环境,import 之前先删掉 token 与 `process.send`,stderr 只留最后 16 KiB 作失败文本,kill 之后等到 `close` 才结算,所以子进程的任何东西都不会活过这次调用。抛错、退出或挂起的子进程得到带原因的 `ok: false`,或点名超时的 rejection;`ok` 只表示主导出 import 成功且 cordis 不是第二份副本,能否启用或添加由 `kind` 与 `addable[].ok` 决定。记录缓存在 profile 的 `.dsh-plugins/` 下,按版本失效。
**被禁用的 bundle 是 manifest 里的事实。** `disableBundle` 把包从 `dsh.profile.bundles` 移到 `dsh.profile.disabledBundles`,`reconcileInstalledBundles` 只启用两个列表都没有的 bundle——本次运行装上的,或更新后声明了 `dsh.bundle` 的依赖——所以无关的 pnpm 运行不会重新启用用户关掉的东西,依赖被移除时记录随之消失。曾考虑比较运行前后的 manifest,但被否决:仅凭已安装状态分不清一个 bundle 是用户禁用的,还是安装之后才变成 bundle 的。
## 考虑过的替代方案
**把运行时 rejection 归属到产生它的插件并把该插件标为失败。** 正确的终态,但 promise 不携带 fiber,cordis 的 effect 包装也只覆盖插件经由它注册的东西。延后:守卫现在只报告并让进程继续;归属需要一个 async-context seam。
+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 apps/cli/README.md
README.md: 2e74ef68cf0b8487083a2e2af0f5175c78ec5212
README.zh.md: 85a75f4c025e1bfe1461b9ffdaca533d308318ba
README.md: 92a5a07c3035e5814e28522c4df915f6931ad96e
README.zh.md: 562fce3d0bd0dc2712d845e30b958f925ed6d5d4
+1 -1
View File
@@ -34,7 +34,7 @@ dsh --help # the launcher's own help
<a id="profiles"></a>
## Profiles
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list and `patchReload` lifecycle) and a `cordis.patch.yml` (the user's own patch layer). `patchReload: live` watches the profile and home-level patch files; `startup` applies them once.
A profile directory holds a `package.json` (out-of-tree plugin dependencies plus the profile manifest `dsh.profile` with its ordered `bundles` list, the `disabledBundles` it keeps out of that list, and `patchReload` lifecycle) and a `cordis.patch.yml` (the user's own patch layer). `patchReload: live` watches the profile and home-level patch files; `startup` applies them once.
The tree composes over an empty root:
- each bundle's patch in `dsh.profile.bundles` order
+1 -1
View File
@@ -34,7 +34,7 @@ dsh --help # the launcher's own help
<a id="profiles"></a>
## Profile
profile 目录包含一个 `package.json`,其中记录树外插件依赖,以及 profile manifest(元数据清单)`dsh.profile`、其中按顺序排列的 `bundles` 列表与 `patchReload` 生命周期;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。`patchReload: live` 监视 profile 与 home 级 patch 文件,`startup` 则只应用一次。
profile 目录包含一个 `package.json`,其中记录树外插件依赖,以及 profile manifest(元数据清单)`dsh.profile`、其中按顺序排列的 `bundles` 列表、被排除在该列表之外的 `disabledBundles`,以及 `patchReload` 生命周期;还包含一个 `cordis.patch.yml`,其中保存用户自己的 patch 层。`patchReload: live` 监视 profile 与 home 级 patch 文件,`startup` 则只应用一次。
配置树以空根为起点,依次叠加以下配置层:
- `dsh.profile.bundles` 中各组合包的 patch
+8 -16
View File
@@ -16,7 +16,7 @@
import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { composeExternalLayer, isContainedLayer, type ComposedExternalLayer } from './external-bundles.ts'
import { visitPatchRows, visitRowTree } from './patch-rows.ts'
import { visitIdentifiedRows, visitRowTree } from './patch-rows.ts'
import type { ProfileLayer } from './profile.ts'
/** One user-owned patch list in the stack: the profile file, the home file, or a `--patch` overlay. */
@@ -114,29 +114,21 @@ export function claimLayerIds(layers: readonly ProfileLayer[]): LayerOwnership {
const declaredUnder = new Map<string, string | undefined>()
for (const layer of layers) {
if (isContainedLayer(layer)) continue
let listed = new Set<string>()
let listIndex = -1
visitPatchRows(layer.patches, (row, source, place) => {
if (typeof row.id !== 'string') return
if (place.patch !== listIndex) {
listIndex = place.patch
listed = new Set()
}
const owner = owners.get(row.id)
visitIdentifiedRows(layer.patches, ({ id, source, place, listed }) => {
const owner = owners.get(id)
if (owner === layer) {
// Restating a row under the group that already holds it is the layer
// keeping its child; inserting it again, listing it twice in one
// config, or setting it under another group is declaring it twice.
if (source === 'insert' || listed.has(row.id) || declaredUnder.get(row.id) !== place.target) {
throw new Error(`row ${JSON.stringify(row.id)} is declared twice by ${layer.packageName}`)
if (source === 'insert' || listed.has(id) || declaredUnder.get(id) !== place.target) {
throw new Error(`row ${JSON.stringify(id)} is declared twice by ${layer.packageName}`)
}
} else if (owner !== undefined) {
throw new Error(`row ${JSON.stringify(row.id)} is declared by both ${owner.packageName} and ${layer.packageName}`)
throw new Error(`row ${JSON.stringify(id)} is declared by both ${owner.packageName} and ${layer.packageName}`)
} else {
owners.set(row.id, layer)
declaredUnder.set(row.id, place.target)
owners.set(id, layer)
declaredUnder.set(id, place.target)
}
listed.add(row.id)
})
}
const skipped = new Map<string, RowConflict[]>()
+43 -36
View File
@@ -16,7 +16,8 @@
import { join } from 'node:path'
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
import { visitPatchRows } from './patch-rows.ts'
import type { DshProfileManifest } from '@deepseek-ai/dsh-package-manifest'
import { visitIdentifiedRows } from './patch-rows.ts'
import {
readProfileManifest, resolveBundleDir, writeProfileManifest, type ProfileLayer, type ProfileManifest,
} from './profile.ts'
@@ -112,30 +113,22 @@ export function composeExternalLayer(layer: ProfileLayer): ComposedExternalLayer
rows.set(rowId, moduleName)
declaredUnder.set(rowId, target)
}
let listed = new Set<string>()
let listIndex = -1
visitPatchRows(layer.patches, (row, source, place) => {
if (typeof row.id !== 'string') return
if (place.patch !== listIndex) {
listIndex = place.patch
listed = new Set()
}
visitIdentifiedRows(layer.patches, ({ id, row, source, place, listed }) => {
// A root insert lands in the bundle's group: that is the group it declares under.
const target = place.target ?? groupId
if (source === 'config') {
// A config override restates the children it keeps: the same id declared
// under the same group before is that row. Twice in one list, or under
// another group, it would mount as a rejected duplicate or move the row.
if (listed.has(row.id) || (rows.has(row.id) && declaredUnder.get(row.id) !== target)) {
duplicates.push({ rowId: row.id, moduleName: row.name })
} else if (!rows.has(row.id)) {
rows.set(row.id, row.name)
declaredUnder.set(row.id, target)
if (listed.has(id) || (rows.has(id) && declaredUnder.get(id) !== target)) {
duplicates.push({ rowId: id, moduleName: row.name })
} else if (!rows.has(id)) {
rows.set(id, row.name)
declaredUnder.set(id, target)
}
} else {
claim(row.id, row.name, target)
claim(id, row.name, target)
}
listed.add(row.id)
})
claim(groupId, CONTAINED_GROUP_MODULE)
const wrappers = new Map<string, string>()
@@ -209,12 +202,14 @@ export interface BundleReconciliation {
/**
* Reconcile `dsh.profile.bundles` against the installed state after a pnpm
* run. A dependency that no longer resolves to a bundle leaves the layer
* list; template bundles (never dependencies) are untouched. A dependency
* the run added that resolves to a bundle joins the list only when
* `autoEnable` is set — the CLI's install-and-enable semantics — and is
* otherwise reported as installed-only, which is the plugin manager's install
* step. A bundle installed before the run keeps its place in or out of the
* list: one the user disabled stays disabled through an unrelated run.
* list; template bundles (never dependencies) are untouched. A bundle in
* neither list — one the run added, or a dependency whose update declared
* `dsh.bundle` — joins the layer list only when `autoEnable` is set, the
* CLI's install-and-enable semantics, and is otherwise reported as
* installed-only, the plugin manager's install step. A bundle the user
* disabled is named in `dsh.profile.disabledBundles` and stays out through
* every later run until enabled again; the record goes when its dependency
* does.
* @param binName - the diagnostic prefix used by manifest reads.
* @param profileDir - the profile directory.
* @param installAnchor - absolute path of the dsh app's package.json.
@@ -233,14 +228,14 @@ export function reconcileInstalledBundles(
const beforeDeps = new Set(Object.keys(before.dependencies ?? {}))
const dependencies = Object.keys(after.dependencies ?? {})
const bundles = [...after.dsh?.profile?.bundles ?? []]
const disabled = after.dsh?.profile?.disabledBundles ?? []
const outcome: BundleReconciliation = { enabled: [], removed: [], plain: [], installedOnly: [] }
for (const packageName of dependencies) {
if (beforeDeps.has(packageName)) continue
if (!exportsBundlePatch(binName, packageName, installAnchor, profileDir)) {
outcome.plain.push(packageName)
if (!beforeDeps.has(packageName)) outcome.plain.push(packageName)
continue
}
if (bundles.includes(packageName)) continue
if (bundles.includes(packageName) || disabled.includes(packageName)) continue
if (options.autoEnable) {
bundles.push(packageName)
outcome.enabled.push(packageName)
@@ -249,29 +244,36 @@ export function reconcileInstalledBundles(
}
}
const dependencySet = new Set(dependencies)
const stillBundle = (packageName: string): boolean => (
dependencySet.has(packageName) && exportsBundlePatch(binName, packageName, installAnchor, profileDir)
)
for (const packageName of [...bundles]) {
// Only dependency-managed entries are subject to removal; template
// bundles are not dependencies.
const wasDependency = beforeDeps.has(packageName) || dependencySet.has(packageName)
const stillBundle = dependencySet.has(packageName) && exportsBundlePatch(binName, packageName, installAnchor, profileDir)
if (wasDependency && !stillBundle) {
if (wasDependency && !stillBundle(packageName)) {
bundles.splice(bundles.indexOf(packageName), 1)
outcome.removed.push(packageName)
}
}
if (outcome.enabled.length > 0 || outcome.removed.length > 0) {
writeProfileManifest(profileDir, withBundles(after, bundles))
// A disabled bundle whose dependency is gone, or no longer a bundle, has nothing left to keep out.
const stillDisabled = disabled.filter(stillBundle)
if (outcome.enabled.length > 0 || outcome.removed.length > 0 || stillDisabled.length !== disabled.length) {
writeProfileManifest(profileDir, withBundles(after, bundles, stillDisabled))
}
return outcome
}
/** The manifest with its bundle layer list replaced, every other field kept. */
function withBundles(manifest: ProfileManifest, bundles: string[]): ProfileManifest {
return { ...manifest, dsh: { ...manifest.dsh, profile: { ...manifest.dsh?.profile, bundles } } }
/** The manifest with its bundle lists replaced, every other field kept; an empty disabled list leaves the field out. */
function withBundles(manifest: ProfileManifest, bundles: string[], disabled: readonly string[]): ProfileManifest {
const profile: DshProfileManifest = { ...manifest.dsh?.profile, bundles }
if (disabled.length > 0) profile.disabledBundles = [...disabled]
else delete profile.disabledBundles
return { ...manifest, dsh: { ...manifest.dsh, profile } }
}
/**
* Add one installed bundle to the profile's layer list.
* Add one installed bundle to the profile's layer list and drop its disabled record.
* @param binName - the diagnostic prefix on thrown errors.
* @param profileDir - the profile directory.
* @param installAnchor - absolute path of the dsh app's package.json.
@@ -289,12 +291,14 @@ export function enableBundle(binName: string, profileDir: string, installAnchor:
}
const bundles = manifest.dsh?.profile?.bundles ?? []
if (bundles.includes(packageName)) return false
writeProfileManifest(profileDir, withBundles(manifest, [...bundles, packageName]))
const disabled = (manifest.dsh?.profile?.disabledBundles ?? []).filter(name => name !== packageName)
writeProfileManifest(profileDir, withBundles(manifest, [...bundles, packageName], disabled))
return true
}
/**
* Remove one dependency-managed bundle from the profile's layer list.
* Remove one dependency-managed bundle from the profile's layer list and
* record it as disabled, so reconciliation leaves it out until `enableBundle`.
* @param binName - the diagnostic prefix on thrown errors.
* @param profileDir - the profile directory.
* @param packageName - the bundle to disable.
@@ -308,6 +312,9 @@ export function disableBundle(binName: string, profileDir: string, packageName:
if (!(packageName in (manifest.dependencies ?? {}))) {
throw new Error(`${binName}: ${packageName} is a template bundle of this profile and cannot be disabled`)
}
writeProfileManifest(profileDir, withBundles(manifest, bundles.filter(name => name !== packageName)))
const disabled = manifest.dsh?.profile?.disabledBundles ?? []
writeProfileManifest(profileDir, withBundles(
manifest, bundles.filter(name => name !== packageName), [...new Set([...disabled, packageName])],
))
return true
}
+36
View File
@@ -76,3 +76,39 @@ export function visitPatchRows(
}
})
}
/** One id-carrying row `visitIdentifiedRows` reports. */
export interface IdentifiedRow {
/** The row's id. */
id: string
/** The row as the patch wrote it. */
row: EntryOptions
/** How the patch introduced the row. */
source: PatchRowSource
/** Where the row mounts. */
place: PatchRowPlace
/** The ids the same patch listed before this row. */
listed: ReadonlySet<string>
}
/**
* Visit every row with an id a patch list introduces, in written order, with
* the ids the same patch listed before it: a config override lists the
* children it keeps, and an id it lists twice would mount as a rejected
* duplicate, which each caller reports in its own terms.
* @param patches - the patch list.
* @param visit - called once per row that carries an id.
*/
export function visitIdentifiedRows(patches: readonly PatchOptions[], visit: (visited: IdentifiedRow) => void): void {
let listed = new Set<string>()
let listIndex = -1
visitPatchRows(patches, (row, source, place) => {
if (typeof row.id !== 'string') return
if (place.patch !== listIndex) {
listIndex = place.patch
listed = new Set()
}
visit({ id: row.id, row, source, place, listed })
listed.add(row.id)
})
}
@@ -269,17 +269,25 @@ describe('reconcileInstalledBundles', () => {
expect(exportsBundlePatch(NAME, 'missing', installAnchor, profileDir)).toBe(false)
})
it('leaves a bundle installed before the run where the user left it, in or out of the list', () => {
const { profileDir, installAnchor } = stageProfile({ 'ext-bundle': { bundle: true }, 'new-bundle': { bundle: true } })
// ext-bundle was installed earlier and is not listed: the user disabled it.
const before = { dependencies: { 'ext-bundle': '1.0.0' } }
expect(reconcileInstalledBundles(NAME, profileDir, installAnchor, before, { autoEnable: true }))
.toEqual({ enabled: ['new-bundle'], removed: [], plain: [], installedOnly: [] })
expect(readProfileManifest(NAME, profileDir).dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base', 'new-bundle'])
disableBundle(NAME, profileDir, 'new-bundle')
expect(reconcileInstalledBundles(NAME, profileDir, installAnchor, readProfileManifest(NAME, profileDir), { autoEnable: true }))
.toEqual({ enabled: [], removed: [], plain: [], installedOnly: [] })
expect(readProfileManifest(NAME, profileDir).dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base'])
it('keeps a bundle the user disabled out of the list through later runs, and enables one an update turned into a bundle', () => {
const { profileDir, installAnchor } = stageProfile({ 'ext-bundle': { bundle: true }, 'late-bundle': {} })
const installed = readProfileManifest(NAME, profileDir)
expect(reconcileInstalledBundles(NAME, profileDir, installAnchor, { dependencies: {} }, { autoEnable: true }))
.toEqual({ enabled: ['ext-bundle'], removed: [], plain: ['late-bundle'], installedOnly: [] })
expect(disableBundle(NAME, profileDir, 'ext-bundle')).toBe(true)
expect(readProfileManifest(NAME, profileDir).dsh?.profile).toEqual({ bundles: ['@deepseek-ai/dsh-base'], disabledBundles: ['ext-bundle'] })
// An update made late-bundle a bundle; ext-bundle is still installed and still disabled.
const late = join(profileDir, 'node_modules', 'late-bundle')
writeFileSync(join(late, 'package.json'), JSON.stringify({ name: 'late-bundle', version: '2.0.0', dsh: { bundle: { patch: './cordis.patch.yml' } } }))
writeFileSync(join(late, 'cordis.patch.yml'), '[]\n')
expect(reconcileInstalledBundles(NAME, profileDir, installAnchor, installed, { autoEnable: false }))
.toEqual({ enabled: [], removed: [], plain: [], installedOnly: ['late-bundle'] })
expect(reconcileInstalledBundles(NAME, profileDir, installAnchor, installed, { autoEnable: true }))
.toEqual({ enabled: ['late-bundle'], removed: [], plain: [], installedOnly: [] })
expect(readProfileManifest(NAME, profileDir).dsh?.profile).toEqual({ bundles: ['@deepseek-ai/dsh-base', 'late-bundle'], disabledBundles: ['ext-bundle'] })
// Enabling again drops the record.
expect(enableBundle(NAME, profileDir, installAnchor, 'ext-bundle')).toBe(true)
expect(readProfileManifest(NAME, profileDir).dsh?.profile).toEqual({ bundles: ['@deepseek-ai/dsh-base', 'late-bundle', 'ext-bundle'] })
})
it('keeps a listed bundle listed once when the run brings its dependency back', () => {
@@ -291,16 +299,18 @@ describe('reconcileInstalledBundles', () => {
expect(readProfileManifest(NAME, profileDir).dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base', 'ext-bundle'])
})
it('drops a layer whose dependency was removed and keeps template bundles', () => {
const { profileDir, installAnchor } = stageProfile({ 'ext-bundle': { bundle: true } })
it('drops a layer whose dependency was removed, and the disabled record of one, keeping template bundles', () => {
const { profileDir, installAnchor } = stageProfile({ 'ext-bundle': { bundle: true }, 'off-bundle': { bundle: true } })
enableBundle(NAME, profileDir, installAnchor, 'ext-bundle')
enableBundle(NAME, profileDir, installAnchor, 'off-bundle')
disableBundle(NAME, profileDir, 'off-bundle')
const manifest = readProfileManifest(NAME, profileDir)
// pnpm removed the dependency; the layer list still names it.
// pnpm removed both dependencies; the lists still name them.
writeFileSync(join(profileDir, 'package.json'), JSON.stringify({ ...manifest, dependencies: {} }))
const outcome = reconcileInstalledBundles(NAME, profileDir, installAnchor, manifest, { autoEnable: true })
expect(outcome.removed).toEqual(['ext-bundle'])
expect(readProfileManifest(NAME, profileDir).dsh?.profile?.bundles).toEqual(['@deepseek-ai/dsh-base'])
expect(readProfileManifest(NAME, profileDir).dsh?.profile).toEqual({ bundles: ['@deepseek-ai/dsh-base'] })
})
})
+8 -1
View File
@@ -232,7 +232,14 @@ describe('probePackage', () => {
'peeks': {
main: 'throw new Error("env=" + Object.keys(process.env).filter(k => k.startsWith("PROBE_TEST") || k === "DSH_PROBE_REPORT").sort().join(","))\n',
},
'floods': { main: 'import { writeSync } from "node:fs"\nwriteSync(2, "x".repeat(200_000))\nwriteSync(2, "tail-marker")\nprocess.exit(3)\n' },
// The child's stderr is a pipe Node put in non-blocking mode (tsx touches
// process.stderr at start), so one raw write returns short or throws EAGAIN
// once the pipe is full: the flood retries until the parent has drained it.
'floods': {
main: 'import { writeSync } from "node:fs"\n'
+ 'const put = (text) => { let rest = Buffer.from(text); while (rest.length > 0) { try { rest = rest.subarray(writeSync(2, rest)) } catch (error) { if (error.code !== "EAGAIN") throw error } } }\n'
+ 'put("x".repeat(200_000))\nput("tail-marker")\nprocess.exit(3)\n',
},
})
const forged = await probePackage({ binName: NAME, profileDir, installAnchor, packageName: 'forges' })
expect(forged).toMatchObject({ kind: 'library', ok: true })
@@ -57,6 +57,12 @@ export interface DshBundleManifest {
export interface DshProfileManifest {
/** Ordered bundle layer list, using installed package names. */
bundles?: string[]
/**
* Installed bundles the user disabled: dependencies that declare
* `dsh.bundle` and stay out of `bundles`, through every later pnpm run,
* until enabled again.
*/
disabledBundles?: string[]
/** User patch lifecycle; omitted means `live` for custom profiles. */
patchReload?: ProfilePatchReload
/** Deployer overrides of each external bundle's mount stage, by package name. */