52 lines
9.8 KiB
Markdown
52 lines
9.8 KiB
Markdown
# Agent Note: 原生 Windows 拉取请求 CI
|
||
|
||
Status: implemented
|
||
|
||
[English](2026-08-08-native-windows-pull-request-ci.md) | 中文
|
||
|
||
## 问题
|
||
|
||
拉取请求必需的 Windows 判定必须为依赖操作系统的行为提供保障,而不能只覆盖由 `process.platform` 选择的工具链分支。Wine 通道在 Linux 内核与区分大小写的 ext4 之上执行 Windows Node 和 PE 二进制文件,要求采用 hoisted 依赖布局和由宿主侧创建的符号链接,也没有覆盖 NTFS、DACL、ConPTY、崩溃持久性与更广泛的观测性 Windows 清单。原生串行参考流程停用期间,常规 CI 没有任何真实的 Windows 内核信号。
|
||
|
||
覆盖率审计发现,PR(Pull Request)#499 已恢复确定性的原生 Windows LSP 覆盖率,后续的 GUI 分支却回放了陈旧分支状态中的 3 个临时源码排除项。当前的 LSP fixture(测试前置数据)只跳过真正属于 POSIX 的原语,除此之外还会检验受支持的 Windows 进程、传输与生命周期路径;因此,排除 `connection.ts`、`index.ts` 和 `instance.ts` 所掩盖的是受支持的行为,而非平台限制。
|
||
|
||
## 决策
|
||
|
||
[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 负责。
|
||
|
||
首次原生运行暴露出两项被兼容性通道掩盖的故障。文档投影测试此前只按 `/` 拆分来派生图片 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 秒预算。这些都是针对受支持测试与解析器行为的可移植性修复,不是按平台跳过测试或设置覆盖率排除项。
|
||
|
||
这项阻断覆盖率门禁又暴露出两项从未在原生通道上运行过的 fixture 契约。JSONL 实体化故障场景现在断言结构化文件系统错误码,因为 Windows 的持久目录实现拥有 `ENOTDIR` 错误码,却不会将其复制进人类可读文本。ACP(Agent Client Protocol)拆卸阶梯现在使用 Node 子进程,不再假定 POSIX shell,并断言 Windows 的强制终止结果而非 POSIX 信号名称;POSIX 仍会证明 `SIGTERM` 与 `SIGKILL` 两级。这些套件会加载原生绑定或拥有真实进程树,因此 Windows 线程池会让它们在现有的 fork 隔离项目中运行,同时仍将这些套件的覆盖率汇入同一项逐文件阈值。
|
||
|
||
分支纳入更新的 `master` 后,下一次原生覆盖率运行发现了最后一条未纳入统一契约的 watcher 路径和一项压力测试预算。`skill-local` 曾以配置时的路径拼写打开现有 Chokidar 根,因此 `%TEMP%` 仍可能以 `C:\\Users\\RUNNER~1` 进入 libuv,而事件使用长目录名;现在它的根模式与祖先模式共用规范化监听路径契约,发现过程仍保留配置路径。新增的 10,000 会话后代遍历在 Windows 覆盖率插桩下还会超过 Vitest 默认超时,因此该栈安全工作负载保持原有规模并获得显式的 20 秒压力测试预算,而不是缩小深度或按平台跳过。
|
||
|
||
下一次分支头精确运行暴露出观测项中剩余的一项 built-bin 故障:其生命周期 fixture 通过 `process.kill()` 或 `subprocess.kill()` 发送 `SIGTERM`;在 Windows 上,这种调用会无条件终止目标进程,而不会交付为优雅释放所注册的进程事件。POSIX 验收仍发送真实信号。在 Windows 上,fixture 改为从子进程内部请求同一个已注册事件:自终止探测直接请求,由父进程控制的生命周期场景则通过标记请求;因此,完整组装后的关闭与释放路径仍得到覆盖,也无需断言操作系统提供了本不存在的信号机制。该项验收随即暴露出底层的提前关闭竞态:boot 返回后,回退 HMR watcher 仍在挂载,此时信号可能对根 fiber 执行 dispose(资源释放),由此产生的服务未激活错误会逸出并被报告为 boot 失败。boot 后 setup 现在只会在权威根 fiber 仍处于活跃状态时接纳工作;只有当本次调用所记录的信号已取得关闭流程所有权时,才会隔离并发 setup 错误,无关的 HMR 故障仍会响亮失败。
|
||
|
||
运行完整的覆盖率插桩图而非此前缩减的清单后,剩余的跨平台 fixture 契约也显现出来。Windows 路径标识现在会在比较或构造 loader 符号链接前处理 8.3 别名、原生分隔符、Git 检出换行、跨盘符相对路径与文件 URL。JSONL 持久目录辅助函数会对探测与临时目录创建应用扩展长度命名空间;真实产品测试会调用可移植的可执行入口,并以有界重试容纳 Windows 句柄释放;压力测试则保留原工作负载并获得显式的覆盖率预算。如果凭据文档或监听路径最深的现有祖先是文件,所有宿主现在都会返回 `ENOTDIR`;`skill-local` 同时改用由 effect 拥有的持久 Chokidar 句柄,使异步 libuv 错误得到收束,不再逸出测试进程。
|
||
|
||
POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。没有任何受支持的产品源码为适应这些差异而从 Windows 覆盖率中排除。
|
||
|
||
受支持的工作流不含 Wine 专属基础设施:不存在 apt 缓存生产者、兼容性脚本、对仓库快照执行的 hoisted 安装、Windows Node 下载或本地 `check:windows-wine` 命令。[已归档的 Wine 实验](../../archived/process/2026-07-27-wine-windows-gates-experiment.md)仍作为其实测延迟与保真度取舍的历史证据,而非当前执行路径。
|
||
|
||
## 曾考虑的替代方案
|
||
|
||
**在必需路径上保留 Wine。** 其热运行墙钟时间接近 Linux CI,也会选择 win32 工具链分支;但兼容性专属布局和内核缺口可能在受支持的原生行为已经损坏时仍报告绿灯。这项延迟收益不足以抵消该信号缺失的代价。
|
||
|
||
**逐字恢复 Wine 之前的工作流。** 旧定义确实捕获了正确的运行器边界,但也携带了当时的供应方式与拓扑假设。按照当前 action、pnpm 设置、门禁图和聚合依赖关系重新构建原生作业,可以避免重新引入过时机制。
|
||
|
||
**只在合并后运行原生 Windows。** 合并后的参考流程只能在可移植性回归进入 `master` 后进行诊断;当这些参考流程被停用或延迟时,无法在拉取请求阶段提供保护。
|
||
|
||
**使用组织自有的大型 Windows 运行器。** 更大规格的运行器镜像可以缩短墙钟时间,但必需的正确性路径将因此依赖仓库外部的运行器标签与分配能力。标准 `windows-2025` 是可移植恢复边界;大型运行器仍作为基准测试目标。
|
||
|
||
## 后果
|
||
|
||
在聚合作业通过之前,拉取请求会获得来自真实 NT 内核、NTFS、PowerShell、Windows 进程和原生插件的信号。该作业比 Wine 兼容性通道更慢,也可能因 Windows 容量而排队,但其绿灯结果描述的是受支持的宿主,而非近似环境。
|
||
|
||
原生通道也是一份可移植性清单:其分支头精确验收要求每个阻断门禁都通过,且最终汇总中不包含任何非阻断失败。这一区分发现了路径契约问题;若只看包装层的成功结论,这些问题本会被掩盖。
|
||
|
||
移除 Wine 缓存生产者和本地脚本后,独立的安装拓扑及其反复出现的兼容性故障也随之消失。原生 Windows 覆盖率现在通过同一项必需作业运行,并强制执行仓库的逐文件覆盖率阈值;该覆盖率门禁不会为受支持的 LSP 行为设置仅针对 Windows 的源码排除项。原生 Windows 快照仍是明确列出的缺口,不会仅由作业名称暗示已经纳入;必须先为其建立专门且经过测试的契约,才能成为该必需通道的一部分。
|