news 2026/9/19 4:44:35

DeepSeek Harness 快照测试 Fixture 精简:以 `session.jsonl` 作为唯一快照会话日志产物

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek Harness 快照测试 Fixture 精简:以 `session.jsonl` 作为唯一快照会话日志产物

DeepSeek Harness 快照测试 Fixture 精简:以session.jsonl作为唯一快照会话日志产物

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

本文讲解 DeepSeek Harness(dsh)ACP 快照测试体系中一次关键的测试基建精简决策:移除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是一个纯粹的冗余副本,白白增加了一次评审、维护与匹配的成本。

而作者化(authored)override 场景(如error-finishcancel)当时采用的是另一套更复杂的布局:用replay.override.json驱动模型行为,session.jsonl退化为一份最小的占位(dummy)fixture,真正的期望持久化日志放在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 log)replay 从session.jsonl中的assistant/chunk事件推导模型脚本replay 运行归一化后的持久化日志 与 归一化后的session.jsonl比较
作者化 override 型(authored)期望产生的会话日志(expected produced log)replay.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.jsonlsession.2.jsonl…,编号不连续或形如session.xxx.jsonl的其他后缀会 fail loud;
  • 一个目录是事实来源(source of truth),场景表无需重复声明子会话数量,避免两者漂移。

session.expected.jsonl这个名字在快照 harness、fixtures、孤儿守卫与文档中已无任何踪迹,可在仓库中全局搜索验证(命中结果仅剩个别 e2e 测试脚本文件名层面的.expected命名,属于另一层级的产物命名,不参与快照 fixture 体系)。

配合这套命名的是两类守卫测试(同样位于 suite.ts):

  • no-orphans 检查snapshots/<dir>下每个目录必须出现在场景表中,重命名/删除场景后遗留的过期目录会直接报错;
  • 必需文件检查:每个场景必须存在input.jsonstdout.expected.jsonlsession.jsonl,且replay.override.json的存在与否必须与场景声明的overridden标志精确匹配——因为 harness 仅凭文件存在性就转发 override(见 harness.ts 中DSH_SNAPSHOT_OVERRIDE环境变量的注入),一个未注册的游离 sidecar 会悄悄改变推导出的脚本,所以守卫必须双向 fail loud。

源码级验证二:同一份session.jsonl身兼二职

在 suite.ts 中可以看到,replay 模式把session.jsonl作为fixtureFile传给 harness(replay 从中推导模型脚本),同时在同一测试末尾(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 期望输出用toMatchFileSnapshot(vitest 在--update下可重写),而会话日志的等值比较永远不会改写 fixture,从而保证评审时可追溯、CI 无副作用。

核心机制:各侧独立上下文归一化与幂等性

既然 replay 运行与录制运行在会话 id、cwd、时间戳上必然不同,比较前必须归一化。关键设计是每一侧都用自己的 header 推导归一化上下文

  • 运行侧(actual)使用本次运行真实产生的sessionIdcwdcwdAliases(见 harness.ts 的ctx构造);
  • fixture 侧(expected)使用fixtureContext(fixture),它从session.jsonl的第一行 header 读取idcwd构造上下文(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', } }

这个设计有两个重要推论:

  1. 幂等性session.jsonl中的值已经是归一化后的 token(如{{session:1}}{{cwd}}),再次对已归一化的 fixture 执行归一化不会产生变化——所以用 fixture 自身的 header 推导上下文是安全的;
  2. 为什么要独立上下文而非共享上下文:这就是"备选方案"被拒绝的根本原因。normalizeSessionLog对 cwd 的擦除采用精确字符串匹配(normalize.ts 的replaceCwdSpellingindexOf逐段匹配)。如果两侧共用一个 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.ts:

export 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 failure),message/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.yml):

version: 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/presetsandbox/modeagent/inbox/spliced用户消息、turn/startstep/start、系统提示注入、request/header(system/tools 均为 token)、流中被打断的assistant/chunkassistant/message"interrupted":true),最终以turn/end+reason.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 transcript);
  • replay 输出与session.jsonl的比较完整保留了循环(agent loop)与持久化(persistence)回归检查,只是不再复制文件。

对于想要在 DeepSeek Harness 中新增快照场景的开发者,本决策后的 fixture 清单是确定性的:input.json+stdout.expected.jsonl+session.jsonl(+ 视情况replay.override.jsonworkspace/snapshot.yml),所有文件必须与场景表声明一一对应,否则守卫测试会在收集期直接失败。完整的录制/重放机制背景可进一步参阅 ACP 快照测试 Agent Note 与 harness.ts 的实现注释。

【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 4:44:15

潮州带本地时蔬小炒的美食店推荐,趣边白粥广受好评

来潮州探寻地道潮汕风味&#xff0c;不少食客都希望找到能吃齐传统白粥、卤水生腌&#xff0c;还能品尝新鲜本地时蔬小炒的靠谱门店&#xff0c;潮州餐饮市场门店众多&#xff0c;品类齐全、定价透明、食材新鲜的门店&#xff0c;往往更受本地食客与外地游客的认可&#xff0c;…

作者头像 李华
网站建设 2026/9/19 4:42:43

Stata离线安装ivreghdfe全攻略:依赖包、路径配置与报错排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 4:42:39

Windows TXT阅读器推荐:从编码到同步的完整选型指南

Windows 上找一款舒服的 TXT 阅读器&#xff0c;听起来是个小事&#xff0c;但真正在电脑上读过小说、翻过技术文档、处理过几百 MB 日志的人都知道&#xff0c;这里面的坑一点都不比选专业软件少。很多老牌阅读器要么只做手机端&#xff0c;要么在 Windows 上界面停留在十年前…

作者头像 李华
网站建设 2026/9/19 4:39:22

SPC控制图选型与Python实现避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 4:38:56

Codex本地部署完整指南:环境变量配置与安装避坑

1. 从"下载完打不开"说起&#xff1a;Codex本地部署到底难在哪很多人第一次接触 Codex&#xff0c;卡住的地方根本不是模型本身&#xff0c;而是"下载完之后怎么办"。官网给的安装包双击没反应、命令行敲进去提示找不到命令、环境变量配完重启终端还是报错…

作者头像 李华