diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml index 08da0e0a91..ddb1e7a1e4 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.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 .agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md -2026-08-08-native-windows-pull-request-ci.md: a9e3b1b3ee2c5af55eccb8d14b0315a8fd5eea66 -2026-08-08-native-windows-pull-request-ci.zh.md: 2ebac333d3c2a1de51083f915c41e7dba8138332 +2026-08-08-native-windows-pull-request-ci.md: 419d1138bbd3ab9296b38ed136e03146ceca686d +2026-08-08-native-windows-pull-request-ci.zh.md: 5353b21c5a69e6ad847065d124d1bddb4d793e56 diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md index a9e3b1b3ee..419d1138bb 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.md @@ -42,7 +42,9 @@ The same exact hosted run showed that three concurrent instrumented Vitest worke The project-skill composition fixture had one separate eventual-consistency race: on a contended host the agent could begin its next model step after `write` returned but before Chokidar invalidated the skill catalog, moving the replacement catalog behind the subsequent `skill` call. The fixture now holds that post-write tool boundary until the real registry observes `hot-skill`, then retains its strict request-order and durable-transcript assertions. Production code remains asynchronous; the test explicitly waits for the watcher contract it intends to exercise instead of relying on scheduler timing or accepting a different request index. -POSIX mode bits, chmod-based unreadability, and chmod-based writer-lock refusal do not exist as equivalent Windows facilities. Those acceptance cases remain enforced on POSIX and are skipped on Windows; content, atomic replacement, symlink safety, rollback and recovery through platform-independent filesystem conflicts, and native Windows long-path behavior remain covered. No supported product source is excluded from Windows coverage to accommodate these differences. +The next exact-head run passed all 10,933 instrumented tests but correctly failed the per-file threshold at 99.95%, exposing five branches that Linux happened to cover. Deterministic cross-platform fixtures now exercise backward PTY scrollback pagination, a settings document that names a directory, an invalid SQLite filename, and an atomic-writer lock beneath a regular-file parent. The credentials provider's remaining `stat` and mode-enforcement arm is intrinsically POSIX, so it carries the same narrow annotated peer ignore used by the durable JSONL and storage backends; its behavior test remains enforced on POSIX. The threshold and source-file inventory remain unchanged. + +POSIX mode bits, chmod-based unreadability, and chmod-based writer-lock refusal do not exist as equivalent Windows facilities. Those acceptance cases remain enforced on POSIX and are skipped on Windows; content, atomic replacement, symlink safety, rollback and recovery through platform-independent filesystem conflicts, and native Windows long-path behavior remain covered. Only intrinsically POSIX source arms carry narrow, explained denominator ignores; no source file or platform-independent branch is excluded from Windows coverage to accommodate these differences. Wine-only infrastructure is absent from the supported workflow: there is no apt-cache producer, compatibility script, hoisted snapshot install, Windows Node download, or local `check:windows-wine` command. The [archived Wine experiment](../../archived/process/2026-07-27-wine-windows-gates-experiment.md) remains historical evidence for its measured latency and fidelity trade-offs, not a current execution path. diff --git a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md index 2ebac333d3..5353b21c5a 100644 --- a/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md +++ b/.agents/notes/implemented/process/2026-08-08-native-windows-pull-request-ci.zh.md @@ -42,7 +42,9 @@ Status: implemented 项目 skill 组合 fixture 另有一项最终一致性竞态:宿主资源紧张时,agent 可能在 `write` 返回后、Chokidar 使 skill 目录缓存失效前就开始下一次模型步骤,导致替换目录消息落到后续 `skill` 调用之后。现在,fixture 会在写入后的工具边界等待真实注册表观察到 `hot-skill`,然后继续严格断言请求顺序与持久转录。生产代码仍保持异步;测试会显式等待其本来要验证的 watcher 契约,而不是依赖调度时序或接受另一个请求索引。 -POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。没有任何受支持的产品源码为适应这些差异而从 Windows 覆盖率中排除。 +下一次分支头精确运行通过了全部 10,933 项插桩测试,但逐文件阈值仍在 99.95% 正确失败,从而暴露出 5 个此前恰由 Linux 覆盖的分支。新增的确定性跨平台 fixture 会分别覆盖 PTY 向后翻页拼接 scrollback、以目录作为 settings 文档、非法 SQLite 文件名,以及 regular file(普通文件)父级之下的原子写入锁。credentials provider 剩余的 `stat` 与模式位强制分支本质上只属于 POSIX,因此采用与持久 JSONL、storage backend 相同的窄范围、带说明的对等分支忽略;其行为测试仍会在 POSIX 上强制执行。阈值与源码文件清单均未改变。 + +POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。只有本质上属于 POSIX 的源码分支带有窄范围且说明明确的分母忽略;没有任何源码文件或平台无关分支为适应这些差异而从 Windows 覆盖率中排除。 受支持的工作流不含 Wine 专属基础设施:不存在 apt 缓存生产者、兼容性脚本、对仓库快照执行的 hoisted 安装、Windows Node 下载或本地 `check:windows-wine` 命令。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。 diff --git a/packages/credentials/credentials-local/src/index.ts b/packages/credentials/credentials-local/src/index.ts index f6aba278cc..0b6b2272ce 100644 --- a/packages/credentials/credentials-local/src/index.ts +++ b/packages/credentials/credentials-local/src/index.ts @@ -108,6 +108,7 @@ async function assertOwnerOnly(filename: string): Promise { return } /* v8 ignore stop */ + /* v8 ignore start -- Windows has no POSIX mode enforcement; POSIX behavior tests enforce this peer. */ let mode: number try { mode = (await stat(filename)).mode @@ -122,6 +123,7 @@ async function assertOwnerOnly(filename: string): Promise { `credentials-local: ${filename} is readable beyond its owner (mode ${(mode & 0o777).toString(8)});` + ` run "chmod 600 ${filename}" before starting again`, ) + /* v8 ignore stop */ } /** Whether a filesystem error means absence; every non-ENOENT failure must surface. */ diff --git a/packages/pty/tool-bash-persistent/tests/tools.spec.ts b/packages/pty/tool-bash-persistent/tests/tools.spec.ts index 7b789cbd1f..73eeeeb69e 100644 --- a/packages/pty/tool-bash-persistent/tests/tools.spec.ts +++ b/packages/pty/tool-bash-persistent/tests/tools.spec.ts @@ -96,6 +96,7 @@ type StubMode = | 'send-error' | 'prompt-after-idle' | 'empty-page-after-latest' + | 'paged-scrollback' class StubPtySession implements PtyBackendSession { readonly motd = '__DSH_PERSISTENT_BASH_PROMPT__ ' @@ -211,6 +212,19 @@ class StubPtySession implements PtyBackendSession { return { text: '', totalLines: 2, lineBegin: 1, lineEnd: 1, truncated: false } } const lines = this.scrollback.split('\n') + if (this.mode === 'paged-scrollback') { + const offset = request.offset ?? 0 + const end = lines.length - offset + const start = Math.max(0, end - 3) + const returnedLines = end - start + return { + text: lines.slice(start, end).join('\n'), + totalLines: lines.length, + lineBegin: offset, + lineEnd: offset + returnedLines, + truncated: this.historyTruncated, + } + } return { text: this.scrollback, totalLines: this.mode === 'empty-page-after-latest' ? lines.length + 1 : lines.length, @@ -402,6 +416,16 @@ describe('tool-bash-persistent', () => { expect(text(await call(ctx, owner, 'empty continuation page'))).toContain('hello from stub') }) + it('assembles retained output across backward scrollback pages', async () => { + const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 }) + await call(ctx, owner, 'warm up') + const session = stub.sessions[0]! + session.mode = 'paged-scrollback' + session.scrollback = '' + + expect(text(await call(ctx, owner, 'paged output'))).toBe('hello from stub') + }) + it('sanitizes a prompt fallback reached after multiple polling rounds', async () => { const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 }) await call(ctx, owner, 'warm up') diff --git a/packages/settings/settings-local/tests/local.spec.ts b/packages/settings/settings-local/tests/local.spec.ts index bcf9bfc276..967091c89b 100644 --- a/packages/settings/settings-local/tests/local.spec.ts +++ b/packages/settings/settings-local/tests/local.spec.ts @@ -137,6 +137,13 @@ describe('boot and reads', () => { await expect(boot({ path, watch: false })).rejects.toThrow(/EACCES|permission/i) }) + it('fails loud when the document path names a directory', async () => { + const dir = await tempDir() + const path = join(dir, 'settings.yaml') + await mkdir(path) + await expect(boot({ path, watch: false })).rejects.toThrow(/EISDIR|directory/i) + }) + it('fails loud on an unsupported extension', async () => { const dir = await tempDir() await expect(boot({ path: join(dir, 'settings.toml'), watch: false })) diff --git a/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts b/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts index 8e64fb40f2..37cf7ba122 100644 --- a/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts +++ b/packages/storage/storage-sqlite/tests/sqlite-backend.spec.ts @@ -217,6 +217,13 @@ describe('sqlite backend specifics', () => { await chmod(dir, 0o700) }) + it('propagates an invalid database filename before opening SQLite', async () => { + const path = await freshDbPath() + const backend = backendAt(`${path}\0invalid`) + await expect(backend.kv.open(DESCRIPTOR)).rejects.toThrow(/null bytes/i) + await backend.close() + }) + it('preserves the mode of an existing database file', async () => { if (process.platform === 'win32') return const path = await freshDbPath() diff --git a/packages/util/atomic-write/tests/atomic-write.spec.ts b/packages/util/atomic-write/tests/atomic-write.spec.ts index a917fd13d9..e71e5b7abd 100644 --- a/packages/util/atomic-write/tests/atomic-write.spec.ts +++ b/packages/util/atomic-write/tests/atomic-write.spec.ts @@ -2,7 +2,7 @@ import { lstat, mkdir, mkdtemp, readFile, readdir, stat, symlink, writeFile } fr import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, expect, it } from 'vitest' -import { writeFileAtomic } from '../src/index.ts' +import { withFileLock, writeFileAtomic } from '../src/index.ts' async function scratch(): Promise { return mkdtemp(join(tmpdir(), 'dsh-atomic-write-')) @@ -46,3 +46,17 @@ describe('writeFileAtomic', () => { expect((await readdir(dir)).filter(entry => entry.includes('.tmp'))).toEqual([]) }) }) + +describe('withFileLock', () => { + it('rejects an invalid parent hierarchy before running the operation', async () => { + const dir = await scratch() + const parent = join(dir, 'not-a-directory') + await writeFile(parent, 'occupied') + let called = false + + await expect(withFileLock(join(parent, 'document'), async () => { + called = true + })).rejects.toThrow(/ENOENT|ENOTDIR|not a directory/i) + expect(called).toBe(false) + }) +})