test(windows): stabilize native coverage

This commit is contained in:
Tianyi Cui
2026-08-09 02:29:09 +08:00
parent 1ce84853de
commit 94799abfb0
10 changed files with 54 additions and 17 deletions
@@ -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: 4fa293c7701264cfaa557c64b9ac897d8e0e5c0f
2026-08-08-native-windows-pull-request-ci.zh.md: 575a4acda1d0ba6b2e97cccffaf107e113608ad8
2026-08-08-native-windows-pull-request-ci.md: a9e3b1b3ee2c5af55eccb8d14b0315a8fd5eea66
2026-08-08-native-windows-pull-request-ci.zh.md: 2ebac333d3c2a1de51083f915c41e7dba8138332
@@ -14,7 +14,7 @@ The coverage audit found that PR #499 had restored deterministic native-Windows
The required `windows` job in [ci.yml](../../../../.github/workflows/ci.yml) runs on GitHub's standard `windows-2025` image under native PowerShell. It enables Developer Mode for workspace symlinks, provisions the repository-pinned pnpm through `pnpm/action-setup`, performs an immutable install without a transferred store archive, and runs `pnpm run check:ci:windows-complete`. The stable `windows` job id remains a dependency of `all checks passed`; its display name is `windows node 24 / native complete`.
The aggregate keeps workspace build, production-site, and 100%-per-file coverage failures blocking while reporting the broader static, documentation, package, and built-artifact portability inventory as observational. Coverage has a four-worker budget; one runner shares installation and build outputs across those gates, and serial gate and publint worker bounds keep the standard image within a predictable resource envelope. Linux remains the owner of duplicate lint and snapshot enforcement.
The aggregate keeps workspace build, production-site, and 100%-per-file coverage failures blocking while reporting the broader static, documentation, package, and built-artifact portability inventory as observational. Coverage has a one-worker budget, and gate concurrency remains one so the instrumented and exempt-heavy suites do not overlap; one runner shares installation and build outputs across those gates, and serial gate and publint worker bounds keep the standard image within a predictable resource envelope. Linux remains the owner of duplicate lint and snapshot enforcement.
The first native run exposed two failures hidden by the compatibility lane. Documentation projection tests derived an image basename by splitting only on `/`; they now use Node's platform basename. Chokidar consumers received `%TEMP%` through the `C:\\Users\\RUNNER~1` 8.3 alias while libuv returned the long directory name, tripping its Windows event-path assertion. Shared settings and credentials watchers, plus Cordis module and exact-config HMR, now canonicalize the existing native watch base or deepest existing ancestor before opening the watcher and preserve a missing suffix, while file access and diagnostics retain the configured path.
@@ -36,7 +36,11 @@ Two product boundaries required foundation repairs. Include's debounced config p
The subsequent exact hosted run isolated seven other fixture contracts. The PowerShell background-output case now waits for process completion before draining and comparing the final delta, while the pi-ai idle-watchdog case retains a bounded one-second close deadline that accommodates the delayed Windows socket notification. The asynchronous workspace projection seeds its in-memory filesystem at the host-resolved root. The Include retry acceptance asserts the injected failure and eventual persistence rather than an incidental total rename count, which may include another valid serialized write. LSP's bare-command fixture supplies a `.cmd` executable through `PATHEXT` on Windows, and URI rendering expectations distinguish the execution world's path convention from the test host's separators. None of these changes skips a supported path or weakens the asserted outcome.
That run also made syntax highlighting sensitive to runner contention rather than source text. Shiki's JavaScript engine deferred TextMate regexes longer than 3,000 characters until their first match, while Shiki counted that compilation against its 500 ms per-line tokenization budget. A busy Windows coverage worker could therefore stop the first TypeScript line after the `const` match and return the remainder under the same keyword style. The client now uses Shiki's default regex translation with lazy compilation disabled: scanner construction compiles each pattern eagerly, while the existing 500 ms budget still bounds scanning of user content. The token-boundary and Markdown DOM fixtures continue to require the complete highlighted result rather than accepting the partial stream.
That run also made syntax highlighting sensitive to runner contention rather than source text. Shiki's JavaScript engine deferred TextMate regexes longer than 3,000 characters until their first match, while Shiki counted that compilation against its 500 ms per-line tokenization budget. A busy Windows coverage worker could therefore stop the first TypeScript line after the `const` match and return the remainder under the same keyword style. The client now uses Shiki's default regex translation with lazy compilation disabled and tokenizes one representative sample for each boot grammar without a startup cutoff while constructing the singleton. Scanner creation and pattern compilation therefore finish before user content enters the unchanged 500 ms per-line budget. The token-boundary and Markdown DOM fixtures continue to require the complete highlighted result rather than accepting the partial stream.
The same exact hosted run showed that three concurrent instrumented Vitest workers were an unsafe budget for the standard Windows image: otherwise independent Git-merge and JSON-RPC HTTP integration cases reached the default five-second ceiling together. The native lane now gives Vitest one worker, while the real Git subprocess suite and the two real HTTP composition cases have explicit fifteen-second integration budgets without changing their workloads or assertions. The translation merge fixture also preserves `import.meta.resolve('tsx/esm')` as a `file:` URL when passing it to Node's `--import`; converting it to a drive-letter path had failed before the driver could print its owned recovery guidance. After the latest package regrouping, the fork-isolated JSONL suite's inventory follows its new `packages/session/` location rather than silently returning that process-bound suite to the shared thread pool.
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.
@@ -14,7 +14,7 @@ Status: implemented
[ci.yml](../../../../.github/workflows/ci.yml) 中必需的 `windows` 作业在 GitHub 标准 `windows-2025` 镜像上使用原生 PowerShell 运行。该作业为工作区符号链接启用开发人员模式,通过 `pnpm/action-setup` 提供仓库固定版本的 pnpm,在不传输 store 归档的情况下执行不可变安装,并运行 `pnpm run check:ci:windows-complete`。稳定的 `windows` 作业 ID 仍是 `all checks passed` 的依赖项;其显示名称为 `windows node 24 / native complete`
工作区构建、生产网站和逐文件 100% 覆盖率检查失败时,聚合作业会继续阻断;更广泛的静态检查、文档、包和构建产物可移植性清单则作为观测项报告。覆盖率检查的工作线程预算为 4 个;同一台运行器在这些门禁之间共享安装结果与构建输出,串行门禁与 publint 工作线程上限使标准镜像的资源使用保持在可预测范围内。重复执行的 lint 与快照强制检查仍由 Linux 负责。
工作区构建、生产网站和逐文件 100% 覆盖率检查失败时,聚合作业会继续阻断;更广泛的静态检查、文档、包和构建产物可移植性清单则作为观测项报告。覆盖率检查的工作线程预算为 1,门禁并发数也保持为 1,因此插桩套件不会与免覆盖率的高负载套件重叠执行;同一台运行器在这些门禁之间共享安装结果与构建输出,串行门禁与 publint 工作线程上限使标准镜像的资源使用保持在可预测范围内。重复执行的 lint 与快照强制检查仍由 Linux 负责。
首次原生运行暴露出两项被兼容性通道掩盖的故障。文档投影测试此前只按 `/` 拆分来派生图片 basename;现在改为使用 Node 根据平台计算的 basename。Chokidar 消费方收到的 `%TEMP%``C:\\Users\\RUNNER~1` 这个 8.3 别名表示,而 libuv 返回的是长目录名,导致其 Windows 事件路径断言失败。共享的设置 watcher 与凭据 watcher,以及 Cordis 的模块 HMR(热模块替换)与精确配置 HMR,现在都会在打开 watcher 前规范化现有的原生监听基准路径或层级最深的现有祖先路径,并保留尚不存在的后缀;文件访问和诊断仍使用配置路径。
@@ -36,7 +36,11 @@ Status: implemented
随后的分支头精确托管运行又隔离出另外 7 项 fixture 契约。PowerShell 后台输出场景现在会等待进程完成,再排空并比较最后一段增量;pi-ai 空闲 watchdog 场景则保留 1 秒的有界关闭期限,以容纳 Windows 延迟送达的 socket 通知。异步工作区投影会在宿主解析后的根目录上填充内存文件系统。Include 重试验收现在断言注入的故障与最终持久化结果,而不再断言可能包含另一项合法串行写入的偶然 rename 总次数。LSP 的裸命令 fixture 会在 Windows 上通过 `PATHEXT` 提供 `.cmd` 可执行文件,URI 渲染预期也会区分执行环境的路径约定与测试宿主的分隔符。上述修改既没有跳过受支持路径,也没有削弱结果断言。
该次运行还暴露出语法高亮会受运行器资源争用影响,而不只取决于源文本。Shiki 的 JavaScript 引擎会把超过 3,000 个字符的 TextMate 正则推迟到首次匹配时再编译,Shiki 同时把这段编译时间计入每行 500 毫秒的 tokenization(词元化)预算。繁忙的 Windows 覆盖率工作线程因此可能在首次 TypeScript 行匹配到 `const` 后提前停止,并让剩余内容沿用同一关键字样式。客户端现在仍使用 Shiki 的默认正则转换,但关闭延迟编译:scanner(扫描器)创建时会立即编译各项模式,而既有的 500 毫秒预算仍会约束对用户内容的扫描。词元边界与 Markdown DOM fixture 会继续要求完整高亮结果,不接受这类部分结果流。
该次运行还暴露出语法高亮会受运行器资源争用影响,而不只取决于源文本。Shiki 的 JavaScript 引擎会把超过 3,000 个字符的 TextMate 正则推迟到首次匹配时再编译,Shiki 同时把这段编译时间计入每行 500 毫秒的 tokenization(词元化)预算。繁忙的 Windows 覆盖率工作线程因此可能在首次 TypeScript 行匹配到 `const` 后提前停止,并让剩余内容沿用同一关键字样式。客户端现在仍使用 Shiki 的默认正则转换,但关闭延迟编译,并在构造单例时以不设启动期截止时间的方式,为每项启动时语法 tokenization 一段代表性样例。因此,scanner(扫描器)创建与模式编译会在用户内容进入仍为每行 500 毫秒预算前完成。词元边界与 Markdown DOM fixture 会继续要求完整高亮结果,不接受这类部分结果流。
同一次分支头精确托管运行还表明,在标准 Windows 镜像上并发使用 3 个插桩 Vitest 工作线程并不安全:彼此独立的 Git merge 集成用例与 JSON-RPC HTTP 集成用例会同时触及默认的 5 秒上限。原生通道现在只为 Vitest 提供 1 个工作线程;真实 Git 子进程套件与两项真实 HTTP 组合用例则获得显式的 15 秒集成预算,其工作负载与断言均未改变。translation merge fixture 在把 `import.meta.resolve('tsx/esm')` 传给 Node 的 `--import` 时,也会保留其 `file:` URL;此前把它转换为盘符路径会在驱动程序输出自有恢复指引前就失败。纳入最新的 package regrouping(包重组)后,采用 fork 隔离的 JSONL 套件清单会跟随它在 `packages/session/` 下的新位置,而不会悄然把这一进程绑定套件送回共享线程池。
项目 skill 组合 fixture 另有一项最终一致性竞态:宿主资源紧张时,agent 可能在 `write` 返回后、Chokidar 使 skill 目录缓存失效前就开始下一次模型步骤,导致替换目录消息落到后续 `skill` 调用之后。现在,fixture 会在写入后的工具边界等待真实注册表观察到 `hot-skill`,然后继续严格断言请求顺序与持久转录。生产代码仍保持异步;测试会显式等待其本来要验证的 watcher 契约,而不是依赖调度时序或接受另一个请求索引。
POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。没有任何受支持的产品源码为适应这些差异而从 Windows 覆盖率中排除。
+1 -1
View File
@@ -310,7 +310,7 @@ jobs:
runs-on: windows-2025
name: windows node 24 / native complete
env:
DSH_COVERAGE_MAX_WORKERS: '4'
DSH_COVERAGE_MAX_WORKERS: '1'
DSH_GATE_CONCURRENCY: '1'
DSH_PUBLINT_CONCURRENCY: '1'
steps:
@@ -156,13 +156,33 @@ const regexEngine = createJavaScriptRegexEngine({
let singleton: HighlighterCore | undefined
/** The synchronous highlighter (one instance per document); pre-warmed below, lazy as the fallback. */
function highlighter(): HighlighterCore {
singleton ??= createHighlighterCoreSync({
/** Representative paths through every boot grammar, compiled before user content is timed. */
const BOOT_GRAMMAR_WARMUPS = [
{ lang: 'typescript', code: 'const answer: number = 42' },
{ lang: 'shellscript', code: 'printf \'%s\\n\' "$HOME"' },
{ lang: 'json', code: '{"ready":true}' },
] as const
/** Construct and pre-tokenize the boot grammars outside the user-content scan budget. */
function createHighlighter(): HighlighterCore {
const instance = createHighlighterCoreSync({
themes: [cssVariablesTheme],
langs: LANGS,
engine: regexEngine,
})
for (const sample of BOOT_GRAMMAR_WARMUPS) {
instance.codeToTokens(sample.code, {
lang: sample.lang,
theme: 'css-variables',
tokenizeTimeLimit: 0,
})
}
return instance
}
/** The synchronous highlighter (one instance per document); pre-warmed below, lazy as the fallback. */
function highlighter(): HighlighterCore {
singleton ??= createHighlighter()
return singleton
}
@@ -416,7 +416,7 @@ describe('dsh-agent-spine-demo bundle', () => {
await ctx.fiber.dispose()
})
it('snapshots a created project skill through catalog refresh and progressive loading', async () => {
it('snapshots a created project skill through catalog refresh and progressive loading', { timeout: 15_000 }, async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-refresh-'))
const home = await mkdtemp(join(tmpdir(), 'dsh-agent-spine-demo-skill-refresh-home-'))
try {
@@ -449,6 +449,15 @@ describe('dsh-agent-spine-demo bundle', () => {
await ctx.plugin(LocalBashExecutor, {})
await ctx.plugin(LocalFileSystem, { cwd: root })
await ctx.plugin(ToolFs)
ctx.on('tools/post-execute', async (exec, _result, next) => {
const decision = await next()
if (exec.callId === 'write-skill') {
await vi.waitFor(async () => {
expect((await ctx.skills.list({ cwd: root })).map(skill => skill.name)).toContain('hot-skill')
}, { timeout: 5_000 })
}
return decision
})
ctx.llm.registerAdapter(['mock'], adapter)
const handle = await ctx.agents.create({
sessionId: SessionId('skill-refresh-session'),
@@ -109,7 +109,7 @@ async function settleSubagent(
}
describe('HarnessSdkServer', () => {
it('creates a harness agent and calls the configured OpenAI-compatible endpoint', async () => {
it('creates a harness agent and calls the configured OpenAI-compatible endpoint', { timeout: 15_000 }, async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-'))
const llmServer = await mockCompletionServer()
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
@@ -295,7 +295,7 @@ describe('HarnessSdkServer', () => {
}
})
it('creates an SDK session without an optional system prompt', async () => {
it('creates an SDK session without an optional system prompt', { timeout: 15_000 }, async () => {
const storageDir = await mkdtemp(join(tmpdir(), 'dsh-jsonrpc-no-system-'))
const llmServer = await mockCompletionServer()
vi.stubEnv('DEEPSEEK_API_KEY', 'test-key')
+1 -1
View File
@@ -42,7 +42,7 @@ describe('CI workflow', () => {
expect(windows['runs-on']).toBe('windows-2025')
expect(windows.name).toBe('windows node 24 / native complete')
expect(windows.env).toMatchObject({ DSH_COVERAGE_MAX_WORKERS: '4' })
expect(windows.env).toMatchObject({ DSH_COVERAGE_MAX_WORKERS: '1' })
expect(commandSteps).toHaveLength(3)
expect(commandSteps.every(step => step.shell === 'pwsh')).toBe(true)
expect(commandSteps.map(step => step.run)).toContain('pnpm run check:ci:windows-complete')
+2 -2
View File
@@ -19,7 +19,7 @@ import {
const driver = fileURLToPath(new URL('./merge-translation-pairing.ts', import.meta.url))
const driverLauncher = fileURLToPath(new URL('./merge-translation-pairing-driver.sh', import.meta.url))
const workspaceRoot = fileURLToPath(new URL('../', import.meta.url))
const tsxLoader = fileURLToPath(import.meta.resolve('tsx/esm'))
const tsxLoader = import.meta.resolve('tsx/esm')
const fixtures: string[] = []
interface Fixture {
@@ -225,7 +225,7 @@ function expectMergedPair(fixture: Fixture): void {
)
}
describe('translation pairing merge composition', () => {
describe('translation pairing merge composition', { timeout: 15_000 }, () => {
it('rejects a pairing-record path outside the repository', () => {
const fixture = createFixture(false)
+1 -1
View File
@@ -70,7 +70,7 @@ const coverageExemptExcludes = coverageExemptRaw === '1'
// that worker threads cannot isolate reliably under aggregate gate contention.
// Keep the narrow exception in forks while the rest of the inventory avoids per-file processes.
const processBoundTests = [
'packages/session-persistence/session-persistence-jsonl/tests/jsonl.spec.ts',
'packages/session/session-persistence-jsonl/tests/jsonl.spec.ts',
'packages/subagent/subagent-acp/tests/subagent-acp.spec.ts',
'packages/subprocess/subprocess-local/tests/spawn.spec.ts',
'packages/context/time-context/tests/time-context.spec.ts',