diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 7d355c9801..d25e3c5ef8 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -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/desktop/README.md -README.md: 87b16ac8fd7ec269e8c02a34a32e6593b38aec0a -README.zh.md: 895fdd972148c90bad00916a35b21cebff8842e8 +README.md: ec3f2f44d5e49db00bd595588a0594bcee0a1538 +README.zh.md: 50fba340a0edc114a75d4b6363b8e6c42be0c45f diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 87b16ac8fd..ec3f2f44d5 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -399,7 +399,7 @@ The downloaded bytes are inert, and the installation call is recorded rather tha ## Low-level development overrides -An unpackaged Electron process uses `.desktop-build/development/project` under its application directory as its development project. `DSH_DESKTOP_PNPM_ENTRY` and `DSH_DESKTOP_DSH_DIR` select explicit runtime resources. Packaged applications ignore these variables, resolve signed resources from `process.resourcesPath`, and use the managed Desktop profile. +An unpackaged Electron process uses `.desktop-build/development/project` under its application directory as its development project. `DSH_DESKTOP_PNPM_ENTRY` and `DSH_DESKTOP_DSH_DIR` are optional overrides with application-path defaults. `DSH_DESKTOP_PRIMARY_RUNTIME_DIR` is required for every unpackaged launch: the development launchers (`dev:desktop`, `start:desktop`, and the workspace-update qualification runner) set it to the primary-runtime directory of the target they prepared, and a launch without it fails with the fatal startup dialog. The launcher must set it because the shell cannot derive that directory from `process.arch`: the build target fixes Windows to x64 while the host may be arm64. Packaged applications ignore these variables, resolve signed resources from `process.resourcesPath`, and use the managed Desktop profile. ## Known limitations @@ -408,6 +408,7 @@ An unpackaged Electron process uses `.desktop-build/development/project` under i - Release signing, notarization, update hosting, and previous-version installed-artifact qualification require the production release environment. - Dependency lifecycle scripts follow pnpm’s build permissions; Desktop provides no separate approval dialog. - The desktop shell shares sessions, settings, credentials, workspaces, and storage under `$DSH_HOME` with CLI dsh, while executable packages, plugin activation, and lockfiles remain separate. +- Unpackaged startup on an Electron win32-arm64 host now succeeds, but the payload remains x64: the architecture check in `packages/skill/tool-workspace-dependencies/src/index.ts` compares the recorded payload architecture against the host `process.arch`, so the `load_workspace_dependencies` tool can still reject the primary runtime. Sign in opens the configured platform page in the system browser. The Host owns PKCE and a temporary loopback callback, saves the credential before entering the workspace, and redirects the browser to the platform completion page. Opened and copied authorization links carry the effective Desktop theme as `theme=light` or `theme=dark`; `system` resolves at the time of the action. Cancel withdraws the local attempt even if the platform page later approves it. Settings offers Account sign-out; without a separate API key, sign-out returns to the welcome window. Packaged applications register dsh://open to show the window without passing credentials. On macOS, the development launcher prepares an ad-hoc-signed `Harness Dev.app` under `.desktop-build/development`, declares `dsh` in its Info.plist, and registers it with Launch Services. It loads the current workspace and records the selected development home, browser-data path, and debug settings for cold starts. Starting this bundle makes it the default `dsh://` handler; starting the packaged application registers the packaged handler again. The generated bundle does not contain account tokens and requires the workspace and prepared runtime to remain available. diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 895fdd9721..50fba340a0 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -399,7 +399,7 @@ node apps/desktop/node_modules/pnpm/bin/pnpm.mjs --dir apps/desktop run test:upd ## 底层开发覆盖项 -未打包的 Electron 进程使用应用目录下的 `.desktop-build/development/project` 作为开发项目。`DSH_DESKTOP_PNPM_ENTRY` 和 `DSH_DESKTOP_DSH_DIR` 用于选择明确的运行时资源。打包应用会忽略这些变量,从 `process.resourcesPath` 解析签名资源,并使用受管 Desktop profile。 +未打包的 Electron 进程使用应用目录下的 `.desktop-build/development/project` 作为开发项目。`DSH_DESKTOP_PNPM_ENTRY` 和 `DSH_DESKTOP_DSH_DIR` 是带应用路径默认值的可选覆盖项。每次未打包启动都必须设置 `DSH_DESKTOP_PRIMARY_RUNTIME_DIR`:开发启动器(`dev:desktop`、`start:desktop` 及工作区更新验证运行器)会把它设置为自己已准备目标的 primary-runtime 目录;缺少该变量的启动会以致命启动对话框失败。启动器必须设置它,因为壳无法从 `process.arch` 推导该目录:构建目标将 Windows 固定为 x64,而宿主可能是 arm64。打包应用会忽略这些变量,从 `process.resourcesPath` 解析签名资源,并使用受管 Desktop profile。 ## 已知限制 @@ -408,6 +408,7 @@ node apps/desktop/node_modules/pnpm/bin/pnpm.mjs --dir apps/desktop run test:upd - 发布签名、公证、更新托管和跨上一版本的已安装产物验证需要生产发布环境。 - 依赖的生命周期脚本遵循 pnpm 的构建权限;Desktop 不提供单独的审批对话框。 - 桌面壳与 CLI dsh 共享 `$DSH_HOME` 下的会话、设置、凭据、工作区和存储,但可执行包、插件激活和锁文件彼此隔离。 +- 在 Electron win32-arm64 宿主上,未打包启动现在可以成功,但载荷仍为 x64:`packages/skill/tool-workspace-dependencies/src/index.ts` 的架构校验会把载荷记录的架构与宿主 `process.arch` 比较,因此 `load_workspace_dependencies` 工具仍可能拒绝 primary runtime。 登录会在系统浏览器中打开配置的平台页面。Host 负责 PKCE 和临时本机回调,在进入工作区前保存凭证,再将浏览器跳转到平台完成页。打开和复制的授权链接通过 `theme=light` 或 `theme=dark` 携带当前生效的 Desktop 主题;`system` 在执行操作时解析。即使平台页面随后批准,取消仍会撤销本地尝试。设置中的账号页面提供退出;没有独立 API Key 时,退出后返回欢迎窗。打包应用注册 dsh://open,只显示窗口而不传递凭证。macOS 开发启动器在 `.desktop-build/development` 下准备经临时签名的 `Harness Dev.app`,在 Info.plist 中声明 `dsh` 并注册到 Launch Services。它加载当前工作区,并记录选定的开发 home、浏览器数据路径和调试设置,以供冷启动使用。启动此应用会将其设为 `dsh://` 默认处理程序;启动打包应用会重新注册打包版处理程序。生成的应用包不包含账号 token,依赖工作区和已准备的运行环境继续存在。 diff --git a/apps/desktop/scripts/desktop-build-paths.d.mts b/apps/desktop/scripts/desktop-build-paths.d.mts index b26c633d9c..9a8e408ed1 100644 --- a/apps/desktop/scripts/desktop-build-paths.d.mts +++ b/apps/desktop/scripts/desktop-build-paths.d.mts @@ -36,6 +36,17 @@ export function resolveDesktopBuildTarget( */ export function desktopTargetBuildPaths(target: DesktopAutoUpdateTarget): DesktopTargetBuildPaths +/** + * Return the platform and architecture of the payload one release target prepares. + * Windows is prepared as x64 only, so this differs from the build host on an arm64 Windows machine. + * @param target - Supported Desktop target name. + * @returns Platform and architecture of the prepared payload. + */ +export function desktopTargetPlatform(target: DesktopAutoUpdateTarget): { + readonly platform: 'darwin' | 'win32' + readonly arch: 'arm64' | 'x64' +} + /** * Resolve the paths owned by the target selected in a packaging environment. * @param env - Packaging environment. @@ -48,3 +59,16 @@ export function resolveDesktopTargetBuildPaths( hostPlatform?: NodeJS.Platform, hostArch?: string, ): DesktopTargetBuildPaths + +/** + * Resolve the primary-runtime directory an unpackaged development launch uses. + * @param env - Packaging environment. + * @param hostPlatform - Build-host platform used when no target override exists. + * @param hostArch - Build-host architecture used when no target override exists. + * @returns Primary-runtime directory prepared for the selected target. + */ +export function developmentRuntimeDirectory( + env?: NodeJS.ProcessEnv, + hostPlatform?: NodeJS.Platform, + hostArch?: string, +): string diff --git a/apps/desktop/scripts/desktop-build-paths.mjs b/apps/desktop/scripts/desktop-build-paths.mjs index 841da2c4ba..4f33e804c3 100644 --- a/apps/desktop/scripts/desktop-build-paths.mjs +++ b/apps/desktop/scripts/desktop-build-paths.mjs @@ -29,15 +29,19 @@ export function resolveDesktopBuildTarget( return /** @type {'mac-arm64' | 'mac-x64' | 'win-x64'} */ (target) } +function assertSupportedTarget(target) { + if (!SUPPORTED_TARGETS.has(target)) { + throw new Error(`desktop build paths: unsupported target ${String(target)}`) + } +} + /** * Return the mutable preparation and artifact directories owned by one release target. * @param {'mac-arm64' | 'mac-x64' | 'win-x64'} target - Supported Desktop target name. * @returns {{ root: string, artifacts: string, unsignedArtifacts: string, runtime: string, packageSet: string, dsh: string, dshPnpm: string, electron: string, packedDsh: string, packedVendor: string, packedLandlock: string, downloads: string }} Target paths plus the shared immutable download cache. */ export function desktopTargetBuildPaths(target) { - if (!SUPPORTED_TARGETS.has(target)) { - throw new Error(`desktop build paths: unsupported target ${String(target)}`) - } + assertSupportedTarget(target) const root = join(BUILD_ROOT, 'targets', target) const packed = join(root, 'packed') return { @@ -56,6 +60,20 @@ export function desktopTargetBuildPaths(target) { } } +/** + * Return the platform and architecture of the payload one release target prepares. + * Windows is prepared as x64 only, so this differs from the build host on an arm64 Windows machine. + * @param {'mac-arm64' | 'mac-x64' | 'win-x64'} target - Supported Desktop target name. + * @returns {{ platform: 'darwin' | 'win32', arch: 'arm64' | 'x64' }} Platform and architecture of the prepared payload. + */ +export function desktopTargetPlatform(target) { + assertSupportedTarget(target) + return { + platform: /** @type {'darwin' | 'win32'} */ (target === 'win-x64' ? 'win32' : 'darwin'), + arch: /** @type {'arm64' | 'x64'} */ (target === 'mac-arm64' ? 'arm64' : 'x64'), + } +} + /** * Resolve the paths owned by the target selected in a packaging environment. * @param {NodeJS.ProcessEnv} env - Packaging environment. @@ -70,3 +88,20 @@ export function resolveDesktopTargetBuildPaths( ) { return desktopTargetBuildPaths(resolveDesktopBuildTarget(env, hostPlatform, hostArch)) } + +/** + * Resolve the primary-runtime directory an unpackaged development launch uses. + * The build target fixes Windows to x64, so the shell cannot derive this directory from + * the architecture of the process that launched it. + * @param {NodeJS.ProcessEnv} env - Packaging environment. + * @param {NodeJS.Platform} hostPlatform - Build-host platform used when no target override exists. + * @param {string} hostArch - Build-host architecture used when no target override exists. + * @returns {string} Primary-runtime directory prepared for the selected target. + */ +export function developmentRuntimeDirectory( + env = process.env, + hostPlatform = process.platform, + hostArch = process.arch, +) { + return join(resolveDesktopTargetBuildPaths(env, hostPlatform, hostArch).runtime, 'primary-runtime') +} diff --git a/apps/desktop/scripts/dev.ts b/apps/desktop/scripts/dev.ts index 74840eae03..fdd724e579 100644 --- a/apps/desktop/scripts/dev.ts +++ b/apps/desktop/scripts/dev.ts @@ -7,6 +7,7 @@ import { join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts' import type { DesktopRelease } from '../src/release.ts' +import { developmentRuntimeDirectory, resolveDesktopBuildTarget } from './desktop-build-paths.mjs' import { prepareDevelopmentProject } from './development-project.ts' import { prepareDevelopmentApp } from './development-app.ts' import { preparePrimaryRuntime } from './prepare-primary-runtime.ts' @@ -67,6 +68,7 @@ async function launchElectron(): Promise { const environment: NodeJS.ProcessEnv = { ...process.env, DSH_HOME: home, + DSH_DESKTOP_PRIMARY_RUNTIME_DIR: process.env.DSH_DESKTOP_PRIMARY_RUNTIME_DIR ?? developmentRuntimeDirectory(), DSH_DESKTOP_HOST_INSPECT_PORT: String(hostPort), DSH_DESKTOP_OPEN_DEVTOOLS: process.env.DSH_DESKTOP_OPEN_DEVTOOLS ?? '1', ELECTRON_ENABLE_LOGGING: process.env.ELECTRON_ENABLE_LOGGING ?? '1', @@ -115,6 +117,7 @@ async function main(): Promise { hostDir: join(REPOSITORY_ROOT, 'apps', 'desktop-host'), dependencyDir: join(REPOSITORY_ROOT, 'node_modules', '.pnpm', 'node_modules'), release, + target: resolveDesktopBuildTarget(), }) await preparePrimaryRuntime() await launchElectron() diff --git a/apps/desktop/scripts/development-project.ts b/apps/desktop/scripts/development-project.ts index 36c70baff0..d544ce6d36 100644 --- a/apps/desktop/scripts/development-project.ts +++ b/apps/desktop/scripts/development-project.ts @@ -16,6 +16,8 @@ import { dirname, join } from 'node:path' import { createDevelopmentProjectMetadata } from '../src/project-manager.ts' import type { DesktopRelease } from '../src/release.ts' import { DESKTOP_RUNTIME_FILE, type DesktopRuntimeDescriptor } from '../src/runtime-tree.ts' +import type { DesktopAutoUpdateTarget } from './desktop-auto-update-environment.mjs' +import { desktopTargetPlatform } from './desktop-build-paths.mjs' interface PackageManifest { readonly name?: string @@ -35,6 +37,8 @@ export interface DevelopmentProjectOptions { readonly dependencyDir: string /** Release identity written into the disposable project metadata. */ readonly release: DesktopRelease + /** Build target whose prepared payload the disposable project runs against. */ + readonly target: DesktopAutoUpdateTarget } function readManifest(path: string): PackageManifest { @@ -115,7 +119,7 @@ function mirrorWorkspaceDependencies(roots: readonly string[], destinationRoot: /** * Replace one disposable project with links to the current built workspace. - * @param options - Project destination, CLI package, and release identity. + * @param options - Project destination, CLI package, release identity, and build target. * @returns the absolute project directory supplied by the caller. */ export function prepareDevelopmentProject(options: DevelopmentProjectOptions): string { @@ -159,7 +163,7 @@ export function prepareDevelopmentProject(options: DevelopmentProjectOptions): s return typeof manifest.version === 'string' ? [{ name, version: manifest.version, path: `node_modules/${name}` }] : [] }) const runtime: DesktopRuntimeDescriptor = { schemaVersion: 1, release: options.release, - platform: process.platform, arch: process.arch, sharedPackages, files: [] } + ...desktopTargetPlatform(options.target), sharedPackages, files: [] } writeFileSync(join(options.projectDir, DESKTOP_RUNTIME_FILE), `${JSON.stringify(runtime, undefined, 2)}\n`) return options.projectDir } diff --git a/apps/desktop/scripts/test-workspace-updates.ts b/apps/desktop/scripts/test-workspace-updates.ts index 669d572b9d..41f40caa09 100644 --- a/apps/desktop/scripts/test-workspace-updates.ts +++ b/apps/desktop/scripts/test-workspace-updates.ts @@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url' import { createDevelopmentProjectMetadata, createPluginProfile } from '../src/project-manager.ts' import { removeOwnedDirectory } from '../src/owned-directory.ts' import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts' +import { desktopTargetPlatform, developmentRuntimeDirectory, resolveDesktopBuildTarget } from './desktop-build-paths.mjs' const repo = resolve(import.meta.dirname, '../../..') const interactive = process.argv.includes('--interactive') @@ -27,10 +28,11 @@ try { nodeVersion: execFileSync(electron, ['-p', 'process.versions.node'], { encoding: 'utf8', env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' }, windowsHide: true }).trim(), pnpmVersion: pnpm.version, hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION } + const target = resolveDesktopBuildTarget() createDevelopmentProjectMetadata(project, release) createPluginProfile(profile) await writeFile(join(project, 'desktop-runtime.json'), JSON.stringify({ - schemaVersion: 1, release, platform: process.platform, arch: process.arch, files: [], + schemaVersion: 1, release, ...desktopTargetPlatform(target), files: [], sharedPackages: ['@deepseek-ai/dsh', '@deepseek-ai/dsh-desktop-host'] .map(name => ({ name, version: manifest.version, path: `node_modules/${name}` })), })) @@ -43,7 +45,6 @@ try { for (const owner of [application, project]) { await symlink(join(repo, 'node_modules/.pnpm/node_modules'), join(owner, 'node_modules'), process.platform === 'win32' ? 'junction' : 'dir') } - const target = `${process.platform === 'darwin' ? 'mac' : 'win'}-${process.arch}` const targetRoot = join(application, '.desktop-build/targets', target) await mkdir(targetRoot, { recursive: true }) await symlink(join(repo, 'apps/desktop/.desktop-build/targets', target, 'runtime'), join(targetRoot, 'runtime'), @@ -61,7 +62,7 @@ try { ...(interactive ? ['--interactive'] : [])], { cwd: root, env: { ...environment, DSH_HOME: join(root, 'home'), USERPROFILE: root, HOME: root, TEMP: root, TMP: root, TMPDIR: root, DSH_WORKSPACE_UPDATE_ROOT: root, DSH_WORKSPACE_UPDATE_TOKEN: randomUUID(), - DSH_DESKTOP_OPEN_DEVTOOLS: '0' }, + DSH_DESKTOP_PRIMARY_RUNTIME_DIR: developmentRuntimeDirectory(), DSH_DESKTOP_OPEN_DEVTOOLS: '0' }, // Hiding the GUI process suppresses its first window and can suspend renderer frame callbacks. stdio: 'inherit', windowsHide: false, }) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 9f8ad5c22f..6690a3c69c 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -139,6 +139,14 @@ function runtimeResources(): RuntimeResources { return { node, nodeBin, pnpm, dsh } } +function developmentPrimaryRuntime(): string { + const directory = process.env.DSH_DESKTOP_PRIMARY_RUNTIME_DIR + if (directory === undefined || directory === '') { + throw new Error('dsh desktop: DSH_DESKTOP_PRIMARY_RUNTIME_DIR is required for an unpackaged launch') + } + return directory +} + function developmentHostInspectPort(enabled: boolean): number | undefined { const configured = process.env.DSH_DESKTOP_HOST_INSPECT_PORT if (!enabled || configured === undefined || configured === '') return undefined @@ -289,6 +297,9 @@ async function main(): Promise { const resources = runtimeResources() const paths = resolveDesktopPaths() const development = !app.isPackaged + const primaryRuntime = development + ? developmentPrimaryRuntime() + : join(process.resourcesPath, 'runtime', 'primary-runtime') const activeProject = paths.profile const manager = new DesktopProjectManager(paths, resources) let quitting = false @@ -370,8 +381,7 @@ async function main(): Promise { const hostInspectPort = developmentHostInspectPort(development) const host = new DesktopHostProcess(resources.node, resources.dsh, activeProject, hostInspectPort, process.env, onFailure, - development ? join(app.getAppPath(), '.desktop-build', 'targets', `${process.platform === 'darwin' ? 'mac' : 'win'}-${process.arch}`, 'runtime', 'primary-runtime') - : join(process.resourcesPath, 'runtime', 'primary-runtime'), + primaryRuntime, resources, (next) => { platformView.setSession(next) }) return { start: async () => { diff --git a/apps/desktop/tests/desktop-build-paths.spec.ts b/apps/desktop/tests/desktop-build-paths.spec.ts index a163885f6d..a5e9faf296 100644 --- a/apps/desktop/tests/desktop-build-paths.spec.ts +++ b/apps/desktop/tests/desktop-build-paths.spec.ts @@ -2,6 +2,8 @@ import { join, sep } from 'node:path' import { describe, expect, it } from 'vitest' import { desktopTargetBuildPaths, + desktopTargetPlatform, + developmentRuntimeDirectory, resolveDesktopBuildTarget, } from '../scripts/desktop-build-paths.mjs' @@ -39,6 +41,22 @@ describe('desktop build paths', () => { expect(arm64.downloads).not.toContain(`${sep}targets${sep}`) }) + it('resolves the development primary runtime from the build target rather than the host architecture', () => { + expect(developmentRuntimeDirectory({}, 'darwin', 'arm64')) + .toContain(join('targets', 'mac-arm64', 'runtime', 'primary-runtime')) + expect(developmentRuntimeDirectory({}, 'darwin', 'x64')) + .toContain(join('targets', 'mac-x64', 'runtime', 'primary-runtime')) + expect(developmentRuntimeDirectory({}, 'win32', 'arm64')) + .toContain(join('targets', 'win-x64', 'runtime', 'primary-runtime')) + }) + + it('maps every target to the platform and architecture of the payload it prepares', () => { + expect(desktopTargetPlatform('mac-arm64')).toEqual({ platform: 'darwin', arch: 'arm64' }) + expect(desktopTargetPlatform('mac-x64')).toEqual({ platform: 'darwin', arch: 'x64' }) + expect(desktopTargetPlatform('win-x64')).toEqual({ platform: 'win32', arch: 'x64' }) + expect(() => desktopTargetPlatform('linux-x64' as 'mac-x64')).toThrow(/unsupported target/u) + }) + it('resolves environment overrides and rejects unsupported targets', () => { expect(resolveDesktopBuildTarget({ DSH_DESKTOP_TARGET_PLATFORM: 'darwin', diff --git a/apps/desktop/tests/development-project.spec.ts b/apps/desktop/tests/development-project.spec.ts index 07e0a0a5ca..d08b82d3ac 100644 --- a/apps/desktop/tests/development-project.spec.ts +++ b/apps/desktop/tests/development-project.spec.ts @@ -46,9 +46,10 @@ describe('desktop development project', () => { writeFileSync(join(host, 'lib/index.js'), '') writeFileSync(join(dependency, 'package.json'), JSON.stringify({ name: 'unhoisted', version: '1.2.3' })) symlinkSync(dependency, join(cli, 'node_modules/unhoisted'), process.platform === 'win32' ? 'junction' : 'dir') - const project = prepareDevelopmentProject({ projectDir: join(root, 'runtime'), cliDir: cli, hostDir: host, dependencyDir: hoisted, release: release() }) + const project = prepareDevelopmentProject({ projectDir: join(root, 'runtime'), cliDir: cli, hostDir: host, dependencyDir: hoisted, release: release(), target: 'mac-arm64' }) expect(realpathSync(join(project, 'node_modules/unhoisted'))).toBe(realpathSync(dependency)) - const descriptor = JSON.parse(readFileSync(join(project, 'desktop-runtime.json'), 'utf8')) as { sharedPackages: unknown[] } + const descriptor = JSON.parse(readFileSync(join(project, 'desktop-runtime.json'), 'utf8')) as { platform: string; arch: string; sharedPackages: unknown[] } + expect(descriptor).toMatchObject({ platform: 'darwin', arch: 'arm64' }) expect(descriptor.sharedPackages).toContainEqual({ name: 'unhoisted', version: '1.2.3', path: 'node_modules/unhoisted' }) }) @@ -76,6 +77,7 @@ describe('desktop development project', () => { hostDir: host, dependencyDir: dependencies, release: release(), + target: 'win-x64', }) expect(realpathSync(join(project, 'node_modules', '@deepseek-ai', 'dsh'))).toBe(realpathSync(cli)) expect(realpathSync(join(project, 'node_modules', '@deepseek-ai', 'dsh-desktop-host'))).toBe(realpathSync(host)) @@ -88,6 +90,8 @@ describe('desktop development project', () => { } expect(manifest.dependencies['@deepseek-ai/dsh']).toBe('1.2.3') expect(manifest.dependencies['@deepseek-ai/dsh-desktop-host']).toBe('1.2.3') + const descriptor = JSON.parse(readFileSync(join(project, 'desktop-runtime.json'), 'utf8')) as { platform: string; arch: string } + expect(descriptor).toMatchObject({ platform: 'win32', arch: 'x64' }) const manager = new DesktopProjectManager(resolveDesktopPaths(join(root, 'home')), { dsh: project, }) @@ -115,6 +119,7 @@ describe('desktop development project', () => { hostDir: host, dependencyDir: dependencies, release: release(), + target: 'mac-x64', })).toThrow(/must be @deepseek-ai\/dsh@1\.2\.3/u) }) }) diff --git a/apps/desktop/tests/main-startup.spec.ts b/apps/desktop/tests/main-startup.spec.ts index 0242d214a3..9cd00f74c4 100644 --- a/apps/desktop/tests/main-startup.spec.ts +++ b/apps/desktop/tests/main-startup.spec.ts @@ -333,6 +333,7 @@ beforeEach(() => { vi.spyOn(console, 'info').mockImplementation(() => {}) vi.stubEnv('DSH_DESKTOP_PNPM_ENTRY', 'test-pnpm') vi.stubEnv('DSH_DESKTOP_DSH_DIR', 'test-runtime') + vi.stubEnv('DSH_DESKTOP_PRIMARY_RUNTIME_DIR', 'test-primary-runtime') vi.stubGlobal('process', { ...process, platform: 'win32', arch: 'x64', resourcesPath: 'desktop-test-resources' }) vi.stubEnv('DSH_DESKTOP_HOST_INSPECT_PORT', undefined) vi.stubEnv('DSH_DESKTOP_DEV_PROJECT_DIR', undefined) @@ -1621,13 +1622,24 @@ describe('desktop main startup', () => { harness.prepared.resolve() await harness.hostStarted.promise const project = join(harness.app.getAppPath(), '.desktop-build', 'development', 'project') - expect(harness.hosts[0]).toMatchObject({ node: process.execPath, runtime: project, profile: 'desktop-test-profile' }) + expect(harness.hosts[0]).toMatchObject({ node: process.execPath, runtime: project, + primaryRuntime: 'test-primary-runtime', profile: 'desktop-test-profile' }) expect(harness.applyRelease).toHaveBeenCalledOnce() harness.hosts[0]!.ready.resolve() await harness.navigated.promise expect(harness.dialog.showErrorBox).not.toHaveBeenCalled() }) + it('fails an unpackaged launch that receives no primary runtime directory', async () => { + harness.app.isPackaged = false + vi.stubEnv('DSH_DESKTOP_PRIMARY_RUNTIME_DIR', undefined) + await import('../src/main.ts') + await harness.dialogShown.promise + const options = harness.dialog.showMessageBox.mock.calls[0]![0] as MessageBoxOptions + expect(options.detail).toContain('DSH_DESKTOP_PRIMARY_RUNTIME_DIR is required for an unpackaged launch') + expect(harness.hosts).toHaveLength(0) + }) + it('keeps startup errors in the existing window without a retry handler', async () => { await import('../src/main.ts') await harness.preparing.promise diff --git a/apps/desktop/tests/welcome-flow.e2e.ts b/apps/desktop/tests/welcome-flow.e2e.ts index 7522430879..9b3dd0f2ed 100644 --- a/apps/desktop/tests/welcome-flow.e2e.ts +++ b/apps/desktop/tests/welcome-flow.e2e.ts @@ -98,6 +98,9 @@ describe.skipIf(!existsSync(builtHost))('built Desktop welcome flow', () => { nodeVersion: process.versions.node, hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION, }, + // This suite assembles a synthetic project on hosts that prepare no Desktop target, and + // nothing it exercises compares the descriptor's platform/arch outside packaging. + target: 'mac-x64', }) cpSync(join(repository, 'packages/skill/skill-office/assets'), join(root, 'runtime/office-skills'), { recursive: true }) const nodeBin = join(root, 'runtime/primary-runtime/dependencies/node/bin') diff --git a/apps/desktop/tests/welcome-startup.spec.ts b/apps/desktop/tests/welcome-startup.spec.ts index 4a823afdf9..146b2297eb 100644 --- a/apps/desktop/tests/welcome-startup.spec.ts +++ b/apps/desktop/tests/welcome-startup.spec.ts @@ -148,6 +148,7 @@ it('starts the Host for welcome onboarding and opens the workspace on skip witho vi.stubEnv('DSH_DESKTOP_NODE_BINARY', '/runtime/node') vi.stubEnv('DSH_DESKTOP_PNPM_ENTRY', '/runtime/pnpm') vi.stubEnv('DSH_DESKTOP_DSH_DIR', '/runtime/dsh') + vi.stubEnv('DSH_DESKTOP_PRIMARY_RUNTIME_DIR', '/runtime/primary-runtime') vi.stubEnv('DSH_DESKTOP_HOST_INSPECT_PORT', undefined) vi.stubEnv('DSH_DESKTOP_OPEN_DEVTOOLS', '0') vi.stubEnv('DSH_DESKTOP_MANDATORY_UPDATE_CONFIG', undefined)