物)
DeepSeek Harness 快照測試 Fixture 精簡以session.jsonl作為唯一快照會話日志產(chǎn)物【免費下載鏈接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.項目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness本文講解 DeepSeek HarnessdshACP 快照測試體系中一次關(guān)鍵的測試基建精簡決策移除session.expected.jsonl這一冗余產(chǎn)物讓每個快照場景只保留一個會話日志 artifact ——session.jsonl使其同時充當(dāng) replay 數(shù)據(jù)源與期望輸出。讀完本文你將理解三類快照場景錄制型、作者化 override 型、無模型型下session.jsonl的不同語義、replay.override.json的三種ReplayEntry寫法以及各側(cè)獨立上下文歸一化這一保證比較穩(wěn)定性的核心機制并掌握如何用源碼與真實 fixture 驗證這套約定。背景一個場景為何會同時攜帶兩份會話日志在 DeepSeek Harness 的快照測試體系中每個場景目錄提交一組 fixture其中會話日志是核心部分。本次決策之前模型驅(qū)動的 ACP 快照場景同時提交兩份日志session.jsonl—— 對普通錄制型場景而言它是從真實運行中采集harvest的 replay fixture即模型流的原始檔案session.expected.jsonl—— replay 測試把新持久化的日志歸一化后與這份期望日志進行比較。問題在于對于普通錄制型場景兩個文件經(jīng)過歸一化之后字節(jié)級完全一致。也就是說session.expected.jsonl是一個純粹的冗余副本白白增加了一次評審、維護與匹配的成本。而作者化authoredoverride 場景如error-finish、cancel當(dāng)時采用的是另一套更復(fù)雜的布局用replay.override.json驅(qū)動模型行為session.jsonl退化為一份最小的占位dummyfixture真正的期望持久化日志放在session.expected.jsonl里。實際上這份拆分同樣沒有必要——當(dāng) override sidecar 存在時llm-replay會用 override 替換由session.jsonl推導(dǎo)出的模型腳本根本不需要從session.jsonl里讀取模型 chunk。因此session.jsonl完全可以同時扮演期望會話日志的角色。決策每個場景至多提交一份會話日志決策很直接徹底移除session.expected.jsonl概念。從此以后每個場景至多提交一個會話日志 artifactsession.jsonl其語義按場景類型劃分場景類型session.jsonl的角色模型行為來源比較方式錄制型recorded原始采集日志raw harvested logreplay 從session.jsonl中的assistant/chunk事件推導(dǎo)模型腳本replay 運行歸一化后的持久化日志 與 歸一化后的session.jsonl比較作者化 override 型authored期望產(chǎn)生的會話日志expected produced logreplay.override.json驅(qū)動模型行為存在 override 時 replay 適配器忽略 fixture 中的模型 chunk同上replay 歸一化結(jié)果與session.jsonl比較無模型型no-model啟動llm-replay所需的最小 fixture無不調(diào)用模型除非場景創(chuàng)建了持久化會話否則無需會話日志比較stdout 期望輸出保持不變。決策原文明確說明stdout 期望輸出是面向編輯器editor-facing的投影與會話 fixture 并不冗余兩者互補——stdout 覆蓋自動化最小傳輸線ACP JSON-RPC 幀JSONL 覆蓋循環(huán)、工具與邊界結(jié)構(gòu)。源碼級驗證一fixture 命名約束與孤兒守衛(wèi)sessionFixtureNames()是這套約定在代碼中的第一道硬約束位于 packages/test-support/session-snapshot/src/suite.ts主 fixture 必須叫session.jsonl缺失直接報missing session.jsonl子代理subagent會話采用連續(xù)編號session.1.jsonl、session.2.jsonl…編號不連續(xù)或形如session.xxx.jsonl的其他后綴會 fail loud一個目錄是事實來源source of truth場景表無需重復(fù)聲明子會話數(shù)量避免兩者漂移。session.expected.jsonl這個名字在快照 harness、fixtures、孤兒守衛(wèi)與文檔中已無任何蹤跡可在倉庫中全局搜索驗證命中結(jié)果僅剩個別 e2e 測試腳本文件名層面的.expected命名屬于另一層級的產(chǎn)物命名不參與快照 fixture 體系。配合這套命名的是兩類守衛(wèi)測試同樣位于 suite.tsno-orphans 檢查snapshots/dir下每個目錄必須出現(xiàn)在場景表中重命名/刪除場景后遺留的過期目錄會直接報錯必需文件檢查每個場景必須存在input.json、stdout.expected.jsonl、session.jsonl且replay.override.json的存在與否必須與場景聲明的overridden標志精確匹配——因為 harness 僅憑文件存在性就轉(zhuǎn)發(fā) override見 harness.ts 中DSH_SNAPSHOT_OVERRIDE環(huán)境變量的注入一個未注冊的游離 sidecar 會悄悄改變推導(dǎo)出的腳本所以守衛(wèi)必須雙向 fail loud。源碼級驗證二同一份session.jsonl身兼二職在 suite.ts 中可以看到replay 模式把session.jsonl作為fixtureFile傳給 harnessreplay 從中推導(dǎo)模型腳本同時在同一測試末尾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——這正是實現(xiàn)說明里session logs use plain equality rather than file-snapshot updates, so comparison never rewrites fixtures的落點stdout 期望輸出用toMatchFileSnapshotvitest 在--update下可重寫而會話日志的等值比較永遠不會改寫 fixture從而保證評審時可追溯、CI 無副作用。核心機制各側(cè)獨立上下文歸一化與冪等性既然 replay 運行與錄制運行在會話 id、cwd、時間戳上必然不同比較前必須歸一化。關(guān)鍵設(shè)計是每一側(cè)都用自己的 header 推導(dǎo)歸一化上下文運行側(cè)actual使用本次運行真實產(chǎn)生的sessionId、cwd與cwdAliases見 harness.ts 的ctx構(gòu)造fixture 側(cè)expected使用fixtureContext(fixture)它從session.jsonl的第一行 header 讀取id與cwd構(gòu)造上下文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, } }這個設(shè)計有兩個重要推論冪等性session.jsonl中的值已經(jīng)是歸一化后的 token如{{session:1}}、{{cwd}}再次對已歸一化的 fixture 執(zhí)行歸一化不會產(chǎn)生變化——所以用 fixture 自身的 header 推導(dǎo)上下文是安全的為什么要獨立上下文而非共享上下文這就是備選方案被拒絕的根本原因。normalizeSessionLog對 cwd 的擦除采用精確字符串匹配normalize.ts 的replaceCwdSpelling用indexOf逐段匹配。如果兩側(cè)共用一個 replay 運行上下文fixture 中記錄的舊 cwd 會因為與當(dāng)前值不同而幸存于擦除導(dǎo)致每次比較必然失敗。因此只能各側(cè)使用自己 header 推導(dǎo)的上下文。歸一化器本身負責(zé)擦除會話 id、運行 cwd、RPC id、時間戳、goal 生命周期時鐘與 hook 時長同時保留語義負載值normalize.ts 的模塊說明。此外還包含針對 macOS/private/tmp軟鏈的cwdAliases處理、快照 spill 路徑 token 化{{spillLocator:name}}等平臺細節(jié)。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事件自動推導(dǎo)按終態(tài)finishchunk 切分每次調(diào)用一般無需手寫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當(dāng)overrideFile存在或環(huán)境變量DSH_SNAPSHOT_OVERRIDE指向它時sidecar 文檔整體替換推導(dǎo)腳本sidecar 還支持{ patches: [...] }增補形式按調(diào)用索引替換/追加單個 entry。override 優(yōu)先是單向的——覆蓋后session.jsonl中的模型 chunk 不再參與模型驅(qū)動所以同一份session.jsonl可以毫無歧義地作為期望日志存在。真實 fixture 解剖cancel 場景snapshots/acp/cancel/目錄是本次決策后作者化 override 型場景的標準形態(tài)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 完整記錄了被取消回合的期望持久化形態(tài)session頭部id/cwd 均為 token、permission/preset、sandbox/mode、agent/inbox/spliced用戶消息、turn/start、step/start、系統(tǒng)提示注入、request/headersystem/tools 均為 token、流中被打斷的assistant/chunk與assistant/messageinterrupted:true最終以turn/endreason.kind: aborted收束。這恰好演示了同一文件既是被 replay 忽略模型 chunk 的 fixture又是被逐事件比較的期望日志。對照地普通錄制型場景如 snapshots/acp/escalation-rejected目錄中沒有replay.override.jsonsession.jsonl保持原始采集日志形態(tài)模型 chunk 由deriveReplayScript消費并重放。備選方案回顧與取舍決策記錄中明確評估過一個替代設(shè)計將兩側(cè)都按共享replay 運行上下文歸一化—— 被拒絕。拒絕理由正是上一節(jié)提到的精確字符串匹配問題normalizeSessionLog對 cwd 的擦除是 exact string match共享上下文下 fixture 里記錄的 cwd 無法被擦除每次比較都會失敗。相比之下各側(cè)用自己的 header 推導(dǎo)上下文不僅讓已歸一化的 fixture 冪等也讓錄制與重放各自的易變值ids、paths、timestamps天然對號入座。這一取舍體現(xiàn)了快照測試的一條通用原則歸一化上下文必須與數(shù)據(jù)的產(chǎn)生方綁定而不是與比較的發(fā)起方綁定。影響與展望移除session.expected.jsonl后評審者失去的只是一個把期望日志與 replay fixture 視覺分開的文件名得到的是一套更簡單的規(guī)則——一個場景、一個會話日志、雙重職責(zé)?;貧w保護并未削弱stdout 期望輸出繼續(xù)守護編輯器會話記錄editor transcriptreplay 輸出與session.jsonl的比較完整保留了循環(huán)agent loop與持久化persistence回歸檢查只是不再復(fù)制文件。對于想要在 DeepSeek Harness 中新增快照場景的開發(fā)者本決策后的 fixture 清單是確定性的input.jsonstdout.expected.jsonlsession.jsonl 視情況replay.override.json、workspace/、snapshot.yml所有文件必須與場景表聲明一一對應(yīng)否則守衛(wèi)測試會在收集期直接失敗。完整的錄制/重放機制背景可進一步參閱 ACP 快照測試 Agent Note 與 harness.ts 的實現(xiàn)注釋?!久赓M下載鏈接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.項目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考