Merge pull request #5523 from deepseek-harness/refactor/desktop-pnpm-dedupe

refactor(desktop): deduplicate bundled pnpm
This commit is contained in:
07akioni authored and GitHub committed 2026-10-09 16:31:58 +08:00
commit d911abce6b
14 files changed
+358 -86

No files matched your search

+2 -2
View File
@@ -7,7 +7,7 @@ import { installOfficeEngineResolution, runtimeArchivePath } from './office-engi
/**
* Run the ordinary CLI with Desktop's bundled package manager and reserved-profile plugin access.
* @param runtimeDir - Prepared or ASAR-contained production DSH package tree.
* @param supportDir - Physical Desktop runtime directory containing pnpm.
* @param supportDir - Physical Desktop runtime directory containing the primary-runtime payload and Node launchers.
* @returns Completion of the selected CLI command; profile plugins own their process lifetime.
*/
export async function runDesktopCli(runtimeDir: string, supportDir: string): Promise<void> {
@@ -16,7 +16,7 @@ export async function runDesktopCli(runtimeDir: string, supportDir: string): Pro
manageDesktopProfile: true,
packageManager: {
command: process.execPath,
args: ['--expose-internals', join(supportDir, 'pnpm', 'bin', 'pnpm.mjs')],
args: ['--expose-internals', join(supportDir, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')],
env: {
ELECTRON_RUN_AS_NODE: '1',
DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
+4 -4
View File
@@ -3,8 +3,8 @@
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/desktop/README.md
/deepseek-harness-desktop:
en: 001b7d11c237078b
zh: eda6b580dc224c7f
en: 5b8518fd5e0030ef
zh: 02efee59c39647dc
/deepseek-harness-desktop/terminal-command:
en: bef92953c237363f
zh: d21d6effd21b8c00
@@ -66,8 +66,8 @@
en: 64836513eb7eda6e
zh: 6ebb9184a7aa51de
/deepseek-harness-desktop/package/desktop-runtime-preparation:
en: 32ef30fc38462109
zh: 093b1b57d084f5bb
en: 9a71990bee9867a7
zh: 76c5c1fdff6e6908
/deepseek-harness-desktop/updates:
en: 41d0f50eb7173342
zh: ce940ef3cbcc6fa5
+2 -2
View File
@@ -12,7 +12,7 @@ The first application-menu command, **About DeepSeek Harness**, opens Electron's
Desktop’s local native directory flow opens an Electron folder dialog attached to the application window, restoring, showing, and focusing that window first. Concurrent requests share one dialog; cancellation returns no path and failures remain retryable. Ordinary Web uses the Host chooser. Browse mode lists Host directories. On Linux without zenity or kdialog, automatic selection uses browse instead of the Electron dialog.
Creator and the Web Plugin Manager use Desktop’s bundled pnpm under Electron Node mode without requiring pnpm on PATH. The private Node launcher environment applies only to package operations.
Desktop ships one pnpm distribution at `resources/runtime/primary-runtime/dependencies/pnpm`. Build-time production installation, the installed `dsh` CLI, Creator and the Web Plugin Manager execute its `bin/pnpm.mjs` with Electron Node mode; package operations do not require pnpm on PATH. The private Node launcher environment applies only to package operations. Agent workspace dependencies copy the same self-contained primary-runtime payload into the Harness home and execute pnpm with its standalone Node.
Embedded Platform documents use a persistent WebContentsView partition keyed by a hash of the Platform origin and stable account ID. localStorage preferences, including dismissed notices, survive view closure and application restart; different accounts and origins use separate storage. The Host supplies the account ID from its latest successful profile read; while no ID is known, the document opens in a disposable partition that keeps no preference across opens. Opening a persistent partition clears cookies, filesystem, IndexedDB, Cache Storage, the HTTP and shader caches, service workers, and HTTP authentication before the document loads; a disposable partition clears all of its storage. Closing a view destroys the document, removes its request interceptors, and schedules the same cleanup, so authentication left by an unclean exit is cleared before the next document loads. The next open and application shutdown await that cleanup, and an update install waits for it before the installer takes over the exit. A failed cleanup rejects that open and blocks the same account until a later cleanup succeeds; other accounts keep opening. Sign-out destroys the document but retains account-scoped preferences for the next sign-in. A document already open in a disposable partition stays mounted when the account ID arrives for the same credential; the next open uses the account-scoped partition. The [storage decision](../../.agents/notes/implemented/architecture/2026-09-22-platform-browser-storage.md) describes the retention policy. Host sends account credentials through private Node IPC; account RPC and the Harness renderer receive no token. Before page scripts execute, the Platform preload performs one synchronous IPC to read the prepared main-process credential. It exposes displayMode, synchronous getAuthToken() and getLocale() getters, and onLocaleChange(listener), which returns an unsubscribe function. Both getters read preload memory without further IPC. The bootstrap includes the resolved Desktop language (`zh_CN` or `en_US`); Settings language changes update the preload cache and notify the open Platform document without reloading. Platform applies this language before its first render and does not persist it as a browser preference. The main-process handler performs only sender validation and an in-memory read; it never waits for Host, disk, or network. Trusted-page initialization failures retain embedded mode and make the getter throw, preventing browser-credential fallback. Only the owned Platform main frame on the configured issuer origin can initialize. Sign-out, credential replacement, Host shutdown, and view closure destroy the document. Cross-origin document navigation is blocked. HTTPS links requesting a new window open in the system browser without the embedded session or token; other schemes and URL credentials are rejected. Native views occupy the viewport below the Account feature’s return bar.
@@ -443,7 +443,7 @@ Desktop [patch policy](scripts/runtime-patch-policy.ts) explicitly classifies ev
Desktop dependency overrides are declared separately in the patch-policy module, not inferred from patch versions. The pi-ai constraint keeps its runtime version at the version qualified by the shared patch. Desktop still resolves a fresh runtime lockfile for each build, then installs with `--frozen-lockfile`; dependency resolution across separate builds is not yet reproducible. CLI packed-installation tests and other distribution flows retain their own configuration.
Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution and pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.
Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.
Signed macOS packaging writes `Contents/Resources/app-update.yml` while assembling the App and before code signing, including the directory build that feeds the parallel ZIP and DMG lanes. The signing hook verifies the exact feed and updater cache directory. Both lane copies and the promoted App are checked again before the release completion record is written; a missing or mismatched configuration prevents artifact promotion and therefore prevents upload.
+2 -2
View File
@@ -12,7 +12,7 @@ Desktop 绑定 `127.0.0.1`,即 Electron 为就绪 URL 与 WebSocket 凭据过
Desktop 的本地原生目录流程打开绑定应用窗口的 Electron 文件夹对话框,并先恢复、显示和聚焦该窗口。并发请求共用一个对话框;取消不返回路径,失败后可以重试。普通 Web 使用 Host 选择器。浏览模式列出 Host 目录。Linux 缺少 zenity 或 kdialog 时,自动选择使用浏览模式,不使用 Electron 对话框。
Creator 和 Web Plugin Manager 在 Electron Node 模式下使用 Desktop 内置 pnpm,无需 PATH 中存在 pnpm。私有 Node 启动器环境仅应用于包操作。
Desktop 在 `resources/runtime/primary-runtime/dependencies/pnpm` 中携带一份 pnpm 分发包。构建时的生产依赖安装、安装版 `dsh` CLI、Creator 和 Web 插件管理器通过 Electron Node 模式执行其中的 `bin/pnpm.mjs`;包操作不要求 PATH 上存在 pnpm。私有 Node 启动器环境仅用于包操作。agent(智能体)的工作区依赖将同一份完整 primary-runtime 复制到 Harness 主目录,并使用其中的独立 Node 执行 pnpm。
Platform 内嵌文档使用持久化 WebContentsView 分区,分区名由 Platform 来源和稳定账号 ID 的哈希决定。localStorage 中的页面偏好(包括已关闭的通知)在关闭视图和重启应用后保留;不同账号和来源使用独立存储。账号 ID 来自 Host 最近一次成功的资料读取;尚无该 ID 时,文档在一次性分区中打开,该分区不跨次保留偏好。打开持久分区会先清理 Cookie、文件系统、IndexedDB、Cache Storage、HTTP 与着色器缓存、Service Worker 及 HTTP 认证状态;一次性分区则清理其全部存储。关闭视图会销毁文档、移除请求拦截器并安排相同的清理,因此异常退出遗留的认证会在下一个文档加载前被清除。下次打开和应用退出都会等待该清理完成,更新安装也会在安装器接管退出前等待。清理失败会使该次打开失败,并在后续清理成功前阻止同一账号打开;其他账号不受影响。退出登录会销毁文档,但保留账号偏好供下次登录使用。同一凭证下账号 ID 迟到时,已以一次性分区打开的文档保持挂载;下次打开使用账号分区。[存储决策](../../.agents/notes/implemented/architecture/2026-09-22-platform-browser-storage.zh.md)说明保留策略。Host 通过私有 Node IPC 发送账号凭证;账号 RPC 和 Harness 渲染进程不接收 token。Platform preload 在页面脚本执行前通过一次同步 IPC 读取主进程中已准备的凭证。它暴露 displayMode、同步的 getAuthToken() 和 getLocale() getter,以及返回取消订阅函数的 onLocaleChange(listener)。两个 getter 都只读取 preload 内存,不再调用 IPC。bootstrap 包含 Desktop 已解析的语言(`zh_CN` 或 `en_US`);Settings 语言变更会更新 preload 缓存并通知已打开的 Platform 文档,无需重载。Platform 在首屏渲染前应用该语言,且不将其持久化为浏览器偏好。主进程处理器仅校验调用来源并读取内存,不等待 Host、磁盘或网络。可信页面初始化失败时保留内嵌模式,由 getter 抛错,避免回退到浏览器凭证。只有受控 Platform 页面中、位于所配置签发来源的主 frame 能完成初始化。退登、凭证替换、Host 关闭及视图关闭都会销毁文档。跨来源文档导航被阻止。请求新窗口的 HTTPS 链接在系统浏览器中打开,不携带内嵌会话或 token;其他协议及带 URL 凭证的链接被拒绝。原生视图占据 Account 功能返回栏下方的视口。
@@ -445,7 +445,7 @@ Desktop [补丁策略](scripts/runtime-patch-policy.ts) 将每份工作区补丁
Desktop 依赖 overrides 在补丁策略模块中单独声明,不从补丁版本推导。pi-ai 约束使运行时版本保持在共享补丁已验证的版本。Desktop 仍会在每次构建时重新解析运行时锁文件,再通过 `--frozen-lockfile` 安装;不同构建之间的依赖解析尚不保证可复现。CLI 打包安装测试及其他交付流程保留各自的配置。
每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包与 pnpm CLI。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。
每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。
macOS 签名打包在组装 App 时、代码签名前写入 `Contents/Resources/app-update.yml`,供并行 ZIP 与 DMG 路线使用的目录构建也执行此操作。签名钩子验证准确的更新源和 updater 缓存目录。写入发布完成记录前,流程会再次检查两条路线的副本和最终移入的 App;配置缺失或不匹配会阻止移入产物,因而也会阻止上传。
+1 -1
View File
@@ -43,7 +43,7 @@ const RUNTIME_ROOT = BUILD_PATHS.runtime
const PNPM_BUILD_STATE = BUILD_PATHS.dshPnpm
const PACKAGE_SET_ROOT = BUILD_PATHS.packageSet
const NODE = join(BUILD_PATHS.electron, process.platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron')
const PNPM = join(RUNTIME_ROOT, 'pnpm', 'bin', 'pnpm.mjs')
const PNPM = join(RUNTIME_ROOT, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
function manifestVersion(path: string, subject: string): string {
const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown }
+37 -35
View File
@@ -1,14 +1,15 @@
/** Prepare the target Electron distribution and pinned pnpm CLI. */
/** Prepare Desktop resources and publish their versions after every preparation stage succeeds. */
import { packagingStep } from './packaging-step.mjs'
import { execFileSync } from 'node:child_process'
import { chmodSync, cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { chmodSync, cpSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { join } from 'node:path'
import { parseArgs } from 'node:util'
import { downloadArtifact } from '@electron/get'
import extractZip from 'extract-zip'
import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
import { readPrimaryRuntime } from '../../../packages/skill/tool-workspace-dependencies/src/index.ts'
import { desktopTargetPlatform, resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
import { packagingStep } from './packaging-step.mjs'
import { preparePrimaryRuntime } from './prepare-primary-runtime.ts'
import { prepareDesktopCli } from './prepare-cli.ts'
import { prepareCommandLink } from './prepare-command-link.ts'
@@ -16,23 +17,10 @@ import { prepareCommandLink } from './prepare-command-link.ts'
const BUILD_PATHS = resolveDesktopTargetBuildPaths()
const RUNTIME_ROOT = BUILD_PATHS.runtime
function preparePnpm(): string {
const require = createRequire(import.meta.url)
const manifestPath = require.resolve('pnpm')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { version?: unknown }
if (typeof manifest.version !== 'string') throw new Error('desktop runtime: pnpm manifest has no version')
const packageDir = dirname(manifestPath)
const destination = join(RUNTIME_ROOT, 'pnpm')
rmSync(destination, { recursive: true, force: true })
cpSync(packageDir, destination, { recursive: true })
return manifest.version
}
type DesktopTarget = ReturnType<typeof desktopTargetPlatform>
async function main(): Promise<void> {
const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } })
const target = resolveDesktopBuildTarget()
const platform = target.startsWith('mac-') ? 'darwin' : 'win32'
const arch = target.endsWith('arm64') ? 'arm64' : 'x64'
/** Extract the target Electron distribution and return its embedded Node version. */
async function prepareElectron({ platform, arch }: DesktopTarget): Promise<string> {
const require = createRequire(import.meta.url)
const { version } = require('electron/package.json') as { version: string }
const archive = await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'download:electron',
@@ -40,28 +28,42 @@ async function main(): Promise<void> {
rmSync(BUILD_PATHS.electron, { recursive: true, force: true })
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'extract:electron', () => extractZip(archive, { dir: BUILD_PATHS.electron }))
const executable = join(BUILD_PATHS.electron, platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron')
const nodeVersion = execFileSync(executable, ['-p', 'process.versions.node'], {
return execFileSync(executable, ['-p', 'process.versions.node'], {
encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
}).trim()
const macosMinimumVersion = platform === 'darwin' ? execFileSync('/usr/libexec/PlistBuddy',
['-c', 'Print LSMinimumSystemVersion', join(BUILD_PATHS.electron, 'Electron.app', 'Contents', 'Info.plist')], { encoding: 'utf8' }).trim() : undefined
rmSync(RUNTIME_ROOT, { recursive: true, force: true })
mkdirSync(RUNTIME_ROOT, { recursive: true })
const pnpmVersion = preparePnpm()
}
function prepareCli({ platform, arch }: DesktopTarget): void {
cpSync(join(import.meta.dirname, 'node-bin'), join(RUNTIME_ROOT, 'bin'), { recursive: true })
chmodSync(join(RUNTIME_ROOT, 'bin', 'node'), 0o755)
const cli = join(RUNTIME_ROOT, 'cli')
prepareDesktopCli(cli, platform)
if (platform === 'darwin') {
const minimumVersion = execFileSync('/usr/libexec/PlistBuddy', [
'-c', 'Print LSMinimumSystemVersion', join(BUILD_PATHS.electron, 'Electron.app', 'Contents', 'Info.plist'),
], { encoding: 'utf8' }).trim()
prepareCommandLink(cli, arch, minimumVersion)
}
cpSync(join(import.meta.dirname, '..', 'lib', 'command-manager-entry.js'), join(cli, 'command-manager.js'))
cpSync(join(import.meta.dirname, 'command-path.ps1'), join(cli, 'command-path.ps1'))
}
async function main(): Promise<void> {
const { values } = parseArgs({ options: { 'defer-primary-runtime-smoke': { type: 'boolean', default: false } } })
const target = desktopTargetPlatform(resolveDesktopBuildTarget())
rmSync(RUNTIME_ROOT, { recursive: true, force: true })
mkdirSync(RUNTIME_ROOT, { recursive: true })
const nodeVersion = await prepareElectron(target)
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime',
() => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] }))
const { pnpm } = await readPrimaryRuntime(join(RUNTIME_ROOT, 'primary-runtime'))
if (pnpm === undefined) throw new Error('desktop runtime: primary-runtime manifest has no pnpm version')
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli', async () => { prepareCli(target) })
writeFileSync(join(RUNTIME_ROOT, 'versions.json'), `${JSON.stringify({
schemaVersion: 1,
node: nodeVersion,
pnpm: pnpmVersion,
pnpm,
}, undefined, 2)}\n`)
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:cli',
async () => prepareDesktopCli(join(RUNTIME_ROOT, 'cli'), platform))
if (macosMinimumVersion !== undefined) prepareCommandLink(join(RUNTIME_ROOT, 'cli'), arch, macosMinimumVersion)
cpSync(join(import.meta.dirname, '..', 'lib', 'command-manager-entry.js'), join(RUNTIME_ROOT, 'cli', 'command-manager.js'))
cpSync(join(import.meta.dirname, 'command-path.ps1'), join(RUNTIME_ROOT, 'cli', 'command-path.ps1'))
await packagingStep(process.env.DSH_DESKTOP_PACKAGING_RUN_DIR, 'prepare:primary-runtime',
() => preparePrimaryRuntime({ deferSmoke: values['defer-primary-runtime-smoke'] }))
}
await main()
+65 -10
View File
@@ -1,6 +1,7 @@
/** Boot the materialized target runtime without access to a user's Harness profile. */
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { appendFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { execFile } from 'node:child_process'
@@ -12,7 +13,7 @@ import { createPluginProfile } from '../src/project-manager.ts'
import type { DesktopRuntimeDescriptor } from '../src/runtime-tree.ts'
/**
* Check Host startup, its matching frontend, external plugins and real Office-to-PDF conversion.
* Check CLI package operations, Host startup, its frontend, external plugins and real Office-to-PDF conversion.
* @param root - Materialized dsh resources.
* @param node - Prepared target Electron executable.
* @param runtime - Verified resource descriptor.
@@ -25,15 +26,50 @@ export async function smokeDesktopRuntime(
): Promise<void> {
const home = mkdtempSync(join(tmpdir(), 'dsh-desktop-smoke-'))
const profile = join(home, 'profiles', 'desktop')
const host = new DesktopHostProcess(node, root, profile, undefined, { ...environment, DSH_HOME: home },
const hostEnvironment = {
...Object.fromEntries(Object.entries(environment).filter(([name]) => !/^(?:npm|pnpm|corepack)_/iu.test(name))),
DSH_HOME: home, HOME: home, USERPROFILE: home,
XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home, XDG_STATE_HOME: home,
APPDATA: home, LOCALAPPDATA: home,
npm_config_userconfig: join(home, 'npmrc'), npm_config_globalconfig: join(home, 'npmrc'),
npm_config_store_dir: join(home, 'store'), npm_config_enable_global_virtual_store: 'false',
}
const host = new DesktopHostProcess(node, root, profile, undefined, hostEnvironment,
undefined, join(resourcesRuntime, 'primary-runtime'),
{ pnpm: join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.cjs'), nodeBin: join(resourcesRuntime, 'bin') })
{ pnpm: join(resourcesRuntime, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'), nodeBin: join(resourcesRuntime, 'bin') })
let timer: ReturnType<typeof setTimeout> | undefined
try {
writeFileSync(join(home, 'npmrc'), '')
createPluginProfile(profile)
const pluginName = 'desktop-runtime-smoke-plugin'
const plugin = join(profile, 'node_modules', pluginName)
const plugin = join(home, pluginName)
mkdirSync(plugin, { recursive: true })
const installedName = 'desktop-installed-smoke-plugin'
const installedPlugin = join(home, installedName)
mkdirSync(installedPlugin)
writeFileSync(join(installedPlugin, 'package.json'), JSON.stringify({
name: installedName, version: '1.0.0', type: 'module', exports: './index.js', dsh: { bundle: { patch: './bundle.yml' } },
}))
writeFileSync(join(installedPlugin, 'index.js'), `
export function apply(ctx) {
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-installed',
handler(_request, response) { response.end('installed plugin ready') } }))
}
`)
writeFileSync(join(installedPlugin, 'bundle.yml'), `- insert:\n - id: ${installedName}\n name: ${installedName}\n inject: [webServer]\n`)
appendFileSync(join(profile, 'pnpm-workspace.yaml'), 'offline: true\nupdateNotifier: false\n')
const cli = join(root, 'node_modules', '@deepseek-ai', 'dsh-desktop-host', 'lib', 'cli.js')
const runCli = (args: string[]) => promisify(execFile)(node,
['--expose-internals', cli, 'plugin', '--profile', 'desktop', ...args],
{ cwd: home, env: { ...hostEnvironment, ELECTRON_RUN_AS_NODE: '1' }, timeout: 120_000, windowsHide: true })
await runCli(['add', `file:${installedPlugin}`, '--offline', '--ignore-scripts'])
const installedEntry = join(profile, 'node_modules', installedName, 'index.js')
if (readFileSync(installedEntry, 'utf8') !== readFileSync(join(installedPlugin, 'index.js'), 'utf8')) {
throw new Error('desktop runtime: CLI did not install the local plugin')
}
await runCli(['remove', installedName])
if (existsSync(installedEntry)) throw new Error('desktop runtime: CLI did not remove the local plugin')
console.log('desktop runtime: CLI offline plugin installation and removal passed')
const primary = join(resourcesRuntime, 'primary-runtime')
const dependencies = workspaceDependencyPaths(primary, await readPrimaryRuntime(primary))
await promisify(execFile)(dependencies.python, ['-I', '-B',
@@ -56,6 +92,15 @@ export function apply(ctx) {
if (!(ctx instanceof Context)) throw new Error('desktop runtime: external plugin loaded another Cordis instance')
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke',
handler(_request, response) { response.end('plugin route ready') } }))
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-install',
async handler(_request, response) {
try {
response.end(JSON.stringify(await ctx.pluginManager.installBundle(${JSON.stringify(installedPlugin)})))
} catch (error) {
response.statusCode = 500
response.end(inspect(error, { depth: 5 }))
}
} }))
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: '/desktop-smoke-office-cli',
async handler(_request, response) {
try {
@@ -90,12 +135,13 @@ export function apply(ctx) {
}
}
`)
writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills]\n')
writeFileSync(join(plugin, 'bundle.yml'), '- insert:\n - id: desktop-runtime-smoke-plugin\n name: desktop-runtime-smoke-plugin\n inject: [webServer, officeToPdf, skills, pluginManager]\n')
cpSync(plugin, join(profile, 'node_modules', pluginName), { recursive: true })
const manifest = JSON.parse(readFileSync(join(profile, 'package.json'), 'utf8')) as {
dependencies: Record<string, string>
dependencies?: Record<string, string>
dsh: { profile: { bundles: string[] } }
}
manifest.dependencies[pluginName] = '1.0.0'
manifest.dependencies = { ...manifest.dependencies, [pluginName]: `file:${plugin}` }
manifest.dsh.profile.bundles.push(pluginName)
writeFileSync(join(profile, 'package.json'), JSON.stringify(manifest))
writeFileSync(join(profile, 'cordis.patch.yml'), '- id: webserver\n config:\n host: 127.0.0.1\n port: 0\n')
@@ -111,6 +157,14 @@ export function apply(ctx) {
}
const pluginResponse = await fetch(new URL('/desktop-smoke', ready.url), { headers: { cookie } })
if (await pluginResponse.text() !== 'plugin route ready') throw new Error('desktop runtime: plugin HTTP route failed')
const installation = await fetch(new URL('/desktop-smoke-install', ready.url), {
headers: { cookie }, signal: AbortSignal.timeout(120_000),
})
const installationResult = await installation.text()
const installed = await fetch(new URL('/desktop-smoke-installed', ready.url), { headers: { cookie } })
if (!installation.ok || !installed.ok || await installed.text() !== 'installed plugin ready') {
throw new Error(`desktop runtime: bundled pnpm plugin installation failed: ${installationResult}`)
}
for (const { extension } of inputs) {
const converted = await fetch(new URL(`/desktop-smoke-office/${extension}`, ready.url), {
headers: { cookie }, signal: AbortSignal.timeout(120_000),
@@ -133,7 +187,8 @@ export function apply(ctx) {
console.log('desktop runtime: DOCX, XLSX, PPTX to PDF and skill CLI discovery passed')
} finally {
clearTimeout(timer)
await host.stop()
rmSync(home, { recursive: true, force: true })
try { await host.stop() } finally {
await rm(home, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
}
}
}
+1 -1
View File
@@ -155,7 +155,7 @@ function runtimeResources(): RuntimeResources {
const nodeBin = development ? join(app.getAppPath(), 'scripts', 'node-bin') : join(process.resourcesPath, 'runtime', 'bin')
const pnpm = (development ? process.env.DSH_DESKTOP_PNPM_ENTRY : undefined)
?? (development ? join(app.getAppPath(), 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
: join(process.resourcesPath, 'runtime', 'pnpm', 'bin', 'pnpm.mjs'))
: join(process.resourcesPath, 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'))
const dsh = (development ? process.env.DSH_DESKTOP_DSH_DIR : undefined)
?? (development ? join(app.getAppPath(), '.desktop-build', 'development', 'project') : join(app.getAppPath(), 'dsh'))
return { node, nodeBin, pnpm, dsh }
+37
View File
@@ -0,0 +1,37 @@
/** The installed CLI uses the shared pnpm distribution with Electron's package-operation environment. */
import { delimiter, join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import { runCli } from '@deepseek-ai/dsh/lib/bin.js'
import { runDesktopCli } from '../../desktop-host/src/cli.ts'
import { installOfficeEngineResolution } from '../../desktop-host/src/office-engine.ts'
vi.mock('@deepseek-ai/dsh/lib/bin.js', () => ({ runCli: vi.fn() }))
vi.mock('../../desktop-host/src/office-engine.ts', () => ({ installOfficeEngineResolution: vi.fn() }))
afterEach(() => {
vi.unstubAllEnvs()
vi.clearAllMocks()
})
it.each(['system-bin', undefined])('dispatches with bundled pnpm when PATH is %s', async (path) => {
vi.stubEnv('PATH', path)
const runtime = join('Application 中文 with spaces', 'resources', 'app.asar', 'dsh')
const support = join('Application 中文 with spaces', 'resources', 'runtime')
await runDesktopCli(runtime, support)
expect(installOfficeEngineResolution).toHaveBeenCalledExactlyOnceWith(runtime)
expect(runCli).toHaveBeenCalledExactlyOnceWith({
manageDesktopProfile: true,
packageManager: {
command: process.execPath,
args: ['--expose-internals', join(support, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')],
env: {
ELECTRON_RUN_AS_NODE: '1',
DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
PATH: `${join(support, 'bin')}${delimiter}${path ?? ''}`,
},
},
})
})
+69
View File
@@ -0,0 +1,69 @@
/** Install and use a local dependency with the packaged pnpm and Electron Node launcher. */
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
/**
* Exercise offline installation and package scripts without a system pnpm or Node installation.
* @param {string} resourcesRuntime - Packaged runtime directory outside ASAR.
* @returns {Promise<void>} Resolves after execution and removal of the private installation.
*/
export async function checkPnpm(resourcesRuntime) {
assert.ok(process.versions.electron, 'Run the package-manager smoke with Electron Node mode')
assert.equal(existsSync(join(resourcesRuntime, 'pnpm')), false, 'Desktop must ship only primary-runtime pnpm')
const primary = join(resourcesRuntime, 'primary-runtime')
const pnpm = join(primary, 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
const versions = JSON.parse(readFileSync(join(resourcesRuntime, 'versions.json'), 'utf8'))
const manifest = JSON.parse(readFileSync(join(primary, 'runtime.json'), 'utf8'))
assert.equal(versions.pnpm, manifest.pnpm)
const scratch = mkdtempSync(join(tmpdir(), 'desktop-pnpm-smoke-'))
try {
mkdirSync(join(scratch, 'dependency'))
writeFileSync(join(scratch, 'dependency', 'package.json'), JSON.stringify({
name: 'desktop-pnpm-smoke-dependency', version: '1.0.0', main: 'index.cjs',
}))
writeFileSync(join(scratch, 'dependency', 'index.cjs'), 'exports.answer = 42\n')
writeFileSync(join(scratch, 'package.json'), JSON.stringify({
name: 'desktop-node-script-smoke', private: true,
dependencies: { 'desktop-pnpm-smoke-dependency': 'file:./dependency' }, scripts: { check: 'node check.cjs' },
}))
writeFileSync(join(scratch, 'npmrc'), '')
writeFileSync(join(scratch, 'check.cjs'), `
const assert = require('node:assert/strict')
assert.equal(require('desktop-pnpm-smoke-dependency').answer, 42)
assert.equal(process.execPath, ${JSON.stringify(process.execPath)})
assert.ok(process.versions.electron)
assert.ok(process.execArgv.includes('--expose-internals'))
assert.equal(typeof require('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function')
console.log('desktop-node-script-ok')
`)
const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) => /^(?:systemroot|windir|comspec|LD_LIBRARY_PATH)$/iu.test(name)))
const systemBin = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32') : '/usr/bin:/bin'
const run = args => execFileSync(process.execPath, ['--expose-internals', pnpm, ...args], {
cwd: scratch, encoding: 'utf8', timeout: 45_000, windowsHide: true,
env: { ...environment, pnpm_config_verify_deps_before_run: 'false',
ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
PATH: `${join(resourcesRuntime, 'bin')}${delimiter}${systemBin}`,
HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch,
XDG_CONFIG_HOME: scratch, XDG_CACHE_HOME: scratch, XDG_STATE_HOME: scratch,
npm_config_userconfig: join(scratch, 'npmrc'), npm_config_globalconfig: join(scratch, 'npmrc'),
npm_config_store_dir: join(scratch, 'store'), npm_config_enable_global_virtual_store: 'false',
npm_config_update_notifier: 'false' },
})
assert.equal(run(['--version']).trim(), versions.pnpm)
run(['install', '--offline', '--ignore-scripts'])
assert.match(run(['run', 'check']), /desktop-node-script-ok/u)
} finally {
await rm(scratch, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
}
}
if (import.meta.main) {
assert.ok(process.argv[2], 'Pass the packaged runtime directory')
await checkPnpm(process.argv[2])
console.log('desktop pnpm: offline installation and Electron Node package script passed')
}
+2 -28
View File
@@ -8,6 +8,7 @@ import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { delimiter, dirname, join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { checkPnpm } from './pnpm-smoke.mjs'
const runtime = process.argv[2]
assert.ok(runtime, 'Pass the filtered resources/dsh directory')
@@ -20,33 +21,6 @@ const resourcesRuntime = process.argv[3] ?? join(dirname(root), 'runtime')
const requireRuntime = createRequire(join(root, 'package.json'))
const scratch = mkdtempSync(join(tmpdir(), 'dsh-runtime-payload-'))
/** Run a package script with only the shipped node launcher available on PATH. */
function checkPnpm() {
const bin = join(resourcesRuntime, 'bin')
const pnpm = join(resourcesRuntime, 'pnpm', 'bin', 'pnpm.mjs')
writeFileSync(join(scratch, 'package.json'), JSON.stringify({
name: 'desktop-node-script-smoke', private: true, scripts: { check: 'node check.cjs' },
}))
writeFileSync(join(scratch, 'check.cjs'), `
const assert = require('node:assert/strict')
assert.equal(process.execPath, ${JSON.stringify(process.execPath)})
assert.ok(process.versions.electron)
assert.ok(process.execArgv.includes('--expose-internals'))
assert.equal(typeof require('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function')
console.log('desktop-node-script-ok')
`)
const environment = Object.fromEntries(Object.entries(process.env).filter(([name]) => /^(?:systemroot|windir|comspec)$/iu.test(name)))
const systemBin = process.platform === 'win32' ? join(process.env.SystemRoot, 'System32') : '/usr/bin:/bin'
// This dependency-free fixture checks script launch, without pnpm's implicit install and update-network check.
const output = execFileSync(process.execPath, ['--expose-internals', pnpm, 'run', 'check'], {
cwd: scratch, encoding: 'utf8', timeout: 45_000,
env: { ...environment, pnpm_config_verify_deps_before_run: 'false',
ELECTRON_RUN_AS_NODE: '1', DSH_DESKTOP_NODE_EXECUTABLE: process.execPath,
PATH: `${bin}${delimiter}${systemBin}`, HOME: scratch, USERPROFILE: scratch, TMP: scratch, TEMP: scratch, TMPDIR: scratch },
})
assert.match(output, /desktop-node-script-ok/u)
}
/** Spawn only a fixed Node program and await the terminal's drained exit event. */
async function checkPty() {
const pty = requireRuntime('node-pty')
@@ -162,7 +136,7 @@ try {
const builtin = requireRuntime('node-addon-require-builtin')
assert.equal(typeof builtin.requireBuiltin('internal/modules/esm/loader').getOrInitializeCascadedLoader, 'function')
console.error('runtime payload: pnpm')
checkPnpm()
await checkPnpm(resourcesRuntime)
console.error('runtime payload: koffi')
checkKoffi()
console.error('runtime payload: sharp')
+1 -1
View File
@@ -93,9 +93,9 @@ describe('desktop macOS release signature', () => {
const ignored = (path: string): boolean => config.mac.signIgnore.some(pattern => new RegExp(pattern).test(path))
expect(ignored('/App.app/Contents/Frameworks/Electron.framework/Versions/A/Resources/en.lproj/locale.pak')).toBe(true)
expect(ignored('/App.app/Contents/Frameworks/Electron.framework/Versions/A/Resources/resources.pak')).toBe(true)
expect(ignored('/App.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/addon.node')).toBe(true)
for (const path of [
'/App.app/Contents/Resources/runtime/node/node',
'/App.app/Contents/Resources/runtime/pnpm/addon.node',
'/App.app/Contents/Frameworks/Electron.framework/Versions/A/library.dylib',
'/App.app/Contents/Frameworks/Electron.framework',
'/App.app',
+15
View File
@@ -443,6 +443,21 @@ afterEach(async () => {
})
describe('desktop main startup', () => {
it.each([true, false])('selects the package manager before Host startup (packaged=%s)', async (packaged) => {
harness.app.isPackaged = packaged
await readyForUpdate()
const host = harness.hosts[0]!
expect(host.node).toBe(process.execPath)
expect(host.primaryRuntime).toBe(packaged
? join('desktop-test-resources', 'runtime', 'primary-runtime') : 'test-primary-runtime')
expect(host.packageManager).toMatchObject({
pnpm: packaged
? join('desktop-test-resources', 'runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs')
: 'test-pnpm',
nodeBin: packaged ? join('desktop-test-resources', 'runtime', 'bin') : join('desktop-test-app', 'scripts', 'node-bin'),
})
})
it('starts the Host with authentication popups disabled when local settings are invalid', async () => {
configureTestAuthPopup(false)
const path = join(harness.app.getPath('userData'), 'desktop', 'settings.json')
@@ -0,0 +1,120 @@
/** Desktop resource preparation publishes versions only for complete resources. */
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
const state = vi.hoisted(() => ({
root: '',
target: 'mac-arm64' as 'mac-arm64' | 'mac-x64' | 'win-x64',
pnpm: '11.7.0' as string | undefined,
failure: undefined as 'electron' | 'primary' | 'cli' | undefined,
preparePrimary: vi.fn(),
commandLink: vi.fn(),
}))
vi.mock('../scripts/desktop-build-paths.mjs', async importOriginal => ({
...await importOriginal<typeof import('../scripts/desktop-build-paths.mjs')>(),
resolveDesktopBuildTarget: () => state.target,
resolveDesktopTargetBuildPaths: () => ({
runtime: join(state.root, 'runtime'), electron: join(state.root, 'electron'), downloads: join(state.root, 'downloads'),
}),
}))
vi.mock('@electron/get', () => ({ downloadArtifact: async () => 'electron.zip' }))
vi.mock('extract-zip', () => ({ default: async () => {
if (state.failure === 'electron') throw new Error('Electron extraction failed')
} }))
vi.mock('node:child_process', async importOriginal => ({
...await importOriginal<typeof import('node:child_process')>(),
execFileSync: (file: string) => file === '/usr/libexec/PlistBuddy' ? '12.0\n' : '24.18.1\n',
}))
vi.mock('../scripts/prepare-command-link.ts', () => ({ prepareCommandLink: state.commandLink }))
// The compiled command manager is a build input; all source resource copies use real files.
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs')>()
return { ...actual, cpSync: (...args: Parameters<typeof actual.cpSync>) => {
if (String(args[0]).endsWith(join('lib', 'command-manager-entry.js'))) {
if (state.failure === 'cli') throw new Error('command manager copy failed')
actual.writeFileSync(args[1], 'command manager fixture')
} else actual.cpSync(...args)
} }
})
vi.mock('../scripts/prepare-primary-runtime.ts', () => ({
preparePrimaryRuntime: state.preparePrimary,
}))
const originalArgv = process.argv
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
state.root = mkdtempSync(join(tmpdir(), 'desktop-runtime-preparation-'))
state.target = 'mac-arm64'
state.pnpm = '11.7.0'
state.failure = undefined
process.argv = [process.execPath, 'prepare-runtime.ts']
vi.stubEnv('DSH_DESKTOP_PACKAGING_RUN_DIR', undefined)
state.preparePrimary.mockImplementation(async () => {
if (state.failure === 'primary') throw new Error('primary preparation failed')
const root = join(state.root, 'runtime', 'primary-runtime')
const pnpm = join(root, 'dependencies', 'pnpm', 'bin')
mkdirSync(pnpm, { recursive: true })
writeFileSync(join(pnpm, 'pnpm.mjs'), '')
writeFileSync(join(root, 'runtime.json'), JSON.stringify({
desktopVersion: '1.0.0', platform: state.target === 'win-x64' ? 'win32' : 'darwin',
arch: state.target === 'mac-arm64' ? 'arm64' : 'x64',
python: '3.12.14', node: '24.17.0', pnpm: state.pnpm, pythonPackages: { numpy: '2.3.5' },
}))
})
})
afterEach(async () => {
process.argv = originalArgv
vi.unstubAllEnvs()
await rm(state.root, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 })
})
it.each(['mac-arm64', 'mac-x64', 'win-x64'] as const)('prepares %s with one pnpm distribution and its CLI resources', async (target) => {
state.target = target
const windows = target === 'win-x64'
if (windows) process.argv.push('--defer-primary-runtime-smoke')
const runtime = join(state.root, 'runtime')
mkdirSync(join(runtime, 'pnpm'), { recursive: true })
await import('../scripts/prepare-runtime.ts')
expect(existsSync(join(runtime, 'pnpm'))).toBe(false)
expect(existsSync(join(runtime, 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.mjs'))).toBe(true)
expect(existsSync(join(runtime, 'bin', 'node'))).toBe(true)
expect(existsSync(join(runtime, 'cli', 'bin', windows ? 'dsh.cmd' : 'dsh'))).toBe(true)
expect(readFileSync(join(runtime, 'cli', 'command-manager.js'), 'utf8')).toBe('command manager fixture')
expect(existsSync(join(runtime, 'cli', 'command-path.ps1'))).toBe(true)
expect(JSON.parse(readFileSync(join(runtime, 'versions.json'), 'utf8'))).toEqual({
schemaVersion: 1, node: '24.18.1', pnpm: '11.7.0',
})
expect(state.preparePrimary).toHaveBeenCalledWith({ deferSmoke: windows })
if (windows) expect(state.commandLink).not.toHaveBeenCalled()
else expect(state.commandLink).toHaveBeenCalledWith(join(runtime, 'cli'), target === 'mac-arm64' ? 'arm64' : 'x64', '12.0')
})
it.each([
['electron', 'Electron extraction failed'],
['primary', 'primary preparation failed'],
['cli', 'command manager copy failed'],
] as const)('does not publish versions when %s preparation fails', async (failure, error) => {
state.failure = failure
const runtime = join(state.root, 'runtime')
mkdirSync(runtime)
writeFileSync(join(runtime, 'versions.json'), 'stale versions')
await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error)
expect(existsSync(join(runtime, 'versions.json'))).toBe(false)
})
it.each([
['not-a-version', 'primary runtime: invalid metadata'],
[undefined, 'primary-runtime manifest has no pnpm version'],
])('rejects pnpm metadata %s even when Windows smoke is deferred', async (pnpm, error) => {
state.target = 'win-x64'
process.argv.push('--defer-primary-runtime-smoke')
state.pnpm = pnpm
await expect(import('../scripts/prepare-runtime.ts')).rejects.toThrow(error)
expect(existsSync(join(state.root, 'runtime', 'versions.json'))).toBe(false)
})