Browse Source

refactor(snapshot): remove dead fixture path plumbing

Tianyi Cui 1 month ago
parent
commit
343a20fb6f
28 changed files with 54 additions and 127 deletions
  1. 2 2
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml
  2. 2 0
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md
  3. 2 0
      .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md
  4. 2 2
      apps/cli/tests/profiles/acp/tests/goal.expected.e2e.ts
  5. 1 1
      apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts
  6. 2 2
      apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts
  7. 2 2
      apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts
  8. 2 2
      apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts
  9. 2 2
      apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts
  10. 11 24
      apps/web/tests/scaffold.ts
  11. 1 1
      apps/web/tests/schedule-after.e2e.ts
  12. 0 1
      apps/web/tests/subagent-conversation.e2e.ts
  13. 1 1
      docs/config-catalog.i18n.yaml
  14. 1 1
      docs/config-catalog.md
  15. 2 2
      packages/test-support/llm-replay/README.i18n.yaml
  16. 1 1
      packages/test-support/llm-replay/README.md
  17. 1 1
      packages/test-support/llm-replay/README.zh.md
  18. 1 15
      packages/test-support/llm-replay/src/index.ts
  19. 3 11
      packages/test-support/llm-replay/tests/llm-replay.spec.ts
  20. 2 2
      packages/test-support/session-snapshot/README.i18n.yaml
  21. 1 1
      packages/test-support/session-snapshot/README.md
  22. 1 1
      packages/test-support/session-snapshot/README.zh.md
  23. 0 1
      packages/test-support/session-snapshot/src/index.ts
  24. 4 14
      packages/test-support/session-snapshot/src/normalize.ts
  25. 2 5
      packages/test-support/session-snapshot/src/suite.ts
  26. 0 10
      packages/test-support/session-snapshot/tests/normalize.spec.ts
  27. 2 6
      snapshots/sdk/sdk.snapshot.ts
  28. 3 16
      snapshots/session/headless.snapshot.ts

+ 2 - 2
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 .agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md
-2026-08-24-session-log-snapshot-corpus.md: d7ade497b08036e577cc0a1516e21e841808f26f
-2026-08-24-session-log-snapshot-corpus.zh.md: 903d2a818aa20ae8b516d2d899a8e4f9a8aaa455
+2026-08-24-session-log-snapshot-corpus.md: 57f90f7fd77a0797be0986bca6009d696d5ed581
+2026-08-24-session-log-snapshot-corpus.zh.md: 135afc0fc76b523a36e2b84488237a84b5c0bd4f

+ 2 - 0
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.md

@@ -18,6 +18,8 @@ This decision supersedes the ACP-specific placement and controller ownership in
 
 The recorded session remains the primary input and expected output. Human-originated messages drive the selected public interface, recorded assistant chunks drive deterministic model replay, and the normalized persisted result must equal the fixture. Parent and child sessions share one typed redaction map. Committed fixtures contain relationship-preserving identity tokens and replace request system prompts and tool schemas with tokens; each distinct header class retains one explicit sidecar owner.
 
+Fixture decoding and comparison depend only on the selected JSONL content; filenames identify inventory roles but are not parser inputs. The same strict static catalog validates replay, seed, record, refresh, and normalized comparison paths.
+
 Each parent or child role uses `session[.<ordinal>][.vN].jsonl`, with v0 encoded by an omitted version and every filename matching its header. Replay, record, and refresh select the numerically highest generation per role. Most owners omit `sessionFormat` and track the current writer; a bounded historical owner declares its exact version and closed coverage names. The v2 corpus keeps selected v0 roles for multi-hop, packed-row, retry/failure, and shipped-profile coverage plus selected v1 roles for the adjacent structural edge. Record and refresh never rewrite an explicitly retained historical fixture, rename a committed generation, or delete one through automatic cleanup. Reviewed source-tree curation removes a predecessor only after the same role has a verified current successor. The corpus policy requires current selected roles to remain the majority and caps historical selected roles at ten; lower predecessor generations may remain beside a selected current successor.
 
 Scenario-owned HTTP fixtures separate the stable authority recorded in the session from their transport listener. Each fixture binds loopback port `0`, lets the operating system allocate and bind the port atomically, and maps the recorded URL or endpoint through the real provider to that listener. Any process-global transport interception matches only the recorded endpoint, is owned by the fixture fiber, and is restored before the listener closes.

+ 2 - 0
.agents/notes/implemented/testing/2026-08-24-session-log-snapshot-corpus.zh.md

@@ -18,6 +18,8 @@ Status: implemented
 
 录制会话仍是主要输入和预期输出。来自用户的消息驱动所选公开接口,录制的 assistant chunk 驱动确定性模型回放,规范化后的持久化结果必须等于 fixture。父会话和子会话共享同一类型化脱敏映射。提交的 fixture 使用保留关系的身份 token,并将请求 system prompt 和工具 schema 替换为 token;每个不同 header 类仍保留一个显式 sidecar 所有者。
 
+Fixture 解码与比较只取决于选定 JSONL 内容;文件名标识 inventory role,但不是 parser 输入。replay、seed、record、refresh 与规范化比较路径都使用同一个严格静态 catalog 校验。
+
 每个 parent 或 child 角色都使用 `session[.<ordinal>][.vN].jsonl`;v0 省略版本,且每个文件名都与其 header 一致。回放、录制与刷新按角色选择数值最高的 generation。大多数 owner 省略 `sessionFormat` 并跟随当前 writer;受限的历史 owner 会声明精确版本与封闭 coverage 名称。v2 语料保留选定 v0 角色,覆盖多跳、打包行、重试/失败与随附 profile,并保留选定 v1 角色覆盖相邻结构 edge。录制与刷新绝不改写显式保留的历史 fixture、重命名已提交 generation 或通过自动清理删除 generation。受审阅的源树整理只有在同角色存在已验证的当前后继后才移除前代。语料策略要求选定当前角色始终占多数,并将选定历史角色上限设为十个;更低的前代 generation 可以保留在选定当前后继旁。
 
 场景拥有的 HTTP fixture 将会话中录制的稳定 authority 与传输 listener 分离。每个 fixture 在回环地址上绑定端口 `0`,由操作系统以一次原子操作分配并绑定端口,再将录制的 URL 或 endpoint 通过真实 provider 映射到该 listener。任何进程全局传输拦截只匹配录制 endpoint,由 fixture fiber 拥有,并在关闭 listener 前恢复。

+ 2 - 2
apps/cli/tests/profiles/acp/tests/goal.expected.e2e.ts

@@ -67,8 +67,8 @@ function normalizeGoalLog(content: string, context: NormalizeContext): string {
 /** Compare one current normalized goal log with its generation-aware committed fixture. */
 async function expectGoalLog(actual: string, expectedPath: string): Promise<void> {
   const expected = await readFile(expectedPath, 'utf8')
-  expect(normalizeSessionSnapshots([actual], fixtureContext(actual), { sourcePaths: [expectedPath] }).map(parseJsonl))
-    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected), { sourcePaths: [expectedPath] }).map(parseJsonl))
+  expect(normalizeSessionSnapshots([actual], fixtureContext(actual)).map(parseJsonl))
+    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected)).map(parseJsonl))
 }
 
 describe('same-session goal snapshot through the ACP automation driver', () => {

+ 1 - 1
apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts

@@ -71,7 +71,7 @@ async function expectSessionSnapshot(
 ): Promise<void> {
   const [normalizedActual] = normalizeSessionSnapshots([actual], context)
   const expected = await readFile(expectedPath, 'utf8')
-  const [normalizedExpected] = normalizeSessionSnapshots([expected], context, { sourcePaths: [expectedPath] })
+  const [normalizedExpected] = normalizeSessionSnapshots([expected], context)
   expect(parseJsonl(normalizedActual ?? '')).toEqual(parseJsonl(normalizedExpected ?? ''))
 }
 

+ 2 - 2
apps/cli/tests/profiles/headless/tests/semantic-checkpoint.expected.e2e.ts

@@ -31,8 +31,8 @@ async function expectSession(actual: string, expectedPath: string): Promise<void
   const parse = (content: string): Record<string, unknown>[] => content.split('\n')
     .filter(line => line.trim().length > 0)
     .map(line => JSON.parse(line) as Record<string, unknown>)
-  expect(normalizeSessionSnapshots([actual], fixtureContext(actual), { sourcePaths: [expectedPath] }).map(parse))
-    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected), { sourcePaths: [expectedPath] }).map(parse))
+  expect(normalizeSessionSnapshots([actual], fixtureContext(actual)).map(parse))
+    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected)).map(parse))
 }
 
 async function seedInterruptedSession(root: string, cwd: string): Promise<string> {

+ 2 - 2
apps/cli/tests/profiles/headless/tests/subagent-diagnostic.expected.e2e.ts

@@ -37,8 +37,8 @@ async function expectSession(actual: string, expectedPath: string): Promise<void
   const parse = (content: string): Record<string, unknown>[] => content.split('\n')
     .filter(line => line.trim().length > 0)
     .map(line => JSON.parse(line) as Record<string, unknown>)
-  expect(normalizeSessionSnapshots([actual], fixtureContext(actual), { sourcePaths: [expectedPath] }).map(parse))
-    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected), { sourcePaths: [expectedPath] }).map(parse))
+  expect(normalizeSessionSnapshots([actual], fixtureContext(actual)).map(parse))
+    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected)).map(parse))
 }
 
 /**

+ 2 - 2
apps/cli/tests/profiles/headless/tests/subagent-inheritance.expected.e2e.ts

@@ -37,8 +37,8 @@ async function expectSession(actual: string, expectedPath: string): Promise<void
   const parse = (content: string): Record<string, unknown>[] => content.split('\n')
     .filter(line => line.trim().length > 0)
     .map(line => JSON.parse(line) as Record<string, unknown>)
-  expect(normalizeSessionSnapshots([actual], fixtureContext(actual), { sourcePaths: [expectedPath] }).map(parse))
-    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected), { sourcePaths: [expectedPath] }).map(parse))
+  expect(normalizeSessionSnapshots([actual], fixtureContext(actual)).map(parse))
+    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected)).map(parse))
 }
 
 /** Seed a completed parent turn with its read-only policy and current LLM selection. */

+ 2 - 2
apps/cli/tests/profiles/headless/tests/workspace-context-resume.expected.e2e.ts

@@ -47,8 +47,8 @@ async function expectSession(actual: string, expectedPath: string): Promise<void
   const parse = (content: string): Record<string, unknown>[] => content.split('\n')
     .filter(line => line.trim().length > 0)
     .map(line => JSON.parse(line) as Record<string, unknown>)
-  expect(normalizeSessionSnapshots([actual], fixtureContext(actual), { sourcePaths: [expectedPath] }).map(parse))
-    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected), { sourcePaths: [expectedPath] }).map(parse))
+  expect(normalizeSessionSnapshots([actual], fixtureContext(actual)).map(parse))
+    .toEqual(normalizeSessionSnapshots([expected], fixtureContext(expected)).map(parse))
 }
 
 interface SeedBaselineOptions {

+ 11 - 24
apps/web/tests/scaffold.ts

@@ -71,7 +71,6 @@ import type { ReplayHandle } from '@deepseek-ai/dsh-llm-replay'
 import {
   installLlmReplay,
   parseSessionLog,
-  parseSessionLogForReplay,
   prepareSessionSnapshotFixtureForComparison,
 } from '@deepseek-ai/dsh-llm-replay'
 import type { SessionFormatEvent } from '@deepseek-ai/dsh-session-format'
@@ -723,7 +722,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
       if (headerType !== 'session') {
         throw new Error('replayProvidersOnly fixture must open with a session header row')
       }
-      const recorded = parseSessionLogForReplay(fixtureText, replayFixture)
+      const recorded = parseSessionLog(fixtureText)
       const hasModelCall = recorded.some(event => (
         event.type === 'assistant/message' || event.type === 'assistant/attempt'
           || event.type === 'request/header' || event.type === 'tool/call'
@@ -958,11 +957,9 @@ function stableSessionFixture(
   session: Session,
   existing: string,
   workspaceCwd: string,
-  sourcePath?: string,
 ): string {
   const prepared = prepareSessionSnapshotFixtureForComparison(
     normalizeWebSessionVolatiles(rawSessionLog(session), workspaceCwd),
-    sourcePath,
   )
   const stabilized = existing === ''
     ? prepared
@@ -988,7 +985,7 @@ async function assertReplaySession(
   const manifestPath = join(fixtureDir, 'snapshot.yml')
   const manifest = parseSnapshotManifest(await readFile(manifestPath, 'utf8'), manifestPath)
   let expectedPath = fixturePath
-  const userPrompts = fixtureUserPrompts(expected, fixturePath)
+  const userPrompts = fixtureUserPrompts(expected)
   const candidates = sessions.filter((session) => {
     if (session.header.parentSession !== undefined) return false
     const actual = session.snapshotEvents().flatMap((event) => {
@@ -1004,7 +1001,7 @@ async function assertReplaySession(
   if (sessionCwd === undefined) throw new Error(`${fixturePath}: replayed session has no cwd`)
   const actual = rawSessionLog(session)
   if (mode === 'refresh' && writesCurrentSessionFixtures(manifest, mode)) {
-    expected = stableSessionFixture(session, expected, sessionCwd, fixturePath)
+    expected = stableSessionFixture(session, expected, sessionCwd)
     expectedPath = recordedSessionFixturePath(fixturePath, session.header.version)
     await writeFile(expectedPath, expected)
   }
@@ -1018,9 +1015,7 @@ async function assertReplaySession(
     cwd: typeof expectedHeader.cwd === 'string' ? expectedHeader.cwd : '\0no-cwd\0',
   }
   expect(normalizeSessionSnapshots([normalizeWebSessionVolatiles(actual)], actualContext)[0], `${fixturePath}: persisted replay`)
-    .toBe(normalizeSessionSnapshots([normalizeWebSessionVolatiles(expected)], expectedContext, {
-      sourcePaths: [expectedPath],
-    })[0])
+    .toBe(normalizeSessionSnapshots([normalizeWebSessionVolatiles(expected)], expectedContext)[0])
 
   if (manifest.header?.pin !== true) return
   const normalizePrompt = (value: string): string => value
@@ -1057,21 +1052,17 @@ export async function recordFixture(scaffold: WebScaffold, sessionId: SessionId,
   const target = recordedSessionFixturePath(fixturePath, agent.session.header.version)
   const existingPath = existsSync(target) ? target : fixturePath
   const existing = existsSync(existingPath) ? await readFile(existingPath, 'utf8') : ''
-  await writeFile(target, stableSessionFixture(agent.session, existing, scaffold.workspaceCwd, existingPath))
+  await writeFile(target, stableSessionFixture(agent.session, existing, scaffold.workspaceCwd))
 }
 
 /**
  * The user prompts recorded in a fixture, in order — the single source tying
  * spec drive steps to recorded reality so script and fixture cannot drift.
  * @param fixtureText - raw session.jsonl contents.
- * @param fixturePath - exact source path for the closed replay-only refusal policy.
  * @returns the recorded user prompt texts.
  */
-export function fixtureUserPrompts(fixtureText: string, fixturePath?: string): string[] {
-  const events = fixturePath === undefined
-    ? parseSessionLog(fixtureText)
-    : parseSessionLogForReplay(fixtureText, fixturePath)
-  return events.flatMap((event) => {
+export function fixtureUserPrompts(fixtureText: string): string[] {
+  return parseSessionLog(fixtureText).flatMap((event) => {
     if (event.type !== 'user/message' || event.data.source.kind !== 'user') return []
     const text = event.data.content.filter(block => block.type === 'text').map(block => block.text).join('')
     return text.length > 0 ? [text] : []
@@ -1126,7 +1117,6 @@ export function realizeSeedFixture(scaffold: WebScaffold, fixtureText: string, i
 /**
  * Parse a committed web seed fixture through the replay reader.
  * @param fixtureText - session JSONL fixture contents.
- * @param fixturePath - exact source path for the closed replay-only refusal policy.
  * @returns the current header line, parsed header, and logical events.
  */
 /** Give a migrated fixture stream positive relative timing before its final wall-clock rebase. */
@@ -1146,7 +1136,7 @@ function spreadMigratedSeedStream(
   })
 }
 
-export function parseSeedFixture(fixtureText: string, fixturePath?: string): {
+export function parseSeedFixture(fixtureText: string): {
   headerLine: string
   header: Record<string, unknown>
   events: SessionEvent[]
@@ -1154,7 +1144,7 @@ export function parseSeedFixture(fixtureText: string, fixturePath?: string): {
   const sourceHeaderLine = fixtureText.split(/\r?\n/).find(line => line.trim().length > 0)
   if (sourceHeaderLine === undefined) throw new Error('seed fixture has no session header')
   const sourceHeader = JSON.parse(sourceHeaderLine) as { version?: unknown }
-  const current = prepareSessionSnapshotFixtureForComparison(fixtureText, fixturePath)
+  const current = prepareSessionSnapshotFixtureForComparison(fixtureText)
   const headerLine = current.split(/\r?\n/).find(line => line.trim().length > 0)
   if (headerLine === undefined) throw new Error('seed fixture has no session header')
   const header = JSON.parse(headerLine) as Record<string, unknown>
@@ -1219,13 +1209,11 @@ function seedStreamEnd(
 
 /**
  * Seed a recorded session fixture into the scaffold's persistence root
- * through the real Session and JSONL APIs. The source identity is consulted
- * only for the two closed replay-only alpha refusals.
+ * through the real Session and JSONL APIs.
  * @param scaffold - the target scaffold.
  * @param fixtureText - raw recorded session.jsonl contents.
  * @param id - the seeded session id.
  * @param agentPreset - preset recorded by scenarios that assert resumed composition.
- * @param fixturePath - exact source path for replay-only refusal policy.
  * @returns the seeded id.
  */
 export async function seedSession(
@@ -1233,9 +1221,8 @@ export async function seedSession(
   fixtureText: string,
   id: string,
   agentPreset?: string,
-  fixturePath?: string,
 ): Promise<SessionId> {
-  const decoded = parseSeedFixture(realizeSeedFixture(scaffold, fixtureText, id), fixturePath)
+  const decoded = parseSeedFixture(realizeSeedFixture(scaffold, fixtureText, id))
   const events = decoded.events
   if (events.length === 0) throw new Error('seed fixture has no events')
   const last = events[events.length - 1]!

+ 1 - 1
apps/web/tests/schedule-after.e2e.ts

@@ -608,7 +608,7 @@ describe.skipIf(MODE === 'record')('web e2e: active Schedule catalog', () => {
     scaffold = await launchWebScaffold({
       extraOverlayPath: OVERLAY,
     })
-    await seedSession(scaffold, fixture, CATALOG_SESSION_ID, 'standard', CATALOG_FIXTURE)
+    await seedSession(scaffold, fixture, CATALOG_SESSION_ID, 'standard')
     const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd)
     await workspace.attachSession(CATALOG_SESSION_ID)
 

+ 0 - 1
apps/web/tests/subagent-conversation.e2e.ts

@@ -107,7 +107,6 @@ describe('web e2e: persisted subagent conversation and human continuation', () =
     const selectedBaseFixture = await selectedSessionFixture(BASE_FIXTURE)
     const baseFixture = prepareSessionSnapshotFixtureForComparison(
       await readFile(selectedBaseFixture, 'utf8'),
-      selectedBaseFixture,
     )
     sidecarRoot = await mkdtemp(join(tmpdir(), 'dsh-web-subagent-'))
     const childFixturePath = join(sidecarRoot, 'child.jsonl')

+ 1 - 1
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/config-catalog.md
-config-catalog.md: c44b0c29fd26d3b8bebbb8c5342d366e831a3a7d
+config-catalog.md: c4d46dcfe8a84705a0d9ba56c1daa51c5032a952
 config-catalog.zh.md: 5761bda9028905a95cb056b7d81cefec0fd11a48

+ 1 - 1
docs/config-catalog.md

@@ -1355,7 +1355,7 @@ export interface ReplayModelConfig {
 
 Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicyConfig`](../packages/llm/llm/src/index.ts)
 
-Source: [`packages/test-support/llm-replay/src/index.ts:1318`](../packages/test-support/llm-replay/src/index.ts)
+Source: [`packages/test-support/llm-replay/src/index.ts:1304`](../packages/test-support/llm-replay/src/index.ts)
 
 <a id="deepseek-aidsh-llm-retry"></a>
 

+ 2 - 2
packages/test-support/llm-replay/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/test-support/llm-replay/README.md
-README.md: 984a7e1a85c2ce3b59e27b1d053dc9458f14a5d1
-README.zh.md: 5a95ad05466c34e01ef3defc015c32477d2d1d38
+README.md: e8a257c2ee9f95f1e7d20197e6db67b2b4c7b45c
+README.zh.md: 5d085959982bcaa090a893089c0295426141bed2

+ 1 - 1
packages/test-support/llm-replay/README.md

@@ -65,7 +65,7 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
 
 ### How the fixture works
 
-The fixture is a projection of one selected persisted Session generation produced by running the real agent once — this plugin does not record. The snapshot harness supplies the numerically highest canonical parent path (`<scenario>/session.jsonl` for v0 or `<scenario>/session.vN.jsonl` for a positive generation), and validates filename/header agreement before replay. The fixture keeps the header and every event payload but omits body `seq`/`time` envelopes (`seq0`/`time0` for historical packed rows). Replay supplies contiguous sequences and deterministic timestamps, restores typed values replaced by snapshot tokens, rejects partial or mixed envelopes, decodes the complete physical artifact through the build-static Session format catalog, and migrates historical input in memory before it exposes events or the inherited cut; current input takes direct restoration. For a projected v0 header only, an absent `delegationDepth` denotes `0`. The parser never rewrites or renames the fixture. Runtime persistence continues to write complete logs. Replay expands the compact stream on each current-view `assistant/message` or `assistant/attempt`, so a recorded fixture replays the same logical stream the live model produced. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}`; replay materializes validation-only values, while derivation reads only Assistant settlements, marked summary events, and Session metadata. Every fixture, including a source-qualified replay or comparison input, must pass the same catalog validation; replay never repairs a refused artifact.
+The fixture is a projection of one selected persisted Session generation produced by running the real agent once — this plugin does not record. The snapshot harness supplies the numerically highest canonical parent path (`<scenario>/session.jsonl` for v0 or `<scenario>/session.vN.jsonl` for a positive generation), and validates filename/header agreement before replay. The fixture keeps the header and every event payload but omits body `seq`/`time` envelopes (`seq0`/`time0` for historical packed rows). Replay supplies contiguous sequences and deterministic timestamps, restores typed values replaced by snapshot tokens, rejects partial or mixed envelopes, decodes the complete physical artifact through the build-static Session format catalog, and migrates historical input in memory before it exposes events or the inherited cut; current input takes direct restoration. For a projected v0 header only, an absent `delegationDepth` denotes `0`. The parser never rewrites or renames the fixture. Runtime persistence continues to write complete logs. Replay expands the compact stream on each current-view `assistant/message` or `assistant/attempt`, so a recorded fixture replays the same logical stream the live model produced. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}`; replay materializes validation-only values, while derivation reads only Assistant settlements, marked summary events, and Session metadata. Every replay and comparison fixture must pass the same content-only catalog validation; replay never repairs a refused artifact.
 
 ### Nested agents
 

+ 1 - 1
packages/test-support/llm-replay/README.zh.md

@@ -65,7 +65,7 @@ kind: "package-reference"
 
 ### fixture 的工作方式
 
-fixture 是运行一次真实 agent 所产生的一份选定持久化 Session generation 投影,本插件不录制。snapshot harness 会提供数值最高的规范 parent 路径(v0 为 `<scenario>/session.jsonl`,正 generation 为 `<scenario>/session.vN.jsonl`),并在 replay 前校验文件名与 header 一致。fixture 保留 header 与每个事件 payload,但省略正文的 `seq`/`time` envelope(历史 packed row 使用 `seq0`/`time0`)。replay 补充连续序号与确定性 timestamp,恢复被 snapshot token 替换的类型化值,拒绝不完整或混合 envelope,通过构建期静态 Session 格式 catalog 解码完整物理产物,并在公开事件或继承 cut 前于内存中迁移历史输入;当前输入直接 restore。仅对投影 v0 header,缺失的 `delegationDepth` 表示 `0`。parser 从不重写或重命名 fixture。runtime persistence 继续写入完整日志。replay 会展开当前视图中每个 `assistant/message` 或 `assistant/attempt` 的紧凑 stream,因此已记录 fixture 会 replay 与在线模型产生的相同逻辑流。fixture 的 `request/header` 内容可能 token 化为 `{{system}}`/`{{tools}}`;replay 会物化仅用于校验的值,而派生只读取 Assistant settlement、带标记的 summary 事件与 Session metadata。每个 fixture(包括带来源路径的 replay 或比较输入)都必须通过相同 catalog 校验;replay 绝不修复被拒绝的产物。
+fixture 是运行一次真实 agent 所产生的一份选定持久化 Session generation 投影,本插件不录制。snapshot harness 会提供数值最高的规范 parent 路径(v0 为 `<scenario>/session.jsonl`,正 generation 为 `<scenario>/session.vN.jsonl`),并在 replay 前校验文件名与 header 一致。fixture 保留 header 与每个事件 payload,但省略正文的 `seq`/`time` envelope(历史 packed row 使用 `seq0`/`time0`)。replay 补充连续序号与确定性 timestamp,恢复被 snapshot token 替换的类型化值,拒绝不完整或混合 envelope,通过构建期静态 Session 格式 catalog 解码完整物理产物,并在公开事件或继承 cut 前于内存中迁移历史输入;当前输入直接 restore。仅对投影 v0 header,缺失的 `delegationDepth` 表示 `0`。parser 从不重写或重命名 fixture。runtime persistence 继续写入完整日志。replay 会展开当前视图中每个 `assistant/message` 或 `assistant/attempt` 的紧凑 stream,因此已记录 fixture 会 replay 与在线模型产生的相同逻辑流。fixture 的 `request/header` 内容可能 token 化为 `{{system}}`/`{{tools}}`;replay 会物化仅用于校验的值,而派生只读取 Assistant settlement、带标记的 summary 事件与 Session metadata。每个 replay 与 comparison fixture 都必须通过同一个只基于内容的 catalog 校验;replay 绝不修复被拒绝的产物。
 
 ### 嵌套 agent
 

+ 1 - 15
packages/test-support/llm-replay/src/index.ts

@@ -208,16 +208,6 @@ export function parseSessionLog(text: string): SessionEvent[] {
   return parseSessionFixture(text).events
 }
 
-/**
- * Parse one fixture for replay through the same format catalog as ordinary parsing.
- * @param text - raw persisted or projected Session JSONL.
- * @param _sourcePath - selected fixture path; parsing never varies by this value.
- * @returns source events for replay-only test adapters.
- */
-export function parseSessionLogForReplay(text: string, _sourcePath: string): SessionEvent[] {
-  return parseSessionFixture(text).events
-}
-
 /** Parse, complete, decode, and migrate one projected snapshot artifact without writing its source. */
 function parseSessionFixture(text: string): ParsedSessionFixture {
   const parsed: FixtureJsonLine[] = []
@@ -321,13 +311,9 @@ export function migrateSessionSnapshotFixture(text: string): string {
 /**
  * Prepare one fixture for expected-output comparison through strict format validation.
  * @param text - one complete Session fixture.
- * @param _sourcePath - selected fixture path, when known; comparison never varies by this value.
  * @returns current-generation comparison JSONL without modifying the source file.
  */
-export function prepareSessionSnapshotFixtureForComparison(
-  text: string,
-  _sourcePath?: string,
-): string {
+export function prepareSessionSnapshotFixtureForComparison(text: string): string {
   const parsed = parseSessionFixture(text)
   return encodeCurrentSessionSnapshotFixture(text, parsed)
 }

+ 3 - 11
packages/test-support/llm-replay/tests/llm-replay.spec.ts

@@ -1,6 +1,6 @@
 import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
-import { join, resolve } from 'node:path'
+import { join } from 'node:path'
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { SESSION_FORMAT_VERSION, SessionSeq } from '@deepseek-ai/dsh-session'
@@ -32,7 +32,6 @@ import {
   name,
   parseSessionHeader,
   parseSessionLog,
-  parseSessionLogForReplay,
   prepareSessionEventNotificationsForComparison,
   prepareSessionSnapshotFixtureForComparison,
   resolveScriptedEntry,
@@ -389,11 +388,6 @@ describe('parseSessionLog', () => {
     expect(parseSessionLog(sessionJsonl(events))).toEqual(events)
   })
 
-  it('keeps replay parsing strict when a source path is supplied', () => {
-    const events: SessionEvent[] = [{ type: 'turn/start', seq: SessionSeq(0), time: 0, data: { turn: 1 } }]
-    expect(parseSessionLogForReplay(sessionJsonl(events), '/fixtures/session.v2.jsonl')).toEqual(events)
-  })
-
   it('ignores blank lines', () => {
     const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 })
     const ev: SessionEvent = { type: 'turn/start', seq: SessionSeq(0), time: 0, data: { turn: 1 } }
@@ -689,7 +683,7 @@ describe('prepareSessionSnapshotFixtureForComparison', () => {
     expect(prepared.endsWith('\n')).toBe(false)
   })
 
-  it('does not let a source path change Session format validation', () => {
+  it('applies current Session format validation before comparison', () => {
     const source = [
       JSON.stringify({
         type: 'session',
@@ -717,9 +711,7 @@ describe('prepareSessionSnapshotFixtureForComparison', () => {
         data: { title: 'Active schedule catalog', messageSeqs: [1], source: { kind: 'user' } },
       }),
     ].join('\n')
-    const sourcePath = resolve(import.meta.dirname, '../../../../snapshots/web/schedule-catalog/session.v2.jsonl')
-
-    expect(() => prepareSessionSnapshotFixtureForComparison(source, sourcePath))
+    expect(() => prepareSessionSnapshotFixtureForComparison(source))
       .toThrow(/session\/title 2 messageSeqs must be empty exactly for a user title/)
   })
 })

+ 2 - 2
packages/test-support/session-snapshot/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/test-support/session-snapshot/README.md
-README.md: 6d53b2902c15cf2bb3543e10d282865b0ea17a58
-README.zh.md: 9bb1a5990a67124c71245eb7e9f92eda39312425
+README.md: c78537739f5e4f0dae0e83829b35c1b7ed0e8bf6
+README.zh.md: dc5a3b0871fae8c8351307a5574ee47ea8cc0642

+ 1 - 1
packages/test-support/session-snapshot/README.md

@@ -70,7 +70,7 @@ defineAcpSnapshotSuite({
 
 Each recorded-session directory carries a closed `snapshot.yml` manifest plus canonical parent and contiguous child roles. Parent filenames are `session[.vN].jsonl`; children are `session.<ordinal>[.vN].jsonl`; v0 omits `.v0`, positive versions use lowercase `.vN`, and every filename agrees with its header. A role may retain older generations, but the harness selects the numerically highest one. An owning manifest may declare `sessionFormat.version` plus one or more closed `coverage` names to keep that historical generation as an explicit migration fixture; absence tracks the current writer. The manifest also names the scenario, shipped profile, composition/header class, recording source, and only the replay, platform, permission, environment, workspace, or input facts the completed Session cannot reconstruct. The adapter registers expected-output, Session-log, and optional `workspace.expected/` comparisons; guards reject orphan directories, missing roles, noncanonical names, absolute paths, malformed manifests, and platform-specific separators.
 
-`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits ordinary `seq`/`time` and packed-row `seq0`/`time0` envelopes from committed fixtures after normalizing paths and scrubbing request headers. Replay synthesizes the envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison restores every selected persisted or projected fixture through the current build-static Session format catalog before identity redaction and normalization, so a retained suffixless v0 replay input and fresh `session.v2.jsonl` writer output compare as one v2 logical Session without rewriting or renaming the v0 file. Aligned `sourcePaths` identify expected inputs but never change validation; any catalog refusal fails before identity redaction or normalization. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Fixtures use canonical packed rows; the [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older row layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion.
+`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits ordinary `seq`/`time` and packed-row `seq0`/`time0` envelopes from committed fixtures after normalizing paths and scrubbing request headers. Replay synthesizes the envelopes in memory, while runtime persistence continues to write complete logs. Multi-session comparison restores every selected persisted or projected fixture through the current build-static Session format catalog before identity redaction and normalization, so a retained suffixless v0 replay input and fresh `session.v2.jsonl` writer output compare as one v2 logical Session without rewriting or renaming the v0 file. Any catalog refusal fails before identity redaction or normalization. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Fixtures use canonical packed rows; the [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older row layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion.
 
 ### Record, replay, and refresh
 

+ 1 - 1
packages/test-support/session-snapshot/README.zh.md

@@ -70,7 +70,7 @@ defineAcpSnapshotSuite({
 
 每个已记录 Session 目录携带封闭的 `snapshot.yml` manifest,以及规范 parent 与连续 child 角色。parent 文件名是 `session[.vN].jsonl`;child 是 `session.<ordinal>[.vN].jsonl`;v0 省略 `.v0`,正版本使用小写 `.vN`,且每个文件名与其 header 一致。一个角色可以保留旧 generation,但 harness 会选择数值最高的一项。拥有 fixture 的 manifest 可以声明 `sessionFormat.version` 与一个或多个封闭 `coverage` 名称,把该历史 generation 保留为显式迁移 fixture;省略此字段时跟随当前 writer。manifest 还会指名场景、随附 profile、组合/header 类别、录制来源,以及已完成 Session 无法重建的 replay、平台、权限、环境、workspace 或输入事实。适配器注册预期输出、Session 日志与可选 `workspace.expected/` 比较;保护会拒绝遗留目录、缺失角色、非规范名称、绝对路径、malformed manifest 与平台专用分隔符。
 
-`normalizeSessionSnapshot` 在规范化路径并清理 request header 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略普通行的 `seq`/`time` 和打包行的 `seq0`/`time0` envelope。回放只在内存中合成这些 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过当前构建期静态 Session 格式目录恢复每个选定的持久化或投影 fixture,再进行身份脱敏与规范化,因此保留的无后缀 v0 replay 输入与新生成的 `session.v2.jsonl` writer 输出会作为同一个 v2 逻辑 Session 比较,且不会重写或重命名 v0 文件。对齐的 `sourcePaths` 只标识预期输入,绝不改变校验;任何 catalog 拒绝都会在身份脱敏或规范化前失败。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。fixture 使用规范打包行;[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写较旧行布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。
+`normalizeSessionSnapshot` 在规范化路径并清理 request header 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略普通行的 `seq`/`time` 和打包行的 `seq0`/`time0` envelope。回放只在内存中合成这些 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过当前构建期静态 Session 格式目录恢复每个选定的持久化或投影 fixture,再进行身份脱敏与规范化,因此保留的无后缀 v0 replay 输入与新生成的 `session.v2.jsonl` writer 输出会作为同一个 v2 逻辑 Session 比较,且不会重写或重命名 v0 文件。任何 catalog 拒绝都会在身份脱敏或规范化前失败。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。fixture 使用规范打包行;[临时仓库迁移器](../../../scripts/migrate-packed-session-fixtures.ts)(`pnpm run migrate:packed-session-fixtures`)会改写较旧行布局,由其[移除提案](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.zh.md)负责删除该迁移器。
 
 ### 录制、回放与刷新
 

+ 0 - 1
packages/test-support/session-snapshot/src/index.ts

@@ -52,7 +52,6 @@ export {
   type CwdPathMode,
   type NormalizeContext,
   type NormalizeOptions,
-  type NormalizeSessionSnapshotsOptions,
 } from './normalize.ts'
 export {
   parseSnapshotManifest,

+ 4 - 14
packages/test-support/session-snapshot/src/normalize.ts

@@ -104,12 +104,6 @@ export interface NormalizeOptions {
   identityMode?: 'legacy' | 'preserve'
 }
 
-/** Multi-Session comparison controls, including optional committed source identities. */
-export interface NormalizeSessionSnapshotsOptions extends Omit<NormalizeOptions, 'identityMode'> {
-  /** Primary-first source paths paired with logs; they never change format validation. */
-  sourcePaths?: readonly (string | undefined)[]
-}
-
 /** Return every known spelling of the generated cwd, most specific first. */
 function cwdSpellings(ctx: NormalizeContext): string[] {
   const spellings = [...new Set([ctx.cwd, ...ctx.cwdAliases ?? []])]
@@ -439,21 +433,17 @@ export function normalizeSessionSnapshot(
 export function normalizeSessionSnapshots(
   rawLogs: readonly string[],
   ctx: NormalizeContext,
-  options: NormalizeSessionSnapshotsOptions = {},
+  options: Omit<NormalizeOptions, 'identityMode'> = {},
 ): string[] {
-  const { sourcePaths, ...normalizeOptions } = options
-  if (sourcePaths !== undefined && sourcePaths.length !== rawLogs.length) {
-    throw new Error('Session snapshot source path count must match its log count')
-  }
-  const currentLogs = rawLogs.map((log, index) => hasSessionFormatVersion(log)
-    ? prepareSessionSnapshotFixtureForComparison(log, sourcePaths?.[index])
+  const currentLogs = rawLogs.map(log => hasSessionFormatVersion(log)
+    ? prepareSessionSnapshotFixtureForComparison(log)
     : log)
   const comparableLogs = currentLogs.map(normalizeSessionFormatProvenance)
   return redactSessionSnapshotIds(comparableLogs).map(log => repackSessionSnapshot(
     scrubSessionSnapshot(normalizeSessionLog(
       log,
       { ...ctx, sessionIds: [] },
-      { ...normalizeOptions, identityMode: 'preserve' },
+      { ...options, identityMode: 'preserve' },
     )),
   ))
 }

+ 2 - 5
packages/test-support/session-snapshot/src/suite.ts

@@ -1367,11 +1367,8 @@ export function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void {
             sessionIds: fixtureContexts.flatMap(context => context.sessionIds),
             cwd: (fixtureContexts[0] as NormalizeContext).cwd,
           }
-          const sourcePaths = fixtureFiles.map(file => join(dir, file))
-          const actualSnapshots = normalizeSessionSnapshots(harvested, ctx, { sourcePaths })
-          const expectedSnapshots = normalizeSessionSnapshots(fixtures, fixtureCtx, {
-            sourcePaths,
-          })
+          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])
           }

+ 0 - 10
packages/test-support/session-snapshot/tests/normalize.spec.ts

@@ -603,16 +603,6 @@ describe('normalizeSessionSnapshot', () => {
     ].join('\n')])
   })
 
-  it('rejects source paths that cannot identify every snapshot', () => {
-    const raw = `${JSON.stringify({ type: 'session', version: 1 })}\n`
-
-    expect(() => normalizeSessionSnapshots(
-      [raw],
-      { sessionIds: [], cwd: '/unused' },
-      { sourcePaths: [] },
-    )).toThrow('Session snapshot source path count must match its log count')
-  })
-
   it('normalizes an already-projected snapshot without a released-format field', () => {
     const raw = `${JSON.stringify({
       type: 'session',

+ 2 - 6
snapshots/sdk/sdk.snapshot.ts

@@ -805,12 +805,8 @@ describe('TypeScript SDK snapshots over the jsonrpc runtime', () => {
 
       // Persisted transcripts match the committed fixtures.
       const expectedContext = contextOfContents(expectedContents)
-      const actualSnapshots = normalizeSessionSnapshots(ordered.map(log => log.content), actualContext, {
-        sourcePaths: files,
-      })
-      const expectedSnapshots = normalizeSessionSnapshots(expectedContents, expectedContext, {
-        sourcePaths: files,
-      })
+      const actualSnapshots = normalizeSessionSnapshots(ordered.map(log => log.content), actualContext)
+      const expectedSnapshots = normalizeSessionSnapshots(expectedContents, expectedContext)
       expect(actualSnapshots.map(records), `${scenario.name}: sessions`).toEqual(expectedSnapshots.map(records))
       await verifyHeaders(scenario, ordered, actualContext, assertions.dshSdkChild?.agentConfig)
 

+ 3 - 16
snapshots/session/headless.snapshot.ts

@@ -197,7 +197,6 @@ async function primaryFixtureFile(dir: string): Promise<string> {
 async function writeSessionFixtures(
   scenario: HeadlessScenario,
   actualLogs: readonly SessionLog[],
-  existingFiles: readonly string[],
   existing: readonly string[],
   ctx: NormalizeContext,
 ): Promise<string[]> {
@@ -213,11 +212,7 @@ async function writeSessionFixtures(
     const stable = tokenizeSessionFixtureCwd(mode === 'refresh'
       ? stabilizeRefreshLog(log.content, prior[index] as string, replacements, ctx)
       : log.content)
-    const sourceFile = existingFiles[index]
-    return scrubSessionSnapshot(prepareSessionSnapshotFixtureForComparison(
-      stable,
-      sourceFile === undefined ? undefined : join(scenario.dir, sourceFile),
-    ))
+    return scrubSessionSnapshot(prepareSessionSnapshotFixtureForComparison(stable))
   })
   const output = redactSessionSnapshotIds(stabilizeFixtureMessageIds(fresh, prior))
   await Promise.all(output.map((content, index) => writeFile(join(scenario.dir, names[index] as string), content)))
@@ -728,7 +723,6 @@ describe('headless recorded-session snapshots', () => {
       const baseComposition = compositionOwners.get('default')
       if (baseComposition === undefined) throw new Error('headless corpus has no default composition')
       let fixtureFiles = sessionFixtureNames(await readdir(scenario.dir))
-      const replayFixtureFiles = [...fixtureFiles]
       const replaying = mode !== 'record'
       const compositionPatch = join(composition.dir, replaying ? 'cordis.snapshot.yml' : 'cordis.yml')
       const patchSources = [
@@ -814,7 +808,6 @@ describe('headless recorded-session snapshots', () => {
         fixtures = await writeSessionFixtures(
           scenario,
           actualLogs,
-          replayFixtureFiles,
           fixtures,
           contextOf(actualLogs.map(log => log.content)),
         )
@@ -829,14 +822,8 @@ describe('headless recorded-session snapshots', () => {
       expect(actualLogs, `${scenario.name}: persisted session count`).toHaveLength(fixtures.length)
       const actualContext = contextOf(actualLogs.map(log => log.content))
       const fixtureContext = contextOf(fixtures)
-      const actualSourcePaths = replayFixtureFiles.map(file => join(scenario.dir, file))
-      const expectedSourcePaths = fixtureFiles.map(file => join(scenario.dir, file))
-      const actualSnapshots = normalizeSessionSnapshots(actualLogs.map(log => log.content), actualContext, {
-        sourcePaths: actualSourcePaths,
-      })
-      const expectedSnapshots = normalizeSessionSnapshots(fixtures, fixtureContext, {
-        sourcePaths: expectedSourcePaths,
-      })
+      const actualSnapshots = normalizeSessionSnapshots(actualLogs.map(log => log.content), actualContext)
+      const expectedSnapshots = normalizeSessionSnapshots(fixtures, fixtureContext)
       for (const [index, actual] of actualSnapshots.entries()) {
         expect(actual, `${scenario.name}: session ${index}`).toBe(expectedSnapshots[index])
       }