test(windows): close native coverage gaps
This commit is contained in:
+2
-2
@@ -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
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ async function assertOwnerOnly(filename: string): Promise<void> {
|
||||
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<void> {
|
||||
`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. */
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<string> {
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user