@deepseek-ai/dsh-session-snapshot 0.1.3-alpha.2 → 0.1.5-alpha.1
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 +13 -10
- package/README.zh.md +14 -11
- package/lib/index.js +227 -170
- package/lib/types/harness.d.ts +3 -2
- package/lib/types/index.d.ts +4 -4
- package/lib/types/manifest.d.ts +4 -2
- package/lib/types/normalize.d.ts +28 -29
- package/lib/types/session-files.d.ts +7 -0
- package/lib/types/suite.d.ts +56 -18
- package/package.json +17 -17
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
|
|
|
14
14
|
import { resolveExampleLaunch } from "@deepseek-ai/dsh-loader-smoke";
|
|
15
15
|
import { clearedProxyEnv } from "@deepseek-ai/dsh-http-proxy";
|
|
16
16
|
import { parseSessionFormatLogFilename, sessionFormatLogFilename } from "@deepseek-ai/dsh-session-format";
|
|
17
|
-
import { decodeSeqRanges } from "@deepseek-ai/dsh-session";
|
|
17
|
+
import { SESSION_FORMAT_VERSION, decodeSeqRanges } from "@deepseek-ai/dsh-session";
|
|
18
18
|
import { prepareSessionSnapshotFixtureForComparison } from "@deepseek-ai/dsh-llm-replay";
|
|
19
19
|
import { isSurfaceEligibleType } from "@deepseek-ai/dsh-session/surface";
|
|
20
20
|
//#region lib/types/identity.js
|
|
@@ -423,6 +423,16 @@ function sessionFixtureName(index, version) {
|
|
|
423
423
|
return `session${index === 0 ? "" : `.${index}`}${version === 0 ? "" : `.v${version}`}.jsonl`;
|
|
424
424
|
}
|
|
425
425
|
/**
|
|
426
|
+
* Name the native-writer oracle for a retained historical replay role.
|
|
427
|
+
* This expected output never participates in replay generation selection.
|
|
428
|
+
* @param index - Parent `0` or a positive child/ordinal slot.
|
|
429
|
+
* @returns The expected-output JSONL basename.
|
|
430
|
+
*/
|
|
431
|
+
function writerSnapshotName(index) {
|
|
432
|
+
nonNegativeSafeInteger(index, "writer snapshot index");
|
|
433
|
+
return `writer${index === 0 ? "" : `.${index}`}.expected.jsonl`;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
426
436
|
* Parse one canonical recorded-session fixture filename.
|
|
427
437
|
*
|
|
428
438
|
* @param name - Basename from a scenario directory.
|
|
@@ -668,7 +678,7 @@ function captureExpectedWorkspaceSnapshot(root) {
|
|
|
668
678
|
const DEFAULT_WAIT_TIMEOUT_MS = 1e4;
|
|
669
679
|
const WAIT_POLL_INTERVAL_MS = 10;
|
|
670
680
|
/**
|
|
671
|
-
* Derive
|
|
681
|
+
* Derive the stable, fixed-length logical spill prefix; never allocate files here.
|
|
672
682
|
* Windows uses a two-character-shorter root because drive resolution adds its drive prefix.
|
|
673
683
|
* @param fixtureFile - The scenario fixture whose parent directory provides the stable identity.
|
|
674
684
|
* @param platform - the host platform, injectable for unit coverage.
|
|
@@ -692,11 +702,12 @@ async function runScenario(input, opts) {
|
|
|
692
702
|
const cwd = await mkdtemp(join(opts.workspaceParent ?? tmpdir(), "acp-snap-cwd-"));
|
|
693
703
|
const cwdAliases = [...new Set([realpathSync(cwd), realpathSync.native(cwd)])];
|
|
694
704
|
const sessionsRoot = await mkdtemp(join(tmpdir(), "acp-snap-sessions-"));
|
|
695
|
-
|
|
705
|
+
let spillRoot;
|
|
696
706
|
let launched;
|
|
697
707
|
let sessionId;
|
|
698
708
|
let sessionLogs = [];
|
|
699
709
|
const outcome = await (async () => {
|
|
710
|
+
spillRoot = await mkdtemp(join(tmpdir(), "acp-snap-spill-"));
|
|
700
711
|
if (opts.workspaceDir !== void 0 && existsSync(opts.workspaceDir)) await cp(opts.workspaceDir, cwd, { recursive: true });
|
|
701
712
|
await opts.prepareWorkspace?.(cwd);
|
|
702
713
|
const initialWorkspace = await captureWorkspaceSnapshot(cwd, { ignoredRootEntries: [
|
|
@@ -712,6 +723,7 @@ async function runScenario(input, opts) {
|
|
|
712
723
|
DSH_SNAPSHOT_FILE: opts.fixtureFile,
|
|
713
724
|
DSH_SNAPSHOT_SESSIONS_ROOT: sessionsRoot,
|
|
714
725
|
DSH_SNAPSHOT_SPILL_ROOT: spillRoot,
|
|
726
|
+
DSH_SNAPSHOT_SPILL_LOCATOR_ROOT: snapshotSpillRoot(opts.fixtureFile),
|
|
715
727
|
DSH_HOME: join(cwd, ".dsh"),
|
|
716
728
|
DSH_AGENTS_HOME: join(cwd, ".agents"),
|
|
717
729
|
...opts.overrideFile !== void 0 ? { DSH_SNAPSHOT_OVERRIDE: opts.overrideFile } : {},
|
|
@@ -789,7 +801,8 @@ async function runScenario(input, opts) {
|
|
|
789
801
|
recursive: true,
|
|
790
802
|
force: true
|
|
791
803
|
}));
|
|
792
|
-
|
|
804
|
+
const allocatedSpillRoot = spillRoot;
|
|
805
|
+
if (allocatedSpillRoot !== void 0) await cleanup(() => rm(allocatedSpillRoot, {
|
|
793
806
|
recursive: true,
|
|
794
807
|
force: true
|
|
795
808
|
}));
|
|
@@ -987,13 +1000,18 @@ async function waitForPersistedTurnEnd(root, sessionId, timeoutMs = DEFAULT_WAIT
|
|
|
987
1000
|
* own model work reached a closed turn.
|
|
988
1001
|
*/
|
|
989
1002
|
async function waitForPersistedChildTurnEnd(root, child, timeoutMs = DEFAULT_WAIT_TIMEOUT_MS, minimumTurn = 1) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1003
|
+
const message = `snapshot-harness: subagent child #${child} did not persist closed turn ${minimumTurn} within ${timeoutMs}ms`;
|
|
1004
|
+
try {
|
|
1005
|
+
await vi.waitFor(async () => {
|
|
1006
|
+
const log = (await harvestSessionLogs(root))[child];
|
|
1007
|
+
if (log === void 0 || !latestTurnIsClosed(log.content) || !hasRequestHeaderAfterDescriptor(log.content) || !hasClosedTurn(log.content, minimumTurn)) throw new Error(message);
|
|
1008
|
+
}, {
|
|
1009
|
+
interval: WAIT_POLL_INTERVAL_MS,
|
|
1010
|
+
timeout: timeoutMs
|
|
1011
|
+
});
|
|
1012
|
+
} catch (cause) {
|
|
1013
|
+
throw new Error(message, { cause });
|
|
1014
|
+
}
|
|
997
1015
|
}
|
|
998
1016
|
/** Whether a raw session log contains the requested closed turn. */
|
|
999
1017
|
function hasClosedTurn(content, turn) {
|
|
@@ -1132,8 +1150,8 @@ async function harvestSessionLogs(root) {
|
|
|
1132
1150
|
/**
|
|
1133
1151
|
* Pure ACP transcript and session-log normalizers. They scrub session ids, run cwd, RPC ids,
|
|
1134
1152
|
* timestamps, goal lifecycle clocks, and hook duration while preserving semantic payload values.
|
|
1135
|
-
*
|
|
1136
|
-
* tool-schema sidecars.
|
|
1153
|
+
* The prompt-text and tool-schema scrubbers stay composable so one scenario per header class can
|
|
1154
|
+
* pin prompt and tool-schema sidecars.
|
|
1137
1155
|
* @module @deepseek-ai/dsh-session-snapshot/normalize
|
|
1138
1156
|
*/
|
|
1139
1157
|
const SESSION_ID = "{{sessionId}}";
|
|
@@ -1392,7 +1410,7 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
|
|
|
1392
1410
|
}).map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
1393
1411
|
}
|
|
1394
1412
|
/**
|
|
1395
|
-
* Canonicalize projected
|
|
1413
|
+
* Canonicalize projected v3 body records. Compact streams are nested event data,
|
|
1396
1414
|
* so persistence flush boundaries cannot change the row layout.
|
|
1397
1415
|
*/
|
|
1398
1416
|
function projectSessionSnapshot(rawLog) {
|
|
@@ -1410,7 +1428,7 @@ function projectSessionSnapshot(rawLog) {
|
|
|
1410
1428
|
/**
|
|
1411
1429
|
* Normalize and project persisted session JSONL for a committed fixture.
|
|
1412
1430
|
* This composes ordinary log normalization with request-header scrubbing and
|
|
1413
|
-
* persistence-envelope projection, then writes the
|
|
1431
|
+
* persistence-envelope projection, then writes the v3 logical event stream as
|
|
1414
1432
|
* one record per event, independent of persistence flush boundaries.
|
|
1415
1433
|
*
|
|
1416
1434
|
* @param rawLog - persisted or already-projected session JSONL.
|
|
@@ -1438,51 +1456,20 @@ function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
|
|
|
1438
1456
|
}))));
|
|
1439
1457
|
}
|
|
1440
1458
|
/**
|
|
1441
|
-
* Omit
|
|
1459
|
+
* Omit the artifact header generation after official migration for comparison.
|
|
1460
|
+
* Delivery and captured-source generations retain their opaque recorded values.
|
|
1442
1461
|
* @param rawLog - Session records or events as compact JSON lines.
|
|
1443
|
-
* @returns the same records
|
|
1462
|
+
* @returns the same records with only the Session header version omitted.
|
|
1444
1463
|
*/
|
|
1445
1464
|
function normalizeSessionFormatProvenance(rawLog) {
|
|
1446
1465
|
return rawLog.split("\n").map((line) => {
|
|
1447
1466
|
if (line.trim().length === 0) return line;
|
|
1448
1467
|
const record = JSON.parse(line);
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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;
|
|
1468
|
+
if (record.type !== "session" || !Object.hasOwn(record, "version")) return line;
|
|
1469
|
+
delete record.version;
|
|
1470
|
+
return JSON.stringify(record);
|
|
1463
1471
|
}).join("\n");
|
|
1464
1472
|
}
|
|
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
1473
|
/** Whether a fixture declares a released Session format and therefore participates in migration burn-in. */
|
|
1487
1474
|
function hasSessionFormatVersion(rawLog) {
|
|
1488
1475
|
const firstLine = rawLog.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
@@ -1492,60 +1479,58 @@ function hasSessionFormatVersion(rawLog) {
|
|
|
1492
1479
|
return Object.hasOwn(header, "version");
|
|
1493
1480
|
}
|
|
1494
1481
|
/**
|
|
1495
|
-
* Replace
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
* idempotent.
|
|
1482
|
+
* Replace the rendered prompt text of every `system/message` event with the
|
|
1483
|
+
* `{{system}}` token. The text block keeps its position and type, so the
|
|
1484
|
+
* fixture still shows one system node per prompt version; an empty `content`
|
|
1485
|
+
* (no system prompt) stays empty. Request headers and every other line pass
|
|
1486
|
+
* through byte-for-byte; the transform is idempotent.
|
|
1501
1487
|
*
|
|
1502
1488
|
* @param rawLog The raw session `.jsonl` content.
|
|
1503
|
-
* @returns The JSONL with system-prompt
|
|
1489
|
+
* @returns The JSONL with system-prompt text tokenized.
|
|
1504
1490
|
*/
|
|
1505
1491
|
function scrubSystemPrompts(rawLog) {
|
|
1506
|
-
return
|
|
1492
|
+
return scrubModelRequestContent(rawLog, { system: true });
|
|
1507
1493
|
}
|
|
1508
1494
|
/**
|
|
1509
1495
|
* Replace tool schemas in full request-header snapshots with `{{tools}}`
|
|
1510
|
-
* tokens while retaining field presence. System
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1496
|
+
* tokens while retaining field presence. System-prompt text stays verbatim so
|
|
1497
|
+
* pinning fixtures can move only schema bulk into their dedicated JSON
|
|
1498
|
+
* sidecar. Lines without a tool payload pass through byte-for-byte; the
|
|
1499
|
+
* transform is idempotent.
|
|
1514
1500
|
*
|
|
1515
1501
|
* @param rawLog The raw session `.jsonl` content.
|
|
1516
1502
|
* @returns The JSONL with tool-schema content tokenized.
|
|
1517
1503
|
*/
|
|
1518
1504
|
function scrubToolSchemas(rawLog) {
|
|
1519
|
-
return
|
|
1505
|
+
return scrubModelRequestContent(rawLog, { tools: true });
|
|
1520
1506
|
}
|
|
1521
1507
|
/**
|
|
1522
|
-
* Replace all bulky request
|
|
1523
|
-
* tokens
|
|
1524
|
-
* {@link scrubSystemPrompts}
|
|
1525
|
-
*
|
|
1526
|
-
* without content to scrub pass through byte-for-byte, and the
|
|
1527
|
-
* idempotent.
|
|
1508
|
+
* Replace all bulky model-request content in a session JSONL with stable
|
|
1509
|
+
* tokens: the `system/message` prompt text handled by
|
|
1510
|
+
* {@link scrubSystemPrompts} and the request-header tool schemas handled by
|
|
1511
|
+
* {@link scrubToolSchemas}. Field presence, config, and reason are kept.
|
|
1512
|
+
* Lines without content to scrub pass through byte-for-byte, and the
|
|
1513
|
+
* transform is idempotent.
|
|
1528
1514
|
*
|
|
1529
1515
|
* @param rawLog The raw session `.jsonl` content.
|
|
1530
|
-
* @returns The JSONL with
|
|
1516
|
+
* @returns The JSONL with prompt text and schema bulk tokenized, other lines byte-identical.
|
|
1531
1517
|
*/
|
|
1532
|
-
function
|
|
1533
|
-
return
|
|
1518
|
+
function scrubModelRequestBulk(rawLog) {
|
|
1519
|
+
return scrubModelRequestContent(rawLog, {
|
|
1534
1520
|
system: true,
|
|
1535
1521
|
tools: true
|
|
1536
1522
|
});
|
|
1537
1523
|
}
|
|
1538
1524
|
/**
|
|
1539
|
-
* Project a persisted session log while tokenizing
|
|
1540
|
-
* Each non-empty line is parsed at most once; the session header stays
|
|
1541
|
-
* byte-identical. Body records omit their persistence-only envelopes
|
|
1542
|
-
* request-header payloads are tokenized.
|
|
1525
|
+
* Project a persisted session log while tokenizing prompt text and schema
|
|
1526
|
+
* bulk. Each non-empty line is parsed at most once; the session header stays
|
|
1527
|
+
* byte-identical. Body records omit their persistence-only envelopes.
|
|
1543
1528
|
*
|
|
1544
1529
|
* @param rawLog - persisted or already-projected session JSONL.
|
|
1545
|
-
* @returns committed snapshot JSONL with
|
|
1530
|
+
* @returns committed snapshot JSONL with prompt text and tool schemas tokenized.
|
|
1546
1531
|
*/
|
|
1547
1532
|
function scrubSessionSnapshot(rawLog) {
|
|
1548
|
-
const scrubbed =
|
|
1533
|
+
const scrubbed = scrubModelRequestBulk(rawLog);
|
|
1549
1534
|
let recordIndex = 0;
|
|
1550
1535
|
return scrubbed.split("\n").map((line) => {
|
|
1551
1536
|
if (line.trim().length === 0) return line;
|
|
@@ -1568,26 +1553,31 @@ function normalizeFeedbackClocks(record) {
|
|
|
1568
1553
|
if ("createdAt" in clocks) clocks.createdAt = 0;
|
|
1569
1554
|
if ("updatedAt" in clocks) clocks.updatedAt = 0;
|
|
1570
1555
|
}
|
|
1571
|
-
/**
|
|
1572
|
-
function
|
|
1556
|
+
/** Return the first text block of a `system/message` payload, or `undefined` when it carries none. */
|
|
1557
|
+
function systemPromptBlock(data) {
|
|
1558
|
+
const message = data.message;
|
|
1559
|
+
if (message === null || typeof message !== "object" || !Array.isArray(message.content)) return void 0;
|
|
1560
|
+
const block = message.content[0];
|
|
1561
|
+
return block !== null && typeof block === "object" && typeof block.text === "string" ? block : void 0;
|
|
1562
|
+
}
|
|
1563
|
+
/** Transform the selected model-request payloads. */
|
|
1564
|
+
function scrubModelRequestContent(rawLog, options) {
|
|
1573
1565
|
return rawLog.split("\n").map((line) => {
|
|
1574
1566
|
if (line.trim().length === 0) return line;
|
|
1575
1567
|
const record = JSON.parse(line);
|
|
1576
1568
|
const data = record.data;
|
|
1577
1569
|
if (data === null || typeof data !== "object") return line;
|
|
1578
|
-
if (record.type === "
|
|
1570
|
+
if (options.system === true && record.type === "system/message") {
|
|
1571
|
+
const block = systemPromptBlock(data);
|
|
1572
|
+
if (block === void 0) return line;
|
|
1573
|
+
block.text = SYSTEM;
|
|
1574
|
+
return JSON.stringify(record);
|
|
1575
|
+
}
|
|
1576
|
+
if (options.tools === true && record.type === "request/header") {
|
|
1579
1577
|
const header = data.header;
|
|
1580
|
-
if (header === null || typeof header !== "object") return line;
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
header.system = SYSTEM;
|
|
1584
|
-
touched = true;
|
|
1585
|
-
}
|
|
1586
|
-
if (options.tools === true && "tools" in header) {
|
|
1587
|
-
header.tools = TOOLS;
|
|
1588
|
-
touched = true;
|
|
1589
|
-
}
|
|
1590
|
-
return touched ? JSON.stringify(record) : line;
|
|
1578
|
+
if (header === null || typeof header !== "object" || !("tools" in header)) return line;
|
|
1579
|
+
header.tools = TOOLS;
|
|
1580
|
+
return JSON.stringify(record);
|
|
1591
1581
|
}
|
|
1592
1582
|
return line;
|
|
1593
1583
|
}).join("\n");
|
|
@@ -1698,10 +1688,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1698
1688
|
"toolSchemasSource",
|
|
1699
1689
|
"childSystemPrompts",
|
|
1700
1690
|
"childToolSchemas",
|
|
1701
|
-
"changes"
|
|
1691
|
+
"changes",
|
|
1692
|
+
"promptChanges"
|
|
1702
1693
|
], "manifest.header");
|
|
1703
1694
|
if (value.pin !== void 0 && value.pin !== true) throw new Error("manifest.header.pin must equal true when present");
|
|
1704
|
-
if (value
|
|
1695
|
+
for (const field of ["changes", "promptChanges"]) if (value[field] !== void 0 && (!Number.isInteger(value[field]) || Number(value[field]) < 0)) throw new Error(`manifest.header.${field} must be a non-negative integer`);
|
|
1705
1696
|
header = {
|
|
1706
1697
|
class: name(value.class, "manifest.header.class"),
|
|
1707
1698
|
...value.pin === true ? { pin: true } : {},
|
|
@@ -1709,7 +1700,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1709
1700
|
...value.toolSchemasSource === void 0 ? {} : { toolSchemasSource: scenarioSource(value.toolSchemasSource, "manifest.header.toolSchemasSource") },
|
|
1710
1701
|
...value.childSystemPrompts === void 0 ? {} : { childSystemPrompts: positiveIndexes(value.childSystemPrompts, "manifest.header.childSystemPrompts") },
|
|
1711
1702
|
...value.childToolSchemas === void 0 ? {} : { childToolSchemas: positiveIndexes(value.childToolSchemas, "manifest.header.childToolSchemas") },
|
|
1712
|
-
...value.changes === void 0 ? {} : { changes: Number(value.changes) }
|
|
1703
|
+
...value.changes === void 0 ? {} : { changes: Number(value.changes) },
|
|
1704
|
+
...value.promptChanges === void 0 ? {} : { promptChanges: Number(value.promptChanges) }
|
|
1713
1705
|
};
|
|
1714
1706
|
}
|
|
1715
1707
|
let replay;
|
|
@@ -1744,11 +1736,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1744
1736
|
"parent"
|
|
1745
1737
|
], "manifest.workspace");
|
|
1746
1738
|
if (value.final !== void 0 && value.final !== true) throw new Error("manifest.workspace.final must equal true when present");
|
|
1747
|
-
if (value.parent !== void 0 && value.parent !== "
|
|
1739
|
+
if (value.parent !== void 0 && value.parent !== "outside-temp") throw new Error("manifest.workspace.parent must equal outside-temp");
|
|
1748
1740
|
workspace = {
|
|
1749
1741
|
...value.setup === void 0 ? {} : { setup: name(value.setup, "manifest.workspace.setup") },
|
|
1750
1742
|
...value.final === true ? { final: true } : {},
|
|
1751
|
-
...value.parent === "
|
|
1743
|
+
...value.parent === "outside-temp" ? { parent: "outside-temp" } : {}
|
|
1752
1744
|
};
|
|
1753
1745
|
if (Object.keys(workspace).length === 0) throw new Error("manifest.workspace must not be empty");
|
|
1754
1746
|
}
|
|
@@ -1838,9 +1830,10 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1838
1830
|
* refresh stay serial while writing.
|
|
1839
1831
|
*
|
|
1840
1832
|
* Exactly one scenario per header-composition class pins the tokenized header
|
|
1841
|
-
* sequence
|
|
1842
|
-
* sidecars, each of which may be
|
|
1843
|
-
*
|
|
1833
|
+
* sequence and the tokenized `system/message` sequence. Its prompt and
|
|
1834
|
+
* tool-schema sequences live in independent sidecars, each of which may be
|
|
1835
|
+
* shared with another class pin when the bytes are identical. Every live
|
|
1836
|
+
* header and system prompt is checked against the composed pin, so
|
|
1844
1837
|
* session-dependent composition must declare a separate class instead of
|
|
1845
1838
|
* escaping coverage.
|
|
1846
1839
|
* @module @deepseek-ai/dsh-session-snapshot/suite
|
|
@@ -1965,10 +1958,13 @@ function fixtureContext(fixture) {
|
|
|
1965
1958
|
}
|
|
1966
1959
|
/** Normalize request-header payloads while retaining the reason that selects a pin revision. */
|
|
1967
1960
|
function normalizedHeaderEvents(rawLog, ctx) {
|
|
1968
|
-
return normalizeSessionLog(rawLog, ctx)
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1961
|
+
return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).filter((record) => record.type === "request/header").map((record) => {
|
|
1962
|
+
const data = record.data;
|
|
1963
|
+
return {
|
|
1964
|
+
header: data?.header,
|
|
1965
|
+
reason: data?.reason
|
|
1966
|
+
};
|
|
1967
|
+
});
|
|
1972
1968
|
}
|
|
1973
1969
|
/**
|
|
1974
1970
|
* Header revisions that own sidecar content. `series` reuses the current revision, while
|
|
@@ -1979,13 +1975,17 @@ function normalizedHeaderEvents(rawLog, ctx) {
|
|
|
1979
1975
|
function pinningHeaderPayloads(rawLog, ctx) {
|
|
1980
1976
|
return normalizedHeaderEvents(rawLog, ctx).filter((event) => event.reason !== "series").map((event) => event.header);
|
|
1981
1977
|
}
|
|
1982
|
-
/**
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1978
|
+
/**
|
|
1979
|
+
* The rendered prompt text of one parsed `system/message` record: its single
|
|
1980
|
+
* text block, or `''` when the empty `content` records "no system prompt".
|
|
1981
|
+
* Any other record yields `undefined`.
|
|
1982
|
+
*/
|
|
1983
|
+
function systemPromptOf(record) {
|
|
1984
|
+
if (record.type !== "system/message") return void 0;
|
|
1985
|
+
const content = record.data?.message?.content;
|
|
1986
|
+
if (!Array.isArray(content)) return void 0;
|
|
1987
|
+
const block = content[0];
|
|
1988
|
+
return content.length === 0 ? "" : typeof block?.text === "string" ? block.text : void 0;
|
|
1989
1989
|
}
|
|
1990
1990
|
/** Extract every array-valued tool catalog from a normalized header sequence. */
|
|
1991
1991
|
function toolSchemasFrom(headers) {
|
|
@@ -1999,8 +1999,7 @@ function toolSchemasFrom(headers) {
|
|
|
1999
1999
|
* The `data.header` payload of every `request/header` event in a session
|
|
2000
2000
|
* JSONL, in log order, with the log's volatile values scrubbed first
|
|
2001
2001
|
* ({@link normalizeSessionLog}) so headers harvested from different runs —
|
|
2002
|
-
* each embedding its own generated cwd
|
|
2003
|
-
* footing.
|
|
2002
|
+
* each embedding its own generated cwd — compare on equal footing.
|
|
2004
2003
|
*
|
|
2005
2004
|
* @param rawLog The session `.jsonl` content to extract headers from.
|
|
2006
2005
|
* @param ctx The volatile values of the run that produced it.
|
|
@@ -2010,16 +2009,35 @@ function normalizedHeaders(rawLog, ctx) {
|
|
|
2010
2009
|
return normalizedHeaderEvents(rawLog, ctx).map((event) => event.header);
|
|
2011
2010
|
}
|
|
2012
2011
|
/**
|
|
2013
|
-
* The normalized
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2012
|
+
* The normalized prompt text of every `system/message` event in a session
|
|
2013
|
+
* JSONL, in log order: the first is the initial system prompt (surface node 0)
|
|
2014
|
+
* and each later one replaced it or, on an in-history route, appended the
|
|
2015
|
+
* changed prompt after the cached history. An empty `content` yields `''`; a
|
|
2016
|
+
* `system/message` without a text block is omitted.
|
|
2016
2017
|
*
|
|
2017
2018
|
* @param rawLog The session `.jsonl` content to inspect.
|
|
2018
2019
|
* @param ctx The volatile values of the run that produced it.
|
|
2019
|
-
* @returns The normalized system prompts, in
|
|
2020
|
+
* @returns The normalized system prompts, in log order.
|
|
2020
2021
|
*/
|
|
2021
2022
|
function normalizedSystemPrompts(rawLog, ctx) {
|
|
2022
|
-
return
|
|
2023
|
+
return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).flatMap((record) => {
|
|
2024
|
+
const prompt = systemPromptOf(record);
|
|
2025
|
+
return prompt === void 0 ? [] : [prompt];
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Whether every model request in a session JSONL is preceded by its system
|
|
2030
|
+
* prompt: the log has no `request/header`, or a `system/message` event comes
|
|
2031
|
+
* before its first `request/header`.
|
|
2032
|
+
*
|
|
2033
|
+
* @param rawLog The session `.jsonl` content to inspect.
|
|
2034
|
+
* @returns True when the first `request/header` (if any) follows a `system/message`.
|
|
2035
|
+
*/
|
|
2036
|
+
function systemPromptPrecedesRequests(rawLog) {
|
|
2037
|
+
const types = parseJsonlRecords(rawLog).map((record) => record.type);
|
|
2038
|
+
const firstHeader = types.indexOf("request/header");
|
|
2039
|
+
const firstPrompt = types.indexOf("system/message");
|
|
2040
|
+
return firstHeader < 0 || firstPrompt >= 0 && firstPrompt < firstHeader;
|
|
2023
2041
|
}
|
|
2024
2042
|
/**
|
|
2025
2043
|
* The normalized tool-schema arrays carried by request headers in a session
|
|
@@ -2077,24 +2095,40 @@ function restorePinnedToolSchemas(header, schemas) {
|
|
|
2077
2095
|
tools: schemas
|
|
2078
2096
|
};
|
|
2079
2097
|
}
|
|
2098
|
+
/** Marker line separating one replaced prompt from the previous one in a prompt sidecar. */
|
|
2099
|
+
const SYSTEM_PROMPT_CHANGE_MARKER = "\n<!-- system/message change ";
|
|
2080
2100
|
/**
|
|
2081
2101
|
* Render a normalized prompt as a repository-friendly Markdown snapshot.
|
|
2082
2102
|
* Prompt text is unchanged except that a missing terminal newline is added so
|
|
2083
2103
|
* the committed file follows the repository newline contract.
|
|
2084
2104
|
*
|
|
2085
|
-
* @param prompt The normalized system prompt.
|
|
2086
|
-
* @param changes Full normalized prompts from later
|
|
2105
|
+
* @param prompt The normalized initial system prompt (surface node 0).
|
|
2106
|
+
* @param changes Full normalized prompts from later `system/message` events, replacements or in-history appends.
|
|
2087
2107
|
* @returns Markdown snapshot text ending in a newline.
|
|
2088
2108
|
*/
|
|
2089
2109
|
function formatSystemPromptSnapshot(prompt, changes = []) {
|
|
2090
2110
|
let snapshot = prompt.endsWith("\n") ? prompt : `${prompt}\n`;
|
|
2091
2111
|
for (const [index, change] of changes.entries()) {
|
|
2092
|
-
snapshot +=
|
|
2112
|
+
snapshot += `${SYSTEM_PROMPT_CHANGE_MARKER}${index + 1} -->\n\n`;
|
|
2093
2113
|
snapshot += change.endsWith("\n") ? change : `${change}\n`;
|
|
2094
2114
|
}
|
|
2095
2115
|
return snapshot;
|
|
2096
2116
|
}
|
|
2097
2117
|
/**
|
|
2118
|
+
* Split a prompt sidecar into its initial prompt and each later change, the
|
|
2119
|
+
* inverse of {@link formatSystemPromptSnapshot}.
|
|
2120
|
+
*
|
|
2121
|
+
* @param snapshot The Markdown sidecar text.
|
|
2122
|
+
* @returns The initial prompt snapshot plus one entry per later `system/message`.
|
|
2123
|
+
*/
|
|
2124
|
+
function parseSystemPromptSnapshot(snapshot) {
|
|
2125
|
+
const parts = snapshot.split(/\n<!-- system\/message change [1-9]\d* -->\n\n/);
|
|
2126
|
+
return {
|
|
2127
|
+
initial: parts[0],
|
|
2128
|
+
changes: parts.slice(1)
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2098
2132
|
* Reject a child prompt sidecar that cannot own distinct, canonical prompt text.
|
|
2099
2133
|
* @param sidecar - committed child prompt snapshot.
|
|
2100
2134
|
* @param classPin - initial prompt snapshot owned by the scenario's header class.
|
|
@@ -2105,10 +2139,9 @@ function assertChildSystemPromptSnapshot(sidecar, classPin, label) {
|
|
|
2105
2139
|
if (!sidecar.endsWith("\n")) throw new Error(`${label} must end in a newline`);
|
|
2106
2140
|
if (sidecar === classPin) throw new Error(`${label} must differ from its class pin`);
|
|
2107
2141
|
}
|
|
2108
|
-
/** Return the initial-prompt portion of a possibly multi-
|
|
2142
|
+
/** Return the initial-prompt portion of a possibly multi-prompt snapshot. */
|
|
2109
2143
|
function initialSystemPromptSnapshot(snapshot) {
|
|
2110
|
-
|
|
2111
|
-
return marker < 0 ? snapshot : snapshot.slice(0, marker);
|
|
2144
|
+
return parseSystemPromptSnapshot(snapshot).initial;
|
|
2112
2145
|
}
|
|
2113
2146
|
/**
|
|
2114
2147
|
* Count changed `request/header` snapshots in a session JSONL.
|
|
@@ -2141,6 +2174,7 @@ function surfaceEventMessage(record) {
|
|
|
2141
2174
|
case "user/message":
|
|
2142
2175
|
message = data;
|
|
2143
2176
|
break;
|
|
2177
|
+
case "system/message":
|
|
2144
2178
|
case "assistant/message":
|
|
2145
2179
|
case "tool/result":
|
|
2146
2180
|
message = data.message;
|
|
@@ -2458,11 +2492,34 @@ function stabilizeRefreshLog(fresh, existing, replacements, freshContext) {
|
|
|
2458
2492
|
return records.map((record) => JSON.stringify(record)).join("\n") + "\n";
|
|
2459
2493
|
}
|
|
2460
2494
|
/**
|
|
2495
|
+
* Check every selected role for tool/path defects and current generations for canonical prompt/identity storage.
|
|
2496
|
+
* Historical roles retain their released bytes, including roles retired by the current writer.
|
|
2497
|
+
* @param dir Scenario directory containing canonical Session fixtures.
|
|
2498
|
+
* @param scenarioName Scenario name used in failure diagnostics.
|
|
2499
|
+
* @returns Resolves when all selected fixtures satisfy the storage checks.
|
|
2500
|
+
*/
|
|
2501
|
+
async function assertSessionFixtureStorage(dir, scenarioName) {
|
|
2502
|
+
const files = await sessionFixtures(dir);
|
|
2503
|
+
const currentFixtures = [];
|
|
2504
|
+
for (const file of files) {
|
|
2505
|
+
const fixture = await readFile(join(dir, file), "utf8");
|
|
2506
|
+
const version = assertSessionFixtureVersion(file, fixture);
|
|
2507
|
+
expect(unknownToolCallIds(fixture), `${scenarioName}/${file} contains UNKNOWN_TOOL`).toEqual([]);
|
|
2508
|
+
expect(fixture, `${scenarioName}/${file} carries a non-canonical macOS cwd token`).not.toContain("/private{{cwd}}");
|
|
2509
|
+
if (version !== SESSION_FORMAT_VERSION) continue;
|
|
2510
|
+
currentFixtures.push(fixture);
|
|
2511
|
+
expect(scrubSystemPrompts(fixture), `${scenarioName}/${file} carries an unscrubbed system prompt`).toEqual(fixture);
|
|
2512
|
+
expect(scrubToolSchemas(fixture), `${scenarioName}/${file} carries unscrubbed tool schemas`).toEqual(fixture);
|
|
2513
|
+
expect(systemPromptPrecedesRequests(fixture), `${scenarioName}/${file} has a request/header with no preceding system/message`).toBe(true);
|
|
2514
|
+
}
|
|
2515
|
+
expect(redactSessionSnapshotIds(currentFixtures), `${scenarioName}: identity redaction fixed point`).toEqual(currentFixtures);
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2461
2518
|
* Register the suite: one test per scenario (the expected-output and log comparisons and
|
|
2462
|
-
* the header
|
|
2519
|
+
* the header and prompt uniformity guard) plus the fixture guard block (no orphan
|
|
2463
2520
|
* scenario dirs, required files present, exactly one pin per header class,
|
|
2464
|
-
* shared sidecars unique and well-formed, every JSONL prompt-
|
|
2465
|
-
*
|
|
2521
|
+
* shared sidecars unique and well-formed, every JSONL prompt- and
|
|
2522
|
+
* schema-scrubbed with a `system/message` before its first request). Must
|
|
2466
2523
|
* run at vitest collection time — it calls `describe`/`it`. Throws
|
|
2467
2524
|
* immediately if any header class lacks a pinning scenario or carries two
|
|
2468
2525
|
* (the uniformity guard needs exactly one comparison anchor per class).
|
|
@@ -2481,7 +2538,12 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2481
2538
|
for (const scenario of scenarios) {
|
|
2482
2539
|
if (scenariosByName.has(scenario.name)) throw new Error(`acp-snapshot: duplicate scenario name "${scenario.name}"`);
|
|
2483
2540
|
scenariosByName.set(scenario.name, scenario);
|
|
2484
|
-
for (const field of [
|
|
2541
|
+
for (const field of [
|
|
2542
|
+
"systemPromptSource",
|
|
2543
|
+
"toolSchemasSource",
|
|
2544
|
+
"expectedHeaderChanges",
|
|
2545
|
+
"expectedPromptChanges"
|
|
2546
|
+
]) if (scenario[field] !== void 0 && scenario.pinsHeader !== true) throw new Error(`acp-snapshot: ${scenario.name}.${field} is only valid on a header-pinning scenario`);
|
|
2485
2547
|
}
|
|
2486
2548
|
/** Each header class's single pinning scenario. Guarded here (and by meta-tests) so a pin cannot silently vanish or split. */
|
|
2487
2549
|
const pinningByClass = /* @__PURE__ */ new Map();
|
|
@@ -2493,14 +2555,19 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2493
2555
|
pinningByClass.set(cls, scenario);
|
|
2494
2556
|
}
|
|
2495
2557
|
for (const scenario of scenarios) if (!pinningByClass.has(classOf(scenario))) throw new Error(`acp-snapshot: no scenario pins the request-header content of class "${classOf(scenario)}" (needed by ${scenario.name})`);
|
|
2558
|
+
/**
|
|
2559
|
+
* Resolve one pin's sidecar source. A prompt sidecar spans `1 + expectedPromptChanges`
|
|
2560
|
+
* prompts and a schema sidecar spans `1 + expectedHeaderChanges` schema sets, so a shared
|
|
2561
|
+
* source must declare the count that sizes the sidecar it lends.
|
|
2562
|
+
*/
|
|
2496
2563
|
const sourceFor = (pinningScenario, field, label) => {
|
|
2497
2564
|
const sourceName = pinningScenario[field] ?? pinningScenario.name;
|
|
2498
2565
|
const source = scenariosByName.get(sourceName);
|
|
2499
2566
|
if (source === void 0) throw new Error(`acp-snapshot: ${pinningScenario.name} names unknown ${label} source "${sourceName}"`);
|
|
2500
2567
|
if (source.pinsHeader !== true) throw new Error(`acp-snapshot: ${pinningScenario.name} names non-pinning ${label} source "${sourceName}"`);
|
|
2501
2568
|
if (source[field] !== void 0 && source[field] !== source.name) throw new Error(`acp-snapshot: ${pinningScenario.name} names ${label} source "${sourceName}", which does not own its sidecar`);
|
|
2502
|
-
const
|
|
2503
|
-
if ((source
|
|
2569
|
+
const countField = field === "systemPromptSource" ? "expectedPromptChanges" : "expectedHeaderChanges";
|
|
2570
|
+
if ((source[countField] ?? 0) !== (pinningScenario[countField] ?? 0)) throw new Error(`acp-snapshot: ${pinningScenario.name} and ${sourceName} declare different ${countField} counts for shared ${label}`);
|
|
2504
2571
|
return source;
|
|
2505
2572
|
};
|
|
2506
2573
|
const promptSourceByClass = /* @__PURE__ */ new Map();
|
|
@@ -2524,7 +2591,7 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2524
2591
|
let fixtureFiles = RECORDING ? [] : await sessionFixtures(dir);
|
|
2525
2592
|
const childFixtureFiles = fixtureFiles.slice(1);
|
|
2526
2593
|
const primaryFixtureFile = fixtureFiles[0] ?? sessionFixtureName(0, 0);
|
|
2527
|
-
const comparesLog = scenario.comparesLog ?? scenario.hasModelTurn;
|
|
2594
|
+
const comparesLog = scenario.comparesLog ?? (scenario.hasModelTurn && manifest.sessionFormat === void 0);
|
|
2528
2595
|
const result = await runScenario(input, {
|
|
2529
2596
|
agent,
|
|
2530
2597
|
mode: childMode,
|
|
@@ -2561,16 +2628,14 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2561
2628
|
fixtureFiles = outputFixtureFiles;
|
|
2562
2629
|
if (scenario.pinsHeader === true) {
|
|
2563
2630
|
const primary = result.sessionLogs[0];
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
expect(prompts.length, `${mode} produced no system prompt to snapshot`).toBeGreaterThan(0);
|
|
2631
|
+
const prompts = normalizedSystemPrompts(primary.content, ctx);
|
|
2632
|
+
expect(prompts.length, `${mode} produced a system prompt count that differs from 1 + expectedPromptChanges`).toBe(1 + (scenario.expectedPromptChanges ?? 0));
|
|
2567
2633
|
const promptSnapshot = formatSystemPromptSnapshot(prompts[0], prompts.slice(1));
|
|
2568
2634
|
const promptPath = join(snapshotsDir, (promptSourceByClass.get(classOf(scenario)) ?? scenario).name, SYSTEM_PROMPT_SNAPSHOT);
|
|
2569
2635
|
claimSharedSnapshot(promptClaims, promptPath, scenario.name, promptSnapshot);
|
|
2570
2636
|
await writeFile(promptPath, promptSnapshot);
|
|
2571
|
-
const schemaSets = toolSchemasFrom(
|
|
2572
|
-
expect(schemaSets.length, `${mode} produced
|
|
2573
|
-
expect(schemaSets.length, `${mode} produced a tool-schema sequence that differs from its prompt sequence`).toBe(prompts.length);
|
|
2637
|
+
const schemaSets = toolSchemasFrom(pinningHeaderPayloads(primary.content, ctx));
|
|
2638
|
+
expect(schemaSets.length, `${mode} produced a tool-schema count that differs from 1 + expectedHeaderChanges`).toBe(1 + (scenario.expectedHeaderChanges ?? 0));
|
|
2574
2639
|
const toolSchemasSnapshot = formatToolSchemasSnapshot(schemaSets[0], schemaSets.slice(1));
|
|
2575
2640
|
const schemaPath = join(snapshotsDir, (schemaSourceByClass.get(classOf(scenario)) ?? scenario).name, TOOL_SCHEMAS_SNAPSHOT);
|
|
2576
2641
|
claimSharedSnapshot(schemaClaims, schemaPath, scenario.name, toolSchemasSnapshot);
|
|
@@ -2586,7 +2651,7 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2586
2651
|
for (const index of childPromptPins) {
|
|
2587
2652
|
const log = result.sessionLogs[index];
|
|
2588
2653
|
expect(log, `${mode}: no child session log at index ${index} to snapshot a prompt from`).toBeDefined();
|
|
2589
|
-
const prompts =
|
|
2654
|
+
const prompts = normalizedSystemPrompts(log.content, ctx);
|
|
2590
2655
|
expect(prompts.length, `${mode}: child ${index} produced no system prompt to snapshot`).toBeGreaterThan(0);
|
|
2591
2656
|
await writeFile(join(dir, childSystemPromptSnapshot(index)), formatSystemPromptSnapshot(prompts[0]));
|
|
2592
2657
|
}
|
|
@@ -2622,6 +2687,7 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2622
2687
|
const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
|
|
2623
2688
|
const initialPromptSnapshot = initialSystemPromptSnapshot(promptSnapshot);
|
|
2624
2689
|
expect(pinned.length, `the pinning fixture (${pinningScenario.name}) has an unexpected request/header count`).toBe(1 + (pinningScenario.expectedHeaderChanges ?? 0));
|
|
2690
|
+
expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `the prompt source (${promptSource.name}) has an unexpected system prompt change count`).toBe(pinningScenario.expectedPromptChanges ?? 0);
|
|
2625
2691
|
const toolSchemasSnapshot = await readFile(join(snapshotsDir, schemaSource.name, TOOL_SCHEMAS_SNAPSHOT), "utf8");
|
|
2626
2692
|
const toolSchemas = parseToolSchemasSnapshot(toolSchemasSnapshot);
|
|
2627
2693
|
const pinnedSchemaSets = [toolSchemas.initial, ...toolSchemas.changes];
|
|
@@ -2636,14 +2702,18 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2636
2702
|
for (const index of childPromptPins) childPinnedPrompts.set(index, await readFile(join(dir, childSystemPromptSnapshot(index)), "utf8"));
|
|
2637
2703
|
for (const [logIndex, log] of result.sessionLogs.entries()) {
|
|
2638
2704
|
const childSchemas = childPinnedSchemas.get(logIndex);
|
|
2639
|
-
const
|
|
2705
|
+
const pinsPrimary = scenario.pinsHeader === true && logIndex === 0;
|
|
2706
|
+
const expectedChanges = pinsPrimary ? scenario.expectedHeaderChanges ?? 0 : 0;
|
|
2707
|
+
const expectedPromptChanges = pinsPrimary ? scenario.expectedPromptChanges ?? 0 : 0;
|
|
2640
2708
|
expect(headerChangeCount(log.content), `session ${log.id}: changed request/header count`).toBe(expectedChanges);
|
|
2641
|
-
const headerEvents = normalizedHeaderEvents(
|
|
2709
|
+
const headerEvents = normalizedHeaderEvents(log.content, ctx);
|
|
2642
2710
|
const headers = headerEvents.map((event) => event.header);
|
|
2643
2711
|
const prompts = normalizedSystemPrompts(log.content, ctx);
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2712
|
+
expect(normalizedToolSchemas(log.content, ctx).length, `session ${log.id}: every request/header must carry an array-valued tools field`).toBe(headers.length);
|
|
2713
|
+
if (headers.length > 0) {
|
|
2714
|
+
expect(systemPromptPrecedesRequests(log.content), `session ${log.id}: a system/message must precede the first request/header`).toBe(true);
|
|
2715
|
+
expect(prompts.length, `session ${log.id}: system/message count`).toBe(1 + expectedPromptChanges);
|
|
2716
|
+
}
|
|
2647
2717
|
if (childSchemas !== void 0) expect(childSchemas.length, `session ${log.id}: ${childToolSchemasSnapshot(logIndex)} has an unexpected tool-schema count`).toBe(1 + headerChangeCount(log.content));
|
|
2648
2718
|
let revision = 0;
|
|
2649
2719
|
for (const [k, header] of headers.entries()) {
|
|
@@ -2654,17 +2724,15 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2654
2724
|
tools: childSchemas[revision]
|
|
2655
2725
|
};
|
|
2656
2726
|
expect(header, `session ${log.id}: request/header #${k + 1} diverged from the pinned (${pinningScenario.name}) header`).toEqual(expected);
|
|
2657
|
-
if (expectedChanges === 0) {
|
|
2658
|
-
const childPrompt = childPinnedPrompts.get(logIndex);
|
|
2659
|
-
const promptOrigin = childPrompt === void 0 ? `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}` : childSystemPromptSnapshot(logIndex);
|
|
2660
|
-
expect(formatSystemPromptSnapshot(prompts[k]), `session ${log.id}: initial system prompt #${k + 1} diverged from ${promptOrigin}`).toEqual(childPrompt ?? initialPromptSnapshot);
|
|
2661
|
-
}
|
|
2662
2727
|
}
|
|
2663
|
-
if (
|
|
2664
|
-
const
|
|
2665
|
-
const
|
|
2666
|
-
const
|
|
2667
|
-
|
|
2728
|
+
if (expectedPromptChanges === 0) {
|
|
2729
|
+
const childPrompt = childPinnedPrompts.get(logIndex);
|
|
2730
|
+
const promptOrigin = childPrompt === void 0 ? `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}` : childSystemPromptSnapshot(logIndex);
|
|
2731
|
+
for (const [k, prompt] of prompts.entries()) expect(formatSystemPromptSnapshot(prompt), `session ${log.id}: system prompt #${k + 1} diverged from ${promptOrigin}`).toEqual(childPrompt ?? initialPromptSnapshot);
|
|
2732
|
+
}
|
|
2733
|
+
if (pinsPrimary) {
|
|
2734
|
+
const pinningSchemas = toolSchemasFrom(pinningHeaderPayloads(log.content, ctx));
|
|
2735
|
+
expect(formatSystemPromptSnapshot(prompts[0], prompts.slice(1)), `session ${log.id}: changed system prompts diverged from ${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}`).toEqual(promptSnapshot);
|
|
2668
2736
|
expect(formatToolSchemasSnapshot(pinningSchemas[0], pinningSchemas.slice(1)), `session ${log.id}: changed tool schemas diverged from ${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT}`).toEqual(toolSchemasSnapshot);
|
|
2669
2737
|
}
|
|
2670
2738
|
}
|
|
@@ -2729,6 +2797,8 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2729
2797
|
for (const [index, header] of headers.entries()) expect(() => restorePinnedToolSchemas(header, schemaSets[index]), `${scenario.name}: tools must use the sidecar token`).not.toThrow();
|
|
2730
2798
|
expect(promptSnapshot.length, `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must not be empty`).toBeGreaterThan(0);
|
|
2731
2799
|
expect(promptSnapshot.endsWith("\n"), `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must end in a newline`).toBe(true);
|
|
2800
|
+
expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `${promptSource.name}: prompt change count must match ${scenario.name}'s expectedPromptChanges`).toBe(scenario.expectedPromptChanges ?? 0);
|
|
2801
|
+
expect(normalizedSystemPrompts(fixture, fixtureContext(fixture)).length, `${scenario.name}: a pinning fixture must carry exactly its declared system/message count`).toBe(1 + (scenario.expectedPromptChanges ?? 0));
|
|
2732
2802
|
expect(toolSchemasSnapshot, `${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT} must use canonical JSON formatting`).toBe(formatToolSchemasSnapshot(toolSchemas.initial, toolSchemas.changes));
|
|
2733
2803
|
expect(headerChangeCount(fixture), `${scenario.name}: a pinning fixture must carry exactly its declared changed headers`).toBe(scenario.expectedHeaderChanges ?? 0);
|
|
2734
2804
|
}
|
|
@@ -2765,22 +2835,9 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2765
2835
|
}
|
|
2766
2836
|
});
|
|
2767
2837
|
it("every committed JSONL has valid tool results and canonical fixture storage", async () => {
|
|
2768
|
-
for (const scenario of scenarios)
|
|
2769
|
-
const dir = join(snapshotsDir, scenario.name);
|
|
2770
|
-
const files = await sessionFixtures(dir);
|
|
2771
|
-
for (const file of files) {
|
|
2772
|
-
const fixture = await readFile(join(dir, file), "utf8");
|
|
2773
|
-
expect(unknownToolCallIds(fixture), `${scenario.name}/${file} contains UNKNOWN_TOOL`).toEqual([]);
|
|
2774
|
-
expect(fixture, `${scenario.name}/${file} carries a non-canonical macOS cwd token`).not.toContain("/private{{cwd}}");
|
|
2775
|
-
expect(scrubSystemPrompts(fixture), `${scenario.name}/${file} carries an unscrubbed system prompt`).toEqual(fixture);
|
|
2776
|
-
expect(scrubToolSchemas(fixture), `${scenario.name}/${file} carries unscrubbed tool schemas`).toEqual(fixture);
|
|
2777
|
-
if (scenario.pinsHeader !== true) expect(scrubRequestHeaders(fixture), `${scenario.name}/${file} carries unscrubbed header content`).toEqual(fixture);
|
|
2778
|
-
}
|
|
2779
|
-
const fixtures = await Promise.all(files.map((file) => readFile(join(dir, file), "utf8")));
|
|
2780
|
-
expect(redactSessionSnapshotIds(fixtures), `${scenario.name}: identity redaction fixed point`).toEqual(fixtures);
|
|
2781
|
-
}
|
|
2838
|
+
for (const scenario of scenarios) await assertSessionFixtureStorage(join(snapshotsDir, scenario.name), scenario.name);
|
|
2782
2839
|
});
|
|
2783
2840
|
});
|
|
2784
2841
|
}
|
|
2785
2842
|
//#endregion
|
|
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,
|
|
2843
|
+
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, parseSystemPromptSnapshot, parseToolSchemasSnapshot, persistedSessionFilename, redactSessionSnapshotIds, refreshFixtureReplacements, restorePinnedToolSchemas, runScenario, scrubModelRequestBulk, scrubSessionSnapshot, scrubSystemPrompts, scrubToolSchemas, sessionFixtureFiles, sessionFixtureName, sessionFixtureNames, sessionHeaderVersion, snapshotSpillRoot, stabilizeFixtureMessageIds, stabilizeRefreshLog, systemPromptPrecedesRequests, tokenizeSessionFixtureCwd, writerSnapshotName, writesCurrentSessionFixtures };
|