From 638842895a18ee14e9b3a3c4ccab91f77a266180 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:00:53 +0800 Subject: [PATCH 1/6] docs(desktop): describe keyboard typecheck ownership --- apps/desktop/README.i18n.yaml | 4 ++-- apps/desktop/README.md | 2 +- apps/desktop/README.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/README.i18n.yaml b/apps/desktop/README.i18n.yaml index 039d4701b1..00a0668e87 100644 --- a/apps/desktop/README.i18n.yaml +++ b/apps/desktop/README.i18n.yaml @@ -21,8 +21,8 @@ en: 0d11af8639977a6d zh: 86b086b12d2210d3 /deepseek-harness-desktop/develop: - en: 9b9b362a9865e763 - zh: c748901168e2db8c + en: 40d2ad4014bf445b + zh: 6a08de06df9ed042 /deepseek-harness-desktop/develop/startup-onboarding: en: 5e2d62207a603d81 zh: 5fb863d4906f4a67 diff --git a/apps/desktop/README.md b/apps/desktop/README.md index 0f579c249b..f1de0eba4c 100644 --- a/apps/desktop/README.md +++ b/apps/desktop/README.md @@ -130,7 +130,7 @@ pnpm run start:desktop The Web counterparts are `pnpm run dev:web` and `pnpm run start:web`, documented in the [development guide](../../docs/development.md). Workspace development runs the current CLI and private Desktop Host packages under Electron RunAsNode. Plugin management and recovery use `$DSH_HOME/profiles/desktop`, separate from the disposable workspace runtime. The Host uses runtime module resolution in both development and packaged builds without creating official-package fallback links; developer-installed packages, including links, retain native priority. Use an unpacked application to exercise Electron RunAsNode, bundled pnpm, bundled dsh resources, plugin installation and repair paths. -The [native/renderer keyboard tests](tests/keyboard.spec.ts) compile in [their own Client test project](../../tsconfig.desktop-keyboard-tests.json), reached by the repository Client typecheck. Its Desktop imports are limited to Cordis-free input, persistence, IPC, browser-guest, and overlay modules. +The [native/renderer keyboard tests](tests/keyboard.spec.ts) compile as part of the repository Client typecheck. Their Desktop imports are limited to Cordis-free input, persistence, IPC, browser-guest, and overlay modules. ### Startup onboarding diff --git a/apps/desktop/README.zh.md b/apps/desktop/README.zh.md index 62e34fad48..c2f7ffbcfb 100644 --- a/apps/desktop/README.zh.md +++ b/apps/desktop/README.zh.md @@ -132,7 +132,7 @@ pnpm run start:desktop Web 侧的对应命令是 `pnpm run dev:web` 与 `pnpm run start:web`,见[开发指南](../../docs/development.zh.md)。Workspace 开发使用 Electron RunAsNode 运行当前 CLI 与私有 Desktop Host 包,插件管理和恢复使用 `$DSH_HOME/profiles/desktop`,与一次性工作区运行时分离。Host 在开发与打包构建中都使用 runtime 模块解析,不创建官方包的 fallback 链接;开发者安装的包(包括链接)保留原生优先级。需要验证 Electron RunAsNode、内置 pnpm、内置 dsh 资源、插件安装和修复时,应运行未封装安装器的应用目录。 -[原生输入与渲染进程键盘测试](tests/keyboard.spec.ts)在[独立的 Client 测试项目](../../tsconfig.desktop-keyboard-tests.json)中编译,由仓库 Client 类型检查纳入。它只导入不依赖 Cordis 的 Desktop 输入、持久化、IPC、浏览器 guest 和蒙层模块。 +[原生输入与渲染进程键盘测试](tests/keyboard.spec.ts)直接纳入仓库 Client 类型检查。它只导入不依赖 Cordis 的 Desktop 输入、持久化、IPC、浏览器 guest 和蒙层模块。 ### 启动引导 From a08472e9828922406ea9b6c6128a86a6e248720f Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:01:08 +0800 Subject: [PATCH 2/6] fix(build): fold desktop keyboard tests into client typecheck --- tsconfig.client.json | 9 ++++++++- tsconfig.desktop-keyboard-tests.json | 24 ------------------------ 2 files changed, 8 insertions(+), 25 deletions(-) delete mode 100644 tsconfig.desktop-keyboard-tests.json diff --git a/tsconfig.client.json b/tsconfig.client.json index 0471bfa13b..2e27307ddb 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -14,6 +14,13 @@ "types": ["node"] }, "include": [ + // The native/renderer keyboard fixture imports only these Cordis-free Desktop modules. + "apps/desktop/tests/keyboard.spec.ts", + "apps/desktop/src/keyboard.ts", + "apps/desktop/src/keybindings.ts", + "apps/desktop/src/browser-guests.ts", + "apps/desktop/src/ipc.ts", + "apps/desktop/src/update-overlay.ts", "apps/desktop/tests/**/*.client.spec.tsx", // Source-subpath test imports can arrive through workspace links whose // realpath semantics vary by host. Load package CSS declarations directly. @@ -43,8 +50,8 @@ "packages/client/*/tests/**/*.host.spec.ts" ], "references": [ - { "path": "./tsconfig.desktop-keyboard-tests.json" }, { "path": "./apps/desktop/tsconfig.client.json" }, + { "path": "./packages/util/atomic-write" }, { "path": "./packages/experimental/client-ui-voice-input" }, { "path": "./packages/client/ui-sidebar-terminal" }, diff --git a/tsconfig.desktop-keyboard-tests.json b/tsconfig.desktop-keyboard-tests.json deleted file mode 100644 index 28cdd1bf9c..0000000000 --- a/tsconfig.desktop-keyboard-tests.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - // The native/renderer integration fixture imports only Cordis-free Desktop modules. - "extends": "./tsconfig.base.client.json", - "compilerOptions": { - "rootDir": ".", - "outDir": "lib/desktop-keyboard-test-types", - "emitDeclarationOnly": true, - "types": ["node"] - }, - "files": [ - "apps/desktop/tests/keyboard.spec.ts", - "apps/desktop/src/keyboard.ts", - "apps/desktop/src/keybindings.ts", - "apps/desktop/src/browser-guests.ts", - "apps/desktop/src/ipc.ts", - "apps/desktop/src/update-overlay.ts" - ], - "include": [], - "references": [ - { "path": "./packages/client/shortcuts/tsconfig.client.json" }, - { "path": "./packages/client/ui-sidebar-browser/tsconfig.client.json" }, - { "path": "./packages/util/atomic-write" } - ] -} From 6e28f315e783541bd02147e2ebc356c4706a85ff Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:43:46 +0800 Subject: [PATCH 3/6] fix(ci): derive the Lefthook fixture lock path from Git's top level The lock-access cases injected a filesystem failure on a path this spec predicted from the mkdtemp fixture directory. The installer resolves its common directory against Git's own top-level path instead, and the host spells that path differently: Git canonicalizes Windows 8.3 short names and the macOS /var symlink. When the spellings diverged the injection never fired, so the probe file was never written and all five cases failed with ENOENT at the probe read - the signature seen in the Windows coverage lane. Resolving the relative common directory against Git's top-level, as the installer does, makes the expected path identical to the one the installer touches on every platform. The five cases fail deterministically on macOS before this change and pass after it. --- scripts/install-lefthook.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index c4e9af44da..6fc42931ca 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -158,9 +158,16 @@ function gitDirectory(fixture: Fixture, root: string): string { return git(fixture, root, ['rev-parse', '--absolute-git-dir']) } +// The installer resolves the common directory against Git's own top-level path, and the host +// spells that path differently from the fixture directory this file created: Git canonicalizes +// Windows 8.3 short names and the macOS `/var` symlink, while `mkdtempSync` returns the temp +// directory's own spelling. Resolving the relative common directory against Git's top-level +// instead of the fixture directory gives the same string the installer computes, so an injected +// failure on this path reaches the installer's own lock instead of missing it. function commonDirectory(fixture: Fixture): string { + const root = git(fixture, fixture.main, ['rev-parse', '--show-toplevel']) const output = git(fixture, fixture.main, ['rev-parse', '--git-common-dir']) - return isAbsolute(output) ? output : resolve(fixture.main, output) + return isAbsolute(output) ? output : resolve(root, output) } function hooksPath(fixture: Fixture, root: string): string { From acb66b4c237efe4b7eb30df9225f10f367758256 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:50:02 +0800 Subject: [PATCH 4/6] test(ci): force the Lefthook lock-path alias on every host The five lock-access cases only diverged on a host whose temp directory already carried a short name or directory alias. Route one case through an aliased worktree root (a junction on Windows, a directory symlink elsewhere) so the installer's resolved common directory necessarily differs from the path this spec composes; the case fails with the same probe ENOENT under the previous derivation on every platform. The alias case shares the injection body through expectInjectedLockAccessFailure, so the five existing case names and their assertions are unchanged. --- scripts/install-lefthook.spec.ts | 35 ++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index 6fc42931ca..6a95b2861a 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -328,14 +328,11 @@ describe('worktree-local Lefthook installer', { timeout: 90_000 }, () => { expect(existsSync(join(hooksPath(fixture, fixture.main), '.fake-lefthook-running'))).toBe(false) }) - it.each([ - { operation: 'openSync', code: 'EPERM', expires: false }, - { operation: 'readFileSync', code: 'EPERM', expires: false }, - { operation: 'lstatSync', code: 'EPERM', expires: false }, - { operation: 'openSync', code: 'EPERM', expires: true }, - { operation: 'openSync', code: 'EACCES', expires: false }, - ])('handles $operation $code with expired deadline=$expires', async ({ operation, code, expires }) => { - const fixture = createFixture() + /** Inject one lock-access failure and assert the installer's documented outcome. */ + async function expectInjectedLockAccessFailure( + fixture: Fixture, + { operation, code, expires }: { operation: string; code: string; expires: boolean }, + ): Promise { const lockPath = installLockPath(fixture) const probe = join(fixture.container, 'lock-access-probe') const preload = join(fixture.container, 'lock-access.cjs') @@ -376,6 +373,28 @@ syncBuiltinESMExports() expect(result.stderr).toContain('injected lock access failure') expect(existsSync(hooksPath(fixture, fixture.main))).toBe(false) } + } + + it.each([ + { operation: 'openSync', code: 'EPERM', expires: false }, + { operation: 'readFileSync', code: 'EPERM', expires: false }, + { operation: 'lstatSync', code: 'EPERM', expires: false }, + { operation: 'openSync', code: 'EPERM', expires: true }, + { operation: 'openSync', code: 'EACCES', expires: false }, + ])('handles $operation $code with expired deadline=$expires', async ({ operation, code, expires }) => { + await expectInjectedLockAccessFailure(createFixture(), { operation, code, expires }) + }) + + // The alias forces the installer's resolved common directory to differ from the path this spec + // composes on every host — a junction on Windows, a directory symlink elsewhere — instead of + // depending on a runner whose temp directory happens to carry a short name. + it('handles an injected lock failure through an aliased worktree root', async () => { + const fixture = createFixture() + const alias = join(fixture.container, 'main-alias') + symlinkSync(fixture.main, alias, process.platform === 'win32' ? 'junction' : 'dir') + fixture.main = alias + + await expectInjectedLockAccessFailure(fixture, { operation: 'openSync', code: 'EPERM', expires: false }) }) it('waits for a concurrent installer to finish publishing its lock record', async () => { From 1829cdfec91fcc63d7dca47b87ed917012d1d6d6 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:10:43 +0800 Subject: [PATCH 5/6] test(ci): read the Lefthook fixture lock path without trimming git() trims its output, which also drops a worktree directory's own trailing whitespace; the installer removes only the line terminator. Derive the expected lock path through gitPath()/stripGitLineTerminator(), which applies the installer's exact rule, so the same-string claim holds for every path. Adds the trailing-space injection case (skipped on Windows, which cannot create such a directory); it fails with the same probe ENOENT under the trimming derivation. --- scripts/install-lefthook.spec.ts | 37 +++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index 6a95b2861a..bb6ab4ec16 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -55,6 +55,24 @@ function git(fixture: Fixture, cwd: string, args: string[]): string { return result.stdout.trim() } +/** Remove the line terminator Git appends to a printed path, as the installer does. */ +function stripGitLineTerminator(output: string): string { + const withoutLineFeed = output.endsWith('\n') ? output.slice(0, -1) : output + return process.platform === 'win32' && withoutLineFeed.endsWith('\r') + ? withoutLineFeed.slice(0, -1) + : withoutLineFeed +} + +// `git()` trims, which also drops a worktree directory's own trailing whitespace. Paths that must +// equal the installer's own reading keep only the line terminator, so a trailing space survives. +function gitPath(fixture: Fixture, cwd: string, args: string[]): string { + const result = gitResult(fixture, cwd, args) + if (result.status !== 0) { + throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`) + } + return stripGitLineTerminator(result.stdout) +} + function write(path: string, content: string, mode?: number): void { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, content, mode === undefined ? undefined : { mode }) @@ -161,12 +179,12 @@ function gitDirectory(fixture: Fixture, root: string): string { // The installer resolves the common directory against Git's own top-level path, and the host // spells that path differently from the fixture directory this file created: Git canonicalizes // Windows 8.3 short names and the macOS `/var` symlink, while `mkdtempSync` returns the temp -// directory's own spelling. Resolving the relative common directory against Git's top-level -// instead of the fixture directory gives the same string the installer computes, so an injected -// failure on this path reaches the installer's own lock instead of missing it. +// directory's own spelling. Reading both values the way the installer reads them (`gitPath`, not +// the trimming `git()`) keeps the relative common directory resolved to the same string the +// installer computes, so an injected failure on this path reaches the installer's own lock. function commonDirectory(fixture: Fixture): string { - const root = git(fixture, fixture.main, ['rev-parse', '--show-toplevel']) - const output = git(fixture, fixture.main, ['rev-parse', '--git-common-dir']) + const root = gitPath(fixture, fixture.main, ['rev-parse', '--show-toplevel']) + const output = gitPath(fixture, fixture.main, ['rev-parse', '--git-common-dir']) return isAbsolute(output) ? output : resolve(root, output) } @@ -397,6 +415,15 @@ syncBuiltinESMExports() await expectInjectedLockAccessFailure(fixture, { operation: 'openSync', code: 'EPERM', expires: false }) }) + // A worktree directory can end in a space on POSIX hosts; Git prints that path verbatim while a + // trimming read would drop the space and name a different directory, so the injection only fires + // when the expected path is read the installer's way. Windows cannot create such a directory. + it.skipIf(process.platform === 'win32')('handles an injected lock failure in a worktree whose path ends with a space', async () => { + await expectInjectedLockAccessFailure(createFixture({ main: 'main ' }), { + operation: 'openSync', code: 'EPERM', expires: false, + }) + }) + it('waits for a concurrent installer to finish publishing its lock record', async () => { const fixture = createFixture() const lockPath = installLockPath(fixture) From b73a7025871436fa0fb1cc76d9663e44a40765a6 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 26 Sep 2026 01:46:03 +0800 Subject: [PATCH 6/6] test(ci): canonicalize the Lefthook fixture common directory Reading Git's path text verbatim to keep trailing whitespace duplicated the line-terminator helper that scripts/change-scope.ts already owns, which the duplication gate rejects. Canonicalizing the fixture-side common directory with realpathSync.native removes the text parsing entirely: short names, junctions, and directory symlinks all collapse onto the physical directory the installer resolves, and the injected path stays exact. Drops the trailing-space case: with no text trimming left in the derivation, that path already matched the installer before this change, so the case could not fail and proved nothing. --- scripts/install-lefthook.spec.ts | 39 +++++--------------------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index bb6ab4ec16..8c994bf50b 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -7,6 +7,7 @@ import { mkdtempSync, lstatSync, readFileSync, + realpathSync, renameSync, rmSync, symlinkSync, @@ -55,24 +56,6 @@ function git(fixture: Fixture, cwd: string, args: string[]): string { return result.stdout.trim() } -/** Remove the line terminator Git appends to a printed path, as the installer does. */ -function stripGitLineTerminator(output: string): string { - const withoutLineFeed = output.endsWith('\n') ? output.slice(0, -1) : output - return process.platform === 'win32' && withoutLineFeed.endsWith('\r') - ? withoutLineFeed.slice(0, -1) - : withoutLineFeed -} - -// `git()` trims, which also drops a worktree directory's own trailing whitespace. Paths that must -// equal the installer's own reading keep only the line terminator, so a trailing space survives. -function gitPath(fixture: Fixture, cwd: string, args: string[]): string { - const result = gitResult(fixture, cwd, args) - if (result.status !== 0) { - throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`) - } - return stripGitLineTerminator(result.stdout) -} - function write(path: string, content: string, mode?: number): void { mkdirSync(dirname(path), { recursive: true }) writeFileSync(path, content, mode === undefined ? undefined : { mode }) @@ -179,13 +162,12 @@ function gitDirectory(fixture: Fixture, root: string): string { // The installer resolves the common directory against Git's own top-level path, and the host // spells that path differently from the fixture directory this file created: Git canonicalizes // Windows 8.3 short names and the macOS `/var` symlink, while `mkdtempSync` returns the temp -// directory's own spelling. Reading both values the way the installer reads them (`gitPath`, not -// the trimming `git()`) keeps the relative common directory resolved to the same string the -// installer computes, so an injected failure on this path reaches the installer's own lock. +// directory's own spelling. Canonicalizing the fixture-side directory collapses both spellings +// onto the one directory the installer touches, so an injected failure on this path reaches the +// installer's own lock instead of a differently spelled name for it. function commonDirectory(fixture: Fixture): string { - const root = gitPath(fixture, fixture.main, ['rev-parse', '--show-toplevel']) - const output = gitPath(fixture, fixture.main, ['rev-parse', '--git-common-dir']) - return isAbsolute(output) ? output : resolve(root, output) + const output = git(fixture, fixture.main, ['rev-parse', '--git-common-dir']) + return realpathSync.native(isAbsolute(output) ? output : resolve(fixture.main, output)) } function hooksPath(fixture: Fixture, root: string): string { @@ -415,15 +397,6 @@ syncBuiltinESMExports() await expectInjectedLockAccessFailure(fixture, { operation: 'openSync', code: 'EPERM', expires: false }) }) - // A worktree directory can end in a space on POSIX hosts; Git prints that path verbatim while a - // trimming read would drop the space and name a different directory, so the injection only fires - // when the expected path is read the installer's way. Windows cannot create such a directory. - it.skipIf(process.platform === 'win32')('handles an injected lock failure in a worktree whose path ends with a space', async () => { - await expectInjectedLockAccessFailure(createFixture({ main: 'main ' }), { - operation: 'openSync', code: 'EPERM', expires: false, - }) - }) - it('waits for a concurrent installer to finish publishing its lock record', async () => { const fixture = createFixture() const lockPath = installLockPath(fixture)