
DeepSeek Harness 快照测试 Fixture 精简以session.jsonl作为唯一快照会话日志产物【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness本文讲解 DeepSeek HarnessdshACP 快照测试体系中一次关键的测试基建精简决策移除session.expected.jsonl这一冗余产物让每个快照场景只保留一个会话日志 artifact ——session.jsonl使其同时充当 replay 数据源与期望输出。读完本文你将理解三类快照场景录制型、作者化 override 型、无模型型下session.jsonl的不同语义、replay.override.json的三种ReplayEntry写法以及各侧独立上下文归一化这一保证比较稳定性的核心机制并掌握如何用源码与真实 fixture 验证这套约定。背景一个场景为何会同时携带两份会话日志在 DeepSeek Harness 的快照测试体系中每个场景目录提交一组 fixture其中会话日志是核心部分。本次决策之前模型驱动的 ACP 快照场景同时提交两份日志session.jsonl—— 对普通录制型场景而言它是从真实运行中采集harvest的 replay fixture即模型流的原始档案session.expected.jsonl—— replay 测试把新持久化的日志归一化后与这份期望日志进行比较。问题在于对于普通录制型场景两个文件经过归一化之后字节级完全一致。也就是说session.expected.jsonl是一个纯粹的冗余副本白白增加了一次评审、维护与匹配的成本。而作者化authoredoverride 场景如error-finish、cancel当时采用的是另一套更复杂的布局用replay.override.json驱动模型行为session.jsonl退化为一份最小的占位dummyfixture真正的期望持久化日志放在session.expected.jsonl里。实际上这份拆分同样没有必要——当 override sidecar 存在时llm-replay会用 override 替换由session.jsonl推导出的模型脚本根本不需要从session.jsonl里读取模型 chunk。因此session.jsonl完全可以同时扮演期望会话日志的角色。决策每个场景至多提交一份会话日志决策很直接彻底移除session.expected.jsonl概念。从此以后每个场景至多提交一个会话日志 artifactsession.jsonl其语义按场景类型划分场景类型session.jsonl的角色模型行为来源比较方式录制型recorded原始采集日志raw harvested logreplay 从session.jsonl中的assistant/chunk事件推导模型脚本replay 运行归一化后的持久化日志 与 归一化后的session.jsonl比较作者化 override 型authored期望产生的会话日志expected produced logreplay.override.json驱动模型行为存在 override 时 replay 适配器忽略 fixture 中的模型 chunk同上replay 归一化结果与session.jsonl比较无模型型no-model启动llm-replay所需的最小 fixture无不调用模型除非场景创建了持久化会话否则无需会话日志比较stdout 期望输出保持不变。决策原文明确说明stdout 期望输出是面向编辑器editor-facing的投影与会话 fixture 并不冗余两者互补——stdout 覆盖自动化最小传输线ACP JSON-RPC 帧JSONL 覆盖循环、工具与边界结构。源码级验证一fixture 命名约束与孤儿守卫sessionFixtureNames()是这套约定在代码中的第一道硬约束位于 packages/test-support/session-snapshot/src/suite.ts主 fixture 必须叫session.jsonl缺失直接报missing session.jsonl子代理subagent会话采用连续编号session.1.jsonl、session.2.jsonl…编号不连续或形如session.xxx.jsonl的其他后缀会 fail loud一个目录是事实来源source of truth场景表无需重复声明子会话数量避免两者漂移。session.expected.jsonl这个名字在快照 harness、fixtures、孤儿守卫与文档中已无任何踪迹可在仓库中全局搜索验证命中结果仅剩个别 e2e 测试脚本文件名层面的.expected命名属于另一层级的产物命名不参与快照 fixture 体系。配合这套命名的是两类守卫测试同样位于 suite.tsno-orphans 检查snapshots/dir下每个目录必须出现在场景表中重命名/删除场景后遗留的过期目录会直接报错必需文件检查每个场景必须存在input.json、stdout.expected.jsonl、session.jsonl且replay.override.json的存在与否必须与场景声明的overridden标志精确匹配——因为 harness 仅凭文件存在性就转发 override见 harness.ts 中DSH_SNAPSHOT_OVERRIDE环境变量的注入一个未注册的游离 sidecar 会悄悄改变推导出的脚本所以守卫必须双向 fail loud。源码级验证二同一份session.jsonl身兼二职在 suite.ts 中可以看到replay 模式把session.jsonl作为fixtureFile传给 harnessreplay 从中推导模型脚本同时在同一测试末尾L1378-L1393把它作为期望输出进行比较const harvested result.sessionLogs.map(log log.content) const fixtures await Promise.all(fixtureFiles.map(file readFile(join(dir, file), utf8))) const fixtureContexts fixtures.map(fixtureContext) const fixtureCtx: NormalizeContext { sessionIds: fixtureContexts.flatMap(context context.sessionIds), cwd: (fixtureContexts[0] as NormalizeContext).cwd, } const actualSnapshots normalizeSessionSnapshots(harvested, ctx) const expectedSnapshots normalizeSessionSnapshots(fixtures, fixtureCtx) for (const [index, actual] of actualSnapshots.entries()) { expect(actual, ${fixtureFiles[index]} mismatch).toEqual(expectedSnapshots[index]) }注意这里的比较用toEqual纯值相等而非toMatchFileSnapshot——这正是实现说明里session logs use plain equality rather than file-snapshot updates, so comparison never rewrites fixtures的落点stdout 期望输出用toMatchFileSnapshotvitest 在--update下可重写而会话日志的等值比较永远不会改写 fixture从而保证评审时可追溯、CI 无副作用。核心机制各侧独立上下文归一化与幂等性既然 replay 运行与录制运行在会话 id、cwd、时间戳上必然不同比较前必须归一化。关键设计是每一侧都用自己的 header 推导归一化上下文运行侧actual使用本次运行真实产生的sessionId、cwd与cwdAliases见 harness.ts 的ctx构造fixture 侧expected使用fixtureContext(fixture)它从session.jsonl的第一行 header 读取id与cwd构造上下文suite.ts。export function fixtureContext(fixture: string): NormalizeContext { const firstLine fixture.split(\n).find(line line.trim().length 0) ?? {} const header JSON.parse(firstLine) as { id?: unknown; cwd?: unknown } return { sessionIds: typeof header.id string ? [header.id] : [], cwd: typeof header.cwd string ? header.cwd : \0no-cwd\0, } }这个设计有两个重要推论幂等性session.jsonl中的值已经是归一化后的 token如{{session:1}}、{{cwd}}再次对已归一化的 fixture 执行归一化不会产生变化——所以用 fixture 自身的 header 推导上下文是安全的为什么要独立上下文而非共享上下文这就是备选方案被拒绝的根本原因。normalizeSessionLog对 cwd 的擦除采用精确字符串匹配normalize.ts 的replaceCwdSpelling用indexOf逐段匹配。如果两侧共用一个 replay 运行上下文fixture 中记录的旧 cwd 会因为与当前值不同而幸存于擦除导致每次比较必然失败。因此只能各侧使用自己 header 推导的上下文。归一化器本身负责擦除会话 id、运行 cwd、RPC id、时间戳、goal 生命周期时钟与 hook 时长同时保留语义负载值normalize.ts 的模块说明。此外还包含针对 macOS/private/tmp软链的cwdAliases处理、快照 spill 路径 token 化{{spillLocator:name}}等平台细节。replay.override.json的三种 ReplayEntry对于assistant/chunk无法表达的模型行为抛错、挂起需要手写replay.override.json。其元素类型定义在 packages/test-support/llm-replay/src/index.tsexport type ReplayEntry | { kind: chunks; chunks: StreamChunk[] } | { kind: throw; chunks: StreamChunk[]; message: string; code: string; accepted?: boolean } | { kind: hang; readyFile?: string }chunks普通成功流。录制型场景由deriveReplayScript()从assistant/chunk事件自动推导按终态finishchunk 切分每次调用一般无需手写throw模拟流中/流前失败。chunks可携带前缀 chunk 模拟中途失败mid-stream failuremessage/code描述错误accepted控制是否被重试策略接受。例如error-finish场景snapshots/session/error-finish/replay.override.json[ { kind: throw, chunks: [], message: simulated provider error (HTTP 401), code: AUTH } ]hang模拟永不返回的挂起流用于取消cancel场景。readyFile为可选标记在前缀 chunk 消费完后、流进入等待取消之前写入供 harness 的waitForFile步骤感知就绪。例如cancel场景snapshots/acp/cancel/replay.override.json[ { kind: hang, readyFile: .dsh-snapshot-stream-ready } ]加载逻辑见loadReplayScript()llm-replay/src/index.ts当overrideFile存在或环境变量DSH_SNAPSHOT_OVERRIDE指向它时sidecar 文档整体替换推导脚本sidecar 还支持{ patches: [...] }增补形式按调用索引替换/追加单个 entry。override 优先是单向的——覆盖后session.jsonl中的模型 chunk 不再参与模型驱动所以同一份session.jsonl可以毫无歧义地作为期望日志存在。真实 fixture 解剖cancel 场景snapshots/acp/cancel/目录是本次决策后作者化 override 型场景的标准形态snapshot.ymlversion: 1 scenario: cancel profile: acp composition: acp-default recording: authored # 绝不参与 live 重录 header: class: acp-default replay: override: true # 声明必须存在 replay.override.json其 session.jsonl 完整记录了被取消回合的期望持久化形态session头部id/cwd 均为 token、permission/preset、sandbox/mode、agent/inbox/spliced用户消息、turn/start、step/start、系统提示注入、request/headersystem/tools 均为 token、流中被打断的assistant/chunk与assistant/messageinterrupted:true最终以turn/endreason.kind: aborted收束。这恰好演示了同一文件既是被 replay 忽略模型 chunk 的 fixture又是被逐事件比较的期望日志。对照地普通录制型场景如 snapshots/acp/escalation-rejected目录中没有replay.override.jsonsession.jsonl保持原始采集日志形态模型 chunk 由deriveReplayScript消费并重放。备选方案回顾与取舍决策记录中明确评估过一个替代设计将两侧都按共享replay 运行上下文归一化—— 被拒绝。拒绝理由正是上一节提到的精确字符串匹配问题normalizeSessionLog对 cwd 的擦除是 exact string match共享上下文下 fixture 里记录的 cwd 无法被擦除每次比较都会失败。相比之下各侧用自己的 header 推导上下文不仅让已归一化的 fixture 幂等也让录制与重放各自的易变值ids、paths、timestamps天然对号入座。这一取舍体现了快照测试的一条通用原则归一化上下文必须与数据的产生方绑定而不是与比较的发起方绑定。影响与展望移除session.expected.jsonl后评审者失去的只是一个把期望日志与 replay fixture 视觉分开的文件名得到的是一套更简单的规则——一个场景、一个会话日志、双重职责。回归保护并未削弱stdout 期望输出继续守护编辑器会话记录editor transcriptreplay 输出与session.jsonl的比较完整保留了循环agent loop与持久化persistence回归检查只是不再复制文件。对于想要在 DeepSeek Harness 中新增快照场景的开发者本决策后的 fixture 清单是确定性的input.jsonstdout.expected.jsonlsession.jsonl 视情况replay.override.json、workspace/、snapshot.yml所有文件必须与场景表声明一一对应否则守卫测试会在收集期直接失败。完整的录制/重放机制背景可进一步参阅 ACP 快照测试 Agent Note 与 harness.ts 的实现注释。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考