perf(app-boot): avoid fallback locks for complete profiles

Resolve the installation fallback generation before locking and return immediately when every required symlink or packaged proxy is complete. Parallel SDK rollouts sharing an initialized DSH_HOME therefore do not queue on profiles/node_modules.lock.

Missing or stale entries still acquire the cross-process writer lock, recheck the generation, and repair under exclusive ownership. Tests hold the lock to prove the steady-state bypass and verify that a partial repair retains already-correct siblings.
This commit is contained in:
Tianyi Cui 2026-08-24 13:11:05 +08:00
parent c2ad69344f
commit ab4e65ba82
8 changed files with 133 additions and 54 deletions

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 .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
2026-08-23-python-sdk-dsh-profile-runtime.md: 19c870b3b0e10b25480bacc85b9db29b01d2577d
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 404798ff4ae1fcafbaa8403c7187297adf374a19
2026-08-23-python-sdk-dsh-profile-runtime.md: e3df2d01e3aef7e6eadaa011d51c9ab87456d35f
2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 3d50cac36d469172e91be450cea658f3a4830ccf

View file

@ -32,7 +32,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe
The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime-<platform>-<arch>`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged.
Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit ESM export map directly under Node import conditions, exposes targets that exist in the installation, and re-exports their virtual module URLs. Export rows without an ESM runtime target and executable-only or declaration-only packages produce no unusable proxy entry; malformed export maps fail startup. A complete matching generation returns without acquiring the cross-process writer lock. A missing or stale entry acquires the lock, rechecks the generation, and repairs it without exposing partial proxies; either carrier can replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target.

View file

@ -32,7 +32,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime-<platform>-<arch>`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。
普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理直接按 Node import 条件解析源包的显式 ESM exports map,公开安装中实际存在的目标,并重新导出其虚拟模块 URL。没有 ESM 运行时目标的 export 项以及仅含可执行入口或类型声明入口的包不会产生不可用的代理条目;格式错误的 exports map 会导致启动失败。完整且匹配的 generation 不会获取跨进程写入锁。缺失或过期的配置项会获取该锁、重新检查 generation,并在不暴露半成品代理的前提下修复;任一载体都可以替换另一载体留下的受管配置项。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。

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: 1fa00eefae367e2a5a44966d2f2debff9f95c074
README.zh.md: ef57478b8a3de723e4fa8ce88f87d563eb5a81e5
README.md: 0adcb0ac20516b1eea97792a11471a383a3ab429
README.zh.md: 1da1725b1e0681f83f2c82b82cb9bbaac8ea90cb

View file

@ -35,7 +35,7 @@ This package carries no loader hooks and no dev-mode surface. The [`dsh` app](..
## Profiles
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory under a cross-process writer lock. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. The lock prevents concurrent launchers from observing partial proxies, and either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged.
A profile is a directory under `$DSH_HOME/profiles/<name>` (the Harness home resolves through [`resolveDshHome`](../../util/home-paths/README.md): `$DSH_HOME`, else `~/.dsh`) holding a `package.json` — out-of-tree plugin `dependencies` plus the profile manifest `dsh.profile` with its ordered `bundles` layer list and `patchReload: live | startup` — and the user's own `cordis.patch.yml`. `live` watches the profile and home-level patch files after boot; `startup` applies every layer once. A missing value keeps the historical `live` default for custom profiles. A bundle is an npm package whose manifest declares `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }`; `loadProfile` resolves each `dsh.profile.bundles` name two-anchored (the dsh installation first, then the profile directory) and fails loud on a listed package without a bundle declaration. `composeEntries` applies patch layers over an empty entry list through the include's own `applyEntryPatches`, so composition, flag derivation, and config dumps cannot drift from what boots. `healProfilesModuleFallback` maintains the flat `$DSH_HOME/profiles/node_modules` directory. Plain Node writes one symlink per package in the installation dependency closure; a pkg executable resolves available explicit exports directly from each installed manifest with Node ESM import conditions and writes real proxy packages that re-export virtual module URLs, because an operating-system symlink cannot enter pkg's `/snapshot` tree. Export targets absent from an installed package remain unavailable without blocking its other exports; malformed export maps fail startup. An executable-only or declaration-only package with no module entry produces no proxy. A complete matching generation returns without acquiring the writer lock. A missing or stale entry acquires the cross-process lock, rechecks the full generation, and repairs it without exposing partial proxies; either carrier replaces the other carrier's managed entry. Both forms let profile plugins resolve installation packages through Node's ordinary parent walk and preserve one module instance for external plugin peers. `PROFILE_TEMPLATES` auto-initializes `web` with live reload and `headless`/`sdk`/`acp` with startup-only patches; other names fail loud until `initProfile` creates them through `dsh plugin`. `loadProfile` normalizes an exact installation-owned bundle tuple and a missing reload choice to its shipped template while preserving every explicit reload choice and every other manifest field; any extra, missing, or reordered bundle makes the list user-owned and leaves it unchanged.
User-level machine-local preferences also live in the Harness home:

View file

@ -35,7 +35,7 @@ Loader 并发挂载各个条目,因此当其他环节失败时,某个界面
## Profiles
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 在跨进程写入锁下维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。该锁防止并发启动器观察到未完整写入的代理,而两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。
profile 是位于 `$DSH_HOME/profiles/<name>` 下的目录(harness home 由 [`resolveDshHome`](../../util/home-paths/README.zh.md) 解析:先取 `$DSH_HOME`,否则取 `~/.dsh`),其中包含一个 `package.json`(树外插件 `dependencies`,加上 profile manifest `dsh.profile` 及其有序的 `bundles` 层列表和 `patchReload: live | startup`)和用户自己的 `cordis.patch.yml`。`live` 会在启动后监视 profile 与 home 级 patch 文件;`startup` 只应用每层一次。缺失值为自定义 profile 保留历史 `live` 默认值。组合包是在 manifest 中声明 `"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }` 的 npm 包;`loadProfile` 以双锚点解析每个 `dsh.profile.bundles` 名称(先从 dsh 安装目录,再从 profile 目录),列出的包若没有组合包声明则明确报错。`composeEntries` 通过 include 自己的 `applyEntryPatches` 在空条目列表之上应用各 patch 层,因此组合、标志推导和配置 dump 绝不会与实际启动内容发生偏离。`healProfilesModuleFallback` 维护扁平的 `$DSH_HOME/profiles/node_modules` 目录。普通 Node 为安装依赖闭包中的每个包写入一个符号链接;pkg 可执行程序则直接从每个已安装 manifest 中按 Node ESM import 条件解析实际存在的显式 exports,并写入重新导出虚拟模块 URL 的真实代理包,因为操作系统符号链接无法进入 pkg 的 `/snapshot` 树。安装包中不存在的 export 目标保持不可用,但不阻塞其他 exports;格式错误的 exports map 会导致启动失败。只有可执行入口或类型声明入口而没有模块入口的包不会生成代理。完整且匹配的 generation 不会获取写入锁。缺失或过期的配置项会获取跨进程锁、重新检查完整 generation,并在不暴露半成品代理的前提下修复;两种载体都会替换另一种载体留下的受管条目。两种形式都使 profile 插件可以通过 Node 常规的逐级向上查找解析安装包,并让外部插件 peer 共用一个模块实例。`PROFILE_TEMPLATES` 首次使用时以实时重载初始化 `web`,以仅启动时 patch 初始化 `headless`/`sdk`/`acp`;其他名称在通过 `dsh plugin` 由 `initProfile` 创建前都会明确报错。`loadProfile` 会把安装自有的精确组合包元组和缺失的重载选择规范化为随附模板,同时保留每个显式重载选择和 manifest 中其他所有字段;组合包一旦有任何额外、缺失或重排,列表就归用户所有并保持不变。
用户级的机器本地偏好同样位于 harness home 中:

View file

@ -407,44 +407,12 @@ function ensureModuleProxy(
}
}
/**
* Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
* entry per package in the dsh app's resolvable dependency CLOSURE (BFS
* over `dependencies` from the app manifest), each resolved from its own
* installation location. Plain Node uses symlinks. A pkg executable resolves
* exports under ESM import conditions and writes small proxy packages because
* the host filesystem cannot follow a symlink into pkg's virtual `/snapshot`
* tree; the proxy re-exports the virtual URL, preserving the executable's
* single module instance. One cross-process writer lock prevents partial
* proxies and serializes carrier transitions. Node's
* parent-directory walk from any profile finds this
* directory after the profile's own `node_modules`, so every in-box plugin
* resolves without pnpm ever managing it — the exact "bundles come from the
* installation" contract. The closure (not just direct dependencies) is
* required for out-of-tree plugins: their peer dependencies name Service
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
* reaches only through its Service Provider packages. Both a symlink target
* and a proxy's virtual target resolve transitive imports from the original
* package directory, so each package needs one flat fallback entry.
* Idempotent: correct entries are kept and changed installation targets are
* rewritten; under plain Node, a stale dangling link stays until its name is
* reused because resolution cannot discover it.
* @param installAnchor - absolute path of the dsh app's package.json.
* @param home - the Harness home; defaults to {@link resolveDshHome}.
* @returns settlement after the locked fallback generation is complete.
*/
export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise<void> {
const profilesDir = join(home, PROFILES_DIR)
const modulesDir = join(profilesDir, 'node_modules')
mkdirSync(modulesDir, { recursive: true })
await withFileLock(modulesDir, () => {
healProfilesModuleFallbackLocked(installAnchor, modulesDir)
return Promise.resolve()
})
}
type ModuleFallbackEntry =
| { kind: 'symlink'; packageName: string; packageDir: string }
| { kind: 'proxy'; packageName: string; version: string; targets: Record<string, string> }
/** Heal one module-fallback generation while the cross-process writer lock is held. */
function healProfilesModuleFallbackLocked(installAnchor: string, modulesDir: string): void {
/** Resolve the installation generation that every profile must find through the fallback directory. */
function resolveModuleFallbackEntries(installAnchor: string): ModuleFallbackEntry[] {
const appManifest = JSON.parse(readFileSync(installAnchor, 'utf8')) as ProfileManifest
const links = new Map<string, string>()
/* v8 ignore next -- a real app manifest always declares its name */
@ -468,16 +436,88 @@ function healProfilesModuleFallbackLocked(installAnchor: string, modulesDir: str
queue.push({ anchor: manifestPath, manifest: JSON.parse(readFileSync(manifestPath, 'utf8')) as ProfileManifest })
}
}
for (const [packageName, target] of links) {
const link = join(modulesDir, packageName)
if (!isPackagedExecutable()) {
return [...links].map(([packageName, packageDir]) => ({ kind: 'symlink', packageName, packageDir }))
}
return [...links].flatMap(([packageName, packageDir]) => {
const source = packageProxySource(packageName, packageDir)
return Object.keys(source.targets).length === 0
? []
: [{ kind: 'proxy' as const, packageName, version: source.version, targets: source.targets }]
})
}
/** Return whether one existing fallback entry already matches its resolved installation generation. */
function moduleFallbackEntryCurrent(modulesDir: string, entry: ModuleFallbackEntry): boolean {
const link = join(modulesDir, entry.packageName)
try {
const stat = lstatSync(link)
if (entry.kind === 'symlink') {
return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir
}
if (!stat.isDirectory()) return false
const existing = readModuleProxyRecord(link)
return existing?.version === entry.version
&& JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets)
&& Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))
} catch {
return false
}
}
/** Return whether every required fallback entry is already ready for this installation. */
function moduleFallbackCurrent(modulesDir: string, entries: readonly ModuleFallbackEntry[]): boolean {
return entries.every(entry => moduleFallbackEntryCurrent(modulesDir, entry))
}
/**
* Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
* entry per package in the dsh app's resolvable dependency CLOSURE (BFS
* over `dependencies` from the app manifest), each resolved from its own
* installation location. Plain Node uses symlinks. A pkg executable resolves
* exports under ESM import conditions and writes small proxy packages because
* the host filesystem cannot follow a symlink into pkg's virtual `/snapshot`
* tree; the proxy re-exports the virtual URL, preserving the executable's
* single module instance. A complete matching generation returns without a
* writer lock; actual repairs acquire and recheck one cross-process lock so
* partial proxies and carrier transitions remain serialized. Node's
* parent-directory walk from any profile finds this
* directory after the profile's own `node_modules`, so every in-box plugin
* resolves without pnpm ever managing it — the exact "bundles come from the
* installation" contract. The closure (not just direct dependencies) is
* required for out-of-tree plugins: their peer dependencies name Service
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
* reaches only through its Service Provider packages. Both a symlink target
* and a proxy's virtual target resolve transitive imports from the original
* package directory, so each package needs one flat fallback entry.
* Idempotent: correct entries are kept and changed installation targets are
* rewritten; under plain Node, a stale dangling link stays until its name is
* reused because resolution cannot discover it.
* @param installAnchor - absolute path of the dsh app's package.json.
* @param home - the Harness home; defaults to {@link resolveDshHome}.
* @returns settlement after current-state validation or a locked repair.
*/
export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise<void> {
const profilesDir = join(home, PROFILES_DIR)
const modulesDir = join(profilesDir, 'node_modules')
mkdirSync(modulesDir, { recursive: true })
const entries = resolveModuleFallbackEntries(installAnchor)
if (moduleFallbackCurrent(modulesDir, entries)) return
await withFileLock(modulesDir, () => {
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir)
return Promise.resolve()
})
}
/** Heal one module-fallback generation while the cross-process writer lock is held. */
function healProfilesModuleFallbackLocked(entries: readonly ModuleFallbackEntry[], modulesDir: string): void {
for (const entry of entries) {
const link = join(modulesDir, entry.packageName)
mkdirSync(dirname(link), { recursive: true })
if (isPackagedExecutable()) {
const source = packageProxySource(packageName, target)
if (Object.keys(source.targets).length > 0) {
ensureModuleProxy(link, packageName, source.version, source.targets)
}
if (entry.kind === 'proxy') {
ensureModuleProxy(link, entry.packageName, entry.version, entry.targets)
} else {
ensureSymlink(link, target)
ensureSymlink(link, entry.packageDir)
}
}
}

View file

@ -4,7 +4,7 @@
* empty-root composition, and the installation module-fallback healing.
*/
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
@ -321,6 +321,20 @@ describe('healProfilesModuleFallback', () => {
expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app')
})
it('retains current links while repairing a missing sibling', async () => {
const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
const home = tmp()
const fallback = join(home, 'profiles', 'node_modules')
await healProfilesModuleFallback(anchor, home)
const appTarget = readlinkSync(join(fallback, 'dsh-app'))
unlinkSync(join(fallback, 'bundle-a'))
await healProfilesModuleFallback(anchor, home)
expect(readlinkSync(join(fallback, 'dsh-app'))).toBe(appTarget)
expect(lstatSync(join(fallback, 'bundle-a')).isSymbolicLink()).toBe(true)
})
it('serializes concurrent healers and retains the identical link', async () => {
const anchor = stageInstallation({})
const home = tmp()
@ -332,6 +346,31 @@ describe('healProfilesModuleFallback', () => {
expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true)
})
it('does not acquire the writer lock for a complete generation', async () => {
const anchor = stageInstallation({})
const home = tmp()
const modules = join(home, 'profiles', 'node_modules')
await healProfilesModuleFallback(anchor, home)
let releaseLock: (() => void) | undefined
let reportLock: (() => void) | undefined
const lockHeld = new Promise<void>((resolve) => { reportLock = resolve })
const release = new Promise<void>((resolve) => { releaseLock = resolve })
const holder = withFileLock(modules, async () => {
reportLock?.()
await release
})
await lockHeld
const healer = healProfilesModuleFallback(anchor, home)
const outcome = await Promise.race([
healer.then(() => 'complete' as const),
new Promise<'blocked'>(resolve => setTimeout(() => { resolve('blocked') }, 100)),
])
releaseLock?.()
await Promise.all([holder, healer])
expect(outcome).toBe('complete')
})
it('waits for the module-fallback writer lock before publishing entries', async () => {
const anchor = stageInstallation({})
const home = tmp()