fix(windows): normalize native coverage tests

This commit is contained in:
Tianyi Cui
2026-08-08 20:01:46 +08:00
parent 331ffd8381
commit ed1ea95176
9 changed files with 27 additions and 12 deletions

View File

@@ -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

View File

@@ -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

View File

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

View File

@@ -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 }
}

View File

@@ -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')

View File

@@ -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)

View File

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

View File

@@ -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)

View File

@@ -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([