fix(windows): normalize native coverage tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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: [] })
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user