# Agent Note: 让受支持平台的测试聚焦语义

Status: implemented

[English](2026-07-22-cross-platform-test-fixtures.md) | 中文

## 问题

单元测试与覆盖率测试套件会在 Windows、macOS 和 Linux 上运行，但平台无关行为可能被平台特有的 fixture（测试前置数据）掩盖。字面 POSIX 路径在 Windows 上会变成相对于驱动器的路径；带主机名的 `file:` URI 在 Windows 上可能是有效的 UNC 路径；子进程管道关闭或事件循环调度在不同宿主上的稳定时点也不一致。FIFO、可执行模式位和目录搜索权限位等仅存在于 POSIX 的文件系统状态，在 Windows 上没有可直接构造的 fixture。

把 fixture 语法当成产品行为，要么会误报回归，要么会促使生产代码引入抹去原生路径语义的归一化。

## 决策

测试平台无关行为时，使用宿主的 `node:path` 和 `node:url` API 构造绝对路径与 `file:` URI，再根据契约要求断言原生绝对输出或稳定的工作区相对输出。无效 URI fixture 使用一种在所有受支持平台上都会被 `fileURLToPath()` 拒绝的编码形式。

传输故障测试会注入连接的消息写入器，并传入与真实 Node 流相同的异步写入回调错误。生产写入器仍会把分帧消息写入子进程 stdin。这种方式让真实子进程保持存活，使测试无需触及平台特有的管道句柄，也能确定性地区分传输故障与进程退出。

语言服务器的资源清理会终止整棵后代进程树：POSIX 使用负数进程组 ID，Windows 同步执行 `taskkill /T /F`。Windows 只会忽略 taskkill 返回的「进程树已经不存在」状态；命令执行失败、权限错误及其他终止进程树的失败仍属于资源清理失败。只读的提供方查询仅在选定的池化传输于该次查询开始前或执行期间失效时重试一次；服务器仍存活时返回的错误不会重放。终端测试会等待可观察的渲染输出，不假设一次事件循环轮转已经足够。

对于真正仅存在于 POSIX 的原语，测试只在该用例上排除 Windows。相邻的跨平台用例仍会固定拒绝非普通文件、不可用命令和无法访问的工作目录的行为。Windows 上受支持的路径仍受逐文件覆盖率门禁约束，不会随测试文件一起排除。

## 曾考虑的替代方案

**将所有路径和 URI 归一化为 POSIX 字符串。**这会使断言保持一致，但也会改变正确的 Windows 行为：外部路径是原生绝对路径，UNC 文件 URI 有效，而且已配置的主目录会按照宿主路径规则解析。

**操纵子进程管道内部状态，直至写入失败。**CRT 描述符与 libuv 句柄在不同宿主和 Node 版本上的所有权不同，因此这种做法测试的是未文档化的 fixture 机制，而非连接的写入失败契约。

**在 Windows 上跳过整个测试文件或包。**过宽的排除会隐藏受支持的行为。只排除无法在 Windows 上构造相应状态的单项 fixture；相关契约仍保持覆盖。

## 后果

可移植 fixture 需要更显式地构造，因为预期路径要从共享的原生常量派生，传输故障则通过狭窄的写入器 seam 注入。仅适用于特定平台的排除项必须配有相邻的跨平台断言，以继续覆盖相应的产品行为。协议级优雅关停失败后，Windows 上的资源清理依赖宿主的 `taskkill` 命令；命令同步执行成功时，dispose 的完成边界明确，并确保清理返回前即可观察到后代进程退出；若进程树终止失败，dispose 的调用方仍能观察到该失败。
