From ed1ea95176799eaee037621a409a86251652d8a7 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sat, 8 Aug 2026 20:01:46 +0800 Subject: [PATCH] fix(windows): normalize native coverage tests --- .../2026-08-08-native-windows-pull-request-ci.i18n.yaml | 4 ++-- .../process/2026-08-08-native-windows-pull-request-ci.md | 2 ++ .../2026-08-08-native-windows-pull-request-ci.zh.md | 2 ++ packages/typert/generator/src/analyzer.ts | 5 +++-- packages/typert/generator/tests/remote-model.spec.ts | 9 +++++++-- packages/typert/generator/tests/type-model.spec.ts | 9 +++++++-- scripts/change-scope.spec.ts | 4 ++-- scripts/install-lefthook.spec.ts | 2 +- scripts/oxlint-contract.spec.ts | 2 +- 9 files changed, 27 insertions(+), 12 deletions(-) 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 e69e8b3e89..cb4123b330 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: ab4f9ec7a9c18b9cf3303ad705145f51ccc4f08d -2026-08-08-native-windows-pull-request-ci.zh.md: e569371647e948b245b1a0db5b7c54eb51238cbf +2026-08-08-native-windows-pull-request-ci.md: d62858106505e9b6ae79c6ad6c4854b1f3270ae0 +2026-08-08-native-windows-pull-request-ci.zh.md: c428e3be028e4201fc178142e54f5a3d02aac742 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 ab4f9ec7a9..d628581065 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 @@ -18,6 +18,8 @@ The aggregate keeps workspace build, production-site, and 100%-per-file coverage 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. +The coverage follow-up then exercised the serial heavy suites on the native host and removed their remaining path-spelling assumptions. Filesystem identity assertions compare native real paths instead of Git's slash convention with Node's temporary-directory spelling; quoted diagnostics are matched in their escaped form; TypeScript-owned file names are compared after separator normalization; and Typert passes a slash-normalized config name consistently across TypeScript's read and parse boundary so malformed Windows configs produce the owned analysis error instead of a compiler debug failure. The Oxlint subprocess contract also uses the same explicit twenty-second budget as its neighboring executable probes. These are portability repairs to supported tests and parser behavior, not platform skips or coverage exclusions. + 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. ## Alternatives considered 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 e569371647..c428e3be02 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 @@ -18,6 +18,8 @@ Status: implemented 首次原生运行暴露出两项被兼容性通道掩盖的故障。文档投影测试此前只按 `/` 拆分来派生图片 basename;现在改为使用 Node 根据平台计算的 basename。Chokidar 消费方收到的 `%TEMP%` 以 `C:\\Users\\RUNNER~1` 这个 8.3 别名表示,而 libuv 返回的是长目录名,导致其 Windows 事件路径断言失败。共享的设置 watcher 与凭据 watcher,以及 Cordis 的模块 HMR(热模块替换)与精确配置 HMR,现在都会在打开 watcher 前规范化现有的原生监听基准路径或层级最深的现有祖先路径,并保留尚不存在的后缀;文件访问和诊断仍使用配置路径。 +随后,覆盖率后续工作在原生宿主上运行了串行的高负载测试套件,并移除了其中残留的路径拼写假设。文件系统标识断言改为比较原生真实路径,不再直接比较遵循 Git 斜杠约定的路径与 Node 的临时目录拼写;带引号的诊断文本按 JSON 转义后的形式匹配;TypeScript 提供的文件名在统一分隔符后再比较;Typert 则让经过斜杠归一化的配置名称一致贯穿 TypeScript 的读取与解析边界,使格式错误的 Windows 配置产生 Typert 自有的分析错误,而非编译器的调试故障。Oxlint 子进程契约也采用与相邻可执行文件探测相同的显式 20 秒预算。这些都是针对受支持测试与解析器行为的可移植性修复,不是按平台跳过测试或设置覆盖率排除项。 + 受支持的工作流不含 Wine 专属基础设施:不存在 apt 缓存生产者、兼容性脚本、对仓库快照执行的 hoisted 安装、Windows Node 下载或本地 `check:windows-wine` 命令。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。 ## 曾考虑的替代方案 diff --git a/packages/typert/generator/src/analyzer.ts b/packages/typert/generator/src/analyzer.ts index 6f495aa318..c932dd395f 100644 --- a/packages/typert/generator/src/analyzer.ts +++ b/packages/typert/generator/src/analyzer.ts @@ -2507,9 +2507,10 @@ function mergeWorkspaceModels(models: readonly WorkspaceModel[]): WorkspaceModel } function parseConfig(path: string): ParsedConfig { - const read = ts.readConfigFile(path, file => ts.sys.readFile(file)) + const compilerPath = path.split(sep).join('/') + const read = ts.readConfigFile(compilerPath, file => ts.sys.readFile(file)) if (read.error !== undefined) throw new TypertAnalysisError(formatDiagnostic(read.error)) - const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(path), undefined, path) + const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(compilerPath), undefined, compilerPath) if (parsed.errors.length > 0) throw new TypertAnalysisError(parsed.errors.map(formatDiagnostic).join('\n')) return { path, parsed } } diff --git a/packages/typert/generator/tests/remote-model.spec.ts b/packages/typert/generator/tests/remote-model.spec.ts index 0e62a56bf4..a41f0c0cd0 100644 --- a/packages/typert/generator/tests/remote-model.spec.ts +++ b/packages/typert/generator/tests/remote-model.spec.ts @@ -10,6 +10,10 @@ import { WorkspaceTypertGenerator } from '../src/workspace.ts' const fixtureRoot = resolve(import.meta.dirname, 'fixtures/remote-model') const temporaryRoots: string[] = [] +function normalizedPath(path: string): string { + return path.replaceAll('\\', '/') +} + interface RuntimeSchema { safeParse(value: unknown): { readonly success: boolean } } @@ -646,7 +650,8 @@ void navigated const navigation = 'ctx.remote.goals.create' const position = consumerSource.indexOf(navigation) + navigation.lastIndexOf('create') + 1 const definitions = languageService.getDefinitionAtPosition(consumerPath, position) - const generatedDefinition = definitions?.find(candidate => candidate.fileName === declarationPath) + const generatedDefinition = definitions?.find(candidate => + normalizedPath(candidate.fileName) === normalizedPath(declarationPath)) if (generatedDefinition === undefined) { throw new Error(`generated Remote definition not found: ${JSON.stringify(definitions, null, 2)}`) } @@ -661,7 +666,7 @@ void navigated pos: generatedDefinition.textSpan.start, }) languageService.dispose() - if (definition === undefined || !definition.fileName.endsWith('/packages/remote/src/index.ts')) { + if (definition === undefined || !normalizedPath(definition.fileName).endsWith('/packages/remote/src/index.ts')) { throw new Error(`generated Remote definition did not map to its Host source: ${JSON.stringify(definition)}`) } const hostSource = readFileSync(join(consumerRoot, 'packages/remote/src/index.ts'), 'utf8') diff --git a/packages/typert/generator/tests/type-model.spec.ts b/packages/typert/generator/tests/type-model.spec.ts index 40a91e3ef5..faf41969d3 100644 --- a/packages/typert/generator/tests/type-model.spec.ts +++ b/packages/typert/generator/tests/type-model.spec.ts @@ -18,6 +18,11 @@ import { WorkspaceTypertGenerator } from '../src/workspace.ts' const fixtureRoot = resolve(import.meta.dirname, 'fixtures/type-model') const temporaryRoots: string[] = [] + +function normalizedPath(path: string): string { + return path.replaceAll('\\', '/') +} + const parseConfigHost: ts.ParseConfigFileHost = { ...ts.sys, onUnRecoverableConfigFileDiagnostic(diagnostic) { @@ -733,8 +738,8 @@ describe('WorkspaceAnalyzer', { timeout: 60_000 }, () => { rootNames: packageConfig.fileNames, options: aggregateConfig.options, }) - expect(diagnosticProgram.getSourceFiles().map(source => source.fileName)) - .toContain(join(externalRoot, 'index.d.ts')) + expect(diagnosticProgram.getSourceFiles().map(source => normalizedPath(source.fileName))) + .toContain(normalizedPath(join(externalRoot, 'index.d.ts'))) const targets = new WorkspaceAnalyzer({ root }).analyze().faces .flatMap(face => face.graph.nodes) diff --git a/scripts/change-scope.spec.ts b/scripts/change-scope.spec.ts index a9ba98407b..b230c8de12 100644 --- a/scripts/change-scope.spec.ts +++ b/scripts/change-scope.spec.ts @@ -104,7 +104,7 @@ describe('change-scope', () => { const headSha = commit(root, 'feature.txt', 'feature\n') const fresh = jsonReport(root, 'origin/master') - expect(fresh.repositoryRoot).toBe(realpathSync(root)) + expect(realpathSync.native(fresh.repositoryRoot)).toBe(realpathSync.native(root)) expect(fresh.resolved).toEqual({ baseSha: git(root, ['rev-parse', 'origin/master']), headSha, @@ -122,7 +122,7 @@ describe('change-scope', () => { const { root } = fixture('worktree ') const report = jsonReport(root, 'HEAD') - expect(report.repositoryRoot).toBe(realpathSync(root)) + expect(realpathSync.native(report.repositoryRoot)).toBe(realpathSync.native(root)) expect(report.paths).toEqual({ committed: [], staged: [], unstaged: [], untracked: [] }) }) diff --git a/scripts/install-lefthook.spec.ts b/scripts/install-lefthook.spec.ts index ca74b91404..35a5fa3216 100644 --- a/scripts/install-lefthook.spec.ts +++ b/scripts/install-lefthook.spec.ts @@ -587,7 +587,7 @@ describe('worktree-local Lefthook installer', { timeout: 15_000 }, () => { expect(result.status).toBe(1) expect(result.stderr).toContain('sibling dormant worktree config') - expect(result.stderr).toContain(linkedConfig) + expect(result.stderr).toContain(JSON.stringify(linkedConfig)) expect(gitResult(fixture, fixture.main, ['config', '--get', 'extensions.worktreeConfig']).status).toBe(1) expect(gitResult(fixture, fixture.linked, ['config', '--get', 'core.hooksPath']).status).toBe(1) expect(git(fixture, fixture.main, ['config', '--file', linkedConfig, '--get', 'core.hooksPath'])).toBe(linkedHooks) diff --git a/scripts/oxlint-contract.spec.ts b/scripts/oxlint-contract.spec.ts index 7592780881..865eee26eb 100644 --- a/scripts/oxlint-contract.spec.ts +++ b/scripts/oxlint-contract.spec.ts @@ -208,7 +208,7 @@ export const longProbe = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + rm(configPath, { force: true }), ]) } - }) + }, 20_000) it('accepts an ignored-only staged selection', () => { const result = runOxlint([