docs(i18n): proofread active Chinese documentation
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-11-property-based-testing.md
|
||||
2026-06-11-property-based-testing.md: a1bd4147a26a3d562899310e238096939fc2d01a
|
||||
2026-06-11-property-based-testing.zh.md: 0e1934a24fcf22442420a664c9824bb74c0fe7f7
|
||||
2026-06-11-property-based-testing.zh.md: 062b2df76597ba16a7ad0682d57b35e54a07d9bd
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
引入 `fast-check`(作为根 devDependency),在每个协议形态的包(package)中编写一个 `tests/properties.spec.ts`。生成器调优为*逼真但对抗性*的输入(而非均匀噪声),`numRuns` 控制在本地套件总耗时远低于约 10 秒。失败时打印可复现的 seed。(原始提案还草拟了一个夜间 CI job,以 100 倍迭代运行;该部分未交付。属性测试套件仅在常规的 `push`/`pull_request` CI 中运行,定时高迭代 job 仍属可能的后续工作。)
|
||||
引入 `fast-check`(作为根 devDependency),在每个协议形态的包中编写一个 `tests/properties.spec.ts`。生成器调优为*逼真但对抗性*的输入(而非均匀噪声),`numRuns` 控制在本地套件总耗时远低于约 10 秒。失败时打印可复现的 seed。(原始提案还草拟了一个夜间 CI job,以 100 倍迭代运行;该部分未交付。属性测试套件仅在常规的 `push`/`pull_request` CI 中运行,定时高迭代 job 仍属可能的后续工作。)
|
||||
|
||||
- **dsh-llm / BlockAssembler:** 任意分片流(合法 + 畸形:重复索引、滞后分片、缺少 block-start)。不变式:`blocks()` 计数 ≤ 已见到的不同索引数;重组幂等(`blocks()` 在重复调用间稳定,且 `message().content` 与之一致);`blocks()` 从不抛异常且仅产出合法的 content-block 标签;`finish` 反映最后一个 `finish` 分片,无此类分片时默认为 `{kind:'stop'}`。
|
||||
- **dsh-session:** 任意事件日志。不变式:`deriveMessages` 确定性;从 seed 回放结果一致;seq 严格单调递增;非消息事件不影响推导出的历史;推导出的内容与日志解耦。
|
||||
@@ -23,7 +23,7 @@ Status: implemented
|
||||
|
||||
- 生成器质量是价值杠杆——生成器偏向小索引池和短字符串,使碰撞与交错频繁发生。
|
||||
- **它已经带来回报:** BlockAssembler 流发现了一个真实 bug——同一索引处重复的 `block-end` 会改写已经完成的块。现已修复(首次关闭优先,与现有迟到项规则一致),并加入专用回归测试。
|
||||
- 属性测试因超时而 flake 是一个发现,不应通过重试消除。循环属性测试在设计上是确定性的(通过 `agent/status` settle),因此挂起即为真实缺陷。
|
||||
- 属性测试因超时而 flake 是一个发现,不应通过重试消除。agent loop(智能体循环)的属性测试在设计上是确定性的(通过 `agent/status` settle),因此挂起即为真实缺陷。
|
||||
- 属性测试是对示例测试的补充而非替代;示例测试固定特定分支,服务于 100% 覆盖率门禁。
|
||||
|
||||
<!-- agent-note-format: alternatives-not-recorded (pre-format Agent Note) -->
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-19-acp-snapshot-tests.md
|
||||
2026-06-19-acp-snapshot-tests.md: e118ada58230fe31fbb2a6bffb83e5612757ab1f
|
||||
2026-06-19-acp-snapshot-tests.zh.md: 5c239d2c2589708050cd6199231ec5047f225107
|
||||
2026-06-19-acp-snapshot-tests.zh.md: b08ac985ee3d4e00dc478df8cf99dc415eb2985e
|
||||
|
||||
@@ -6,11 +6,11 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
单元测试不会覆盖组装后的完整 agent(智能体)子进程及其 ACP(Agent Client Protocol)自动化线协议,而真实 API 测试不具确定性且受密钥门控。因此,即使单元覆盖率为绿色,Loader 接线、后端行为和协议输出仍可能回归,[默认导出事后分析](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)已经证明了这一点。
|
||||
单元测试不会覆盖组装后的完整 agent 子进程及其 ACP 自动化协议格式,而真实 API 测试不具确定性且受密钥门控。因此,即使单元测试覆盖率检查通过,Loader 接线、后端行为和协议输出仍可能回归,[默认导出事故复盘(postmortem)](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)已经证明了这一点。
|
||||
|
||||
全 transcript(文本记录)测试的阻塞因素在于模型:agent 的输出由非确定性的 LLM(大语言模型)驱动,而每次运行都命中真实 API 的密钥门控测试既不确定也无法在 CI 中运行。我们需要真实运行的保真度与 fixture(测试前置数据)的确定性兼得。
|
||||
完整 transcript(文本记录)测试的阻塞因素在于模型:agent 的输出由非确定性的 LLM(大语言模型)驱动,而每次运行都命中真实 API 的密钥门控测试既不确定也无法在 CI 中运行。我们需要真实运行的保真度与 fixture(测试前置数据)的确定性兼得。
|
||||
|
||||
本 Agent Note(agent 决策记录)记下了新增第三层测试——**快照测试**——的决策,以及让它具备确定性、在 CI 中无需密钥、且维护成本低廉的设计选择。
|
||||
本 Agent Note 记下了新增第三层测试——**快照测试**——的决策,以及让它具备确定性、在 CI 中无需密钥、且维护成本低廉的设计选择。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -18,9 +18,9 @@ Status: implemented
|
||||
|
||||
### fixture 即持久化的会话 JSONL
|
||||
|
||||
每个场景的 `session.jsonl` 都从真实运行中采集。`assistant/chunk` 事件复现模型流;工具、消息和边界事件捕获 harness 行为。因此,一份普通会话产物同时充当重放来源和行为预期输出。
|
||||
每个场景的 `session.jsonl` 都从真实运行中采集。`assistant/chunk` 事件复现模型流;工具、消息和边界事件捕获 harness 行为。因此,一份普通会话产物同时充当回放来源和行为预期输出。
|
||||
|
||||
每个签入仓库的会话格式 fixture 都使用规范的打包物理布局。覆盖所有行类型的场景从一份独立的真实录制机械派生;测试要求它包含每一种打包存储行类型,并在两份 fixture 解码后逐事件精确相等;随后,普通重放与日志比较会证明组装后的进程能够消费并复现该布局。
|
||||
每个签入仓库的会话格式 fixture 都使用规范的打包物理布局。覆盖所有行类型的场景从一份独立的真实录制机械派生;测试要求它包含每一种打包存储行类型,并在两份 fixture 解码后逐事件精确相等;随后,普通回放与日志比较会证明组装后的进程能够消费并复现该布局。
|
||||
|
||||
### 回放从日志推导模型脚本
|
||||
|
||||
@@ -44,42 +44,42 @@ Status: implemented
|
||||
|
||||
### 录制采集日志;无密钥回放需要无提供方的配置
|
||||
|
||||
记录模式使用真实 `llm-deepseek` 适配器和配置为 `persistenceCompression: 'none'` 的 JSONL 持久化后端运行场景,再把生成的 `.jsonl` 复制到场景目录。显式 raw 模式让已提交重放 fixture 保持逐行可读,而普通部署使用后端的压缩默认值;符合条件的分片连续段仍使用默认的打包存储行。逐事件追加具有持久性,但 harness 会在采集前优雅关闭子进程(关闭 stdin → `await ctx.dispose()`),以确保最终事件已刷出。`llm-replay` 本身不执行记录——它只负责重放。
|
||||
记录模式使用真实 `llm-deepseek` 适配器和配置为 `persistenceCompression: 'none'` 的 JSONL 持久化后端运行场景,再把生成的 `.jsonl` 复制到场景目录。显式 raw 模式让已提交回放 fixture 保持逐行可读,而普通部署使用后端的压缩默认值;符合条件的分片连续段仍使用默认的打包存储行。逐事件追加具有持久性,但 harness 会在采集前优雅关闭子进程(关闭 stdin → `await ctx.dispose()`),以确保最终事件已刷出。`llm-replay` 本身不执行记录——它只负责回放。
|
||||
|
||||
重放使用 `cordis.snapshot.yml` overlay,以 `llm-replay` 替换真实适配器,同时保留实时组合。记录使用普通配置和由 harness 提供的持久化根目录。重放模式跳过 `.env` 加载,因此意外存在的 API 密钥不会触发实时调用。参见[单一来源配置 Agent Note](../../archived/testing/2026-07-04-single-source-acp-replay-config.md)。
|
||||
回放使用 `cordis.snapshot.yml` overlay,以 `llm-replay` 替换真实适配器,同时保留实际组合。记录使用普通配置和由 harness 提供的持久化根目录。回放模式跳过 `.env` 加载,因此意外存在的 API 密钥不会触发真实调用。参见[单一来源配置 Agent Note](../../archived/testing/2026-07-04-single-source-acp-replay-config.md)。
|
||||
|
||||
### 两个表面:归一化后比对
|
||||
|
||||
快照运行断言**两个**归一化后的表面,因为 harness 的外部表面是不同的:
|
||||
|
||||
1. **stdout transcript**——自动化客户端收到的、经过 framing 的 ACP JSON-RPC 响应与已提交的消息更新。它捕获传输契约的回归,与已提交的 `stdout.expected.jsonl` 比较。
|
||||
2. **重新持久化的会话 JSONL**,经过规范化后与 `session.jsonl` 比较。同一 fixture 同时作为重放来源和预期日志。提示词与工具的主体内容会被清理;每种请求头类别由一个场景固定余下的请求头序列。该 pin 默认拥有可读的提示词与工具 schema sidecar;当完整的对应序列相同时,也可将另一个 pin 指定为其中任一来源,因此每个不同的 sidecar 版本只提交一次。fixture 保护会拒绝重复的 sidecar 内容,录制/刷新会拒绝生成不同字节的共享引用方。最初的请求头固定理由保留在[请求头固定 Agent Note](../../archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)中。Override 场景仅从其 sidecar 派生模型行为。
|
||||
1. **stdout transcript**——自动化客户端收到的、分帧后的 ACP JSON-RPC 响应与已提交的消息更新。它捕获传输契约的回归,与已提交的 `stdout.expected.jsonl` 比较。
|
||||
2. **重新持久化的会话 JSONL**,经过规范化后与 `session.jsonl` 比较。同一 fixture 同时作为回放来源和预期日志。提示词与工具的主体内容会被清理;每种请求头类别由一个场景固定余下的请求头序列。该 pin 默认拥有可读的提示词与工具 schema 伴随文件;当完整的对应序列相同时,也可将另一个 pin 指定为其中任一来源,因此每个不同的伴随文件版本只提交一次。fixture 保护会拒绝重复的伴随文件内容,录制/刷新会拒绝生成不同字节的共享引用方。最初的请求头固定理由保留在[请求头固定 Agent Note](../../archived/testing/2026-07-06-pin-request-header-content-in-one-scenario.md)中。Override 场景仅从其伴随文件派生模型行为。
|
||||
|
||||
两个表面互补:stdout 覆盖精简的自动化线协议,JSONL 覆盖线协议有意省略的 loop、工具和 boundary 结构。
|
||||
两个表面互补:stdout 覆盖精简的自动化协议格式,JSONL 覆盖协议格式有意省略的 loop、工具和 boundary 结构。
|
||||
|
||||
规范化会替换会话、cwd、协议 id、时间戳、路径和进程易变值,同时保留确定性序号。录制与刷新还会在回放 fixture 中将生成的 workspace 及其文件系统解析出的别名存储为 `{{cwd}}`,使平台临时根目录和随机 basename 不影响录制结果;手工编写的临时路径与显式 `workspaceParent` 下的 cwd 值仍保留字面值。场景把真实 bash 使用限制在稳定命令上。stdout 预期输出仍是线协议形状的 JSONL,每个原始行都必须可解析为 JSON。普通 Vitest 快照更新只写入 stdout 预期输出;回放 fixture 的写入由显式 `record` 和 `refresh` 模式负责。
|
||||
规范化会替换会话、cwd、协议 id、时间戳、路径和进程易变值,同时保留确定性序号。录制与刷新还会在回放 fixture 中将生成的 workspace 及其文件系统解析出的别名存储为 `{{cwd}}`,使平台临时根目录和随机 basename 不影响录制结果;手工编写的临时路径与显式 `workspaceParent` 下的 cwd 值仍保留字面值。场景把真实 bash 使用限制在稳定命令上。stdout 预期输出仍是符合协议格式的 JSONL,每个原始行都必须可解析为 JSON。普通 Vitest 快照更新只写入 stdout 预期输出;回放 fixture 的写入由显式 `record` 和 `refresh` 模式负责。
|
||||
|
||||
### 隔离:当前靠归一化,后续可加沙箱
|
||||
|
||||
工具确定性来自生成的 cwd、清理后的环境、全新的非登录 shell、受限命令和规范化。cwd 默认为平台临时目录;当临时目录是始终可写的策略根,而行为需要独立项目位置时,场景可以改为提供其父目录。并发重放运行各自拥有独立 cwd、持久化目录和由定长场景键区分的 spill 根目录,因此一个场景的拆除无法删除另一个场景仍在进行的完整输出恢复,同时真实路径预览预算保持稳定。该层不声称提供 OS 级隔离。如果需要更强层级,沙箱执行器可以通过现有[能力 seam](../architecture/2026-06-13-capability-seams.md)替换本地后端。
|
||||
工具确定性来自生成的 cwd、清理后的环境、全新的非登录 shell、受限命令和规范化。cwd 默认为平台临时目录;当临时目录是始终可写的策略根,而行为需要独立项目位置时,场景可以改为提供其父目录。并发回放运行各自拥有独立 cwd、持久化目录和由定长场景键区分的 spill 根目录,因此一个场景的清理操作无法删除另一个场景仍在进行的完整输出恢复,同时真实路径预览预算保持稳定。该层不声称提供 OS 级隔离。如果需要更强层级,沙箱执行器可以通过现有[能力 seam](../architecture/2026-06-13-capability-seams.md)替换本地后端。
|
||||
|
||||
### 回放插件是独立的包
|
||||
|
||||
`@deepseek-ai/dsh-llm-replay` 是一个支撑包(package),而非示例本地的胶水代码。它通过用从 JSONL 重建的流短路 `llm/stream` 来替换真实适配器,其包级放置使回放逻辑处于正常覆盖率门禁之下。
|
||||
`@deepseek-ai/dsh-llm-replay` 是一个支撑包,而非示例本地的胶水代码。它通过用从 JSONL 重建的流短路 `llm/stream` 来替换真实适配器,其包级放置使回放逻辑处于正常覆盖率门禁之下。
|
||||
|
||||
### 两个子命令,回放在默认门禁中
|
||||
|
||||
`pnpm run test:snapshot` 无需密钥即可重放已提交 fixture;`test:snapshot:record` 使用真实 API,并重写采集的会话日志与 stdout 预期输出。同一无密钥门禁会通过 `session` header 发现仓库中的 JSONL,并拒绝与共享编解码器的规范打包表示不同的任何 fixture。缺少 fixture 时会响亮失败。每个场景都包含 `input.json`、`stdout.expected.jsonl` 和 `session.jsonl`;不调用模型的情况使用仅有请求头的日志。只有标记为 `overridden` 的场景才需要 `replay.override.json`,因为它一旦存在就会取代派生重放。Fixture 守卫会拒绝缺失、不匹配和孤立文件。两个命令都接受场景过滤器。
|
||||
`pnpm run test:snapshot` 无需密钥即可回放已提交 fixture;`test:snapshot:record` 使用真实 API,并重写采集的会话日志与 stdout 预期输出。同一无密钥门禁会通过 `session` header 发现仓库中的 JSONL,并拒绝与共享编解码器的规范打包表示不同的任何 fixture。缺少 fixture 时会明确报错。每个场景都包含 `input.json`、`stdout.expected.jsonl` 和 `session.jsonl`;不调用模型的情况使用仅有请求头的日志。只有标记为 `overridden` 的场景才需要 `replay.override.json`,因为它一旦存在就会取代派生回放。fixture 守卫会拒绝缺失、不匹配和孤立文件。两个命令都接受场景过滤器。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **手工编写包含模型分片的 `llm.json`**——早期草案;复用真实会话日志,使 fixture 成为系统的真实产物而非手工构建的 mock,并让它同时充当行为预期输出。
|
||||
- **字节级 HTTP 录制库(Polly/nock/MSW)**:否决。与适配器耦合,处理流式 SSE(Server-Sent Events)时笨拙,且层级低于被测对象。
|
||||
- **从 `turn/end {kind:'error'|'aborted'}` 合成抛错/取消条目**:否决。这会将 `llm-replay` 耦合到 loop 内部的轮次关闭语义,且 `turn/end` 原因是有损的(无法区分抛出的 401 与 finish-error);显式的 `replay.override.json` 伴随文件是更清晰的 seam。
|
||||
- **在每个类别 pin 旁复制两个请求头 sidecar**:否决。提示词与工具 schema 的组合各自独立变化,因此一个共享组件发生变更,就会使不相关类别 pin 中字节完全相同的文件产生无意义改动。显式的分组件来源可在不重复内容的情况下,为每个类别保留一个结构性 pin。
|
||||
- **在每个类别 pin 旁复制两个请求头伴随文件**:否决。提示词与工具 schema 的组合各自独立变化,因此一个共享组件发生变更,就会使不相关类别 pin 中字节完全相同的文件产生无意义改动。显式的分组件来源可在不重复内容的情况下,为每个类别保留一个结构性 pin。
|
||||
|
||||
## 后果
|
||||
|
||||
该测试层为每个场景增加经过评审的输入、会话、stdout、可选 override 和可选 workspace fixture,并为每个不同的已固定提示词序列、每个不同的已固定工具 schema 序列各增加一个文件。记录与重放都会把 workspace seed 复制到生成的 cwd。作为回报,该层通过真实 Loader 和工具组合提供确定性的无密钥覆盖。保留下来的大多数场景测试的是组装后的后端而非 ACP;[仅面向自动化的 ACP 决策](../simplification/2026-07-23-acp-automation-only-protocol.md#snapshot-boundary)将该语料保留在此处,并把向传输无关 headless 套件的任何迁移推迟为一项独立的测试变更(套件级 FIXME 标记了这一点)。
|
||||
该测试层为每个场景增加经过评审的输入、会话、stdout、可选 override 和可选 workspace fixture,并为每个不同的已固定提示词序列、每个不同的已固定工具 schema 序列各增加一个文件。记录与回放都会把 workspace seed 复制到生成的 cwd。作为回报,该层通过真实 Loader 和工具组合提供确定性的无密钥覆盖。保留下来的大多数场景测试的是组装后的后端而非 ACP;[仅面向自动化的 ACP 决策](../simplification/2026-07-23-acp-automation-only-protocol.md#snapshot-boundary)将该语料保留在此处,并把向传输无关 headless 套件的任何迁移推迟为一项独立的测试变更(套件级 FIXME 标记了这一点)。
|
||||
|
||||
本 Agent Note 与[拟议的确定性 Agent Note](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md)相关,但不取代它:该提案的“通用重放 fixture”在每次测试后重新派生会话*消息历史*(内部一致性不变量),而这些快照固定组装后的行为与外部自动化输出。在后端语料迁出 ACP 之前,两者相互补充。
|
||||
本 Agent Note 与[拟议的确定性 Agent Note](../../proposed/testing/2026-06-11-deterministic-and-stress-testing.md)相关,但不取代它:该提案的“通用回放 fixture”在每次测试后重新派生会话*消息历史*(内部一致性不变量),而这些快照固定组装后的行为与外部自动化输出。在后端语料迁出 ACP 之前,两者相互补充。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md
|
||||
2026-06-19-real-api-e2e-ci.md: 935664fd01df4844ee19be7b4f2f297ebf5bd29b
|
||||
2026-06-19-real-api-e2e-ci.zh.md: 9c10614f5a7e6b38f6850b29fab87d0e09806c5f
|
||||
2026-06-19-real-api-e2e-ci.zh.md: 06d7c5c0417e5cb50780c2ae179d822795052f03
|
||||
|
||||
@@ -6,11 +6,11 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
根据策略,harness 高度依赖真实 API 测试:[docs/testing.md](../../../../docs/testing.md) 指出,无密钥套件证明的是管线,而非产品;[ACP(Agent Client Protocol)inject 事后分析](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)则是常设证据——178 项无密钥测试保持绿色时,真实 ACP 客户端会话却立即崩溃。真实 API e2e 套件(`pnpm run test:e2e`,即 `*.e2e.ts` 文件)的存在正是为了弥合这一缺口:它针对实时 DeepSeek API 驱动 agent(智能体)——真实模型调用、真实 bash 工具、多轮次、恢复、ACP-over-stdio。
|
||||
根据策略,harness 高度依赖真实 API 测试:[docs/testing.md](../../../../docs/testing.md) 指出,无密钥套件证明的是管线,而非产品;[ACP(Agent Client Protocol)inject 事故复盘(postmortem)](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)则是常设证据——178 项无密钥测试保持绿色时,真实 ACP 客户端会话却立即崩溃。真实 API e2e 套件(`pnpm run test:e2e`,即 `*.e2e.ts` 文件)的存在正是为了弥合这一缺口:它针对线上 DeepSeek API 驱动 agent——真实模型调用、真实 bash 工具、多轮次、恢复、ACP-over-stdio。
|
||||
|
||||
默认门禁([.github/workflows/ci.yml](../../../../.github/workflows/ci.yml))刻意无密钥:不携带 secret,可供 fork 运行。`test:e2e` 在无密钥时自动跳过(`describe.skipIf(!process.env.DEEPSEEK_API_KEY)`),因此将其加入该工作流只会报绿而不会真正执行真实套件。要让真实 API 覆盖率成为合并信号,需要一个独立的、携带 secret 的工作流。
|
||||
|
||||
本 Agent Note(agent 决策记录)记下了新增**第二条消费 secret 的工作流**以在 CI 中运行真实 API 套件的决策;由于向未来可能公开的仓库引入第一个 CI secret 属于安全/隔离决策,本文也记录其依赖的威胁模型,以及仓库公开时需要做出的变更。
|
||||
本 Agent Note 记下了新增**第二条消费 secret 的工作流**以在 CI 中运行真实 API 套件的决策;由于向未来可能公开的仓库引入第一个 CI secret 属于安全/隔离决策,本文也记录其依赖的威胁模型,以及仓库公开时需要做出的变更。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -26,7 +26,7 @@ ci.yml 的价值在于它无密钥、可 fork、始终为绿:任何贡献者
|
||||
|
||||
### 触发条件:仅限可信事件
|
||||
|
||||
`workflow_dispatch` + `push` 到 `main`/`master` + 每夜 `schedule`(`17 0 * * *`,即北京时间 08:17)+ `pull_request`。push 提供合并后信号;schedule 捕捉外部 API 漂移;dispatch 是手动逃生通道;可信 pull request 获得合并前门禁。该合并前信号有意接受 § 安全性中描述的更大密钥暴露面。
|
||||
`workflow_dispatch` + `push` 到 `main`/`master` + 每夜 `schedule`(`17 0 * * *`,即北京时间 08:17)+ `pull_request`。push 提供合并后信号;schedule 捕捉外部 API 漂移;dispatch 是手动逃生通道;可信 PR 获得合并前门禁。该合并前信号有意接受 § 安全性中描述的更大密钥暴露面。
|
||||
|
||||
### 不可信 PR 的门禁
|
||||
|
||||
@@ -41,7 +41,7 @@ Dependabot 子句基于 PR **作者**(`pull_request.user.login`)而非 `gith
|
||||
|
||||
该门禁是一个*干净跳过的便利措施*,而非 secret 的安全边界(见 § 安全性——边界是 GitHub 自身在 `pull_request` 下对 fork 的 secret 扣留机制)。没有该门禁,fork 仍然无法读取密钥;只是会遇到令人困惑的 preflight 硬失败并浪费计算资源。
|
||||
|
||||
### Preflight:大声失败,绝不虚假为绿
|
||||
### Preflight:明确失败,绝不虚假报绿
|
||||
|
||||
由于 job 仅在 secret 应当存在的可信事件上运行,preflight 是一个无条件的存在性检查:密钥为空→`exit 1` 并附带 `::error::` 注解指明需要配置的 secret 名称。这是让自跳过套件可以安全地作为门禁的关键。没有它,被删除/重命名/错误配置的 secret 会让 `test:e2e` 跳过所有真实套件并报告全绿——整个安全网的静默退化。该守卫将「secret 缺失」从不可见的虚假通过转化为可见的失败。(其正确性已在实际中验证:secret 存在之前的运行恰好在此步骤失败。)
|
||||
|
||||
@@ -51,14 +51,14 @@ repo secret 命名为 `DEEPSEEK_API_KEY_EXTERNAL`;映射到适配器和测试
|
||||
|
||||
- **步骤级 secret。** `DEEPSEEK_API_KEY` 仅在 preflight 和 e2e 步骤的 `env:` 中设置,从不在 job 级设置——因此 checkout/setup-node/install 永远看不到它。依赖中被入侵的安装时生命周期脚本无法读取不在其环境中的 secret。
|
||||
- **`permissions: contents: read`。** job 仅读取仓库以运行测试;不需要写权限(无 PR 评论、无 status 写入),因此 `GITHUB_TOKEN` 降至最小权限。
|
||||
- **`DEEPSEEK_BASE_URL` 固定**为 e2e 步骤上的 `https://api.deepseek.com`。适配器在未设置时会默认使用此值([packages/llm/llm-deepseek/src/index.ts](../../../../packages/llm/llm-deepseek/src/index.ts) `PUBLIC_BASE_URL`),但显式固定具有自文档性和密封性——仓库根目录的 `.env`(`vitest.e2e.config.ts` 存在时会加载)无法静默地将运行重定向到其他端点。
|
||||
- **`DEEPSEEK_BASE_URL` 固定**为 e2e 步骤上的 `https://api.deepseek.com`。适配器在未设置时会默认使用此值([packages/llm/llm-deepseek/src/index.ts](../../../../packages/llm/llm-deepseek/src/index.ts) `PUBLIC_BASE_URL`),但显式固定具有自文档性和密封性——仓库根目录的 `.env`(如果存在,`vitest.e2e.config.ts` 会加载它)无法静默地将运行重定向到其他端点。
|
||||
- **不回显 secret。** preflight 仅打印 `DEEPSEEK_API_KEY present.`——不打印值或长度。
|
||||
|
||||
### 范围与运行时形态
|
||||
|
||||
job 仅在 Node 24 上运行 `test:e2e`;无密钥门禁和版本兼容性属于主 CI 工作流。测试通过 workspace paths 映射以未构建形式运行,使用有界的可配置 worker 池、逐测试重试和 job 超时。被取代的 PR 运行会被取消,而 push 和 schedule 运行完整执行以提供合并后信号。
|
||||
|
||||
DeepSeek 原生 `web_search` 探测已注册但会跳过。实时 Anthropic 兼容端点可能返回成功响应却没有结构化来源块,因此对来源存在性的正向断言不是可靠的合并信号;单元覆盖率仍会固定响应解析,但 CI 不会证明实时来源块的线协议形状。
|
||||
DeepSeek 原生 `web_search` 探测已注册但会跳过。线上 Anthropic 兼容端点可能返回成功响应却没有结构化来源块,因此对来源存在性的正向断言不是可靠的合并信号;单元测试仍会锁定响应解析行为,但 CI 不会验证线上端点返回的来源块协议格式。
|
||||
|
||||
## 安全性
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md
|
||||
2026-06-22-fork-child-replay-seed-boundary.md: ed3ec095bc14128f5ebc0a9188bc022ef97b1c8b
|
||||
2026-06-22-fork-child-replay-seed-boundary.zh.md: 84cd56ccea69aab0246582512908b4a73ce3c36a
|
||||
2026-06-22-fork-child-replay-seed-boundary.zh.md: 44e6b76b8c433fd6b0ab44f090392cd6f12055aa
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[逐会话快照重放 Agent Note(agent 决策记录)](2026-06-22-subagent-snapshot-replay.md)使快照层能够表达嵌套 agent 形状:一个父项加上每个进程内 subagent 的一份记录日志,每份日志都按调用会话作为键,以独立脚本重放。它曾指出(§ 范围,最后一个项目符号),fork 快照“只是未来很容易添加的一项,并非键控缺口”。这一判断对 fork 子项而言是错误的——问题不在键控,而在*脚本派生*。
|
||||
[逐会话快照回放 Agent Note](2026-06-22-subagent-snapshot-replay.md)使快照层能够表达嵌套 agent 形状:一个父项加上每个进程内 subagent 的一份记录日志,每份日志都按调用会话作为键,以独立脚本回放。它曾指出(§ 范围,最后一个项目符号),fork 快照「只是未来很容易添加的一项,并非键控缺口」。这一判断对 fork 子项而言是错误的——问题不在键控,而在*脚本派生*。
|
||||
|
||||
subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-replay) 从已录制的会话日志推导:它按 `(turn, step)` 对日志中的 `assistant/chunk` 事件分组,每次 `stream()` 调用对应一条回放条目。对 **spawn** 子会话而言这是正确的,因为其日志只包含自身的模型调用。
|
||||
|
||||
@@ -22,7 +22,7 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-repla
|
||||
|
||||
`SessionHeader` 新增可选字段 `seedLength: number`——表示有多少前导事件是通过 seed 继承而来、而非本会话产生的。fork 后端在创建子会话时设置它(= 播种前缀的长度);全新的 spawn 子会话不设置(等同于 0)。它通过 `CreateSessionOptions.meta`(及 `CreateAgentOptions.meta`)传递,在 `SessionStore.prepare` 中设置。
|
||||
|
||||
`seedLength` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为从加载的 header 中取回持久化的 `seedLength`。(形状与 `createdAt` 相同:恢复时显式保留,而非重新默认为当前时间。)
|
||||
`seedLength` 是**显式**的,绝不从 `seed.length` 推断。恢复/加载时用会话的完整已存储日志作为 seed,此时 `seed.length` 是全长而非原始边界——恢复路径改为从加载的 header 中取回持久化的 `seedLength`。(做法与 `createdAt` 相同:恢复时显式保留,而非重新默认为当前时间。)
|
||||
|
||||
### 2. 两个持久化后端均完整往返
|
||||
|
||||
@@ -35,11 +35,11 @@ subagent 脚本由 [`deriveReplayScript`](../../../../packages/support/llm-repla
|
||||
|
||||
`dsh-llm-replay` 的 `parseSessionHeader` 现在也读取 `seedLength`(缺失则为 0),`loadSessionScripts` 从 `parseSessionLog(text).slice(seedLength)` 推导子会话条目——即边界及之后的事件,也就是子会话自身的模型调用。对 spawn 子会话而言 `seedLength` 为 0,此操作是空操作,spawn 场景逐字节不变。
|
||||
|
||||
这关闭了路由正确性的缺口,两个已录制的 fork 场景对其进行端到端验证——见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md)。
|
||||
这弥补了路由正确性的缺口,两个已录制的 fork 场景对其进行端到端验证——见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md)。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
- **在 `llm-replay` 中启发式推导边界**(播种前缀是连续的父事件,止于子会话第一条 `user/message` 之前的最后一个 `turn/end`)。否决:在测试 harness 中用脆弱的启发式重新推导一个生产者已经知道的事实。在源头(fork 后端)持久化边界,是「在包(package)seam 处显式优于隐式」这条规则跨越持久化边界的应用——子会话 fixture(测试前置数据)的读取者永远不需要重建继承在哪里结束。
|
||||
- **在 `llm-replay` 中启发式推导边界**(播种前缀是连续的父事件,止于子会话第一条 `user/message` 之前的最后一个 `turn/end`)。否决:在测试 harness 中用脆弱的启发式重新推导一个生产者已经知道的事实。在源头(fork 后端)持久化边界,是「在包seam 处显式优于隐式」这条规则跨越持久化边界的应用——子会话 fixture(测试前置数据)的读取者永远不需要重建继承在哪里结束。
|
||||
- **固定格式版本而不递增**(事件日志使用的 `SESSION_FORMAT_VERSION = 0`「不稳定」姿态)。对 SQLite *表*布局否决:`SCHEMA_VERSION` 是单调递增并拒绝旧版的旋钮(一组小的、值得区分的修订),与事件词汇表的 `version` 不同。新增列正是它所版本化的那种破坏性表变更,因此需要递增。
|
||||
|
||||
## 后果
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-06-22-subagent-snapshot-replay.md
|
||||
2026-06-22-subagent-snapshot-replay.md: b8fefce5ff27b0cd3cfa2920b137e78cda0d696d
|
||||
2026-06-22-subagent-snapshot-replay.zh.md: a673d6e5dd124986b827fcc6708db447090173c7
|
||||
2026-06-22-subagent-snapshot-replay.zh.md: 3100132a2626819753f3a71c6166fcff858cf7d3
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: 嵌套 agent 的逐会话快照回放
|
||||
# Agent Note: 嵌套 agent的逐会话快照回放
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,14 +6,14 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
快照层(`pnpm run test:snapshot`)会启动真实 `acp-agent` 子进程,通过 [`dsh-llm-replay`](../../../../packages/support/llm-replay) 重放已记录会话,并将规范化后的自动化线协议 + 重新持久化的会话日志与已提交预期输出进行 diff。大多数场景通过这条真实进程边界测试组装后的后端行为。
|
||||
快照层(`pnpm run test:snapshot`)会启动真实 `acp-agent` 子进程,通过 [`dsh-llm-replay`](../../../../packages/support/llm-replay) 回放已记录会话,并将规范化后的自动化协议输出 + 重新持久化的会话日志与已提交预期输出进行 diff。大多数场景通过这条真实进程边界测试组装后的后端行为。
|
||||
|
||||
该层最初为每个进程只有一个会话而构建,这一假设硬编码在两处:
|
||||
|
||||
- **`dsh-llm-replay` 没有做任何键控。** 它用一个全局游标,将第 N 次 `llm/stream` 调用对应到单一录制序列的第 N 条。当父 agent(智能体)和一个进程内 subagent 在同一个上下文上同时流式输出时,调用交错,单一游标会把子 agent 的脚本发给父 agent(反之亦然)。
|
||||
- **`dsh-llm-replay` 没有做任何键控。** 它用一个全局游标,将第 N 次 `llm/stream` 调用对应到单一录制序列的第 N 条。当父 agent和一个进程内 subagent 在同一个上下文上同时流式输出时,调用交错,单一游标会把子 agent 的脚本发给父 agent(反之亦然)。
|
||||
- **harness 只收集一份日志。** `findSessionLog` 遍历 sessions 根目录,返回找到的第一个 `.jsonl`。subagent 作为第二个 `Session` 运行并拥有自己的日志,因此子 agent 的 transcript(文本记录)被静默丢弃。
|
||||
|
||||
这就是 [subagent seam Agent Note(agent 决策记录)](../feature/2026-06-21-subagent-capability-seam.md)中通过 `TODO(subagent-snapshots)` 推迟的工作:进程内后端(PR2)落地时已有单元 + e2e 覆盖,但在这套基础设施落地前,完整 transcript 快照层无法表达嵌套 agent 形状。本 Agent Note 就是该堆叠式后续工作。
|
||||
这就是 [subagent seam Agent Note](../feature/2026-06-21-subagent-capability-seam.md)中通过 `TODO(subagent-snapshots)` 推迟的工作:进程内后端(PR2)落地时已有单元 + e2e 覆盖,但在这套基础设施落地前,完整 transcript 快照层无法表达嵌套 agent 形状。本 Agent Note 就是该堆叠式后续工作。
|
||||
|
||||
## 决策
|
||||
|
||||
@@ -21,7 +21,7 @@ Status: implemented
|
||||
|
||||
### 1. 调用方会话 id 附着在模型请求上
|
||||
|
||||
`GenerateOptions` 新增可选字段 `sessionId`,在请求组装时从 `agent.session.id` 赋值。适配器忽略它;`llm/stream` 监听器用它按发起会话路由。其类型为 `Branded<'SessionId'>`(来自 `dsh-brand`)而非 `dsh-session` 的 `SessionId`,因为后者所在包(package)导入了 `dsh-llm` 的 `Message`,反向导入会形成循环。两个类型等价,因此会话 id 赋值无需类型转换。将 brand 移到一个专用 ids 包属于独立工作,因为它会影响所有 id 导入。
|
||||
`GenerateOptions` 新增可选字段 `sessionId`,在请求组装时从 `agent.session.id` 赋值。适配器忽略它;`llm/stream` 监听器用它按发起会话路由。其类型为 `Branded<'SessionId'>`(来自 `dsh-brand`)而非 `dsh-session` 的 `SessionId`,因为后者所在包导入了 `dsh-llm` 的 `Message`,反向导入会形成循环。两个类型等价,因此会话 id 赋值无需类型转换。将 brand 移到一个专用 ids 包属于独立工作,因为它会影响所有 id 导入。
|
||||
|
||||
### 2. 回放按首次调用顺序将活跃会话绑定到录制脚本
|
||||
|
||||
@@ -29,17 +29,17 @@ Status: implemented
|
||||
|
||||
活跃会话 id 每次运行都是全新随机值,永远不等于录制时的 id,因此活跃会话无法通过 id 相等绑定到脚本。取而代之的是**首次调用顺序**绑定:第一个发起任何模型调用的活跃会话认领第一份有序脚本(即父会话:`createdAt` 最早,且必然最先流式输出,因为它必须先运行一个轮次才能委派),下一个新活跃会话认领下一份脚本,依此类推。此后每个会话独立推进自己的游标。
|
||||
|
||||
这种方式按谁在调用键控,而非按全局调用顺序。因此即使 subagent 将来并发或在后台运行(全局游标会导致交错),它仍然正确。不携带 `sessionId` 的调用(直接在单元测试中调用 `stream()`)被视为一个匿名会话、绑定到主脚本,因此单会话路径与旧行为逐字节一致。活跃会话数多于录制脚本数时会快速失败报错(出现了未录制的 subagent),绝不会静默错误路由。
|
||||
这种方式按谁在调用键控,而非按全局调用顺序。因此即使 subagent 将来并发或在后台运行(全局游标会导致交错),它仍然正确。不携带 `sessionId` 的调用(直接在单元测试中调用 `stream()`)被视为一个匿名会话、绑定到主脚本,因此单会话路径与旧行为逐字节一致。活跃会话数多于录制脚本数时会明确报错(出现了未录制的 subagent),绝不会静默错误路由。
|
||||
|
||||
子 fixture(测试前置数据)按 `createdAt` 排序,在兄弟会话严格顺序执行时与调用顺序一致。id 平局打破仅使退化碰撞具有确定性。并发或后台子会话必须引入显式的首次调用序号,而非依赖时间戳。
|
||||
子 fixture(测试前置数据)按 `createdAt` 排序,在兄弟会话严格顺序执行时与调用顺序一致。id 决胜规则仅用于让极端情况下的时间戳冲突获得确定顺序。并发或后台子会话必须引入显式的首次调用序号,而非依赖时间戳。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
曾考虑但否决的方案是:**将父子日志按调用顺序合并**为一份全局脚本(仅在进程内 subagent 执行严格嵌套——父 agent 阻塞等待子 agent——时才正确)。对当前的同步裁剪而言更简单,但将「父阻塞于子」这一不变式固化了进去;未来若引入后台/并发 subagent 就会失效。逐会话键控则不会。
|
||||
曾考虑但否决的方案是:**将父子日志按调用顺序合并**为一份全局脚本(仅在进程内 subagent 执行严格嵌套——父 agent 阻塞等待子 agent——时才正确)。对当前的同步实现而言更简单,但将「父阻塞于子」这一不变式固化了进去;未来若引入后台/并发 subagent 就会失效。逐会话键控则不会。
|
||||
|
||||
### 3. harness 收集所有日志,主会话优先
|
||||
|
||||
`harvestSessionLogs` 递归收集 sessions 根目录下所有固定命名为 `session.jsonl` 的 transcript(JSONL 后端为每个父会话和子会话分别提供独立的项目/会话目录),解析各自的 header,并按主会话优先排序:顶层会话(无 `parentSession`)在前,各子会话按 `createdAt` 升序排列。`RunResult.sessionLogs` 是复数结果;spec 在录制时将每份日志写回对应 fixture(`session.jsonl` + `session.<n>.jsonl`),在回放时将每份收集到的日志与其 fixture 做 diff。归一化器已支持复数会话 id 并会折叠任何游离 UUID,因此无需修改归一化器。
|
||||
`harvestSessionLogs` 递归收集 sessions 根目录下所有固定命名为 `session.jsonl` 的 transcript(JSONL 后端为每个父会话和子会话分别提供独立的项目/会话目录),解析各自的 header,并按主会话优先排序:顶层会话(无 `parentSession`)在前,各子会话按 `createdAt` 升序排列。`RunResult.sessionLogs` 包含多份日志;spec 在录制时将每份日志写回对应 fixture(`session.jsonl` + `session.<n>.jsonl`),在回放时将每份收集到的日志与其 fixture 做 diff。归一化器已支持多个会话 id 并会折叠任何游离 UUID,因此无需修改归一化器。
|
||||
|
||||
### 4. 场景
|
||||
|
||||
@@ -54,5 +54,5 @@ Status: implemented
|
||||
|
||||
- `TODO(subagent-snapshots)` 延期项已解决:嵌套 agent 的 transcript 现在是快照层的一等形态。
|
||||
- `GenerateOptions.sessionId` 是一个小而诚实的 core-seam 新增,在回放之外同样有用(遥测、请求路由)。
|
||||
- `subagent` 工具绑定到单一提供方,因此 `subagent-multi` 中的两个子 agent 都是 spawn(全新创建)。键控按会话路由而非按后端路由,因此对 fork 同样正确。但脚本*派生*逻辑此前不正确:fork 子会话的日志以种子化的父前缀(父会话的 `assistant/chunk` 事件)开头,如果从完整日志派生脚本,就会把父 agent 的响应当作子 agent 的来回放。这一正确性缺口通过持久化种子边界来弥合——见[持久化种子边界,使 fork 子项重放能够正确路由](2026-06-22-fork-child-replay-seed-boundary.md)——录制的 fork 与混合 spawn+fork 场景现在通过一份 transcript 同时验证两种传输方式(见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md))。
|
||||
- `subagent` 工具绑定到单一提供方,因此 `subagent-multi` 中的两个子 agent 都是 spawn(全新创建)。键控按会话路由而非按后端路由,因此对 fork 同样正确。但脚本*派生*逻辑此前不正确:fork 子会话的日志以种子化的父前缀(父会话的 `assistant/chunk` 事件)开头,如果从完整日志派生脚本,就会把父 agent 的响应当作子 agent 的来回放。这一正确性缺口通过持久化种子边界来弥合——见[持久化 seed 边界以确保 fork 子会话回放正确路由](2026-06-22-fork-child-replay-seed-boundary.md)——录制的 fork 与混合 spawn+fork 场景现在通过一份 transcript 同时验证两种传输方式(见[记录 fork 与混合 spawn+fork 快照场景](../../archived/testing/2026-06-22-fork-snapshot-scenarios.md))。
|
||||
- 进程外(ACP(Agent Client Protocol))subagent 是完全不同的回放形态(每个子 agent 是自己的进程、有自己的回放),作为 `TODO(acp-subagent-replay)` 记录在 PR3 计划中。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-22-cross-platform-test-fixtures.md
|
||||
2026-07-22-cross-platform-test-fixtures.md: 6217aabfdbe8f14f869004c8dafb7e19f4b7443a
|
||||
2026-07-22-cross-platform-test-fixtures.zh.md: 43942ec0468df822d04b39e318010c2b260c734f
|
||||
2026-07-22-cross-platform-test-fixtures.zh.md: ce9c65106b904b4c16360c9cb2545b0316bd7120
|
||||
|
||||
@@ -16,7 +16,7 @@ Status: implemented
|
||||
|
||||
传输故障测试会注入连接的消息写入器,并传入与真实 Node 流相同的异步写入回调错误。生产写入器仍会把分帧消息写入子进程 stdin。这种方式让真实子进程保持存活,使测试无需触及平台特有的管道句柄,也能确定性地区分传输故障与进程退出。
|
||||
|
||||
语言服务器的资源清理会终止整棵后代进程树:POSIX 使用负数进程组 ID,Windows 同步执行 `taskkill /T /F`。Windows 只会忽略 taskkill 返回的「进程树已经不存在」状态;命令执行失败、权限错误及其他终止进程树的失败仍属于资源清理失败。只读的提供方查询仅在选定的池化传输于该次查询开始前或执行期间失效时重试一次;服务器仍存活时返回的错误不会重放。终端测试会等待可观察的渲染输出,不假设一次事件循环轮转已经足够。
|
||||
语言服务器的资源清理会终止整棵后代进程树:POSIX 使用负数进程组 ID,Windows 同步执行 `taskkill /T /F`。Windows 只会忽略 taskkill 返回的「进程树已经不存在」状态;命令执行失败、权限错误及其他终止进程树的失败仍属于资源清理失败。只读的提供方查询仅在选定的池化传输于该次查询开始前或执行期间失效时重试一次;服务器仍存活时返回的错误不会触发重试。终端测试会等待可观察的渲染输出,不假设一次事件循环轮转已经足够。
|
||||
|
||||
对于真正仅存在于 POSIX 的原语,测试只在该用例上排除 Windows。相邻的跨平台用例仍会固定拒绝非普通文件、不可用命令和无法访问的工作目录的行为。Windows 上受支持的路径仍受逐文件覆盖率门禁约束,不会随测试文件一起排除。
|
||||
|
||||
@@ -30,4 +30,4 @@ Status: implemented
|
||||
|
||||
## 后果
|
||||
|
||||
可移植 fixture 需要更显式地构造,因为预期路径要从共享的原生常量派生,传输故障则通过狭窄的写入器 seam 注入。仅适用于特定平台的排除项必须配有相邻的跨平台断言,以继续覆盖相应的产品行为。协议级优雅关停失败后,Windows 上的资源清理依赖宿主的 `taskkill` 命令;命令同步执行成功时,dispose 的完成边界明确,并确保清理返回前即可观察到后代进程退出;若进程树终止失败,dispose 的调用方仍能观察到该失败。
|
||||
可移植 fixture 需要更显式地构造,因为预期路径要从共享的原生常量派生,传输故障则通过狭窄的写入器 seam 注入。仅适用于特定平台的排除项必须配有相邻的跨平台断言,以继续覆盖相应的产品行为。协议级优雅关停失败后,Windows 上的资源清理依赖宿主的 `taskkill` 命令;命令同步执行成功时,可确保 dispose(资源释放)在有限时间内完成,并确保清理返回前即可观察到后代进程退出;若进程树终止失败,资源释放逻辑仍能观察到该失败。
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-25-scriptable-llm-wire-fault-server.md
|
||||
2026-07-25-scriptable-llm-wire-fault-server.md: b8e64d92db224c199d0f8c547f0caa588d0ca65e
|
||||
2026-07-25-scriptable-llm-wire-fault-server.zh.md: 35b27efa99b625fa3c815bbe58fef6a138477e55
|
||||
2026-07-25-scriptable-llm-wire-fault-server.zh.md: 88902fb376ac01886d9766ad17f85c108dacc923
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: 可脚本控制的 LLM(大语言模型)协议层故障服务器
|
||||
# Agent Note: 可脚本控制的 LLM协议层故障服务器
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
`@deepseek-ai/dsh-llm-mock-server` 是一个私有支持包(package),提供可导入的 Node HTTP 服务器。仓库内的 `pnpm run mock:llm` 源码入口提供一个用于手动故障注入的独立进程;该包不公开可安装的二进制命令。它接受兼容 OpenAI 的根路径和 `/v1` chat-completions 路径,校验可选的 bearer token,捕获请求,并对每个已接受请求消耗一个显式行为。脚本耗尽时快速失败;只有设置 `repeatLast` 才会重复最后一个行为。
|
||||
`@deepseek-ai/dsh-llm-mock-server` 是一个私有支持包,提供可导入的 Node HTTP 服务器。仓库内的 `pnpm run mock:llm` 源码入口提供一个用于手动故障注入的独立进程;该包不公开可安装的二进制命令。它接受兼容 OpenAI 的根路径和 `/v1` chat-completions 路径,校验可选的 bearer token,捕获请求,并对每个已接受请求消耗一个显式行为。脚本耗尽时会明确报错;只有设置 `repeatLast` 才会重复最后一个行为。
|
||||
|
||||
请求行为覆盖 socket 重置、发送 header 后断开、发送部分内容后断开、停滞、合法空完成、正常关闭但被截断的流、畸形 payload、典型 HTTP 故障、完整的文本/推理/工具调用响应、慢速流式输出以及达到 token 上限的完成。真正的 `connection_refused` 由 CLI(命令行界面)的监听器生命周期阶段实现,因为已经绑定端口的请求处理器无法拒绝自身的 TCP 连接。
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-07-26-execa-for-test-subprocess-plumbing.md
|
||||
2026-07-26-execa-for-test-subprocess-plumbing.md: 958abc4aee94adb3e6206cc299595ad92bde4044
|
||||
2026-07-26-execa-for-test-subprocess-plumbing.zh.md: 7027a8bde51f81bfa7774743f84639cbd4b667d8
|
||||
2026-07-26-execa-for-test-subprocess-plumbing.zh.md: 5ccadd93a182ba299be80d48881fe1c470a2d537
|
||||
|
||||
@@ -16,11 +16,11 @@ Status: implemented
|
||||
|
||||
## 决定
|
||||
|
||||
- `execa` 是根 devDependency,同时是 `@deepseek-ai/dsh-loader-smoke`(唯一的 `src/` 消费者)的运行时依赖。上述 spawn、收集、超时的代码位置统一经由 `await execa(cmd, args, { cwd, env, timeout, killSignal: 'SIGKILL', reject: false })` 运行:其结果以相互独立的字段报告 `{ stdout, stderr, exitCode, signal, timedOut, failed }`,与本仓库防御模式中「正交的子进程结果各自独立上报」的规则一致。`runLoaderSmoke` 传 `input: ''` 以兑现其 stdin 关闭契约;断言固定精确流字节的位置传 `stripFinalNewline: false`。
|
||||
- 真正定制的部分继续保持定制,只是架在 execa 拥有的子进程之上:cli-demo 在流中遇到标记即中断的逻辑、jsonrpc 基于行谓词的协议驱动,以及 crash-recovery 在故障点发送 SIGKILL 的编排。`smoke-real.e2e.ts` 的三个长驻交互式服务器保留原生 `spawn`——跨双流监听就绪行加上分级的 SIGTERM→等待→SIGKILL 拆除就是该处的全部内容,execa 在那里删不掉任何东西;它在本 note 中的份额是那份死的 `.env` 解析器。
|
||||
- `llm-mock-server` 的 CLI 经由 `parseArgs` 切分(strict、不允许位置参数);数值转换、边界检查与跨选项约束仍手工实现,被固定的错误消息测试改为携带 `parseArgs` 自己的切分器文本。
|
||||
- `execa` 是根 devDependency,同时是 `@deepseek-ai/dsh-loader-smoke`(唯一的 `src/` 消费方)的运行时依赖。上述 spawn、收集、超时的代码位置统一经由 `await execa(cmd, args, { cwd, env, timeout, killSignal: 'SIGKILL', reject: false })` 运行:其结果以相互独立的字段报告 `{ stdout, stderr, exitCode, signal, timedOut, failed }`,与本仓库防御模式中「正交的子进程结果各自独立上报」的规则一致。`runLoaderSmoke` 传 `input: ''` 以兑现其 stdin 关闭契约;断言固定精确流字节的位置传 `stripFinalNewline: false`。
|
||||
- 真正定制的部分继续保持定制,只是架在 execa 拥有的子进程之上:cli-demo 在流中遇到标记即中断的逻辑、jsonrpc 基于行谓词的协议驱动,以及 crash-recovery 在故障点发送 SIGKILL 的编排。`smoke-real.e2e.ts` 的三个长驻交互式服务器保留原生 `spawn`——跨双流监听就绪行加上分级的 SIGTERM→等待→SIGKILL 拆除就是该处的全部内容,execa 在那里删不掉任何东西;本 Agent Note 涉及该文件的部分,仅是那份已成为死代码的 `.env` 解析器。
|
||||
- `llm-mock-server` 的 CLI(命令行界面)经由 `parseArgs` 切分(strict、不允许位置参数);数值转换、边界检查与跨选项约束仍手工实现,固定错误消息的测试改为采用 `parseArgs` 自己的切分器文本。
|
||||
- 两份 `loadRootEnv` 拷贝被整体删除:拥有它们的 vitest 配置(`vitest.web.config.ts` 无条件、`vitest.snapshot.config.ts` 在 record 模式下)在这些文件运行之前就加载了仓库根部的 `.env`。
|
||||
- 那四个轮询循环改乘 `vi.waitFor`,显式传入 `{ interval, timeout }`,并在回调中抛出带描述信息的错误;`waitForPersistedTurnStart` 把「持久化记录格式非法」的校验错误捕获到重试循环之外,使其立即让运行失败,而不是被重试到截止时间。
|
||||
- 那四个轮询循环改用 `vi.waitFor`,显式传入 `{ interval, timeout }`,并在回调中抛出带描述信息的错误;`waitForPersistedTurnStart` 把「持久化记录格式非法」的校验错误捕获到重试循环之外,使其立即让运行失败,而不是被重试到截止时间。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -32,6 +32,6 @@ Status: implemented
|
||||
|
||||
- 手写的收集/超时代码块全部移除,包括 `loader-smoke` 中两个标注 `/* v8 ignore */`、无法人为诱发的 OS 错误分支:spawn 与流故障如今经由 execa 的结果字段结算,这个 `src/` 文件不再携带任何覆盖率豁免,逐文件门禁覆盖其余全部分支。
|
||||
- 捕获的输出如今受 execa 默认 100 MB `maxBuffer` 约束(溢出即终止子进程),此前是无界的;`loader-smoke` README 的局限条目反映了这一点。
|
||||
- 直接子进程的超时终止以及退出/信号结果规范化均由 execa 跨平台负责,不再逐处手写;如 `loader-smoke` README 所述,这些辅助函数依然不负责终止进程树。每个改写后的套件在本次变更中已在 POSIX 上重新运行,另一平台由 Windows CI 车道负责。
|
||||
- 直接子进程的超时终止以及退出/信号结果规范化均由 execa 跨平台负责,不再逐处手写;如 `loader-smoke` README 所述,这些辅助函数依然不负责终止进程树。每个改写后的套件在本次变更中已在 POSIX 上重新运行,另一平台由 Windows CI 通道负责。
|
||||
- execa 是新增的根 devDependency(此前完全不存在于 lockfile 中);它是 npm 上被依赖最多的包之一且维护活跃,exe/运行时闭包不受影响(仅测试使用)。
|
||||
- mock-server CLI 切分器层面的错误文本不再由本仓库决定:未知选项、缺失取值与多余位置参数报告 `parseArgs` 的措辞,并在 `tests/cli.spec.ts` 中如此固定。
|
||||
|
||||
Reference in New Issue
Block a user