Преглед изворни кода

fix(windows): normalize native coverage tests

Tianyi Cui пре 2 месеци
родитељ
комит
ed1ea95176

+ 2 - 2
.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

+ 2 - 0
.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

+ 2 - 0
.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)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。
 
 ## 曾考虑的替代方案

+ 3 - 2
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 }
 }

+ 7 - 2
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')

+ 7 - 2
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)

+ 2 - 2
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: [] })
   })
 

+ 1 - 1
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)

+ 1 - 1
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([