WSL Runner 三层验证体系:Orca 如何测试 W1–W3 的 Windows/WSL 执行边界
2026/9/8 22:23:22 网站建设 项目流程

WSL Runner 三层验证体系:Orca 如何测试 W1–W3 的 Windows/WSL 执行边界

【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca

W1–W3 是 Orca(面向并行 Agent 舰队的多平台 ADE)在 Windows 上打通 WSL 调用链的三层改造:W1 收敛"所有 WSL 调用只经过同一个 spawn 入口"、W2 负责 Windows 子进程的所有权与清理、W3 将"在 WSL 发行版里运行程序"收敛为唯一一个 runner(即 wsl-runner.ts)。围绕这套改造,Orca 在 wsl-runner-verification.md 中沉淀了一套"单元测试 + 静态守卫(ratchet)+ 真二进制测试"的三层验证体系,因为每一层都能捕获其它层在结构上无法捕获的问题。读完本文,你将掌握 Orca 为这条跨进程边界设计的完整回归防线:每个测试套件盯住什么、每条守卫如何做到"只降不升"、真实的 ConPTY 与真实 WSL 发行版测试如何运行,以及守卫自身已知的盲区该如何应对。

为什么是三层:一次 WSL 调用在结构上容易漏掉的三种错误

src/main/wsl/wsl-runner.ts 的文件头注释写得很直白:"Orca 在 WSL 内运行程序的唯一地点",而每次调用都有五件事需要决定——分隔符(--execvs--)、shell、stdout 围栏(fencing)、WSLENV、payload 传输方式——这五件事每一项都曾以错误形态发布过(对应缺陷号 #12964、#14288 / #9768 / #9725、#11327、#12557、#14292)。而 wsl-w1-w3-contract.test.ts 的注释进一步点明三层验证的由来:

一次 WSL 调用 = 一次 Windows spawn = 一个子进程,W1–W3 各自修复了同一次调用的一个层面,失败会叠加,因此必须组合验证。

换句话说:单元测试能在纯逻辑层面逐条锁定契约;守卫(ratchet)能在仓库尺度上持续拦截新写的违规代码;而只有"真二进制"测试才能在真实 Windows 进程树与真实发行版上断言那些模拟永远无法给出的结论。这就是文档第一句"三种覆盖层次,因为每一种都能捕获其它层在结构上无法捕获的东西"的含义。

第一层:单元测试——每次 PR、所有平台都跑

套件钉死的内容(Pins)
src/main/wsl/wsl-runner.test.ts分隔符、lane 选择、fencing、WSLENV、guest cwd、脚本解释器、预算拆分、拒绝"未解析的 PATH"
src/main/wsl/wsl-guest-environment.test.tsburst 坍缩、按发行版隔离、畸形 payload 拒绝、transient 与 permanent 区分、重试窗口、joiner 预算
src/main/wsl/wsl-w1-w3-contract.test.tsW1→W3 链端到端:绝对路径wsl.exe、argv 数组、有界调用、不使用--、脚本字节级一致、WSLENV、探测 lane 不跑 shell、login PATH 仍被应用
src/shared/source-scan/source-tree-scan.test.ts守卫辅助函数(guard helpers)。一个漏报的守卫比没有更糟

这套件划分体现了责任边界:runner 的决策正确性(每调用五件事怎么选)由wsl-runner.test.ts负责;guest 环境的缓存与容错语义wsl-guest-environment.test.ts负责;而 W1–W3 是横切整个调用链的契约,所以单设wsl-w1-w3-contract.test.ts做端到端断言。

从源码看 runner 测试锁住的四个关键决策

打开 src/main/wsl/wsl-runner.ts 可以看到,这四个"Pins"分别对应真实的防御代码:

  • 分隔符与 argv 数组。runner 永远用buildWslExecArgs拼出wsl.exe ... --exec形态,测试则断言args是数组、'a b'这样的带空格参数原样保留(wsl-w1-w3-contract.test.ts)。注释写明:命令字符串会把引号责任交回给调用方,这正是 W1 想消灭的整类问题。
  • "不使用--"。契约测试不是简单断言"没有--",而是检查wsl.exe自身分隔符的位置——因为sh -s --中属于 guest shell 的--是合法 end-of-options(wsl-w1-w3-contract.test.ts),对应仓库里独立的 wsl-exec-mode-separator.test.ts。原因在源码注释:--会让$name之类在宿主机侧先被展开。
  • 脚本字节级一致。曾经存在 base64 + eval 包装,因为宿主机 shell 会重新解析引号(#14292);现在脚本原样进入 argv、stdin 留给命令本身(wsl-w1-w3-contract.test.ts)。测试用的 payload 同时包含单引号嵌套与case语法这类历史雷区。
  • 探测 lane 不跑 shell,但 login PATH 仍被应用。探测 lane 必须无 shell(否则~/.profile里一行sleep就能吃掉整个超时,即 #14288),可 nvm 这类装在用户 PATH 里的二进制又必须找得到(#9725、#7563、#8366)。于是代码用env二进制做PATH=... HOME=... <program>前缀(wsl-runner.ts),测试同时断言"argv 中不含_orca_wsl_shell"且"包含PATH=/home/u/.nvm/bin:/usr/bin"(wsl-w1-w3-contract.test.ts)。

从源码看 guest 环境测试锁住的缓存语义

src/main/wsl/wsl-guest-environment.ts 是整个登录环境探测与缓存的实现,单元测试锁定的每条语义都能在源码里找到对应物:

  • 畸形 payload 拒绝:探测脚本以 NUL 分隔输出PATH\0HOME\0env三个值,解析器要求三个都是"干净的绝对路径"(以/开头、无换行、PATH 不超 32768 字节),否则判定探测失败(wsl-guest-environment.ts)。
  • transient vs permanentexit 127("找不到可用 env")才是永久性拒绝;超时、非 0 但非 127 的退出码、甚至无法解析的 payload 都被归类为 transient——因为畸形 payload 通常只是 chatty 的 rc 文件在 64 KiB 输出上限处截断了围栏,会自行恢复(wsl-guest-environment.ts)。
  • 两类重试窗口:transient 失败 5 秒后可重试(TRANSIENT_RETRY_MS = 5_000),permanent 拒绝则 10 分钟后才解禁(REJECTED_RETRY_MS = 10 * 60_000)——因为apt升级窗口也会短暂表现为"没有可用 env",若无过期时间,一次此类事件会在进程生命周期内禁掉该发行版上所有 WSL 功能(wsl-guest-environment.ts)。
  • burst 坍缩与 joiner 预算inFlightMap 让同一瞬间的并发请求坍缩成一次探测;joiner 不再傻等发起者用完整个预算,而是与自己的budgetMsPromise.race,避免"发起者耗尽预算导致 joiner 到自己命令时只剩 1ms"(wsl-guest-environment.ts)。测试还覆盖 1.5× 预算差触发重新探测、显式all标志区分"刷新全部发行版"与"只刷默认发行版"等边界(wsl-guest-environment.ts)。
  • runProcess 的 rejection 必须被吞掉:子进程无法启动(宿主机没有System32\wsl.exe的 ENOENT、内存压力下的 EAGAIN)时 runProcess 会 reject,若不 catch,这个 rejected promise 会一直驻留在inFlight里,让进程生命周期内所有后续 WSL 调用全部抛错直到重启(wsl-guest-environment.ts)。

值得一提的还有 runner 的预算拆分拒绝未解析 PATHrunWslProcess把探测预算压到"剩余预算的一半且上限 4 秒"(wsl-runner.ts),防止 5 秒的调用方把 3333ms 花在探测上、只剩 1667ms 给自己;缺失 login PATH 时调用照常运行于发行版默认 PATH,只在WslResult.environmentResolved上如实报告false(wsl-runner.ts),把"装了 nvm 却查不到二进制"这类问题显式暴露给调用方而不是默默吞掉。

第二层:守卫(Ratchets)——持续强制推进的"球门柱"

单元测试只能验证被写出来的用例,无法阻止未来在 runner 之外新开wsl.exe调用。第二层是仓库级的静态守卫:

守卫度量内容
src/main/wsl/wsl-invocation-boundary.test.ts在 runner 之外产生wsl.exe的文件;以及声明了shell: 'bash'之外的 bash-only payload
src/shared/child-process/windows-console-visibility.test.ts缺少windowsHide的直接子进程调用
src/shared/child-process/child-process-import-boundary.test.ts在 chokepoint 之外 importchild_process的文件
src/shared/wsl-exec-mode-separator.test.ts被禁用的--分隔符
src/main/pty-descendant-termination-job-coverage.test.ts每次进程清扫(sweep)都经过terminateOwnedTree

守卫的底层辅助逻辑集中在 src/shared/source-scan/source-tree-scan.ts(其自身由 source-tree-scan.test.ts 测试),这也是为什么文档强调"一个漏报的守卫比没有更糟"——守卫本身是扫描器,扫描器出错就会放过真违规。

守卫能"只降不升"的机制

这些守卫并非一次性审计,而是可度量的门槛:它们统计的违规数被固定进基线,每一个守卫都会在新违规者身上失败,也会在"过期条目"上失败(守卫在修复代码后仍会指向旧记录),因此计数只可能下降、不可能回升。文档特意给出验证方法:

种植一个违规,看它是否被点名——这个步骤已经三次发现了守卫自身的 bug。

失败模式保持可区分

单元层同样覆盖"失败必须可区分":非零退出码是数据而非异常——runProcess在非零退出时正常 resolve,调用方必须能看到code而不是被异常甩过(wsl-w1-w3-contract.test.ts);超时则显式上报timedOut: true,绝不伪装成空答案(wsl-w1-w3-contract.test.ts)。这是"探测失败 ≠ 工具不存在"语义的根基:调用方拿不到 PATH 时应当报"无法验证",而不是误报"未安装"。

第三层:真二进制测试——其它任何断言都无法给出的结论

Windows CI:真实 ConPTY 上的进程树验证

文档指明,Windows CI(.github/workflows/pr.yml 中的package (windows)job)会从打了补丁的源码重建 node-pty,并针对真实 ConPTY 运行win32套件,验证三件事:

  • 一个真实脱离的孙进程(detached grandchild)能被创建;
  • 一次真实的 Job kill能真正杀掉它;
  • 反过来——一次干净的exit必须把后台工作留在原处(不能误杀)。

这层覆盖的是 node-pty 这类原生依赖在补丁后的真实行为,模拟层无法替代。仓库中相关补丁脚本可在 config/scripts/rebuild-native-deps.mjs(native 依赖重建)与 config/relay-assets/node-pty-1.1.0-console-list-agent-patch.cjs 等补丁中追溯,而pty-descendant-termination-job-coverage守卫则从静态侧保证清扫路径全部经过terminateOwnedTree

真实 WSL 发行版:不在 CI 中,必须手动跑

WSL 在托管 runner 上不可用,因此这层不在 CI 中,需要开发者本机执行(wsl-runner.wsl.test.ts):

ORCA_REAL_WSL_RUNNER_TEST=1 ORCA_WSL_TEST_DISTRO=Ubuntu-24.04 \ pnpm vitest run src/main/wsl/wsl-runner.wsl.test.ts

测试通过两个环境变量门控,且仅在win32平台生效(非 Windows 或未设变量时整组describe被跳过,见 wsl-runner.wsl.test.ts)。它做的事比看起来更"危险":

  1. 复现 #14288,而不是模拟:先备份发行版的~/.profile,再向其中追加一行sleep 60,然后断言探测 lane 仍能在自己的预算内应答(wsl-runner.wsl.test.ts)——60 秒阻塞远超探测预算,而调用必须带着orca-probe-ok的输出、在 20 秒内完成,证明"失败的探测不致命、调用走降级路径照常应答"。afterAll会还原 profile 并删除备份。
  2. 断言第二次调用不再支付 login shell 的开销(缓存生效,应快于 5 秒)。
  3. banner 剥离:stock Ubuntu 会把 rc 提示写进 stdout,任何解析该流的代码都会把 banner 当数据读进去(#11327、#11823),因此断言stdout恰为ORCA_PAYLOAD
  4. 携带引号与$的脚本字节级到达:payload 同时包含printf '%s\n' "$1"、单引号嵌套echo 'it'\''s fine'awk '{print $1}',断言三段输出逐一还原(对应 #12964、#14292)。
  5. WSLENV 跨边界传递:宿主机env中的ORCA_WSLENV_PROBE在 guest 内原样读到crossed
  6. guest cwd:以 guest(POSIX)路径进入工作目录后执行。

文档对它的定位很明确:在改动src/main/wsl/之前运行它——这是"探测 lane 真的如工作流所宣称的那样工作"的唯一证据,而且它已经在 CI 全绿的情况下、因 CI 跳过它而对一次 runner 改动过期失效过一次

windowsHide 守卫的已知缺口:记录而非掩盖

文档专门用一节"记录"守卫的已知边界,理由是:看起来完整的守卫比一个有据可查的缺口更糟。三条缺口如下:

  • fork不被扫描。Node 运行时会把windowsHide转发给spawn,但ForkOptions类型并未声明该字段,因此现存两个使用位点(daemon 与 plugin host 的子进程)在不做类型断言(cast)的情况下无法修复——而这两个位点都是 Windows 上的 console-subsystem 子进程。
  • allowlist 是文件粒度的,被放行的文件等于"失明"。其中约 18 个条目是误报(RegExp.prototype.execprovider.exec,以及 lexer 失步的文件),每一条都等于给该文件里的真实回归发了一张长期"预批准"。这些条目无法靠修代码退役,因此该清单永远无法归零——改为调用粒度(call-granular)才是正解。
  • stripComments没有失步报告。失败关闭(fail-closed)检查作用在已剥离注释的文本上,因此含/ *的正则字面量仍可能悄悄吞掉代码。目前src/中尚无实例,但守卫自身无法发现这种失步。

这三条记录的共同点是诚实:它们既写清了影响,也给出了演进方向,让后来者知道"当前防线到哪为止"。

如何验证一次守卫修改

本节给出守卫开发的方法论,且来自真实教训:

本工作流中每一个"只靠读代码验证"的守卫修复都是错的——连续三次对精确 lexer 的尝试,每次发布的失步都降低了违规计数,看起来像进步。

因此必须种植违规并看着它失败。文档要求至少种植以下六种形态:

  1. 一次普通调用;
  2. 一个 template-literal 密集文件中的调用;
  3. 一个正则密集文件中的调用;
  4. windowsHide: false
  5. 三目运算符作第一参数的调用;
  6. 重命名过的 import。

只有这六种形态全被点名,才能确认守卫改动真的守住了边界,而不是在 lexer 失步后自我感觉良好地"通过"。

如何把这套验证用起来

如果你在为本仓库贡献 WSL 相关改动,落地顺序建议是:

  1. 每次提交前:让第一层单元测试在本地跑通——它们无平台依赖,任何环境都能运行。
  2. 改动src/main/wsl/:按上文命令运行真实发行版测试(需 Windows + 已安装目标发行版,测试会临时改动其~/.profile,务必让afterAll完成还原);同时留意其"CI 会跳过、可能悄悄过期"的教训——它过期时不会有人替你发现。
  3. 改动任何 spawn/子进程代码时:确保第二层守卫(windowsHidechild_processimport 边界、--分隔符、terminateOwnedTree覆盖)在你的 diff 上全绿,并主动检查是否触碰了三个已记录缺口之一(fork位点、文件粒度 allowlist、stripComments失步)。
  4. 守卫自身有改动时:不要只读代码,按上节的六种形态种植违规逐一验证。

这套体系的价值在于把"WSL 调用正确性"从一次性人工审计变成了可持续的多层防线:单元测试守住决策逻辑,静态守卫守住仓库尺度的蔓延,真二进制测试守住模拟永远够不到的真实进程树与真实发行版语义——而文档与源码中随处可见的缺陷号(#14288、#9725、#14292……)时刻提醒着,这三层中的每一层,背后都是一次真实到达过用户的线上故障。

【免费下载链接】orcaOrca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.项目地址: https://gitcode.com/GitHub_Trending/orca48/orca

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询