跳转至

测试运行器内部实现

共享测试状态与进程辅助工具

TypeScript 工具链使用原生 typescript 包。pnpm tsgo 各执行通道会直接解析其可执行文件;语法与语义工具使用其不稳定 API,并明确管理进程生命周期。包根目录导出版本元数据,而 AST、文件系统和编译器 API 位于 typescript/unstable/* 下。打包的声明构建运行同一原生编译器并启用语义检查,并在打包之前验证其完整源码和包清单收据。tsdown 包装器默认一次只对一个配置进行声明构建,将原生编译器进程限制在现有 Node 堆预算内。仅运行时构建保持其并行度,显式 --concurrency 值则保留 tsdown 自身行为。Code Mode 直接执行 JavaScript,不使用此编译器;其 TypeScript 风格的工具声明是面向模型的文档。

build-all、独立 tsdown 构建、tsgo、SDK 声明准备、包边界检查以及相关 lint 使用位于 .artifacts/dist-artifacts.lock 的检出本地所有权。所有权涵盖清理、生成、缓存恢复以及消费这些输出的检查;独立检出之间互不影响。竞争命令会打印等待消息,并等待一个存活的所有者,且没有获取截止时间。编译器和构建执行的超时时间保持不变。独立 tsgo 运行会串行执行,包括仅源码检查;核心测试分片运行器在一个所有者内部保持其显式并发。这些命令运行期间,请勿手动删除 dist。所有者或嵌套包装器突然退出,或子进程清理未经验证,都会保留锁。如果所有者 PID 缺失或无法验证,或记录了子进程清理失败,则会立即获取失败,且不回收任何内容。PID 死亡并不能证明已分离的子代进程已经停止。在手动移除被遗弃的锁目录之前,请检查其中的 owner.json,并确认所有相关的构建、编译器和 lint 进程(包括已分离的子代进程)都已停止;然后重试该命令。

消耗运行时的测试通过显式构建所有者来准备检出工件,而不是通过使用 --version 启动 CLI。准备过程复用源码运行器的新鲜度检查和检出工件所有权,不涉及更新器服务或数据库维护托管。现有工件无需可写检出或服务检查。

在写入之前,自动准备要求已验证的工件隔离,或观察到处于离线状态的受管 Gateway。在 Linux 上,它通过现有原生管理器绑定读取已加载的命令位置,而不读取服务环境文件。原生 GetUnit 返回“未加载”结果表示没有已加载的运行时,而不表示其已保存的定义不存在。一个已保存但未加载的单元不会使原本可写的源码检出变得不可变;这是准入时的检查,而非服务启动排除或沙箱。物理共享的 dist 路径、不可读的工件路径、不完整的发现结果以及未知的服务状态,永远不会授予重建权限。不可变部署和已知的活跃重叠仍然会被拒绝。对于普通的独立 worktree,无需新增 CLI 标志或配置;在无法建立原生隔离时,请使用现有隔离运行器。此检查不是沙箱。显式 pnpm build、自动源码 CLI 重建以及实际更新发布均保留其现有准入策略。

Lint 在子进程汇合和工件所有权确定后,将最终失败信息输出到 stderr,包括在清理不确定时保留所有权的情况。独立 Oxlint 及其分片 CLI 以 [oxlint] FAILED (exit N) 结束;pnpm lint 拥有整个管道,并以一个 [lint] FAILED (exit N) 结束。分片进度区分 passed 和 failed (exit N),stdout 仍可用于机器可读的工具输出。成功运行没有失败尾标。子进程执行期间转发的信号以及分片超时都会导致命令失败;整机丢失或报告进程被 SIGKILL 可能导致最终行缺失。

本地插件 lint 使用 packages/plugin-sdk/dist 中的原生 SDK 声明。专用包边界编译器还使用 .artifacts/extension-package-boundary/plugins 下的七个插件 API 树。每个声明和编译所有者都会验证其消费的源码内容、继承配置、所选编译器以及完整输出清单。无关的现有源码或测试编辑保持缓存命中。解析拓扑变化会保守地使缓存失效,包括在声明根目录之外新增模块候选。过时声明在清除其私有输入收据后执行完整的原生 emit;随后成功的输出清单驱动过时声明清理。缺失或被篡改的输出会使所有者失效。内容记录位于 .artifacts/extension-package-boundary,不在打包构建清理范围之内。私有 .inputs.json 收据包含归一化的、相对于检出的源码与清单路径,而非原生 .tsbuildinfo 状态。热运行会验证这些记录,但不发出声明。

打包的声明构建和包边界记录只接受属于检出的输入 realpath(真实路径),包括编译器库、继承配置、依赖链接和包清单。这些路径共用 compileNativeProject,它使用固定版本的原生编译器异步 API 进行检查,并在内存中发射声明。其文件系统回调使检出之外的候选在原生解析读取它们之前就表现为缺失。源码与包清单读取会被直接捕获;准入判定不依赖对解析跟踪的解析。编译器版本固定于 package.json 中,因为此 API 不稳定。配置和请求的语义检查在发射之前运行。声明错误来自内存中 emit 的结果,从而避免仅为诊断而进行单独的声明转换。任何错误都会阻止工件发布。

支持嵌套的物理工作树,每个工作树都可以使用自己的 pnpm install --frozen-lockfile,即使祖先目录中包含 node_modules。祖先依赖不能满足缺失的本地输入,也不能更改输出的声明。当本地 pnpm 链接的目标保持在检出内时,这些链接仍然受支持。解析到外部的本地链接仍会以 Declaration input escapes checkout 失败,而不会发布成功记录或清理过时的声明。外部候选项即使是指向检出内部的符号链接,也仍然不可访问。热记录使用相同的输入检查。不要过滤编译器回执或移植声明以绕过这些检查。

声明的检出连接点和平台路径别名在验证和实际快照读取时映射到同一原生根。本地声明准备还会将编译器的 PWD 与其工作目录对齐,因此 shell 别名不会更改输出的清单路径。从子目录发起的调用仍然使用包含它们的检出作为所有权边界。其他 pnpm tsgo 通道继续使用原生 CLI;其包装器不会创建或复用共享的外部安装。

打包的 SDK 声明属于一个暂存所有者,由完整构建、包构建和 ciArtifacts 构建共享。在缓存未命中时,它会串行化两个规范的 tsdown SDK 组,并缓存它们完整的暂存生成。每个成功的编译器都会通过私有暂存回执提供其源代码和包清单成员关系;缺失的回执或在编译期间发生变化的输入会阻止发布。共享输入快照策略在命中时不会启动编译器,而是验证已消费的字节、继承的配置、生成器和清单输入以及解析拓扑。缓存命中会恢复到新的暂存区,并在发布前通过相同的入口和相对声明闭包检查。 所有 tsdown 声明构建(八个 SDK/统一组、工作区包和 AI 包)都使用与本地声明准备和包边界检查相同的有界编译器。成功的构建需要检出拥有的编译器库和声明依赖项;不支持共享的外部安装。编译器回执保留完整的源代码和包清单成员关系,包括 JSON 输入。默认类型根保持在检出内,显式类型根必须是本地的,并且有效的根 package.json 限定源包作用域查找。

共享快照策略仍然验证已消费的字节和解析拓扑。编译期间发生的源代码和命名空间更改会阻止接受。新的本地模块候选项会使缓存记录失效,祖先安装的出现和移除也会使其失效。外部探测始终看到缺失的文件,因此后续对祖先包内容的更改无法进入编译器的文件系统视图。 每个输出的声明必须在成功的编译器成员关系中拥有一个 source-map 所有者。打包器在其原始源路径下消费这些声明;私有编译器阶段只在其子进程结束后才移除。 编译器配置、原生可执行文件和 API 文件、输入字节以及解析拓扑都参与缓存失效。运行时模块解析保持不变。

本地准备永远不会覆盖打包的声明,也不会写入工作区转发桥接。

插件 SDK 声明准备和 scripts/run-tsgo.mjs 要求子工作完成后再报告成功。在 POSIX 上,每个都会验证其自身管理的进程组:遗留的子进程会被终止,并且命令会失败,而不是允许制品戳记或下游检查继续。POSIX 进程组无法检测故意离开该组的后代进程。

在 Windows 上,受管理的命令和 Gateway 测试实例使用保留的内核 Job。平台代码仅在请求 Windows 命令时加载;planner 导入保持独立于已安装的应用程序包。加载在 spawn 之前完成,因此监听器注册与子进程创建保持同步。工具使用 Node 内置功能解析其 worker URL,并复用来自 core 的原生 Job 绑定。启动器在能够启动命令之前加入该 Job。清理等待空 Job、leader 退出和输出关闭;仅 leader 退出永远不能证明后代已完成。终止失败会报告观察到的存活 PID,并在 Job 未解决时保留资源声明。正常的 leader 退出也会终止剩余的 Job 成员。最终化在关闭 Job 句柄之前记录其结果,包括失败时;仅关闭句柄不会验证终止或释放资源声明。拥有自己 IPC 通道的现有调用方保持直接启动契约;没有拥有 Job 的 taskkill 失败即使 leader 及其管道已关闭也保持不确定。

run-vitest(包括项目分片)、插件批次、test-live (包括 live 分片)、run-vitest-profile 以及 TUI PTY 监视器通过 TMPDIR、TMP 和 TEMP 为每个 Vitest 调用提供拥有的临时命名空间。在 Vitest 启动之前,隔离调用还会在该命名空间内接收原生 HOME 和 USERPROFILE。这保护了 worker 线程、命名内置导入和导入时捕获所使用的 home 回退;仅更改某个 worker 的 JavaScript process.env 不会更改原生线程 home 查找。每个 worker 和每个测试的 fixture home 保持独立。已安装的 Corepack 和 Playwright 浏览器缓存保留调用方选择的位置。

Gateway 端口声明保留在所有包含 Vitest 命名空间之外的公共临时目录中,并通过其显式资源所有者找到。因此,当 fixture 将其保留的 socket 交给子进程时,并行调用共享端口所有权;删除一个调用的文件不会删除另一个 fixture 的端口声明。

Live 感知设置仍会加载原始 profile,并在请求时暂存 live 状态。有界调用制品将原始 home 携带到该设置;它不会授予 live 访问权限,且 hermetic 设置从不查询它。已知的 hermetic 选择会忽略环境中的 live 和 real-home 标志。已知的完全 live 感知选择保留显式 OPENCLAW_LIVE_USE_REAL_HOME 行为。如果显式 real-home live 调用的选择混合了 home 策略或无法分类,则会在配置加载之前被拒绝,包括自定义配置和模糊的项目选择器。使用 node scripts/run-vitest.mjs <test-path> 在没有 LIVE、OPENCLAW_LIVE_TEST、OPENCLAW_LIVE_GATEWAY 和 OPENCLAW_LIVE_USE_REAL_HOME 的情况下运行 hermetic 测试,然后使用 node scripts/test-live.mts -- <live-test-path> 单独运行预期的 live 选择。启动器不会拆分运行,也不会更改 watch、filter 或 report 语义。

The namespace contains isolated homes, their JIT caches, SDK/shared-home allocation roots, and fallback SQLite state; its lifetime spans shared-worker files and module resets. On POSIX detached launches, the parent removes only that namespace after its child process group has stopped, output pipes have closed, and nested resource owners have released their pending claims, including passing and failing runs, child crashes, caught SIGINT/SIGTERM signals, and watchdog termination where supported. Explicit state, profile output, and mirror artifacts outside the namespace remain untouched. Failed or unverified group joins or unresolved nested claims retain the namespace and report the exact path for manual recovery. Nested namespaces, fixture lifetimes, and managed commands register ephemeral filesystem ownership before admitting work. Release requires positive completion evidence; caught cleanup failures, module resets, worker exit, or an intermediate runner crash cannot release a pending claim or its ancestors. Managed commands keep their existing output-drain contract; Windows Job commands also join kernel Job completion. Failed finalization never releases ownership. Stop all remaining writers before manually removing the reported exact directory. Windows and non-detached launches allocate the same isolated native home, but retain their namespace and enclosing claims with a diagnostic after child exit and pipe closure because descendant completion cannot be verified. Raw external invocations do not gain this boundary. Forced parent or supervisor death (such as SIGKILL) can prevent cleanup; unregistered descendants that intentionally escape the owned group remain outside this contract. The wrappers do not sweep old directories or infer ownership from names, ages, or PIDs. The CI shard runner also removes its default include-file and transform-cache scratch directory after every admitted group has joined. Caller-supplied scratch and persistent cache roots remain caller-owned. Unverified descendant completion retains the shard scratch directory and reports its exact path. This is home isolation, not a filesystem sandbox: explicit absolute paths, os.userInfo() account lookup, children with stripped or replaced home variables, and intentionally real-home live execution remain outside its protection.

Codex app-server fixtures await agent and shared-state SQLite drainage between cases. Their file teardown drains the shared disk-budget scan worker, preserving reuse during the file and releasing it before isolated fork shutdown.

  • src/test-utils/openclaw-test-state.ts: use from Vitest when a test needs an isolated HOME, OPENCLAW_STATE_DIR, OPENCLAW_CONFIG_PATH, config fixture, workspace, agent dir, or auth-profile store.
  • pnpm test:env-mutations:report: non-blocking report of tests/harnesses that mutate HOME, OPENCLAW_STATE_DIR, OPENCLAW_CONFIG_PATH, OPENCLAW_WORKSPACE_DIR, or related env keys directly. Use it to find migration candidates for the shared test-state helper.
  • test/helpers/openclaw-test-instance.ts: process-level E2E tests needing a running Gateway, CLI env, log capture, and cleanup in one place.
  • Docker/Bash E2E lanes that source scripts/lib/docker-e2e-image.sh can pass docker_e2e_test_state_shell_b64 <label> <scenario> into the container and decode it with scripts/lib/openclaw-e2e-instance.sh; multi-home scripts can pass docker_e2e_test_state_function_b64 and call openclaw_test_state_create <label> <scenario> in each flow. node --import tsx scripts/lib/openclaw-test-state.mts -- create --label <name> --scenario <name> --env-file <path> --json writes a sourceable host env file (the -- before create keeps newer Node runtimes from treating --env-file as a Node flag). Lanes that launch a Gateway can source scripts/lib/openclaw-e2e-instance.sh for entrypoint resolution, mock OpenAI startup, foreground/background launch, readiness probes, state env export, log dumps, and process cleanup.

createOpenClawTestState selects and owns temporary paths and process environment selectors. It is not filesystem sandboxing and does not stop external producers. Await its asynchronous restoreEnv(); stop and join required producers before restoring selectors or removing state. Runtime reproductions of state-selection leaks require enforced storage isolation, such as a VM or container without access to operator stores, not merely temporary HOME or state-directory overrides.

Public test diagnostics

The shared Vitest reporter factory redacts credential-shaped fields in assertion messages, diffs, expected/received values, stacks, source excerpts, and annotations before forwarding them to the selected reporters. Keys remain visible and values become <redacted len=N>. This also applies to explicit --reporter selections, UI/browser configurations, and JSON/JUnit reports. Hosted logs are public, and runner-issued tokens may not be registered for GitHub masking.

Unquoted environment records use one assignment per line: spaces and punctuation on the right-hand side belong to that value. Multiline strings split into quoted fragments by Node's inspector are redacted as one value.

Redaction is unconditional and affects diagnostic output, not assertion behavior. Test console capture is outside this boundary; tests must still avoid logging credentials directly.

Configured extension fork projects use the openclaw-forks diagnostic adapter around Vitest's native fork transport. If the existing stop deadline fails while the child remains alive, the adapter spends at most two additional seconds collecting a Node report before native termination and pipe cleanup. The timeout remains a test failure. The report distinguishes a missing stop acknowledgement from a stall after acknowledgement and includes native stacks, libuv handles, and worker-thread reports. Environment variables, command arguments, and socket endpoints are omitted. A blocked event loop can prevent signal reporting; that case explicitly reports that no complete report was captured.

成功关闭时保持静默。显式指定 --pool=forks 会选择 Vitest 的内置池并绕过此适配器。

跨原生进程的 JSON 报告

对于多项目或分块运行,请显式请求带输出文件的原生 JSON,例如:

pnpm test test/vitest/vitest.unit-fast-isolated.config.ts test/vitest/vitest.agents-embedded-agent.config.ts --reporter=verbose --reporter=json --outputFile=.artifacts/test-results.json

项目运行器和插件批量运行器会为每次尝试生成独立的原生 JSON 和 blob 文件,然后从 Vitest 的原生报告合并中发布所请求的 JSON。它们会打印一个伴随的 <output>.reports-<unique> 目录。请保留该目录:其中包含原始报告、启用覆盖率时的每次尝试覆盖率文件,以及一个 index.json,其中包含子进程退出码、信号、超时和未开始的工作。每次调用只运行一次。无输出超时会使命令失败,并使报告集不完整;它永远不会启动替代尝试。Blob 报告是精确版本产物。在合并由其他版本生成的产物之前,请使用当前 Vitest 版本重新运行子报告。

聚合结果保留已接受的用例清单,但不是原始报告的无损替代。原生合并不会恢复快照摘要或 JSON coverageMap,其 startTime 是合并时间。通过的快照测试仍然成功。请阅读原生原始报告以获取这些详细信息,并阅读索引以获取进程结果:JSON success 并未编码所有包装器或未处理错误失败。独立的内置覆盖率报告仍按尝试保留在伴随目录中。自定义覆盖率提供程序/报告器以及覆盖率报告器元组选项需要以唯一目标进行单独调用。

完整的失败测试聚合结果会随失败的命令退出而保留。证据缺失或无效、取消、必需工作未开始或发布失败时,不会发布完整聚合结果;已存在的输出文件不是新运行的证明。诊断信息会打印保留的报告集位置。报告集不会被自动清理。

重叠的选择可能共享原生任务 ID,因此合并它们可能会替换独立的失败详情,即使用例数量匹配。此类报告集会保留其原始报告并导致发布失败。请只选择每个配置一次,或分别运行重叠的选择并使用不同的输出文件。

此归属规则适用于使用命名、基于文件的 Node 项目和原生控制台报告器的显式 CLI JSON 文件请求。标量 --outputFile 和 --outputFile.json 均可使用。由配置拥有的报告器选项、其他文件格式、自定义报告器以及内联/浏览器项目组合需要以唯一输出目标进行单独的原生调用。不要假设这些输出已被聚合。单进程和仅控制台运行保持其现有原生行为。原生帮助和其他非测试控制保留在子 CLI 中,并且不分配报告集。run --version 仍然会运行测试,正如原生 Vitest 中一样。仅配置的报告器不会被拦截:多个子进程仍可能覆盖同一配置文件。请分别运行这些配置并使用不同路径;仅添加 --reporter=json 不会覆盖报告器元组自身的 outputFile。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw