19 KiB
Agent Note: Wine 与原生 Windows 双通道拉取请求 CI
Status: implemented
English | 中文
问题
拉取请求必需的 Windows 判定既需要快速的 win32 工具链信号,也不能让聚合流程等待稀缺的 Windows 容量。Wine 通道提供这项关键路径信号,但它运行在 Linux 内核与区分大小写的 ext4 之上,要求采用 hoisted 依赖布局和由宿主侧创建的符号链接,且无法证明 NTFS、DACL、ConPTY、崩溃持久性或原生进程行为。原生串行参考流程停用期间,即使真实 Windows 内核结果不属于分支保护,常规 CI 也需要针对每个拉取请求分支头自动产出该结果。
覆盖率审计发现,PR(Pull Request)#499 已恢复确定性的原生 Windows LSP 覆盖率,后续的 GUI 分支却回放了陈旧分支状态中的 3 个临时源码排除项。当前的 LSP fixture(测试前置数据)只跳过真正属于 POSIX 的原语,除此之外还会检验受支持的 Windows 进程、传输与生命周期路径;因此,排除 connection.ts、index.ts 和 instance.ts 所掩盖的是受支持的行为,而非平台限制。
决策
ci.yml 中必需的 windows 作业仍是在 ubuntu-latest 上运行的 windows node 24 / wine blocking。它保留经过校验和验证的 Windows Node、Wine apt 与 pnpm 缓存、仅限工作区快照的 hoisted 安装,以及运行工作区构建与生产网站的共享 Wine 门禁脚本。稳定的 windows 作业 ID 仍是 all checks passed 的依赖项。已归档的 Wine 实验保留其实测取舍,而本文负责当前双通道拓扑。
每个拉取请求还会在组织自有的 dsh-windows-2025-16core 运行器上启动一个独立的 windows-native 作业,名称为 windows node 24 / native complete。该作业为工作区符号链接启用开发人员模式,通过 pnpm/action-setup 提供仓库固定版本的 pnpm,在不传输 store 归档的情况下执行不可变安装,并在原生 PowerShell 下运行 pnpm run check:ci:windows-complete。该作业被刻意排除在 all-checks-passed.needs 之外:聚合流程既不等待它,也不会因它改变结论;原生作业则保留自身未被掩盖的成功或失败结果。
windows-native 内的工作区构建、生产网站和逐文件 100% 覆盖率故障会导致该作业失败,而更广泛的静态检查、文档、包和构建产物可移植性清单仍作为观测项报告。16 核通道把覆盖率的 6 个工作线程拆分为 4 个插桩线程和 2 个免覆盖率高负载线程,同时运行 2 项顶层门禁,并允许 publint 使用 8 个工作线程。所有 Vitest 项目都使用 fork 工作线程,因为 Node 24 的 CJS lexer 致命故障现已在 Windows 和 POSIX 的共享工作线程中复现;2 项门禁的调度也避免免覆盖率的 Oxlint 探针与工作区构建争用其临时契约文件。两项真实进程或延迟语法启动可能超过 Vitest 默认轮询窗口的异步 fixture 使用显式的 5 秒等待,且不改变所断言的结果。重复执行的 lint 与快照强制检查仍由 Linux 负责。
16 核是这份清单实测得到的稳定点。相较此前双核串行作业,完整原生通道从 32 分 11 秒降至 6 分 27 秒,同时 41 项门禁全部通过,逐文件覆盖率阈值也保持不变。32 核运行仅将门禁总耗时再缩短 1.47 秒,却仍在一个 fork 工作线程中触发相同的 CJS lexer 致命故障,因此继续增加核心数没有带来可靠的墙钟时间收益。
首次原生运行暴露出两项被兼容性通道掩盖的故障。文档投影测试此前只按 / 拆分来派生图片 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 错误得到收束,不再逸出测试进程。
最后一个根路径探测失败源于扩展长度命名空间既应用到长后代路径,也应用到了驱动器根目录。Node 将裸根目录探测拒绝为 EISDIR,从而连锁影响所有会物化会话的 JSONL fixture 和组装后二进制。Windows 持久目录辅助函数现在以原生写法探测本来就很短的驱动器根目录,仅对后代路径添加命名空间;注入 Win32 路径语义的单元测试固定两种写法,原生覆盖率则验证真实文件系统。
下一次完整覆盖率运行触及的是 6 项相互独立的末端故障,不再是同一问题的连锁结果。React 队列动作覆盖现在会在 awaited act() 中解析模拟请求,再观察渲染完成后的状态。未闭合 Markdown 工作负载保留全部 6,400 个候选项,并采用显式的 3 秒覆盖率预算;异步工作区投影告警测试则为外层用例设置 20 秒预算,大于其 10 秒轮询预算。真实 Claude Code 拆卸会在所有受管句柄均报告退出后,采用带 10 次有界重试的异步递归删除,以容纳 Windows 延迟释放句柄的行为,同时不削弱完全停稳断言。
另有两个产品边界需要基础性修复。Include 的防抖配置持久化此前会从计时器启动一个无人观察的 Promise;Windows 在替换 cordis.yml 时若瞬时返回 EPERM,既可能丢失已禁用行,也会让 rejection 以未处理形式逸出。现在,vendored writer 会串行化写入,只对瞬时的访问/忙碌错误执行有界退避重试,观察每个 rejection,并在拆卸时排空最新写入;真实 Loader 组合测试会注入一次 EPERM 并证明持久化重试。Codex 0.146 在 Windows 上会把 exec_command 提供给回环 Responses 模型,却在自身路由器中拒绝模型返回的调用;这与 openai/codex#31665 跟踪的上游故障属于同一类。开发证据现锁定当前稳定版 0.147.0:重新生成的上游 schema 保留了提供方拥有的握手、线程/轮次、审批、用户输入和 elicitation 契约。当宿主无法使用 unified exec 时,Codex 可能改为提供旧版 shell_command;因此,回环模型现在会选择实际提供的命令工具,并使用该工具对应的参数形态,而不再无条件注入 exec_command。真实产品测试由此会通过各宿主的实际默认工具清单,证明无人值守拒绝不产生副作用,且整棵进程树退出。
随后的分支头精确托管运行又隔离出另外 7 项 fixture 契约。PowerShell 后台输出场景现在会等待进程完成,再排空并比较最后一段增量;pi-ai 空闲 watchdog 场景则保留 1 秒的有界关闭期限,以容纳 Windows 延迟送达的 socket 通知。异步工作区投影会在宿主解析后的根目录上填充内存文件系统。Include 重试验收现在断言注入的故障与最终持久化结果,而不再断言可能包含另一项合法串行写入的偶然 rename 总次数。LSP 的裸命令 fixture 会在 Windows 上通过 PATHEXT 提供 .cmd 可执行文件,URI 渲染预期也会区分执行环境的路径约定与测试宿主的分隔符。上述修改既没有跳过受支持路径,也没有削弱结果断言。
该次运行还暴露出语法高亮会受运行器资源争用影响,而不只取决于源文本。Shiki 的 JavaScript 引擎会把超过 3,000 个字符的 TextMate 正则推迟到首次匹配时再编译,Shiki 同时把这段编译时间计入每行 500 毫秒的 tokenization(词元化)预算。繁忙的 Windows 覆盖率工作线程因此可能在首次 TypeScript 行匹配到 const 后提前停止,并让剩余内容沿用同一关键字样式。客户端现在仍使用 Shiki 的默认正则转换,但会关闭延迟编译,并在构造单例时以不设启动期截止时间的方式,为每项启动时语法 tokenization 一段代表性样例。因此,scanner(扫描器)创建与模式编译会在用户内容进入仍为每行 500 毫秒的预算前完成。词元边界与 Markdown DOM fixture 会继续要求完整高亮结果,不接受这类部分结果流。
同一次分支头精确托管运行还表明,在标准 Windows 镜像上并发使用 3 个插桩 Vitest 工作线程并不安全:彼此独立的 Git merge 集成用例与 JSON-RPC HTTP 集成用例会同时触及默认的 5 秒上限。在那个阶段,原生通道曾暂时只为 Vitest 提供 1 个工作线程;真实 Git 子进程套件与两项真实 HTTP 组合用例则获得显式的 15 秒集成预算,其工作负载与断言均未改变。translation merge fixture 在把 import.meta.resolve('tsx/esm') 传给 Node 的 --import 时,也会保留其 file: URL;此前把它转换为盘符路径会在驱动程序输出自有恢复指引前就失败。纳入最新的 package regrouping(包重组)后,采用 fork 隔离的 JSONL 套件清单会跟随它在 packages/session/ 下的新位置,而不会悄然把这一进程绑定套件送回共享线程池。
项目 skill 组合 fixture 另有一项最终一致性竞态:宿主资源紧张时,agent 可能在 write 返回后、Chokidar 使 skill 目录缓存失效前就开始下一次模型步骤,导致替换目录消息落到后续 skill 调用之后。现在,fixture 会在写入后的工具边界等待真实注册表观察到 hot-skill,然后继续严格断言请求顺序与持久转录。生产代码仍保持异步;测试会显式等待其本来要验证的 watcher 契约,而不是依赖调度时序或接受另一个请求索引。
下一次分支头精确运行通过了全部 10,933 项插桩测试,但逐文件阈值仍在 99.95% 正确失败,从而暴露出 5 个此前恰由 Linux 覆盖的分支。新增的确定性跨平台 fixture 会分别覆盖 PTY 向后翻页拼接 scrollback、以目录作为 settings 文档、非法 SQLite 文件名,以及 regular file(普通文件)父级之下的原子写入锁。credentials provider 剩余的 stat 与模式位强制分支本质上只属于 POSIX,因此采用与持久 JSONL、storage backend 相同的窄范围、带说明的对等分支忽略;其行为测试仍会在 POSIX 上强制执行。阈值与源码文件清单均未改变。
后续分支头精确运行通过了全部 10,937 项插桩测试,并把阈值结果收窄到 99.99%。剩余的两行表明,首版 PTY fixture 已到达页偏移 helper,却只提供了两页数据;Windows 路径规范化还会在 readFile 到达 reload policy(重载策略)分支前,先拒绝真实的非法路径 fixture。PTY fixture 现在会提供 3 个向后翻页页面;watcher fixture 则会在真实权限检查之后注入一次非“文件不存在”的读取失败。两项 fixture 仍保留其本来要证明的可观察输出或 last-good snapshot(最后有效快照)断言。
再下一次运行已到达修复后的分支,但一项真实 PowerShell executor 组合用例会在生成覆盖率报告前,触及 Vitest 的 5 秒上限。该 fixture 把产品超时和测试超时都配置成了同样的 5 秒;插桩环境下,executor 因而没有余量返回其自有结果或自有超时分类。现在,命令的产品预算为 10 秒,集成测试上限为 15 秒;退出码、输出和解析后超时值的断言均未改变。
随后的分支头精确运行通过了全部 10,938 项插桩测试,并隔离出 4 个现有 fixture 依赖宿主调度的剩余位置。E2B 服务会保留真实的存活进程组清理 fixture,并另行注入并观察一次立即发生的终端自动释放拒绝,再证明服务释放会重试该终端。pi-ai 发现 fixture 改为从受控响应 body 的读取过程触发取消,不再与本地 socket 定时器竞速;persistent-bash fixture 则让 PTY 增量片段成为唯一可恢复输出,再断言渲染后的回退结果。这些用例会在每种宿主上直接执行受支持分支;覆盖率清单与分母均未改变。
POSIX 模式位、基于 chmod 的不可读状态和基于 chmod 的 writer lock 拒绝在 Windows 上没有等价机制。这些验收场景继续在 POSIX 上强制执行,并在 Windows 上跳过;内容、原子替换、符号链接安全、通过平台无关文件系统冲突验证的回滚与恢复,以及原生 Windows 长路径行为仍保有覆盖。只有本质上属于 POSIX 的源码分支带有窄范围且说明明确的分母忽略;没有任何源码文件或平台无关分支为适应这些差异而从 Windows 覆盖率中排除。
曾考虑的替代方案
让原生 Windows 成为 all checks passed 的依赖项。 这会为聚合流程提供保真度最高的 Windows 判定,但也会让每次合并等待最长的托管作业与 Windows 容量。独立结果能让该信号保持自动产生,而不改变现有必需路径。
只在拉取请求上运行 Wine。 Wine 能快速触达阻断性的 win32 工具链分支,但即使真实 NT、NTFS、PowerShell、进程或原生插件契约已经损坏,也可能报告绿灯。
将原生作业标记为 continue-on-error。 门禁失败后,该设置会让其检查显示为成功。保留普通独立作业可维持诊断结论;仅从聚合流程的 needs 中省略它,才是不阻断的机制。
只在合并后运行原生 Windows。 合并后的参考流程只能在可移植性回归进入 master 后进行诊断;它无法向评审者提供分支头精确的原生结果。
保留 GitHub 标准 windows-2025 运行器。 这个可移植的双核镜像可以可靠完成同一份清单,但串行结果需要 32 分钟,因此自动原生信号的实用性明显低于最终选择的 16 核运行器。
使用 32 核或更大的运行器。 32 核对照仅比 16 核缩短了 1.47 秒门禁总耗时,却在 Node 的 CJS lexer 中失败;此前的高并发 32 核与 64 核实验也以同类故障失败。因此,更多容量只增加了分配成本,没有带来稳定的端到端收益。
后果
Wine 保留必需聚合流程现有的关键路径和作业身份。all checks passed 变绿时,原生 Windows 仍可能处于待处理或红灯状态,因此分支保护采用 Wine 结果,而评审者和后续自动化采用独立的原生结果。
尽管如此,每个拉取请求都会获得来自真实 NT 内核、NTFS、PowerShell、Windows 进程和原生插件的信号。原生作业比 Wine 更慢,并重复执行设置流程和两项阻断构建,但它也会运行那份可移植性清单;兼容性通道隐藏的路径、watcher 与生命周期缺陷正是由该清单暴露。
维护者必须保留两种有意设计的执行拓扑:Wine 快照使用 Linux 安装加 hoisted 布局来触达 win32 二进制文件,而原生作业在 Windows 上使用不可变工作区。任一作业独有的失败都必须依据该边界分类,不得削弱或静默跳过。原生覆盖率会强制执行仓库的逐文件阈值,且不会为受支持的 LSP 行为设置仅针对 Windows 的源码排除项。原生快照仍是明确列出的缺口,不会仅由作业名称暗示已经纳入;必须先为其建立专门且经过测试的契约,才能加入原生通道。