@deepseek-ai/dsh-session-snapshot 0.1.2-rc.1 → 0.1.3-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +8 -5
- package/README.zh.md +8 -5
- package/lib/index.js +351 -99
- package/lib/types/index.d.ts +4 -3
- package/lib/types/manifest.d.ts +25 -2
- package/lib/types/normalize.d.ts +11 -5
- package/lib/types/session-files.d.ts +97 -0
- package/lib/types/suite.d.ts +12 -20
- package/package.json +18 -14
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/test-support/session-snapshot/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 12acb8d426a2966a852f98d85843d3c246ffeb5f
|
|
6
|
+
README.zh.md: 0b30cbf205f6055ef7b11b791d47adb38f6f704d
|
package/README.md
CHANGED
|
@@ -68,17 +68,19 @@ defineAcpSnapshotSuite({
|
|
|
68
68
|
})
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
Each recorded-session directory carries a closed `snapshot.yml` manifest plus
|
|
71
|
+
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.
|
|
72
72
|
|
|
73
|
-
`normalizeSessionSnapshot` retains the complete
|
|
73
|
+
`normalizeSessionSnapshot` retains the complete Session header and event payloads but omits top-level `seq`/`time` envelopes from committed fixtures after normalizing paths and scrubbing request headers; it also normalizes embedded stream clocks and historical packed-row `seq0`/`time0` envelopes. Replay synthesizes the top-level 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 retained v0/v1 replay input and fresh `session.v2.jsonl` writer output compare as one v2 logical Session without rewriting or renaming historical files. Expected and harvested logs use the same strict restoration path; source filenames cannot alter format validation. Versionless protocol-adapter unit fixtures remain outside the released Session format corpus. Current v2 fixtures use one row per event; retained v0/v1 fixtures may use canonical packed rows. The [temporary repository migrator](../../../scripts/migrate-packed-session-fixtures.ts) (`pnpm run migrate:packed-session-fixtures`) rewrites older historical layouts, and its [removal proposal](../../../.agents/notes/proposed/process/2026-07-26-remove-packed-session-fixture-migrator.md) owns its deletion.
|
|
74
|
+
|
|
75
|
+
Known snapshot spill paths normalize to stable locator tokens, including paths quoted inside JSON omission notices with JSON-escaped Windows separators. Refresh extraction preserves the matched serialized path spelling for literal replacement. Normalization changes only the locator: saved byte lengths and omission counts remain comparison evidence.
|
|
74
76
|
|
|
75
77
|
### Record, replay, and refresh
|
|
76
78
|
|
|
77
|
-
`pnpm run test:snapshot:record` calls the live LLM and
|
|
79
|
+
`pnpm run test:snapshot:record` calls the live LLM and writes the harvested current generation under its canonical versioned filename. Record and refresh never rename or delete a completed generation, including generations of a child role absent from a later run; reviewed source-tree curation removes a predecessor only after the same role has a verified current replacement. Scenarios with an explicit `sessionFormat` remain read-only in record mode. `pnpm run test:snapshot:refresh` stays keyless, runs the selected highest replay input, and writes stdout, owned prompt and tool-schema sidecars, and a fresh current-generation comparable Session output unless the manifest retains a historical generation. Each composition owner keeps its replay patch beside its live patch; top-level `snapshots/` owns Session-driven scenarios, while other expected outputs stay beside their owning package. [`dsh-llm-replay`](../llm-replay/README.md) serves the recorded streams selected through `DSH_SNAPSHOT_*` environment values.
|
|
78
80
|
|
|
79
81
|
### Pinning request headers
|
|
80
82
|
|
|
81
|
-
A pin owns its generated `system-prompt.expected.md` or `tool-schemas.expected.json` sidecar by default; `systemPromptSource` and `toolSchemasSource` name another pin when the complete corresponding sequence is identical, so each distinct version is committed once. The
|
|
83
|
+
A pin owns its generated `system-prompt.expected.md` or `tool-schemas.expected.json` sidecar by default; `systemPromptSource` and `toolSchemasSource` name another pin when the complete corresponding sequence is identical, so each distinct version is committed once. The selected highest parent fixture stores `"system":"{{system}}","tools":"{{tools}}"` while retaining config, reason, and any model-visible prefix. A child Session whose own scope composes a different request declares it per fixture index with `pinsChildToolSchemas` and `pinsChildSystemPrompts`. A scenario that changes the request header mid-run declares `expectedHeaderChanges`.
|
|
82
84
|
|
|
83
85
|
### Platform and composition variants
|
|
84
86
|
|
|
@@ -102,7 +104,7 @@ This section explains the design of the kit; the observable behavior is fully co
|
|
|
102
104
|
|
|
103
105
|
### Design
|
|
104
106
|
|
|
105
|
-
The shared core owns manifests, workspace setup/comparison, typed identity mapping, normalizers, and fixture invariants. The ACP adapter adds four composable layers: launcher, scenario harness, normalizers, and suite factory. `launchAcpTestAgent` boots a source profile under tsx or a built `lib` profile under plain Node, connects the SDK client over a raw-byte stdout tee, collects
|
|
107
|
+
The shared core owns manifests, generation-qualified role selection, workspace setup/comparison, typed identity mapping, normalizers, and fixture invariants. The ACP adapter adds four composable layers: launcher, scenario harness, normalizers, and suite factory. `launchAcpTestAgent` boots a source profile under tsx or a built `lib` profile under plain Node, connects the SDK client over a raw-byte stdout tee, collects Session updates and stderr, fails closed on unhandled permission requests, and owns shutdown. `runScenario` drives ACP JSON-RPC stdio and harvests the numerically highest persisted raw JSONL generation for every Session directory. The pure normalizers replace cwd paths and typed identities with stable tokens, zero times, expand physical provenance ranges, and scrub request-header bulk. `defineAcpSnapshotSuite` registers comparisons, generation-qualified fixture write-back, and the live uniformity guard.
|
|
106
108
|
|
|
107
109
|
### Source map
|
|
108
110
|
|
|
@@ -111,6 +113,7 @@ The shared core owns manifests, workspace setup/comparison, typed identity mappi
|
|
|
111
113
|
| [`src/launcher.ts`](src/launcher.ts) | Subprocess/client launcher and shutdown ownership |
|
|
112
114
|
| [`src/harness.ts`](src/harness.ts) | Scripted scenario driver and session-log harvest |
|
|
113
115
|
| [`src/manifest.ts`](src/manifest.ts) | Closed `snapshot.yml` schema, collection, and ownership rules |
|
|
116
|
+
| [`src/session-files.ts`](src/session-files.ts) | Canonical parent/child generation grammar, header agreement, and highest-role selection |
|
|
114
117
|
| [`src/identity.ts`](src/identity.ts) | Typed first-seen identity tokenization across parent and child logs |
|
|
115
118
|
| [`src/normalize.ts`](src/normalize.ts) | Pure normalizers and scrubbing helpers |
|
|
116
119
|
| [`src/workspace.ts`](src/workspace.ts) | Scenario workspace setup and complete expected-state comparison |
|
package/README.zh.md
CHANGED
|
@@ -68,17 +68,19 @@ defineAcpSnapshotSuite({
|
|
|
68
68
|
})
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
每个已记录 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 与平台专用分隔符。
|
|
72
72
|
|
|
73
|
-
`normalizeSessionSnapshot` 在规范化路径并清理 request header
|
|
73
|
+
`normalizeSessionSnapshot` 在规范化路径并清理 request header 后,会保留完整 Session header 与事件 payload,但从已提交 fixture 中省略顶层 `seq`/`time` envelope;它还会规范化嵌入式 stream clock 与历史 packed-row 的 `seq0`/`time0` envelope。Replay 只在内存中合成顶层 envelope,而运行时持久化仍写入完整日志。多 Session 比较会先通过当前构建期静态 Session 格式目录恢复每个选定的持久化或投影 fixture,再进行身份脱敏与规范化,因此保留的 v0/v1 replay 输入与新生成的 `session.v2.jsonl` writer 输出会作为同一个 v2 logical Session 比较,且不会重写或重命名历史文件。预期日志与收集日志使用同一条严格恢复路径;来源文件名不能改变格式校验。无版本的协议适配器单元测试 fixture 不属于已发布 Session 格式语料。当前 v2 fixture 每个事件占一行;保留的 v0/v1 fixture 可以使用规范 packed row。[临时仓库迁移器](../../../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)负责删除该迁移器。
|
|
74
|
+
|
|
75
|
+
已知的快照 spill 路径会规范化为稳定的定位信息 token,包括 JSON 省略通知中带引号、使用 JSON 转义 Windows 分隔符的路径。刷新提取会保留匹配路径的序列化写法,以便进行字面替换。规范化只改变定位信息:保存字节数与省略计数仍作为比较证据。
|
|
74
76
|
|
|
75
77
|
### 录制、回放与刷新
|
|
76
78
|
|
|
77
|
-
`pnpm run test:snapshot:record` 调用在线 LLM
|
|
79
|
+
`pnpm run test:snapshot:record` 调用在线 LLM(大语言模型),并在规范具名版本文件下写入收集到的当前 generation。record 与 refresh 绝不重命名或删除已完成的 generation,即使后续运行不再产生某个 child 角色也一样;受审阅的源树整理只有在同角色存在已验证的当前替代文件后才移除前代。显式声明 `sessionFormat` 的场景在录制模式下保持只读。`pnpm run test:snapshot:refresh` 保持无密钥,运行选定的最高 replay 输入,并写入 stdout、各 pin 自有的 prompt 与工具 schema sidecar;只有 manifest 未保留历史 generation 时,才写入新鲜当前 generation 的可比较 Session 输出。每个组合 owner 把 replay patch 放在 live patch 旁;顶层 `snapshots/` 拥有 Session 驱动场景,其他预期输出留在其 package owner 旁。[`dsh-llm-replay`](../llm-replay/README.zh.md) 提供通过 `DSH_SNAPSHOT_*` 环境值选择的已记录流。
|
|
78
80
|
|
|
79
81
|
### 固定请求 header
|
|
80
82
|
|
|
81
|
-
每个 pin 默认拥有其生成的 `system-prompt.expected.md` 或 `tool-schemas.expected.json`
|
|
83
|
+
每个 pin 默认拥有其生成的 `system-prompt.expected.md` 或 `tool-schemas.expected.json` sidecar;当完整的对应序列相同时,`systemPromptSource` 与 `toolSchemasSource` 指定另一个 pin 作为来源,因此每个不同版本只提交一次。选定的最高 parent fixture 存储 `"system":"{{system}}","tools":"{{tools}}"`,同时保留配置、原因与任何模型可见前缀。自身 scope 组合出不同请求的 child Session 按 fixture index 以 `pinsChildToolSchemas` 与 `pinsChildSystemPrompts` 单独声明。运行中改变请求 header 的场景声明 `expectedHeaderChanges`。
|
|
82
84
|
|
|
83
85
|
### 平台与组合变体
|
|
84
86
|
|
|
@@ -102,7 +104,7 @@ defineAcpSnapshotSuite({
|
|
|
102
104
|
|
|
103
105
|
### 设计
|
|
104
106
|
|
|
105
|
-
共享核心拥有 manifest、workspace
|
|
107
|
+
共享核心拥有 manifest、generation 限定角色选择、workspace 设置/比较、类型化身份映射、normalizer 与 fixture 不变式。ACP 适配器增加四个可组合层:launcher、场景 harness、normalizer 与 suite factory。`launchAcpTestAgent` 在 tsx 下启动源码 profile,或在普通 Node 下启动已构建 `lib` profile,通过原始字节 stdout tee 连接 SDK client,收集 Session update 与 stderr,默认拒绝未处理的权限请求,并负责关闭。`runScenario` 驱动 ACP JSON-RPC stdio,并收集每个 Session 目录中数值最高的持久原始 JSONL generation。纯 normalizer 把 cwd 路径与类型化身份变为稳定 token,将时间归零、展开物理来源区间,并清理 request header bulk。`defineAcpSnapshotSuite` 注册比较、generation 限定 fixture 回写与实时一致性保护。
|
|
106
108
|
|
|
107
109
|
### 源码地图
|
|
108
110
|
|
|
@@ -111,6 +113,7 @@ defineAcpSnapshotSuite({
|
|
|
111
113
|
| [`src/launcher.ts`](src/launcher.ts) | 子进程/客户端启动器与关闭所有权 |
|
|
112
114
|
| [`src/harness.ts`](src/harness.ts) | 脚本化场景驱动与会话日志收集 |
|
|
113
115
|
| [`src/manifest.ts`](src/manifest.ts) | 封闭 `snapshot.yml` schema、收集与归属规则 |
|
|
116
|
+
| [`src/session-files.ts`](src/session-files.ts) | 规范 parent/child generation grammar、header 一致性与最高角色选择 |
|
|
114
117
|
| [`src/identity.ts`](src/identity.ts) | 跨父子日志的类型化首次出现身份 token 化 |
|
|
115
118
|
| [`src/normalize.ts`](src/normalize.ts) | 纯规范化器与擦除辅助 |
|
|
116
119
|
| [`src/workspace.ts`](src/workspace.ts) | 场景 workspace 设置与完整预期状态比较 |
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,10 @@ import { pathToFileURL } from "node:url";
|
|
|
12
12
|
import * as yaml from "js-yaml";
|
|
13
13
|
import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
|
|
14
14
|
import { resolveExampleLaunch } from "@deepseek-ai/dsh-loader-smoke";
|
|
15
|
-
import {
|
|
15
|
+
import { clearedProxyEnv } from "@deepseek-ai/dsh-http-proxy";
|
|
16
|
+
import { parseSessionFormatLogFilename, sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
17
|
+
import { decodeSeqRanges } from "@deepseek-ai/dsh-session";
|
|
18
|
+
import { prepareSessionSnapshotFixtureForComparison } from "@deepseek-ai/dsh-llm-replay";
|
|
16
19
|
import { isSurfaceEligibleType } from "@deepseek-ai/dsh-session/surface";
|
|
17
20
|
//#region lib/types/identity.js
|
|
18
21
|
/** Relationship-preserving identity redaction for committed session snapshots. */
|
|
@@ -88,7 +91,10 @@ function redactSessionSnapshotIds(logs) {
|
|
|
88
91
|
collect(item, recordType);
|
|
89
92
|
}
|
|
90
93
|
};
|
|
91
|
-
for (const log of parsed) for (const record of log.records)
|
|
94
|
+
for (const log of parsed) for (const record of log.records) {
|
|
95
|
+
if (record.type === "feedback/message-put" && isRecord$1(record.data) && isRecord$1(record.data.item)) claim(record.data.item.version, "id");
|
|
96
|
+
collect(record, record.type);
|
|
97
|
+
}
|
|
92
98
|
const replacements = [...tokenByValue].sort(([left], [right]) => right.length - left.length);
|
|
93
99
|
const replace = (value) => {
|
|
94
100
|
if (typeof value === "string") {
|
|
@@ -398,6 +404,188 @@ function isRunning(child) {
|
|
|
398
404
|
return child.exitCode === null && child.signalCode === null;
|
|
399
405
|
}
|
|
400
406
|
//#endregion
|
|
407
|
+
//#region lib/types/session-files.js
|
|
408
|
+
/** Immutable Session-generation filenames used by recorded-session fixtures. */
|
|
409
|
+
const FIXTURE_FILE = /^session(?:\.([1-9]\d*))?(?:\.v([1-9]\d*))?\.jsonl$/u;
|
|
410
|
+
function nonNegativeSafeInteger(value, label) {
|
|
411
|
+
if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new Error(`${label} must be a non-negative safe integer`);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Return the canonical fixture filename for one parent/ordinal and generation.
|
|
415
|
+
*
|
|
416
|
+
* @param index - Parent `0` or a positive child/ordinal slot.
|
|
417
|
+
* @param version - Physical format generation; `0` is omitted.
|
|
418
|
+
* @returns The lowercase canonical JSONL filename.
|
|
419
|
+
*/
|
|
420
|
+
function sessionFixtureName(index, version) {
|
|
421
|
+
nonNegativeSafeInteger(index, "session fixture index");
|
|
422
|
+
nonNegativeSafeInteger(version, "Session format version");
|
|
423
|
+
return `session${index === 0 ? "" : `.${index}`}${version === 0 ? "" : `.v${version}`}.jsonl`;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Parse one canonical recorded-session fixture filename.
|
|
427
|
+
*
|
|
428
|
+
* @param name - Basename from a scenario directory.
|
|
429
|
+
* @returns Parsed role and generation, or `undefined` for an unrelated file.
|
|
430
|
+
*/
|
|
431
|
+
function parseSessionFixtureName(name) {
|
|
432
|
+
const match = FIXTURE_FILE.exec(name);
|
|
433
|
+
if (match === null) {
|
|
434
|
+
if (name.startsWith("session") && name.endsWith(".jsonl")) throw new Error(`invalid session fixture name: ${name}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const index = match[1] === void 0 ? 0 : Number(match[1]);
|
|
438
|
+
const version = match[2] === void 0 ? 0 : Number(match[2]);
|
|
439
|
+
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(version)) throw new Error(`invalid session fixture name: ${name}`);
|
|
440
|
+
return {
|
|
441
|
+
index,
|
|
442
|
+
version,
|
|
443
|
+
name
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Select the highest generation for every parent/ordinal fixture role.
|
|
448
|
+
* Older generations remain in the directory but do not count as extra Sessions.
|
|
449
|
+
*
|
|
450
|
+
* @param names - File basenames in one scenario directory.
|
|
451
|
+
* @returns Parent first, followed by contiguous child/ordinal roles.
|
|
452
|
+
*/
|
|
453
|
+
function sessionFixtureFiles(names) {
|
|
454
|
+
const selected = /* @__PURE__ */ new Map();
|
|
455
|
+
const identities = /* @__PURE__ */ new Set();
|
|
456
|
+
for (const name of names) {
|
|
457
|
+
const fixture = parseSessionFixtureName(name);
|
|
458
|
+
if (fixture === void 0) continue;
|
|
459
|
+
const identity = `${fixture.index}/${fixture.version}`;
|
|
460
|
+
if (identities.has(identity)) throw new Error(`duplicate session fixture generation: ${name}`);
|
|
461
|
+
identities.add(identity);
|
|
462
|
+
const previous = selected.get(fixture.index);
|
|
463
|
+
if (previous === void 0 || fixture.version > previous.version) selected.set(fixture.index, fixture);
|
|
464
|
+
}
|
|
465
|
+
if (selected.get(0) === void 0) throw new Error("missing parent session fixture");
|
|
466
|
+
const ordered = [...selected.values()].sort((left, right) => left.index - right.index);
|
|
467
|
+
for (const [offset, fixture] of ordered.entries()) if (fixture.index !== offset) throw new Error(`session fixture roles must be contiguous: expected index ${offset}, found ${fixture.name}`);
|
|
468
|
+
return ordered;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Validate and order a scenario directory's selected Session fixture filenames.
|
|
472
|
+
*
|
|
473
|
+
* @param names - File basenames in one scenario directory.
|
|
474
|
+
* @returns Highest-generation parent and child/ordinal filenames.
|
|
475
|
+
*/
|
|
476
|
+
function sessionFixtureNames(names) {
|
|
477
|
+
return sessionFixtureFiles(names).map((file) => file.name);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Read the declared physical generation from one Session JSONL header.
|
|
481
|
+
*
|
|
482
|
+
* @param content - Complete UTF-8 JSONL content.
|
|
483
|
+
* @param label - Diagnostic filename or path.
|
|
484
|
+
* @returns The declared non-negative format generation.
|
|
485
|
+
*/
|
|
486
|
+
function sessionHeaderVersion(content, label) {
|
|
487
|
+
const line = content.split(/\r?\n/u).find((candidate) => candidate.trim().length > 0);
|
|
488
|
+
if (line === void 0) throw new Error(`${label}: session fixture is empty`);
|
|
489
|
+
let value;
|
|
490
|
+
try {
|
|
491
|
+
value = JSON.parse(line);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
throw new Error(`${label}: session header contains invalid JSON`, { cause: error });
|
|
494
|
+
}
|
|
495
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || value.type !== "session") throw new Error(`${label}: first record must be a Session header`);
|
|
496
|
+
const version = value.version;
|
|
497
|
+
if (!Number.isSafeInteger(version) || version < 0 || Object.is(version, -0)) throw new Error(`${label}: Session header version must be a non-negative safe integer`);
|
|
498
|
+
return version;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Require one fixture's canonical filename generation to equal its header.
|
|
502
|
+
*
|
|
503
|
+
* @param name - Canonical fixture basename.
|
|
504
|
+
* @param content - Complete UTF-8 JSONL content.
|
|
505
|
+
* @returns The validated format generation.
|
|
506
|
+
*/
|
|
507
|
+
function assertSessionFixtureVersion(name, content) {
|
|
508
|
+
const fixture = parseSessionFixtureName(name);
|
|
509
|
+
if (fixture === void 0) throw new Error(`not a session fixture name: ${name}`);
|
|
510
|
+
const first = content.split(/\r?\n/u).find((candidate) => candidate.trim().length > 0);
|
|
511
|
+
if (first !== void 0) {
|
|
512
|
+
let projected;
|
|
513
|
+
try {
|
|
514
|
+
projected = JSON.parse(first);
|
|
515
|
+
} catch {
|
|
516
|
+
projected = void 0;
|
|
517
|
+
}
|
|
518
|
+
if (projected !== null && typeof projected === "object" && !Array.isArray(projected) && projected.type === "session" && !Object.hasOwn(projected, "version")) {
|
|
519
|
+
if (fixture.version !== 0) throw new Error(`${name}: a versionless projected Session header is format v0`);
|
|
520
|
+
return 0;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const headerVersion = sessionHeaderVersion(content, name);
|
|
524
|
+
if (headerVersion !== fixture.version) throw new Error(`${name}: filename declares Session format v${fixture.version}, header declares v${headerVersion}`);
|
|
525
|
+
return headerVersion;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Return the canonical persistence basename for one generation and compression.
|
|
529
|
+
*
|
|
530
|
+
* @param version - Physical format generation; `0` is omitted.
|
|
531
|
+
* @param compression - Backend compression mode.
|
|
532
|
+
* @returns The canonical persistence basename.
|
|
533
|
+
*/
|
|
534
|
+
function persistedSessionFilename(version, compression = "raw") {
|
|
535
|
+
return `${sessionFormatLogFilename(version)}${compression === "zstd" ? ".zstd" : ""}`;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Parse a canonical persistence basename from a Session's own directory.
|
|
539
|
+
*
|
|
540
|
+
* @param name - Candidate basename.
|
|
541
|
+
* @returns Its generation and compression, or `undefined` for noise and noncanonical names.
|
|
542
|
+
*/
|
|
543
|
+
function parsePersistedSessionFilename(name) {
|
|
544
|
+
const compression = name.endsWith(".zstd") ? "zstd" : "raw";
|
|
545
|
+
const version = parseSessionFormatLogFilename(compression === "zstd" ? name.slice(0, -5) : name);
|
|
546
|
+
if (version === void 0) return void 0;
|
|
547
|
+
return {
|
|
548
|
+
version,
|
|
549
|
+
compression,
|
|
550
|
+
name
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Select one highest-generation persistence path per physical Session directory.
|
|
555
|
+
*
|
|
556
|
+
* @param paths - Relative or absolute paths beneath a sessions root.
|
|
557
|
+
* @param compression - Compression selected by the snapshot composition.
|
|
558
|
+
* @returns Stable path order with older generations and filesystem noise omitted.
|
|
559
|
+
*/
|
|
560
|
+
function latestPersistedSessionPaths(paths, compression = "raw") {
|
|
561
|
+
const selected = /* @__PURE__ */ new Map();
|
|
562
|
+
for (const path of paths) {
|
|
563
|
+
const parsed = parsePersistedSessionFilename(basename(path));
|
|
564
|
+
if (parsed === void 0 || parsed.compression !== compression) continue;
|
|
565
|
+
const directory = dirname(path);
|
|
566
|
+
const previous = selected.get(directory);
|
|
567
|
+
if (previous === void 0 || parsed.version > previous.version) selected.set(directory, {
|
|
568
|
+
path,
|
|
569
|
+
version: parsed.version
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
return [...selected.values()].map((entry) => entry.path).sort();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Require one persistence basename's generation to equal its Session header.
|
|
576
|
+
*
|
|
577
|
+
* @param name - Canonical persistence basename.
|
|
578
|
+
* @param content - Complete uncompressed UTF-8 JSONL content.
|
|
579
|
+
* @returns The validated generation.
|
|
580
|
+
*/
|
|
581
|
+
function assertPersistedSessionVersion(name, content) {
|
|
582
|
+
const persisted = parsePersistedSessionFilename(name);
|
|
583
|
+
if (persisted === void 0) throw new Error(`not a canonical Session persistence filename: ${name}`);
|
|
584
|
+
const headerVersion = sessionHeaderVersion(content, name);
|
|
585
|
+
if (headerVersion !== persisted.version) throw new Error(`${name}: filename declares Session format v${persisted.version}, header declares v${headerVersion}`);
|
|
586
|
+
return headerVersion;
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
401
589
|
//#region lib/types/workspace.js
|
|
402
590
|
/** Capture readable, path-stable workspace state for recorded-session tests. */
|
|
403
591
|
/** Marker that lets Git retain an expected empty directory without becoming expected workspace state. */
|
|
@@ -519,6 +707,7 @@ async function runScenario(input, opts) {
|
|
|
519
707
|
] });
|
|
520
708
|
const env = {
|
|
521
709
|
...opts.env,
|
|
710
|
+
...clearedProxyEnv(),
|
|
522
711
|
DSH_SNAPSHOT: opts.mode,
|
|
523
712
|
DSH_SNAPSHOT_FILE: opts.fixtureFile,
|
|
524
713
|
DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot,
|
|
@@ -902,13 +1091,14 @@ function latestOpenTurn(content) {
|
|
|
902
1091
|
return turn;
|
|
903
1092
|
}
|
|
904
1093
|
/**
|
|
905
|
-
* Harvest
|
|
1094
|
+
* Harvest every latest-generation raw JSONL Session under a sessions root, parse each
|
|
906
1095
|
* header line, and return them ordered primary-first: the top-level session (no
|
|
907
1096
|
* `parentSession`) leads, then each subagent child by ascending `createdAt`.
|
|
908
1097
|
*
|
|
909
|
-
* Snapshot configs select the JSONL backend's raw mode, which lays
|
|
910
|
-
*
|
|
911
|
-
*
|
|
1098
|
+
* Snapshot configs select the JSONL backend's raw mode, which lays each immutable
|
|
1099
|
+
* generation beneath `<root>/<project>/<session-id>/`. Recursive collection
|
|
1100
|
+
* chooses the numerically highest generation for the primary and every child.
|
|
1101
|
+
* Returns `[]` if no log was
|
|
912
1102
|
* produced (a no-session scenario).
|
|
913
1103
|
*/
|
|
914
1104
|
async function harvestSessionLogs(root) {
|
|
@@ -919,9 +1109,10 @@ async function harvestSessionLogs(root) {
|
|
|
919
1109
|
return [];
|
|
920
1110
|
}
|
|
921
1111
|
const logs = [];
|
|
922
|
-
for (const file of files) {
|
|
923
|
-
if (basename(file) !== "session.jsonl") continue;
|
|
1112
|
+
for (const file of latestPersistedSessionPaths(files)) {
|
|
924
1113
|
const content = await readFile(join(root, file), "utf8");
|
|
1114
|
+
assertPersistedSessionVersion(basename(file), content);
|
|
1115
|
+
/* v8 ignore next -- the generation validator above rejects header-less content. */
|
|
925
1116
|
const firstLine = content.split("\n").find((line) => line.trim().length > 0) ?? "{}";
|
|
926
1117
|
const header = JSON.parse(firstLine);
|
|
927
1118
|
logs.push({
|
|
@@ -978,8 +1169,8 @@ const PATH_TEXT_BOUNDARY_RE = /[\s<>'"`()\[\]{},;:!?=]/;
|
|
|
978
1169
|
const FILE_URI_PATH_PREFIX_RE = /(?:^|[^a-z0-9+.-])file:\/\/\/?$/i;
|
|
979
1170
|
/** A UUID v4 string, the shape `randomUUID()` produces for session ids. */
|
|
980
1171
|
const UUID_RE$1 = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
|
|
981
|
-
const LOCAL_SPILL_PATH_RE = new RegExp(String.raw`\{\{cwd\}\}[\\/]
|
|
982
|
-
const SNAPSHOT_SPILL_PATH_RE = new RegExp(String.raw`(?:[A-Za-z]:)?[\\/](?:tmp|t)[\\/](?:dsh-acp-snap-[0-9a-f]{9}|dsh-acp-snapshot-spill)[\\/]session-[0-9a-f]{12}[\\/][0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)]|$)`, "g");
|
|
1172
|
+
const LOCAL_SPILL_PATH_RE = new RegExp(String.raw`\{\{cwd\}\}[\\/]+\.spill[\\/]+session-[0-9a-f]{12}[\\/]+[0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)"]|\\+"|$)`, "g");
|
|
1173
|
+
const SNAPSHOT_SPILL_PATH_RE = new RegExp(String.raw`(?:[A-Za-z]:)?[\\/]+(?:tmp|t)[\\/]+(?:dsh-acp-snap-[0-9a-f]{9}|dsh-acp-snapshot-spill)[\\/]+session-[0-9a-f]{12}[\\/]+[0-9a-f]{12}-([A-Za-z0-9._~-]+?)` + String.raw`(?=\. Use read with offset/limit|[\s)"]|\\+"|$)`, "g");
|
|
983
1174
|
/**
|
|
984
1175
|
* Extract every snapshot-mode spill path from a session log, keyed by spill
|
|
985
1176
|
* filename. Used by refresh write-back to keep spill paths stable across runs.
|
|
@@ -1151,9 +1342,9 @@ function normalizeStdout(rawStdout, ctx, options = {}) {
|
|
|
1151
1342
|
}
|
|
1152
1343
|
/**
|
|
1153
1344
|
* Normalize a session JSONL log into a stable expected output: the header line's
|
|
1154
|
-
* volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed,
|
|
1155
|
-
*
|
|
1156
|
-
* zeroed
|
|
1345
|
+
* volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed; event,
|
|
1346
|
+
* historical packed-row, embedded Assistant-stream, and goal lifecycle clocks
|
|
1347
|
+
* are zeroed; and all volatile strings are scrubbed. Projected inputs remain
|
|
1157
1348
|
* projected. Packed `data.dt` gaps are normalized even when the projected row
|
|
1158
1349
|
* omits its `time0` anchor.
|
|
1159
1350
|
* Output is JSONL in the same shape as the input — one compact record per
|
|
@@ -1176,10 +1367,21 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
|
|
|
1176
1367
|
const data = record.data;
|
|
1177
1368
|
if (data !== null && typeof data === "object" && Array.isArray(data.dt)) data.dt = data.dt.map(() => 0);
|
|
1178
1369
|
} else if ("time" in record) record.time = 0;
|
|
1370
|
+
if ((record.type === "assistant/message" || record.type === "assistant/attempt") && record.data !== null && typeof record.data === "object") {
|
|
1371
|
+
const stream = record.data.stream;
|
|
1372
|
+
if (Array.isArray(stream)) for (const member of stream) {
|
|
1373
|
+
if (member === null || typeof member !== "object") continue;
|
|
1374
|
+
const timed = member;
|
|
1375
|
+
if (typeof timed.time === "number") timed.time = 0;
|
|
1376
|
+
if (typeof timed.time0 === "number") timed.time0 = 0;
|
|
1377
|
+
if (Array.isArray(timed.dt)) timed.dt = timed.dt.map(() => 0);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1179
1380
|
if (record.type === "hook/result" && record.data !== null && typeof record.data === "object") {
|
|
1180
1381
|
const data = record.data;
|
|
1181
1382
|
if ("durationMs" in data) data.durationMs = 0;
|
|
1182
1383
|
}
|
|
1384
|
+
normalizeFeedbackClocks(record);
|
|
1183
1385
|
if (record.type === "goal/change" && record.data !== null && typeof record.data === "object") {
|
|
1184
1386
|
const data = record.data;
|
|
1185
1387
|
if ("createdAt" in data) data.createdAt = 0;
|
|
@@ -1190,36 +1392,15 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
|
|
|
1190
1392
|
}).map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
1191
1393
|
}
|
|
1192
1394
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
* reconstructs and packs the logical event stream; returned rows stay projected.
|
|
1395
|
+
* Canonicalize projected v2 body records. Compact streams are nested event data,
|
|
1396
|
+
* so persistence flush boundaries cannot change the row layout.
|
|
1196
1397
|
*/
|
|
1197
|
-
function
|
|
1398
|
+
function projectSessionSnapshot(rawLog) {
|
|
1198
1399
|
const lines = rawLog.split("\n").filter((line) => line.trim().length > 0);
|
|
1199
|
-
const header = lines.shift();
|
|
1200
|
-
let nextSeq = SessionLogOffset(0);
|
|
1201
1400
|
return [
|
|
1202
|
-
|
|
1203
|
-
...
|
|
1204
|
-
const
|
|
1205
|
-
if (isPackedFixtureRow(record)) {
|
|
1206
|
-
const decoded = decodeStorageRecord({
|
|
1207
|
-
...record,
|
|
1208
|
-
seq0: nextSeq,
|
|
1209
|
-
time0: 0
|
|
1210
|
-
});
|
|
1211
|
-
nextSeq = SessionLogOffset(nextSeq + decoded.length);
|
|
1212
|
-
return decoded;
|
|
1213
|
-
}
|
|
1214
|
-
const event = {
|
|
1215
|
-
...record,
|
|
1216
|
-
seq: SessionSeq(nextSeq),
|
|
1217
|
-
time: 0
|
|
1218
|
-
};
|
|
1219
|
-
nextSeq = SessionLogOffset(nextSeq + 1);
|
|
1220
|
-
return [event];
|
|
1221
|
-
})).map((stored) => {
|
|
1222
|
-
const projected = { ...stored };
|
|
1401
|
+
lines.shift(),
|
|
1402
|
+
...lines.map((line) => {
|
|
1403
|
+
const projected = { ...JSON.parse(line) };
|
|
1223
1404
|
omitFixtureEnvelope(projected);
|
|
1224
1405
|
return JSON.stringify(projected);
|
|
1225
1406
|
}),
|
|
@@ -1229,8 +1410,8 @@ function repackSessionSnapshot(rawLog) {
|
|
|
1229
1410
|
/**
|
|
1230
1411
|
* Normalize and project persisted session JSONL for a committed fixture.
|
|
1231
1412
|
* This composes ordinary log normalization with request-header scrubbing and
|
|
1232
|
-
* persistence-envelope projection, then
|
|
1233
|
-
*
|
|
1413
|
+
* persistence-envelope projection, then writes the v2 logical event stream as
|
|
1414
|
+
* one record per event, independent of persistence flush boundaries.
|
|
1234
1415
|
*
|
|
1235
1416
|
* @param rawLog - persisted or already-projected session JSONL.
|
|
1236
1417
|
* @param ctx - the run's volatile values to scrub.
|
|
@@ -1238,7 +1419,7 @@ function repackSessionSnapshot(rawLog) {
|
|
|
1238
1419
|
* @returns normalized committed session snapshot JSONL.
|
|
1239
1420
|
*/
|
|
1240
1421
|
function normalizeSessionSnapshot(rawLog, ctx, options = {}) {
|
|
1241
|
-
return
|
|
1422
|
+
return projectSessionSnapshot(scrubSessionSnapshot(normalizeSessionLog(rawLog, ctx, options)));
|
|
1242
1423
|
}
|
|
1243
1424
|
/**
|
|
1244
1425
|
* Normalize one scenario's primary and child logs with shared typed identity redaction.
|
|
@@ -1248,7 +1429,7 @@ function normalizeSessionSnapshot(rawLog, ctx, options = {}) {
|
|
|
1248
1429
|
* @returns normalized session fixtures in input order.
|
|
1249
1430
|
*/
|
|
1250
1431
|
function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
|
|
1251
|
-
return redactSessionSnapshotIds(rawLogs).map((log) =>
|
|
1432
|
+
return redactSessionSnapshotIds(rawLogs.map((log) => hasSessionFormatVersion(log) ? prepareSessionSnapshotFixtureForComparison(log) : log).map(normalizeSessionFormatProvenance)).map((log) => projectSessionSnapshot(scrubSessionSnapshot(normalizeSessionLog(log, {
|
|
1252
1433
|
...ctx,
|
|
1253
1434
|
sessionIds: []
|
|
1254
1435
|
}, {
|
|
@@ -1257,6 +1438,60 @@ function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
|
|
|
1257
1438
|
}))));
|
|
1258
1439
|
}
|
|
1259
1440
|
/**
|
|
1441
|
+
* Omit only generation-qualified operational provenance from expected-output comparison.
|
|
1442
|
+
* @param rawLog - Session records or events as compact JSON lines.
|
|
1443
|
+
* @returns the same records without delivery or captured-source generation qualifiers.
|
|
1444
|
+
*/
|
|
1445
|
+
function normalizeSessionFormatProvenance(rawLog) {
|
|
1446
|
+
return rawLog.split("\n").map((line) => {
|
|
1447
|
+
if (line.trim().length === 0) return line;
|
|
1448
|
+
const record = JSON.parse(line);
|
|
1449
|
+
let changed = normalizeCapturedFormatProvenance(record);
|
|
1450
|
+
if (record.type === "session" && Object.hasOwn(record, "version")) {
|
|
1451
|
+
delete record.version;
|
|
1452
|
+
changed = true;
|
|
1453
|
+
}
|
|
1454
|
+
if (record.type === "session-log-deepseek/delivery-accepted" && record.data !== null && typeof record.data === "object" && !Array.isArray(record.data)) {
|
|
1455
|
+
const data = { ...record.data };
|
|
1456
|
+
if (Object.hasOwn(data, "sessionFormatVersion")) {
|
|
1457
|
+
delete data.sessionFormatVersion;
|
|
1458
|
+
record.data = data;
|
|
1459
|
+
changed = true;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return changed ? JSON.stringify(record) : line;
|
|
1463
|
+
}).join("\n");
|
|
1464
|
+
}
|
|
1465
|
+
/** Omit captured generations only from an actual current Message source position. */
|
|
1466
|
+
function normalizeCapturedFormatProvenance(event) {
|
|
1467
|
+
if (event.data === null || typeof event.data !== "object" || Array.isArray(event.data)) return false;
|
|
1468
|
+
const data = event.data;
|
|
1469
|
+
const message = event.type === "user/message" ? data : event.type === "assistant/message" || event.type === "tool/result" ? data.message : void 0;
|
|
1470
|
+
if (message === null || typeof message !== "object" || Array.isArray(message)) return false;
|
|
1471
|
+
const source = message.source;
|
|
1472
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) return false;
|
|
1473
|
+
const record = source;
|
|
1474
|
+
if (record.kind !== "session-reference" || record.form !== "recall" || record.version !== 1 || !Array.isArray(record.references)) return false;
|
|
1475
|
+
let changed = false;
|
|
1476
|
+
for (const reference of record.references) {
|
|
1477
|
+
if (reference === null || typeof reference !== "object" || Array.isArray(reference)) continue;
|
|
1478
|
+
const captured = reference;
|
|
1479
|
+
if (Object.hasOwn(captured, "capturedFormatVersion")) {
|
|
1480
|
+
delete captured.capturedFormatVersion;
|
|
1481
|
+
changed = true;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return changed;
|
|
1485
|
+
}
|
|
1486
|
+
/** Whether a fixture declares a released Session format and therefore participates in migration burn-in. */
|
|
1487
|
+
function hasSessionFormatVersion(rawLog) {
|
|
1488
|
+
const firstLine = rawLog.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
1489
|
+
if (firstLine === void 0) throw new Error("session snapshot must start with a session header");
|
|
1490
|
+
const header = JSON.parse(firstLine);
|
|
1491
|
+
if (header === null || typeof header !== "object" || Array.isArray(header) || header["type"] !== "session") throw new Error("session snapshot must start with a session header");
|
|
1492
|
+
return Object.hasOwn(header, "version");
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1260
1495
|
* Replace system-prompt content in request headers with `{{system}}` tokens
|
|
1261
1496
|
* while retaining field presence.
|
|
1262
1497
|
* Other header content stays verbatim, so a header-pinning fixture can keep
|
|
@@ -1320,9 +1555,19 @@ function scrubSessionSnapshot(rawLog) {
|
|
|
1320
1555
|
return line;
|
|
1321
1556
|
}
|
|
1322
1557
|
omitFixtureEnvelope(record);
|
|
1558
|
+
normalizeFeedbackClocks(record);
|
|
1323
1559
|
return JSON.stringify(record);
|
|
1324
1560
|
}).join("\n");
|
|
1325
1561
|
}
|
|
1562
|
+
/** Normalize service-owned feedback clocks without touching user-authored payloads. */
|
|
1563
|
+
function normalizeFeedbackClocks(record) {
|
|
1564
|
+
if (record.type !== "feedback/message-put" || record.data === null || typeof record.data !== "object") return;
|
|
1565
|
+
const item = record.data.item;
|
|
1566
|
+
if (item === null || typeof item !== "object") return;
|
|
1567
|
+
const clocks = item;
|
|
1568
|
+
if ("createdAt" in clocks) clocks.createdAt = 0;
|
|
1569
|
+
if ("updatedAt" in clocks) clocks.updatedAt = 0;
|
|
1570
|
+
}
|
|
1326
1571
|
/** Transform the selected request-header payloads. */
|
|
1327
1572
|
function scrubHeaderContent(rawLog, options) {
|
|
1328
1573
|
return rawLog.split("\n").map((line) => {
|
|
@@ -1350,6 +1595,18 @@ function scrubHeaderContent(rawLog, options) {
|
|
|
1350
1595
|
//#endregion
|
|
1351
1596
|
//#region lib/types/manifest.js
|
|
1352
1597
|
/** Parse and validate one recorded-session snapshot manifest. */
|
|
1598
|
+
/**
|
|
1599
|
+
* Whether one run writes current-writer Session fixtures for this scenario.
|
|
1600
|
+
* Explicit historical generations remain immutable replay inputs; record and
|
|
1601
|
+
* refresh may still update their non-Session expected outputs.
|
|
1602
|
+
*
|
|
1603
|
+
* @param manifest - Parsed scenario ownership and retained-generation metadata.
|
|
1604
|
+
* @param mode - Snapshot execution mode.
|
|
1605
|
+
* @returns True only when a write-capable mode tracks the current writer.
|
|
1606
|
+
*/
|
|
1607
|
+
function writesCurrentSessionFixtures(manifest, mode) {
|
|
1608
|
+
return mode !== "replay" && manifest.session === void 0 && manifest.sessionFormat === void 0;
|
|
1609
|
+
}
|
|
1353
1610
|
const PROFILES = new Set([
|
|
1354
1611
|
"headless",
|
|
1355
1612
|
"sdk",
|
|
@@ -1363,6 +1620,13 @@ const PERMISSIONS = new Set([
|
|
|
1363
1620
|
"workspace-write",
|
|
1364
1621
|
"danger-full-access"
|
|
1365
1622
|
]);
|
|
1623
|
+
const SESSION_FORMAT_COVERAGE = new Set([
|
|
1624
|
+
"multi-hop",
|
|
1625
|
+
"packed-row",
|
|
1626
|
+
"retry-failure",
|
|
1627
|
+
"shipped-profile",
|
|
1628
|
+
"adjacent-migration"
|
|
1629
|
+
]);
|
|
1366
1630
|
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1367
1631
|
function record(value, label) {
|
|
1368
1632
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be a mapping`);
|
|
@@ -1412,7 +1676,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1412
1676
|
"environment",
|
|
1413
1677
|
"workspace",
|
|
1414
1678
|
"input",
|
|
1415
|
-
"session"
|
|
1679
|
+
"session",
|
|
1680
|
+
"sessionFormat"
|
|
1416
1681
|
], "manifest");
|
|
1417
1682
|
if (root.version !== 1) throw new Error("manifest.version must equal 1");
|
|
1418
1683
|
const scenario = root.scenario === void 0 ? void 0 : name(root.scenario, "manifest.scenario");
|
|
@@ -1527,6 +1792,18 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1527
1792
|
if (isAbsolute(value.source) || value.source.includes("\\") || value.source.includes("\0")) throw new Error("manifest.session.source must be a relative POSIX path");
|
|
1528
1793
|
session = { source: value.source };
|
|
1529
1794
|
}
|
|
1795
|
+
let sessionFormat;
|
|
1796
|
+
if (root.sessionFormat !== void 0) {
|
|
1797
|
+
const value = record(root.sessionFormat, "manifest.sessionFormat");
|
|
1798
|
+
exactKeys(value, ["version", "coverage"], "manifest.sessionFormat");
|
|
1799
|
+
if (!Number.isSafeInteger(value.version) || Number(value.version) < 0 || Object.is(value.version, -0)) throw new Error("manifest.sessionFormat.version must be a non-negative safe integer");
|
|
1800
|
+
if (!Array.isArray(value.coverage) || value.coverage.length === 0 || value.coverage.some((item) => typeof item !== "string" || !SESSION_FORMAT_COVERAGE.has(item)) || new Set(value.coverage).size !== value.coverage.length) throw new Error("manifest.sessionFormat.coverage must be a non-empty array of unique supported coverage names");
|
|
1801
|
+
if (session !== void 0) throw new Error("manifest.sessionFormat is only valid when the scenario owns its Session fixtures");
|
|
1802
|
+
sessionFormat = {
|
|
1803
|
+
version: Number(value.version),
|
|
1804
|
+
coverage: [...value.coverage]
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1530
1807
|
return {
|
|
1531
1808
|
version: 1,
|
|
1532
1809
|
...scenario === void 0 ? {} : { scenario },
|
|
@@ -1540,7 +1817,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1540
1817
|
...environment === void 0 ? {} : { environment },
|
|
1541
1818
|
...workspace === void 0 ? {} : { workspace },
|
|
1542
1819
|
...input === void 0 ? {} : { input },
|
|
1543
|
-
...session === void 0 ? {} : { session }
|
|
1820
|
+
...session === void 0 ? {} : { session },
|
|
1821
|
+
...sessionFormat === void 0 ? {} : { sessionFormat }
|
|
1544
1822
|
};
|
|
1545
1823
|
} catch (error) {
|
|
1546
1824
|
/* v8 ignore next -- every parser and validator above throws Error instances. */
|
|
@@ -1592,9 +1870,10 @@ const PACKED_CHUNK_ROW_TYPES = new Set([
|
|
|
1592
1870
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1593
1871
|
/**
|
|
1594
1872
|
* Whether a scenario's run test is skipped for this mode and host: record mode
|
|
1595
|
-
* skips authored (non-`recorded`) scenarios
|
|
1596
|
-
*
|
|
1597
|
-
* when the caller's `hasPwsh` probe
|
|
1873
|
+
* skips authored (non-`recorded`) scenarios and explicit historical Session
|
|
1874
|
+
* generations, {@link Scenario.posixOnly} scenarios skip on Windows, and
|
|
1875
|
+
* {@link Scenario.pwshOnly} scenarios skip when the caller's `hasPwsh` probe
|
|
1876
|
+
* is false.
|
|
1598
1877
|
*
|
|
1599
1878
|
* @param scenario The scenario whose run test is being registered.
|
|
1600
1879
|
* @param recording Whether the suite runs in record mode.
|
|
@@ -1604,7 +1883,7 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
1604
1883
|
* @returns True when the scenario's run test must not execute.
|
|
1605
1884
|
*/
|
|
1606
1885
|
function scenarioSkipped(scenario, recording, platform = process.platform, hasPwsh) {
|
|
1607
|
-
if (recording && !scenario.recorded) return true;
|
|
1886
|
+
if (recording && (!scenario.recorded || scenario.sessionFormat !== void 0)) return true;
|
|
1608
1887
|
if (scenario.posixOnly === true && platform === "win32") return true;
|
|
1609
1888
|
return scenario.pwshOnly === true && hasPwsh !== true;
|
|
1610
1889
|
}
|
|
@@ -1660,47 +1939,20 @@ function assertUniqueSnapshotContents(kind, snapshots) {
|
|
|
1660
1939
|
firstPathByContent.set(snapshot.content, snapshot.path);
|
|
1661
1940
|
}
|
|
1662
1941
|
}
|
|
1663
|
-
/**
|
|
1664
|
-
* Validate and order a scenario directory's session-fixture filenames.
|
|
1665
|
-
*
|
|
1666
|
-
* The primary fixture is always `session.jsonl`; child sessions are discovered
|
|
1667
|
-
* from contiguous `session.1.jsonl` … filenames. The directory is the source of
|
|
1668
|
-
* truth, so scenario tables do not duplicate a child count that can drift from
|
|
1669
|
-
* the files. A session-like JSONL with any other suffix fails loud.
|
|
1670
|
-
*
|
|
1671
|
-
* @param names File names in one scenario directory.
|
|
1672
|
-
* @returns The primary and child fixture names in replay/harvest order.
|
|
1673
|
-
*/
|
|
1674
|
-
function sessionFixtureNames(names) {
|
|
1675
|
-
if (!names.includes("session.jsonl")) throw new Error("missing session.jsonl");
|
|
1676
|
-
const children = [];
|
|
1677
|
-
for (const name of names) {
|
|
1678
|
-
if (name === "session.jsonl") continue;
|
|
1679
|
-
if (!name.startsWith("session.") || !name.endsWith(".jsonl")) continue;
|
|
1680
|
-
const match = /^session\.([1-9]\d*)\.jsonl$/.exec(name);
|
|
1681
|
-
if (match === null) throw new Error(`invalid child session fixture name: ${name}`);
|
|
1682
|
-
children.push({
|
|
1683
|
-
name,
|
|
1684
|
-
index: Number(match[1])
|
|
1685
|
-
});
|
|
1686
|
-
}
|
|
1687
|
-
children.sort((a, b) => a.index - b.index);
|
|
1688
|
-
for (const [offset, child] of children.entries()) {
|
|
1689
|
-
const expected = offset + 1;
|
|
1690
|
-
if (child.index !== expected) throw new Error(`child session fixtures must be contiguous: expected session.${expected}.jsonl, found ${child.name}`);
|
|
1691
|
-
}
|
|
1692
|
-
return ["session.jsonl", ...children.map((child) => child.name)];
|
|
1693
|
-
}
|
|
1694
1942
|
/** Read one scenario directory's validated session-fixture inventory. */
|
|
1695
1943
|
async function sessionFixtures(dir) {
|
|
1696
|
-
|
|
1944
|
+
const names = sessionFixtureNames((await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile()).map((entry) => entry.name));
|
|
1945
|
+
await Promise.all(names.map(async (name) => {
|
|
1946
|
+
assertSessionFixtureVersion(name, await readFile(join(dir, name), "utf8"));
|
|
1947
|
+
}));
|
|
1948
|
+
return names;
|
|
1697
1949
|
}
|
|
1698
1950
|
/**
|
|
1699
1951
|
* Derive normalization values from a fixture's own session header. Recorded ids and cwd differ
|
|
1700
1952
|
* from the live replay run; the non-empty sentinel for missing cwd avoids accidental empty-
|
|
1701
1953
|
* string replacement.
|
|
1702
1954
|
*
|
|
1703
|
-
* @param fixture The committed
|
|
1955
|
+
* @param fixture The selected committed parent Session fixture content.
|
|
1704
1956
|
* @returns The fixture's own volatile values, ready for {@link normalizeSessionLog}.
|
|
1705
1957
|
*/
|
|
1706
1958
|
function fixtureContext(fixture) {
|
|
@@ -2271,11 +2523,12 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2271
2523
|
const workspaceDir = join(dir, "workspace");
|
|
2272
2524
|
let fixtureFiles = RECORDING ? [] : await sessionFixtures(dir);
|
|
2273
2525
|
const childFixtureFiles = fixtureFiles.slice(1);
|
|
2526
|
+
const primaryFixtureFile = fixtureFiles[0] ?? sessionFixtureName(0, 0);
|
|
2274
2527
|
const comparesLog = scenario.comparesLog ?? scenario.hasModelTurn;
|
|
2275
2528
|
const result = await runScenario(input, {
|
|
2276
2529
|
agent,
|
|
2277
2530
|
mode: childMode,
|
|
2278
|
-
fixtureFile: join(dir,
|
|
2531
|
+
fixtureFile: join(dir, primaryFixtureFile),
|
|
2279
2532
|
...scenario.env !== void 0 ? { env: scenario.env } : {},
|
|
2280
2533
|
...existsSync(overrideFile) ? { overrideFile } : {},
|
|
2281
2534
|
...!RECORDING && childFixtureFiles.length > 0 ? { childFiles: childFixtureFiles.map((file) => join(dir, file)) } : {},
|
|
@@ -2293,23 +2546,19 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2293
2546
|
const childSchemaPins = new Set(scenario.pinsChildToolSchemas ?? []);
|
|
2294
2547
|
const childPromptPins = new Set(scenario.pinsChildSystemPrompts ?? []);
|
|
2295
2548
|
const portableFixture = scenario.workspaceParent === void 0 ? tokenizeSessionFixtureCwd : (log) => log;
|
|
2296
|
-
if (RECORDING && scenario.recorded && scenario.hasModelTurn || REFRESHING && comparesLog) {
|
|
2549
|
+
if (writesCurrentSessionFixtures(manifest, mode) && (RECORDING && scenario.recorded && scenario.hasModelTurn || REFRESHING && comparesLog)) {
|
|
2297
2550
|
expect(result.sessionLogs.length, `${mode} produced no session log to harvest`).toBeGreaterThan(0);
|
|
2298
2551
|
if (REFRESHING) expect(result.sessionLogs.length, `expected ${fixtureFiles.length} session logs (parent + children)`).toBe(fixtureFiles.length);
|
|
2299
|
-
const outputFixtureFiles =
|
|
2300
|
-
const existingFixtures = await Promise.all(outputFixtureFiles.map(async (
|
|
2301
|
-
const
|
|
2302
|
-
|
|
2552
|
+
const outputFixtureFiles = result.sessionLogs.map((log, index) => sessionFixtureName(index, sessionHeaderVersion(log.content, `harvested Session ${index}`)));
|
|
2553
|
+
const existingFixtures = await Promise.all(outputFixtureFiles.map(async (_file, index) => {
|
|
2554
|
+
const file = fixtureFiles[index];
|
|
2555
|
+
if (file === void 0) return "";
|
|
2556
|
+
return readFile(join(dir, file), "utf8");
|
|
2303
2557
|
}));
|
|
2304
2558
|
const refreshReplacements = REFRESHING ? refreshFixtureReplacements(result.sessionLogs, existingFixtures) : [];
|
|
2305
2559
|
const outputFixtures = redactSessionSnapshotIds(stabilizeFixtureMessageIds(REFRESHING ? result.sessionLogs.map((log, index) => scrubSessionSnapshot(portableFixture(stabilizeRefreshLog(log.content, existingFixtures[index], refreshReplacements, ctx)))) : result.sessionLogs.map((log) => scrubSessionSnapshot(portableFixture(log.content))), existingFixtures));
|
|
2306
2560
|
await Promise.all(outputFixtures.map((fixture, index) => writeFile(join(dir, outputFixtureFiles[index]), fixture)));
|
|
2307
|
-
|
|
2308
|
-
const outputNames = new Set(outputFixtureFiles);
|
|
2309
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
2310
|
-
await Promise.all(entries.filter((entry) => entry.isFile() && /^session\.[1-9]\d*\.jsonl$/.test(entry.name) && !outputNames.has(entry.name)).map((entry) => rm(join(dir, entry.name))));
|
|
2311
|
-
fixtureFiles = outputFixtureFiles;
|
|
2312
|
-
}
|
|
2561
|
+
fixtureFiles = outputFixtureFiles;
|
|
2313
2562
|
if (scenario.pinsHeader === true) {
|
|
2314
2563
|
const primary = result.sessionLogs[0];
|
|
2315
2564
|
const pinningHeaders = pinningHeaderPayloads(primary.content, ctx);
|
|
@@ -2366,7 +2615,9 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2366
2615
|
const promptSource = promptSourceByClass.get(classOf(scenario)) ?? pinningScenario;
|
|
2367
2616
|
/* v8 ignore next -- registration guarantees every scenario class has resolved sources. */
|
|
2368
2617
|
const schemaSource = schemaSourceByClass.get(classOf(scenario)) ?? pinningScenario;
|
|
2369
|
-
const
|
|
2618
|
+
const pinningDir = join(snapshotsDir, pinningScenario.name);
|
|
2619
|
+
const [pinningFixtureFile] = await sessionFixtures(pinningDir);
|
|
2620
|
+
const pinnedFixture = await readFile(join(pinningDir, pinningFixtureFile), "utf8");
|
|
2370
2621
|
const pinned = pinningHeaderPayloads(pinnedFixture, fixtureContext(pinnedFixture));
|
|
2371
2622
|
const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
|
|
2372
2623
|
const initialPromptSnapshot = initialSystemPromptSnapshot(promptSnapshot);
|
|
@@ -2444,7 +2695,6 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2444
2695
|
expect(existsSync(join(dir, "input.json")), `${name}/input.json`).toBe(true);
|
|
2445
2696
|
expect(existsSync(join(dir, "stdout.expected.jsonl")), `${name}/stdout.expected.jsonl`).toBe(true);
|
|
2446
2697
|
expect(existsSync(join(dir, WINDOWS_STDOUT_SNAPSHOT)), `${name}/${WINDOWS_STDOUT_SNAPSHOT} presence must match \`pinsNativeWindowsStdout\``).toBe(pinsNativeWindowsStdout === true);
|
|
2447
|
-
expect(existsSync(join(dir, "session.jsonl")), `${name}/session.jsonl`).toBe(true);
|
|
2448
2698
|
expect(existsSync(join(dir, "replay.override.json")), `${name}/replay.override.json presence must match \`overridden\``).toBe(overridden === true);
|
|
2449
2699
|
expect(existsSync(join(dir, SYSTEM_PROMPT_SNAPSHOT)), `${name}/${SYSTEM_PROMPT_SNAPSHOT} presence must match snapshot-source ownership`).toBe(promptOwners.has(name));
|
|
2450
2700
|
expect(existsSync(join(dir, TOOL_SCHEMAS_SNAPSHOT)), `${name}/${TOOL_SCHEMAS_SNAPSHOT} presence must match snapshot-source ownership`).toBe(schemaOwners.has(name));
|
|
@@ -2466,7 +2716,9 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2466
2716
|
const promptSource = promptSourceByClass.get(classOf(scenario)) ?? scenario;
|
|
2467
2717
|
/* v8 ignore next -- registration guarantees every pin has resolved sources. */
|
|
2468
2718
|
const schemaSource = schemaSourceByClass.get(classOf(scenario)) ?? scenario;
|
|
2469
|
-
const
|
|
2719
|
+
const fixtureDir = join(snapshotsDir, scenario.name);
|
|
2720
|
+
const [fixtureFile] = await sessionFixtures(fixtureDir);
|
|
2721
|
+
const fixture = await readFile(join(fixtureDir, fixtureFile), "utf8");
|
|
2470
2722
|
const headers = pinningHeaderPayloads(fixture, fixtureContext(fixture));
|
|
2471
2723
|
const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
|
|
2472
2724
|
expect(headers.length, `${scenario.name}: unexpected request/header count`).toBe(1 + (scenario.expectedHeaderChanges ?? 0));
|
|
@@ -2531,4 +2783,4 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2531
2783
|
});
|
|
2532
2784
|
}
|
|
2533
2785
|
//#endregion
|
|
2534
|
-
export { EMPTY_WORKSPACE_MARKER, captureExpectedWorkspaceSnapshot, captureWorkspaceSnapshot, defineAcpSnapshotSuite, extractSnapshotSpillPaths, fixtureContext, formatSystemPromptSnapshot, formatToolSchemasSnapshot, headerChangeCount, launchAcpTestAgent, materializeProfilePatch, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parseSnapshotManifest, parseToolSchemasSnapshot, redactSessionSnapshotIds, refreshFixtureReplacements, restorePinnedToolSchemas, runScenario, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, sessionFixtureNames, snapshotSpillRoot, stabilizeFixtureMessageIds, stabilizeRefreshLog, tokenizeSessionFixtureCwd };
|
|
2786
|
+
export { EMPTY_WORKSPACE_MARKER, assertPersistedSessionVersion, assertSessionFixtureVersion, captureExpectedWorkspaceSnapshot, captureWorkspaceSnapshot, defineAcpSnapshotSuite, extractSnapshotSpillPaths, fixtureContext, formatSystemPromptSnapshot, formatToolSchemasSnapshot, headerChangeCount, latestPersistedSessionPaths, launchAcpTestAgent, materializeProfilePatch, normalizeSessionFormatProvenance, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parsePersistedSessionFilename, parseSessionFixtureName, parseSnapshotManifest, parseToolSchemasSnapshot, persistedSessionFilename, redactSessionSnapshotIds, refreshFixtureReplacements, restorePinnedToolSchemas, runScenario, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, sessionFixtureFiles, sessionFixtureName, sessionFixtureNames, sessionHeaderVersion, snapshotSpillRoot, stabilizeFixtureMessageIds, stabilizeRefreshLog, tokenizeSessionFixtureCwd, writesCurrentSessionFixtures };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
export { redactSessionSnapshotIds, } from './identity.ts';
|
|
20
20
|
export { runScenario, snapshotSpillRoot, type HarvestedLog, type InputScript, type InputStep, type PermissionAnswer, type RunOptions, type RunResult, } from './harness.ts';
|
|
21
21
|
export { launchAcpTestAgent, materializeProfilePatch, type AcpTestLaunchOptions, type AgentUnderTest, type LaunchedAcpTestAgent, } from './launcher.ts';
|
|
22
|
-
export { extractSnapshotSpillPaths, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, tokenizeSessionFixtureCwd, type CwdPathMode, type NormalizeContext, type NormalizeOptions, } from './normalize.ts';
|
|
23
|
-
export { parseSnapshotManifest, type SnapshotHeaderManifest, type SnapshotInputAttachment, type SnapshotInputManifest, type SnapshotManifest, type SnapshotPermission, type SnapshotPlatform, type SnapshotProfile, type SnapshotRecording, type SnapshotReplayManifest, type SnapshotSessionReference, type SnapshotWorkspaceManifest, } from './manifest.ts';
|
|
24
|
-
export {
|
|
22
|
+
export { extractSnapshotSpillPaths, normalizeSessionFormatProvenance, normalizeSessionLog, normalizeSessionSnapshot, normalizeSessionSnapshots, normalizeStdout, scrubRequestHeaders, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, tokenizeSessionFixtureCwd, type CwdPathMode, type NormalizeContext, type NormalizeOptions, } from './normalize.ts';
|
|
23
|
+
export { parseSnapshotManifest, writesCurrentSessionFixtures, type SnapshotHeaderManifest, type SnapshotInputAttachment, type SnapshotInputManifest, type SnapshotManifest, type SnapshotPermission, type SnapshotPlatform, type SnapshotProfile, type SnapshotRecording, type SnapshotReplayManifest, type SnapshotSessionReference, type SnapshotSessionFormatCoverage, type SnapshotSessionFormatManifest, type SnapshotSessionWriteMode, type SnapshotWorkspaceManifest, } from './manifest.ts';
|
|
24
|
+
export { assertPersistedSessionVersion, assertSessionFixtureVersion, latestPersistedSessionPaths, parsePersistedSessionFilename, parseSessionFixtureName, persistedSessionFilename, sessionFixtureFiles, sessionFixtureName, sessionFixtureNames, sessionHeaderVersion, type PersistedSessionFile, type SessionFixtureFile, } from './session-files.ts';
|
|
25
|
+
export { formatSystemPromptSnapshot, formatToolSchemasSnapshot, fixtureContext, headerChangeCount, defineAcpSnapshotSuite, normalizedHeaders, normalizedSystemPrompts, normalizedToolSchemas, parseToolSchemasSnapshot, refreshFixtureReplacements, restorePinnedToolSchemas, stabilizeFixtureMessageIds, stabilizeRefreshLog, type Scenario, type SnapshotSuiteOptions, } from './suite.ts';
|
|
25
26
|
export { captureExpectedWorkspaceSnapshot, captureWorkspaceSnapshot, EMPTY_WORKSPACE_MARKER, type CaptureWorkspaceSnapshotOptions, type WorkspaceBinaryFileSnapshot, type WorkspaceEmptyDirectorySnapshot, type WorkspaceSnapshotEntry, type WorkspaceSymlinkSnapshot, type WorkspaceTextFileSnapshot, } from './workspace.ts';
|
|
26
27
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/manifest.d.ts
CHANGED
|
@@ -56,9 +56,18 @@ export interface SnapshotInputManifest {
|
|
|
56
56
|
}
|
|
57
57
|
/** Optional reference to another scenario's canonical session. */
|
|
58
58
|
export interface SnapshotSessionReference {
|
|
59
|
-
/** Repository-relative POSIX path
|
|
59
|
+
/** Repository-relative POSIX path to the owning scenario's selected parent Session fixture. */
|
|
60
60
|
source: string;
|
|
61
61
|
}
|
|
62
|
+
/** Historical-format behavior one retained scenario permanently exercises. */
|
|
63
|
+
export type SnapshotSessionFormatCoverage = 'multi-hop' | 'packed-row' | 'retry-failure' | 'shipped-profile' | 'adjacent-migration';
|
|
64
|
+
/** Explicit historical generation retained by an owning scenario. */
|
|
65
|
+
export interface SnapshotSessionFormatManifest {
|
|
66
|
+
/** Selected fixture generation; absent manifest metadata tracks the current writer. */
|
|
67
|
+
readonly version: number;
|
|
68
|
+
/** Migration behaviors that require this historical fixture. */
|
|
69
|
+
readonly coverage: readonly SnapshotSessionFormatCoverage[];
|
|
70
|
+
}
|
|
62
71
|
/** Declarative ownership metadata stored beside a recorded session. */
|
|
63
72
|
export interface SnapshotManifest {
|
|
64
73
|
/** Manifest format version. */
|
|
@@ -85,9 +94,23 @@ export interface SnapshotManifest {
|
|
|
85
94
|
workspace?: SnapshotWorkspaceManifest;
|
|
86
95
|
/** Exceptional controller input absent for ordinary log-driven scenarios. */
|
|
87
96
|
input?: SnapshotInputManifest;
|
|
88
|
-
/** Absent when this directory owns
|
|
97
|
+
/** Absent when this directory owns its selected parent fixture; present for a read-only borrower. */
|
|
89
98
|
session?: SnapshotSessionReference;
|
|
99
|
+
/** Historical generation retained by an owner instead of tracking the current writer. */
|
|
100
|
+
sessionFormat?: SnapshotSessionFormatManifest;
|
|
90
101
|
}
|
|
102
|
+
/** Snapshot execution modes that may read or replace committed fixture generations. */
|
|
103
|
+
export type SnapshotSessionWriteMode = 'replay' | 'record' | 'refresh';
|
|
104
|
+
/**
|
|
105
|
+
* Whether one run writes current-writer Session fixtures for this scenario.
|
|
106
|
+
* Explicit historical generations remain immutable replay inputs; record and
|
|
107
|
+
* refresh may still update their non-Session expected outputs.
|
|
108
|
+
*
|
|
109
|
+
* @param manifest - Parsed scenario ownership and retained-generation metadata.
|
|
110
|
+
* @param mode - Snapshot execution mode.
|
|
111
|
+
* @returns True only when a write-capable mode tracks the current writer.
|
|
112
|
+
*/
|
|
113
|
+
export declare function writesCurrentSessionFixtures(manifest: SnapshotManifest, mode: SnapshotSessionWriteMode): boolean;
|
|
91
114
|
/**
|
|
92
115
|
* Parse one `snapshot.yml` without admitting JavaScript YAML tags or unknown fields.
|
|
93
116
|
* @param source - complete manifest text.
|
package/lib/types/normalize.d.ts
CHANGED
|
@@ -55,9 +55,9 @@ export declare function tokenizeSessionFixtureCwd(rawLog: string): string;
|
|
|
55
55
|
export declare function normalizeStdout(rawStdout: string, ctx: NormalizeContext, options?: NormalizeOptions): string;
|
|
56
56
|
/**
|
|
57
57
|
* Normalize a session JSONL log into a stable expected output: the header line's
|
|
58
|
-
* volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed,
|
|
59
|
-
*
|
|
60
|
-
* zeroed
|
|
58
|
+
* volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed; event,
|
|
59
|
+
* historical packed-row, embedded Assistant-stream, and goal lifecycle clocks
|
|
60
|
+
* are zeroed; and all volatile strings are scrubbed. Projected inputs remain
|
|
61
61
|
* projected. Packed `data.dt` gaps are normalized even when the projected row
|
|
62
62
|
* omits its `time0` anchor.
|
|
63
63
|
* Output is JSONL in the same shape as the input — one compact record per
|
|
@@ -72,8 +72,8 @@ export declare function normalizeSessionLog(rawLog: string, ctx: NormalizeContex
|
|
|
72
72
|
/**
|
|
73
73
|
* Normalize and project persisted session JSONL for a committed fixture.
|
|
74
74
|
* This composes ordinary log normalization with request-header scrubbing and
|
|
75
|
-
* persistence-envelope projection, then
|
|
76
|
-
*
|
|
75
|
+
* persistence-envelope projection, then writes the v2 logical event stream as
|
|
76
|
+
* one record per event, independent of persistence flush boundaries.
|
|
77
77
|
*
|
|
78
78
|
* @param rawLog - persisted or already-projected session JSONL.
|
|
79
79
|
* @param ctx - the run's volatile values to scrub.
|
|
@@ -89,6 +89,12 @@ export declare function normalizeSessionSnapshot(rawLog: string, ctx: NormalizeC
|
|
|
89
89
|
* @returns normalized session fixtures in input order.
|
|
90
90
|
*/
|
|
91
91
|
export declare function normalizeSessionSnapshots(rawLogs: readonly string[], ctx: NormalizeContext, options?: Omit<NormalizeOptions, 'identityMode'>): string[];
|
|
92
|
+
/**
|
|
93
|
+
* Omit only generation-qualified operational provenance from expected-output comparison.
|
|
94
|
+
* @param rawLog - Session records or events as compact JSON lines.
|
|
95
|
+
* @returns the same records without delivery or captured-source generation qualifiers.
|
|
96
|
+
*/
|
|
97
|
+
export declare function normalizeSessionFormatProvenance(rawLog: string): string;
|
|
92
98
|
/**
|
|
93
99
|
* Replace system-prompt content in request headers with `{{system}}` tokens
|
|
94
100
|
* while retaining field presence.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Immutable Session-generation filenames used by recorded-session fixtures. */
|
|
2
|
+
/** One canonical recorded-session fixture filename. */
|
|
3
|
+
export interface SessionFixtureFile {
|
|
4
|
+
/** Parent is `0`; positive values are child/ordinal slots. */
|
|
5
|
+
readonly index: number;
|
|
6
|
+
/** Physical Session format generation; zero is encoded by omission. */
|
|
7
|
+
readonly version: number;
|
|
8
|
+
/** Canonical filename. */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
}
|
|
11
|
+
/** One canonical persistence filename in a Session's own storage directory. */
|
|
12
|
+
export interface PersistedSessionFile {
|
|
13
|
+
/** Physical Session format generation; zero is encoded by omission. */
|
|
14
|
+
readonly version: number;
|
|
15
|
+
/** Physical compression selected by the backend. */
|
|
16
|
+
readonly compression: 'raw' | 'zstd';
|
|
17
|
+
/** Canonical basename. */
|
|
18
|
+
readonly name: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Return the canonical fixture filename for one parent/ordinal and generation.
|
|
22
|
+
*
|
|
23
|
+
* @param index - Parent `0` or a positive child/ordinal slot.
|
|
24
|
+
* @param version - Physical format generation; `0` is omitted.
|
|
25
|
+
* @returns The lowercase canonical JSONL filename.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sessionFixtureName(index: number, version: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Parse one canonical recorded-session fixture filename.
|
|
30
|
+
*
|
|
31
|
+
* @param name - Basename from a scenario directory.
|
|
32
|
+
* @returns Parsed role and generation, or `undefined` for an unrelated file.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseSessionFixtureName(name: string): SessionFixtureFile | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Select the highest generation for every parent/ordinal fixture role.
|
|
37
|
+
* Older generations remain in the directory but do not count as extra Sessions.
|
|
38
|
+
*
|
|
39
|
+
* @param names - File basenames in one scenario directory.
|
|
40
|
+
* @returns Parent first, followed by contiguous child/ordinal roles.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sessionFixtureFiles(names: readonly string[]): SessionFixtureFile[];
|
|
43
|
+
/**
|
|
44
|
+
* Validate and order a scenario directory's selected Session fixture filenames.
|
|
45
|
+
*
|
|
46
|
+
* @param names - File basenames in one scenario directory.
|
|
47
|
+
* @returns Highest-generation parent and child/ordinal filenames.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionFixtureNames(names: readonly string[]): string[];
|
|
50
|
+
/**
|
|
51
|
+
* Read the declared physical generation from one Session JSONL header.
|
|
52
|
+
*
|
|
53
|
+
* @param content - Complete UTF-8 JSONL content.
|
|
54
|
+
* @param label - Diagnostic filename or path.
|
|
55
|
+
* @returns The declared non-negative format generation.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sessionHeaderVersion(content: string, label: string): number;
|
|
58
|
+
/**
|
|
59
|
+
* Require one fixture's canonical filename generation to equal its header.
|
|
60
|
+
*
|
|
61
|
+
* @param name - Canonical fixture basename.
|
|
62
|
+
* @param content - Complete UTF-8 JSONL content.
|
|
63
|
+
* @returns The validated format generation.
|
|
64
|
+
*/
|
|
65
|
+
export declare function assertSessionFixtureVersion(name: string, content: string): number;
|
|
66
|
+
/**
|
|
67
|
+
* Return the canonical persistence basename for one generation and compression.
|
|
68
|
+
*
|
|
69
|
+
* @param version - Physical format generation; `0` is omitted.
|
|
70
|
+
* @param compression - Backend compression mode.
|
|
71
|
+
* @returns The canonical persistence basename.
|
|
72
|
+
*/
|
|
73
|
+
export declare function persistedSessionFilename(version: number, compression?: 'raw' | 'zstd'): string;
|
|
74
|
+
/**
|
|
75
|
+
* Parse a canonical persistence basename from a Session's own directory.
|
|
76
|
+
*
|
|
77
|
+
* @param name - Candidate basename.
|
|
78
|
+
* @returns Its generation and compression, or `undefined` for noise and noncanonical names.
|
|
79
|
+
*/
|
|
80
|
+
export declare function parsePersistedSessionFilename(name: string): PersistedSessionFile | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Select one highest-generation persistence path per physical Session directory.
|
|
83
|
+
*
|
|
84
|
+
* @param paths - Relative or absolute paths beneath a sessions root.
|
|
85
|
+
* @param compression - Compression selected by the snapshot composition.
|
|
86
|
+
* @returns Stable path order with older generations and filesystem noise omitted.
|
|
87
|
+
*/
|
|
88
|
+
export declare function latestPersistedSessionPaths(paths: readonly string[], compression?: 'raw' | 'zstd'): string[];
|
|
89
|
+
/**
|
|
90
|
+
* Require one persistence basename's generation to equal its Session header.
|
|
91
|
+
*
|
|
92
|
+
* @param name - Canonical persistence basename.
|
|
93
|
+
* @param content - Complete uncompressed UTF-8 JSONL content.
|
|
94
|
+
* @returns The validated generation.
|
|
95
|
+
*/
|
|
96
|
+
export declare function assertPersistedSessionVersion(name: string, content: string): number;
|
|
97
|
+
//# sourceMappingURL=session-files.d.ts.map
|
package/lib/types/suite.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* @module @deepseek-ai/dsh-session-snapshot/suite
|
|
18
18
|
*/
|
|
19
19
|
import { type AgentUnderTest, type HarvestedLog } from './harness.ts';
|
|
20
|
+
import { type SnapshotSessionFormatManifest } from './manifest.ts';
|
|
20
21
|
import { type CwdPathMode, type NormalizeContext } from './normalize.ts';
|
|
21
22
|
/** A snapshot scenario and how its fixtures are produced. */
|
|
22
23
|
export interface Scenario {
|
|
@@ -27,24 +28,26 @@ export interface Scenario {
|
|
|
27
28
|
hasModelTurn: boolean;
|
|
28
29
|
/**
|
|
29
30
|
* Whether the run persists a comparable session log to diff against the
|
|
30
|
-
*
|
|
31
|
+
* selected parent Session fixture. Defaults to {@link hasModelTurn} (a model turn
|
|
31
32
|
* always produces a log worth comparing). Set it independently for a scenario
|
|
32
33
|
* that produces a non-trivial durable log without calling the model.
|
|
33
34
|
*/
|
|
34
35
|
comparesLog?: boolean;
|
|
35
36
|
/**
|
|
36
|
-
* Whether `test:snapshot:record` regenerates this scenario's
|
|
37
|
-
* from the LIVE API. `recorded` scenarios are model-driven and reproducible;
|
|
37
|
+
* Whether `test:snapshot:record` regenerates this scenario's current-version
|
|
38
|
+
* Session fixtures from the LIVE API. `recorded` scenarios are model-driven and reproducible;
|
|
38
39
|
* `authored` scenarios (fixtures hand-written or hand-harvested — e.g. a
|
|
39
40
|
* provider error or a cancel the live API can't be coaxed into
|
|
40
41
|
* deterministically, a deterministic hook scenario, or a scripted repetition
|
|
41
42
|
* a live model won't reproduce) are NEVER re-recorded.
|
|
42
43
|
*/
|
|
43
44
|
recorded: boolean;
|
|
45
|
+
/** Historical generation retained as a read-only migration fixture. */
|
|
46
|
+
sessionFormat?: SnapshotSessionFormatManifest;
|
|
44
47
|
/**
|
|
45
48
|
* Whether replay is driven by a hand-written `replay.override.json` sidecar
|
|
46
49
|
* (a `ReplayOverrideDoc` that replaces or patches the script derived from
|
|
47
|
-
*
|
|
50
|
+
* selected parent Session fixture) — the throw/hang cases chunks cannot express. The fixture
|
|
48
51
|
* guard requires the sidecar exactly when this is set: the harness forwards
|
|
49
52
|
* the file purely on existence, so an unregistered stray sidecar would
|
|
50
53
|
* silently alter the derived script. The guard fails loud on either
|
|
@@ -142,9 +145,10 @@ export interface Scenario {
|
|
|
142
145
|
}
|
|
143
146
|
/**
|
|
144
147
|
* Whether a scenario's run test is skipped for this mode and host: record mode
|
|
145
|
-
* skips authored (non-`recorded`) scenarios
|
|
146
|
-
*
|
|
147
|
-
* when the caller's `hasPwsh` probe
|
|
148
|
+
* skips authored (non-`recorded`) scenarios and explicit historical Session
|
|
149
|
+
* generations, {@link Scenario.posixOnly} scenarios skip on Windows, and
|
|
150
|
+
* {@link Scenario.pwshOnly} scenarios skip when the caller's `hasPwsh` probe
|
|
151
|
+
* is false.
|
|
148
152
|
*
|
|
149
153
|
* @param scenario The scenario whose run test is being registered.
|
|
150
154
|
* @param recording Whether the suite runs in record mode.
|
|
@@ -223,24 +227,12 @@ export declare function claimSharedSnapshot(claims: Map<string, SharedSnapshotCl
|
|
|
223
227
|
* @returns Nothing.
|
|
224
228
|
*/
|
|
225
229
|
export declare function assertUniqueSnapshotContents(kind: string, snapshots: readonly NamedSnapshotContent[]): void;
|
|
226
|
-
/**
|
|
227
|
-
* Validate and order a scenario directory's session-fixture filenames.
|
|
228
|
-
*
|
|
229
|
-
* The primary fixture is always `session.jsonl`; child sessions are discovered
|
|
230
|
-
* from contiguous `session.1.jsonl` … filenames. The directory is the source of
|
|
231
|
-
* truth, so scenario tables do not duplicate a child count that can drift from
|
|
232
|
-
* the files. A session-like JSONL with any other suffix fails loud.
|
|
233
|
-
*
|
|
234
|
-
* @param names File names in one scenario directory.
|
|
235
|
-
* @returns The primary and child fixture names in replay/harvest order.
|
|
236
|
-
*/
|
|
237
|
-
export declare function sessionFixtureNames(names: readonly string[]): string[];
|
|
238
230
|
/**
|
|
239
231
|
* Derive normalization values from a fixture's own session header. Recorded ids and cwd differ
|
|
240
232
|
* from the live replay run; the non-empty sentinel for missing cwd avoids accidental empty-
|
|
241
233
|
* string replacement.
|
|
242
234
|
*
|
|
243
|
-
* @param fixture The committed
|
|
235
|
+
* @param fixture The selected committed parent Session fixture content.
|
|
244
236
|
* @returns The fixture's own volatile values, ready for {@link normalizeSessionLog}.
|
|
245
237
|
*/
|
|
246
238
|
export declare function fixtureContext(fixture: string): NormalizeContext;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session-snapshot",
|
|
3
3
|
"description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -30,25 +30,29 @@
|
|
|
30
30
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
31
31
|
"js-yaml": "^4.2.0",
|
|
32
32
|
"vitest": "^4.1.8",
|
|
33
|
-
"@deepseek-ai/
|
|
34
|
-
"@deepseek-ai/
|
|
33
|
+
"@deepseek-ai/cordis-plugin-include": "1.0.7",
|
|
34
|
+
"@deepseek-ai/dsh-llm-replay": "^0.1.3-alpha.2",
|
|
35
|
+
"@deepseek-ai/dsh-loader-smoke": "0.1.3-alpha.2",
|
|
36
|
+
"@deepseek-ai/dsh-session-format": "^0.1.3-alpha.2"
|
|
35
37
|
},
|
|
36
38
|
"peerDependencies": {
|
|
37
39
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
38
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
40
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
|
|
41
|
+
"@deepseek-ai/dsh-http-proxy": "^0.1.3-alpha.2"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@types/js-yaml": "^4.0.9",
|
|
42
45
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
44
|
-
"@deepseek-ai/dsh-agent-loop": "^0.1.
|
|
45
|
-
"@deepseek-ai/dsh-compaction": "^0.1.
|
|
46
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
47
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.
|
|
48
|
-
"@deepseek-ai/dsh-sandbox-policy": "^0.1.
|
|
49
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
50
|
-
"@deepseek-ai/dsh-subagent": "^0.1.
|
|
51
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-user-questions": "^0.1.
|
|
46
|
+
"@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
|
|
47
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.3-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.3-alpha.2",
|
|
51
|
+
"@deepseek-ai/dsh-sandbox-policy": "^0.1.3-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-subagent": "^0.1.3-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-user-questions": "^0.1.3-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-http-proxy": "^0.1.3-alpha.2"
|
|
53
57
|
}
|
|
54
58
|
}
|