@deepseek-ai/dsh-session-snapshot 0.1.3-alpha.2 → 0.1.5-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 +13 -10
- package/README.zh.md +14 -11
- package/lib/index.js +239 -176
- 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 +33 -32
- 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}}";
|
|
@@ -1343,8 +1361,9 @@ function normalizeStdout(rawStdout, ctx, options = {}) {
|
|
|
1343
1361
|
/**
|
|
1344
1362
|
* Normalize a session JSONL log into a stable expected output: the header line's
|
|
1345
1363
|
* volatile fields (`createdAt`, `id`, `cwd`) are zeroed/scrubbed; event,
|
|
1346
|
-
* historical packed-row, embedded Assistant-stream,
|
|
1347
|
-
* are zeroed; and all volatile strings are
|
|
1364
|
+
* historical packed-row, embedded Assistant-stream, goal lifecycle, and
|
|
1365
|
+
* catalog child-creation clocks are zeroed; and all volatile strings are
|
|
1366
|
+
* scrubbed. Projected inputs remain
|
|
1348
1367
|
* projected. Packed `data.dt` gaps are normalized even when the projected row
|
|
1349
1368
|
* omits its `time0` anchor.
|
|
1350
1369
|
* Output is JSONL in the same shape as the input — one compact record per
|
|
@@ -1387,12 +1406,16 @@ function normalizeSessionLog(rawLog, ctx, options = {}) {
|
|
|
1387
1406
|
if ("createdAt" in data) data.createdAt = 0;
|
|
1388
1407
|
if ("updatedAt" in data) data.updatedAt = 0;
|
|
1389
1408
|
}
|
|
1409
|
+
if (record.type === "subagent/catalog" && record.data !== null && typeof record.data === "object") {
|
|
1410
|
+
const data = record.data;
|
|
1411
|
+
if ("childCreatedAt" in data) data.childCreatedAt = 0;
|
|
1412
|
+
}
|
|
1390
1413
|
if (Object.hasOwn(record, "sourceEventSeqs")) record.sourceEventSeqs = decodeSeqRanges(record.sourceEventSeqs);
|
|
1391
1414
|
return scrubValue(record, ctx, cwdPathMode, identityMode);
|
|
1392
1415
|
}).map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
1393
1416
|
}
|
|
1394
1417
|
/**
|
|
1395
|
-
* Canonicalize projected
|
|
1418
|
+
* Canonicalize projected v3 body records. Compact streams are nested event data,
|
|
1396
1419
|
* so persistence flush boundaries cannot change the row layout.
|
|
1397
1420
|
*/
|
|
1398
1421
|
function projectSessionSnapshot(rawLog) {
|
|
@@ -1400,9 +1423,9 @@ function projectSessionSnapshot(rawLog) {
|
|
|
1400
1423
|
return [
|
|
1401
1424
|
lines.shift(),
|
|
1402
1425
|
...lines.map((line) => {
|
|
1403
|
-
const
|
|
1404
|
-
omitFixtureEnvelope(
|
|
1405
|
-
return JSON.stringify(
|
|
1426
|
+
const record = JSON.parse(line);
|
|
1427
|
+
omitFixtureEnvelope(record);
|
|
1428
|
+
return JSON.stringify(record);
|
|
1406
1429
|
}),
|
|
1407
1430
|
""
|
|
1408
1431
|
].join("\n");
|
|
@@ -1410,8 +1433,9 @@ function projectSessionSnapshot(rawLog) {
|
|
|
1410
1433
|
/**
|
|
1411
1434
|
* Normalize and project persisted session JSONL for a committed fixture.
|
|
1412
1435
|
* This composes ordinary log normalization with request-header scrubbing and
|
|
1413
|
-
* persistence-envelope projection, then writes the
|
|
1414
|
-
* one record per event, independent of persistence flush boundaries.
|
|
1436
|
+
* persistence-envelope projection, then writes the v3 logical event stream as
|
|
1437
|
+
* one record per event, independent of persistence flush boundaries. Event order
|
|
1438
|
+
* and source-event references are preserved.
|
|
1415
1439
|
*
|
|
1416
1440
|
* @param rawLog - persisted or already-projected session JSONL.
|
|
1417
1441
|
* @param ctx - the run's volatile values to scrub.
|
|
@@ -1438,51 +1462,20 @@ function normalizeSessionSnapshots(rawLogs, ctx, options = {}) {
|
|
|
1438
1462
|
}))));
|
|
1439
1463
|
}
|
|
1440
1464
|
/**
|
|
1441
|
-
* Omit
|
|
1465
|
+
* Omit the artifact header generation after official migration for comparison.
|
|
1466
|
+
* Delivery and captured-source generations retain their opaque recorded values.
|
|
1442
1467
|
* @param rawLog - Session records or events as compact JSON lines.
|
|
1443
|
-
* @returns the same records
|
|
1468
|
+
* @returns the same records with only the Session header version omitted.
|
|
1444
1469
|
*/
|
|
1445
1470
|
function normalizeSessionFormatProvenance(rawLog) {
|
|
1446
1471
|
return rawLog.split("\n").map((line) => {
|
|
1447
1472
|
if (line.trim().length === 0) return line;
|
|
1448
1473
|
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;
|
|
1474
|
+
if (record.type !== "session" || !Object.hasOwn(record, "version")) return line;
|
|
1475
|
+
delete record.version;
|
|
1476
|
+
return JSON.stringify(record);
|
|
1463
1477
|
}).join("\n");
|
|
1464
1478
|
}
|
|
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
1479
|
/** Whether a fixture declares a released Session format and therefore participates in migration burn-in. */
|
|
1487
1480
|
function hasSessionFormatVersion(rawLog) {
|
|
1488
1481
|
const firstLine = rawLog.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
@@ -1492,60 +1485,58 @@ function hasSessionFormatVersion(rawLog) {
|
|
|
1492
1485
|
return Object.hasOwn(header, "version");
|
|
1493
1486
|
}
|
|
1494
1487
|
/**
|
|
1495
|
-
* Replace
|
|
1496
|
-
*
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1499
|
-
*
|
|
1500
|
-
* idempotent.
|
|
1488
|
+
* Replace the rendered prompt text of every `system/message` event with the
|
|
1489
|
+
* `{{system}}` token. The text block keeps its position and type, so the
|
|
1490
|
+
* fixture still shows one system node per prompt version; an empty `content`
|
|
1491
|
+
* (no system prompt) stays empty. Request headers and every other line pass
|
|
1492
|
+
* through byte-for-byte; the transform is idempotent.
|
|
1501
1493
|
*
|
|
1502
1494
|
* @param rawLog The raw session `.jsonl` content.
|
|
1503
|
-
* @returns The JSONL with system-prompt
|
|
1495
|
+
* @returns The JSONL with system-prompt text tokenized.
|
|
1504
1496
|
*/
|
|
1505
1497
|
function scrubSystemPrompts(rawLog) {
|
|
1506
|
-
return
|
|
1498
|
+
return scrubModelRequestContent(rawLog, { system: true });
|
|
1507
1499
|
}
|
|
1508
1500
|
/**
|
|
1509
1501
|
* Replace tool schemas in full request-header snapshots with `{{tools}}`
|
|
1510
|
-
* tokens while retaining field presence. System
|
|
1511
|
-
*
|
|
1512
|
-
*
|
|
1513
|
-
*
|
|
1502
|
+
* tokens while retaining field presence. System-prompt text stays verbatim so
|
|
1503
|
+
* pinning fixtures can move only schema bulk into their dedicated JSON
|
|
1504
|
+
* sidecar. Lines without a tool payload pass through byte-for-byte; the
|
|
1505
|
+
* transform is idempotent.
|
|
1514
1506
|
*
|
|
1515
1507
|
* @param rawLog The raw session `.jsonl` content.
|
|
1516
1508
|
* @returns The JSONL with tool-schema content tokenized.
|
|
1517
1509
|
*/
|
|
1518
1510
|
function scrubToolSchemas(rawLog) {
|
|
1519
|
-
return
|
|
1511
|
+
return scrubModelRequestContent(rawLog, { tools: true });
|
|
1520
1512
|
}
|
|
1521
1513
|
/**
|
|
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.
|
|
1514
|
+
* Replace all bulky model-request content in a session JSONL with stable
|
|
1515
|
+
* tokens: the `system/message` prompt text handled by
|
|
1516
|
+
* {@link scrubSystemPrompts} and the request-header tool schemas handled by
|
|
1517
|
+
* {@link scrubToolSchemas}. Field presence, config, and reason are kept.
|
|
1518
|
+
* Lines without content to scrub pass through byte-for-byte, and the
|
|
1519
|
+
* transform is idempotent.
|
|
1528
1520
|
*
|
|
1529
1521
|
* @param rawLog The raw session `.jsonl` content.
|
|
1530
|
-
* @returns The JSONL with
|
|
1522
|
+
* @returns The JSONL with prompt text and schema bulk tokenized, other lines byte-identical.
|
|
1531
1523
|
*/
|
|
1532
|
-
function
|
|
1533
|
-
return
|
|
1524
|
+
function scrubModelRequestBulk(rawLog) {
|
|
1525
|
+
return scrubModelRequestContent(rawLog, {
|
|
1534
1526
|
system: true,
|
|
1535
1527
|
tools: true
|
|
1536
1528
|
});
|
|
1537
1529
|
}
|
|
1538
1530
|
/**
|
|
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.
|
|
1531
|
+
* Project a persisted session log while tokenizing prompt text and schema
|
|
1532
|
+
* bulk. Each non-empty line is parsed at most once; the session header stays
|
|
1533
|
+
* byte-identical. Body records omit their persistence-only envelopes.
|
|
1543
1534
|
*
|
|
1544
1535
|
* @param rawLog - persisted or already-projected session JSONL.
|
|
1545
|
-
* @returns committed snapshot JSONL with
|
|
1536
|
+
* @returns committed snapshot JSONL with prompt text and tool schemas tokenized.
|
|
1546
1537
|
*/
|
|
1547
1538
|
function scrubSessionSnapshot(rawLog) {
|
|
1548
|
-
const scrubbed =
|
|
1539
|
+
const scrubbed = scrubModelRequestBulk(rawLog);
|
|
1549
1540
|
let recordIndex = 0;
|
|
1550
1541
|
return scrubbed.split("\n").map((line) => {
|
|
1551
1542
|
if (line.trim().length === 0) return line;
|
|
@@ -1568,26 +1559,31 @@ function normalizeFeedbackClocks(record) {
|
|
|
1568
1559
|
if ("createdAt" in clocks) clocks.createdAt = 0;
|
|
1569
1560
|
if ("updatedAt" in clocks) clocks.updatedAt = 0;
|
|
1570
1561
|
}
|
|
1571
|
-
/**
|
|
1572
|
-
function
|
|
1562
|
+
/** Return the first text block of a `system/message` payload, or `undefined` when it carries none. */
|
|
1563
|
+
function systemPromptBlock(data) {
|
|
1564
|
+
const message = data.message;
|
|
1565
|
+
if (message === null || typeof message !== "object" || !Array.isArray(message.content)) return void 0;
|
|
1566
|
+
const block = message.content[0];
|
|
1567
|
+
return block !== null && typeof block === "object" && typeof block.text === "string" ? block : void 0;
|
|
1568
|
+
}
|
|
1569
|
+
/** Transform the selected model-request payloads. */
|
|
1570
|
+
function scrubModelRequestContent(rawLog, options) {
|
|
1573
1571
|
return rawLog.split("\n").map((line) => {
|
|
1574
1572
|
if (line.trim().length === 0) return line;
|
|
1575
1573
|
const record = JSON.parse(line);
|
|
1576
1574
|
const data = record.data;
|
|
1577
1575
|
if (data === null || typeof data !== "object") return line;
|
|
1578
|
-
if (record.type === "
|
|
1576
|
+
if (options.system === true && record.type === "system/message") {
|
|
1577
|
+
const block = systemPromptBlock(data);
|
|
1578
|
+
if (block === void 0) return line;
|
|
1579
|
+
block.text = SYSTEM;
|
|
1580
|
+
return JSON.stringify(record);
|
|
1581
|
+
}
|
|
1582
|
+
if (options.tools === true && record.type === "request/header") {
|
|
1579
1583
|
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;
|
|
1584
|
+
if (header === null || typeof header !== "object" || !("tools" in header)) return line;
|
|
1585
|
+
header.tools = TOOLS;
|
|
1586
|
+
return JSON.stringify(record);
|
|
1591
1587
|
}
|
|
1592
1588
|
return line;
|
|
1593
1589
|
}).join("\n");
|
|
@@ -1698,10 +1694,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1698
1694
|
"toolSchemasSource",
|
|
1699
1695
|
"childSystemPrompts",
|
|
1700
1696
|
"childToolSchemas",
|
|
1701
|
-
"changes"
|
|
1697
|
+
"changes",
|
|
1698
|
+
"promptChanges"
|
|
1702
1699
|
], "manifest.header");
|
|
1703
1700
|
if (value.pin !== void 0 && value.pin !== true) throw new Error("manifest.header.pin must equal true when present");
|
|
1704
|
-
if (value
|
|
1701
|
+
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
1702
|
header = {
|
|
1706
1703
|
class: name(value.class, "manifest.header.class"),
|
|
1707
1704
|
...value.pin === true ? { pin: true } : {},
|
|
@@ -1709,7 +1706,8 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1709
1706
|
...value.toolSchemasSource === void 0 ? {} : { toolSchemasSource: scenarioSource(value.toolSchemasSource, "manifest.header.toolSchemasSource") },
|
|
1710
1707
|
...value.childSystemPrompts === void 0 ? {} : { childSystemPrompts: positiveIndexes(value.childSystemPrompts, "manifest.header.childSystemPrompts") },
|
|
1711
1708
|
...value.childToolSchemas === void 0 ? {} : { childToolSchemas: positiveIndexes(value.childToolSchemas, "manifest.header.childToolSchemas") },
|
|
1712
|
-
...value.changes === void 0 ? {} : { changes: Number(value.changes) }
|
|
1709
|
+
...value.changes === void 0 ? {} : { changes: Number(value.changes) },
|
|
1710
|
+
...value.promptChanges === void 0 ? {} : { promptChanges: Number(value.promptChanges) }
|
|
1713
1711
|
};
|
|
1714
1712
|
}
|
|
1715
1713
|
let replay;
|
|
@@ -1744,11 +1742,11 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1744
1742
|
"parent"
|
|
1745
1743
|
], "manifest.workspace");
|
|
1746
1744
|
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 !== "
|
|
1745
|
+
if (value.parent !== void 0 && value.parent !== "outside-temp") throw new Error("manifest.workspace.parent must equal outside-temp");
|
|
1748
1746
|
workspace = {
|
|
1749
1747
|
...value.setup === void 0 ? {} : { setup: name(value.setup, "manifest.workspace.setup") },
|
|
1750
1748
|
...value.final === true ? { final: true } : {},
|
|
1751
|
-
...value.parent === "
|
|
1749
|
+
...value.parent === "outside-temp" ? { parent: "outside-temp" } : {}
|
|
1752
1750
|
};
|
|
1753
1751
|
if (Object.keys(workspace).length === 0) throw new Error("manifest.workspace must not be empty");
|
|
1754
1752
|
}
|
|
@@ -1838,9 +1836,10 @@ function parseSnapshotManifest(source, path = "snapshot.yml") {
|
|
|
1838
1836
|
* refresh stay serial while writing.
|
|
1839
1837
|
*
|
|
1840
1838
|
* Exactly one scenario per header-composition class pins the tokenized header
|
|
1841
|
-
* sequence
|
|
1842
|
-
* sidecars, each of which may be
|
|
1843
|
-
*
|
|
1839
|
+
* sequence and the tokenized `system/message` sequence. Its prompt and
|
|
1840
|
+
* tool-schema sequences live in independent sidecars, each of which may be
|
|
1841
|
+
* shared with another class pin when the bytes are identical. Every live
|
|
1842
|
+
* header and system prompt is checked against the composed pin, so
|
|
1844
1843
|
* session-dependent composition must declare a separate class instead of
|
|
1845
1844
|
* escaping coverage.
|
|
1846
1845
|
* @module @deepseek-ai/dsh-session-snapshot/suite
|
|
@@ -1965,10 +1964,13 @@ function fixtureContext(fixture) {
|
|
|
1965
1964
|
}
|
|
1966
1965
|
/** Normalize request-header payloads while retaining the reason that selects a pin revision. */
|
|
1967
1966
|
function normalizedHeaderEvents(rawLog, ctx) {
|
|
1968
|
-
return normalizeSessionLog(rawLog, ctx)
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1967
|
+
return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).filter((record) => record.type === "request/header").map((record) => {
|
|
1968
|
+
const data = record.data;
|
|
1969
|
+
return {
|
|
1970
|
+
header: data?.header,
|
|
1971
|
+
reason: data?.reason
|
|
1972
|
+
};
|
|
1973
|
+
});
|
|
1972
1974
|
}
|
|
1973
1975
|
/**
|
|
1974
1976
|
* Header revisions that own sidecar content. `series` reuses the current revision, while
|
|
@@ -1979,13 +1981,17 @@ function normalizedHeaderEvents(rawLog, ctx) {
|
|
|
1979
1981
|
function pinningHeaderPayloads(rawLog, ctx) {
|
|
1980
1982
|
return normalizedHeaderEvents(rawLog, ctx).filter((event) => event.reason !== "series").map((event) => event.header);
|
|
1981
1983
|
}
|
|
1982
|
-
/**
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1984
|
+
/**
|
|
1985
|
+
* The rendered prompt text of one parsed `system/message` record: its single
|
|
1986
|
+
* text block, or `''` when the empty `content` records "no system prompt".
|
|
1987
|
+
* Any other record yields `undefined`.
|
|
1988
|
+
*/
|
|
1989
|
+
function systemPromptOf(record) {
|
|
1990
|
+
if (record.type !== "system/message") return void 0;
|
|
1991
|
+
const content = record.data?.message?.content;
|
|
1992
|
+
if (!Array.isArray(content)) return void 0;
|
|
1993
|
+
const block = content[0];
|
|
1994
|
+
return content.length === 0 ? "" : typeof block?.text === "string" ? block.text : void 0;
|
|
1989
1995
|
}
|
|
1990
1996
|
/** Extract every array-valued tool catalog from a normalized header sequence. */
|
|
1991
1997
|
function toolSchemasFrom(headers) {
|
|
@@ -1999,8 +2005,7 @@ function toolSchemasFrom(headers) {
|
|
|
1999
2005
|
* The `data.header` payload of every `request/header` event in a session
|
|
2000
2006
|
* JSONL, in log order, with the log's volatile values scrubbed first
|
|
2001
2007
|
* ({@link normalizeSessionLog}) so headers harvested from different runs —
|
|
2002
|
-
* each embedding its own generated cwd
|
|
2003
|
-
* footing.
|
|
2008
|
+
* each embedding its own generated cwd — compare on equal footing.
|
|
2004
2009
|
*
|
|
2005
2010
|
* @param rawLog The session `.jsonl` content to extract headers from.
|
|
2006
2011
|
* @param ctx The volatile values of the run that produced it.
|
|
@@ -2010,16 +2015,35 @@ function normalizedHeaders(rawLog, ctx) {
|
|
|
2010
2015
|
return normalizedHeaderEvents(rawLog, ctx).map((event) => event.header);
|
|
2011
2016
|
}
|
|
2012
2017
|
/**
|
|
2013
|
-
* The normalized
|
|
2014
|
-
*
|
|
2015
|
-
*
|
|
2018
|
+
* The normalized prompt text of every `system/message` event in a session
|
|
2019
|
+
* JSONL, in log order: the first is the initial system prompt (surface node 0)
|
|
2020
|
+
* and each later one replaced it or, on an in-history route, appended the
|
|
2021
|
+
* changed prompt after the cached history. An empty `content` yields `''`; a
|
|
2022
|
+
* `system/message` without a text block is omitted.
|
|
2016
2023
|
*
|
|
2017
2024
|
* @param rawLog The session `.jsonl` content to inspect.
|
|
2018
2025
|
* @param ctx The volatile values of the run that produced it.
|
|
2019
|
-
* @returns The normalized system prompts, in
|
|
2026
|
+
* @returns The normalized system prompts, in log order.
|
|
2020
2027
|
*/
|
|
2021
2028
|
function normalizedSystemPrompts(rawLog, ctx) {
|
|
2022
|
-
return
|
|
2029
|
+
return parseJsonlRecords(normalizeSessionLog(rawLog, ctx)).flatMap((record) => {
|
|
2030
|
+
const prompt = systemPromptOf(record);
|
|
2031
|
+
return prompt === void 0 ? [] : [prompt];
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Whether every model request in a session JSONL is preceded by its system
|
|
2036
|
+
* prompt: the log has no `request/header`, or a `system/message` event comes
|
|
2037
|
+
* before its first `request/header`.
|
|
2038
|
+
*
|
|
2039
|
+
* @param rawLog The session `.jsonl` content to inspect.
|
|
2040
|
+
* @returns True when the first `request/header` (if any) follows a `system/message`.
|
|
2041
|
+
*/
|
|
2042
|
+
function systemPromptPrecedesRequests(rawLog) {
|
|
2043
|
+
const types = parseJsonlRecords(rawLog).map((record) => record.type);
|
|
2044
|
+
const firstHeader = types.indexOf("request/header");
|
|
2045
|
+
const firstPrompt = types.indexOf("system/message");
|
|
2046
|
+
return firstHeader < 0 || firstPrompt >= 0 && firstPrompt < firstHeader;
|
|
2023
2047
|
}
|
|
2024
2048
|
/**
|
|
2025
2049
|
* The normalized tool-schema arrays carried by request headers in a session
|
|
@@ -2077,24 +2101,40 @@ function restorePinnedToolSchemas(header, schemas) {
|
|
|
2077
2101
|
tools: schemas
|
|
2078
2102
|
};
|
|
2079
2103
|
}
|
|
2104
|
+
/** Marker line separating one replaced prompt from the previous one in a prompt sidecar. */
|
|
2105
|
+
const SYSTEM_PROMPT_CHANGE_MARKER = "\n<!-- system/message change ";
|
|
2080
2106
|
/**
|
|
2081
2107
|
* Render a normalized prompt as a repository-friendly Markdown snapshot.
|
|
2082
2108
|
* Prompt text is unchanged except that a missing terminal newline is added so
|
|
2083
2109
|
* the committed file follows the repository newline contract.
|
|
2084
2110
|
*
|
|
2085
|
-
* @param prompt The normalized system prompt.
|
|
2086
|
-
* @param changes Full normalized prompts from later
|
|
2111
|
+
* @param prompt The normalized initial system prompt (surface node 0).
|
|
2112
|
+
* @param changes Full normalized prompts from later `system/message` events, replacements or in-history appends.
|
|
2087
2113
|
* @returns Markdown snapshot text ending in a newline.
|
|
2088
2114
|
*/
|
|
2089
2115
|
function formatSystemPromptSnapshot(prompt, changes = []) {
|
|
2090
2116
|
let snapshot = prompt.endsWith("\n") ? prompt : `${prompt}\n`;
|
|
2091
2117
|
for (const [index, change] of changes.entries()) {
|
|
2092
|
-
snapshot +=
|
|
2118
|
+
snapshot += `${SYSTEM_PROMPT_CHANGE_MARKER}${index + 1} -->\n\n`;
|
|
2093
2119
|
snapshot += change.endsWith("\n") ? change : `${change}\n`;
|
|
2094
2120
|
}
|
|
2095
2121
|
return snapshot;
|
|
2096
2122
|
}
|
|
2097
2123
|
/**
|
|
2124
|
+
* Split a prompt sidecar into its initial prompt and each later change, the
|
|
2125
|
+
* inverse of {@link formatSystemPromptSnapshot}.
|
|
2126
|
+
*
|
|
2127
|
+
* @param snapshot The Markdown sidecar text.
|
|
2128
|
+
* @returns The initial prompt snapshot plus one entry per later `system/message`.
|
|
2129
|
+
*/
|
|
2130
|
+
function parseSystemPromptSnapshot(snapshot) {
|
|
2131
|
+
const parts = snapshot.split(/\n<!-- system\/message change [1-9]\d* -->\n\n/);
|
|
2132
|
+
return {
|
|
2133
|
+
initial: parts[0],
|
|
2134
|
+
changes: parts.slice(1)
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2098
2138
|
* Reject a child prompt sidecar that cannot own distinct, canonical prompt text.
|
|
2099
2139
|
* @param sidecar - committed child prompt snapshot.
|
|
2100
2140
|
* @param classPin - initial prompt snapshot owned by the scenario's header class.
|
|
@@ -2105,10 +2145,9 @@ function assertChildSystemPromptSnapshot(sidecar, classPin, label) {
|
|
|
2105
2145
|
if (!sidecar.endsWith("\n")) throw new Error(`${label} must end in a newline`);
|
|
2106
2146
|
if (sidecar === classPin) throw new Error(`${label} must differ from its class pin`);
|
|
2107
2147
|
}
|
|
2108
|
-
/** Return the initial-prompt portion of a possibly multi-
|
|
2148
|
+
/** Return the initial-prompt portion of a possibly multi-prompt snapshot. */
|
|
2109
2149
|
function initialSystemPromptSnapshot(snapshot) {
|
|
2110
|
-
|
|
2111
|
-
return marker < 0 ? snapshot : snapshot.slice(0, marker);
|
|
2150
|
+
return parseSystemPromptSnapshot(snapshot).initial;
|
|
2112
2151
|
}
|
|
2113
2152
|
/**
|
|
2114
2153
|
* Count changed `request/header` snapshots in a session JSONL.
|
|
@@ -2141,6 +2180,7 @@ function surfaceEventMessage(record) {
|
|
|
2141
2180
|
case "user/message":
|
|
2142
2181
|
message = data;
|
|
2143
2182
|
break;
|
|
2183
|
+
case "system/message":
|
|
2144
2184
|
case "assistant/message":
|
|
2145
2185
|
case "tool/result":
|
|
2146
2186
|
message = data.message;
|
|
@@ -2458,11 +2498,34 @@ function stabilizeRefreshLog(fresh, existing, replacements, freshContext) {
|
|
|
2458
2498
|
return records.map((record) => JSON.stringify(record)).join("\n") + "\n";
|
|
2459
2499
|
}
|
|
2460
2500
|
/**
|
|
2501
|
+
* Check every selected role for tool/path defects and current generations for canonical prompt/identity storage.
|
|
2502
|
+
* Historical roles retain their released bytes, including roles retired by the current writer.
|
|
2503
|
+
* @param dir Scenario directory containing canonical Session fixtures.
|
|
2504
|
+
* @param scenarioName Scenario name used in failure diagnostics.
|
|
2505
|
+
* @returns Resolves when all selected fixtures satisfy the storage checks.
|
|
2506
|
+
*/
|
|
2507
|
+
async function assertSessionFixtureStorage(dir, scenarioName) {
|
|
2508
|
+
const files = await sessionFixtures(dir);
|
|
2509
|
+
const currentFixtures = [];
|
|
2510
|
+
for (const file of files) {
|
|
2511
|
+
const fixture = await readFile(join(dir, file), "utf8");
|
|
2512
|
+
const version = assertSessionFixtureVersion(file, fixture);
|
|
2513
|
+
expect(unknownToolCallIds(fixture), `${scenarioName}/${file} contains UNKNOWN_TOOL`).toEqual([]);
|
|
2514
|
+
expect(fixture, `${scenarioName}/${file} carries a non-canonical macOS cwd token`).not.toContain("/private{{cwd}}");
|
|
2515
|
+
if (version !== SESSION_FORMAT_VERSION) continue;
|
|
2516
|
+
currentFixtures.push(fixture);
|
|
2517
|
+
expect(scrubSystemPrompts(fixture), `${scenarioName}/${file} carries an unscrubbed system prompt`).toEqual(fixture);
|
|
2518
|
+
expect(scrubToolSchemas(fixture), `${scenarioName}/${file} carries unscrubbed tool schemas`).toEqual(fixture);
|
|
2519
|
+
expect(systemPromptPrecedesRequests(fixture), `${scenarioName}/${file} has a request/header with no preceding system/message`).toBe(true);
|
|
2520
|
+
}
|
|
2521
|
+
expect(redactSessionSnapshotIds(currentFixtures), `${scenarioName}: identity redaction fixed point`).toEqual(currentFixtures);
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2461
2524
|
* Register the suite: one test per scenario (the expected-output and log comparisons and
|
|
2462
|
-
* the header
|
|
2525
|
+
* the header and prompt uniformity guard) plus the fixture guard block (no orphan
|
|
2463
2526
|
* scenario dirs, required files present, exactly one pin per header class,
|
|
2464
|
-
* shared sidecars unique and well-formed, every JSONL prompt-
|
|
2465
|
-
*
|
|
2527
|
+
* shared sidecars unique and well-formed, every JSONL prompt- and
|
|
2528
|
+
* schema-scrubbed with a `system/message` before its first request). Must
|
|
2466
2529
|
* run at vitest collection time — it calls `describe`/`it`. Throws
|
|
2467
2530
|
* immediately if any header class lacks a pinning scenario or carries two
|
|
2468
2531
|
* (the uniformity guard needs exactly one comparison anchor per class).
|
|
@@ -2481,7 +2544,12 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2481
2544
|
for (const scenario of scenarios) {
|
|
2482
2545
|
if (scenariosByName.has(scenario.name)) throw new Error(`acp-snapshot: duplicate scenario name "${scenario.name}"`);
|
|
2483
2546
|
scenariosByName.set(scenario.name, scenario);
|
|
2484
|
-
for (const field of [
|
|
2547
|
+
for (const field of [
|
|
2548
|
+
"systemPromptSource",
|
|
2549
|
+
"toolSchemasSource",
|
|
2550
|
+
"expectedHeaderChanges",
|
|
2551
|
+
"expectedPromptChanges"
|
|
2552
|
+
]) 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
2553
|
}
|
|
2486
2554
|
/** Each header class's single pinning scenario. Guarded here (and by meta-tests) so a pin cannot silently vanish or split. */
|
|
2487
2555
|
const pinningByClass = /* @__PURE__ */ new Map();
|
|
@@ -2493,14 +2561,19 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2493
2561
|
pinningByClass.set(cls, scenario);
|
|
2494
2562
|
}
|
|
2495
2563
|
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})`);
|
|
2564
|
+
/**
|
|
2565
|
+
* Resolve one pin's sidecar source. A prompt sidecar spans `1 + expectedPromptChanges`
|
|
2566
|
+
* prompts and a schema sidecar spans `1 + expectedHeaderChanges` schema sets, so a shared
|
|
2567
|
+
* source must declare the count that sizes the sidecar it lends.
|
|
2568
|
+
*/
|
|
2496
2569
|
const sourceFor = (pinningScenario, field, label) => {
|
|
2497
2570
|
const sourceName = pinningScenario[field] ?? pinningScenario.name;
|
|
2498
2571
|
const source = scenariosByName.get(sourceName);
|
|
2499
2572
|
if (source === void 0) throw new Error(`acp-snapshot: ${pinningScenario.name} names unknown ${label} source "${sourceName}"`);
|
|
2500
2573
|
if (source.pinsHeader !== true) throw new Error(`acp-snapshot: ${pinningScenario.name} names non-pinning ${label} source "${sourceName}"`);
|
|
2501
2574
|
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
|
|
2575
|
+
const countField = field === "systemPromptSource" ? "expectedPromptChanges" : "expectedHeaderChanges";
|
|
2576
|
+
if ((source[countField] ?? 0) !== (pinningScenario[countField] ?? 0)) throw new Error(`acp-snapshot: ${pinningScenario.name} and ${sourceName} declare different ${countField} counts for shared ${label}`);
|
|
2504
2577
|
return source;
|
|
2505
2578
|
};
|
|
2506
2579
|
const promptSourceByClass = /* @__PURE__ */ new Map();
|
|
@@ -2524,7 +2597,7 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2524
2597
|
let fixtureFiles = RECORDING ? [] : await sessionFixtures(dir);
|
|
2525
2598
|
const childFixtureFiles = fixtureFiles.slice(1);
|
|
2526
2599
|
const primaryFixtureFile = fixtureFiles[0] ?? sessionFixtureName(0, 0);
|
|
2527
|
-
const comparesLog = scenario.comparesLog ?? scenario.hasModelTurn;
|
|
2600
|
+
const comparesLog = scenario.comparesLog ?? (scenario.hasModelTurn && manifest.sessionFormat === void 0);
|
|
2528
2601
|
const result = await runScenario(input, {
|
|
2529
2602
|
agent,
|
|
2530
2603
|
mode: childMode,
|
|
@@ -2561,16 +2634,14 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2561
2634
|
fixtureFiles = outputFixtureFiles;
|
|
2562
2635
|
if (scenario.pinsHeader === true) {
|
|
2563
2636
|
const primary = result.sessionLogs[0];
|
|
2564
|
-
const
|
|
2565
|
-
|
|
2566
|
-
expect(prompts.length, `${mode} produced no system prompt to snapshot`).toBeGreaterThan(0);
|
|
2637
|
+
const prompts = normalizedSystemPrompts(primary.content, ctx);
|
|
2638
|
+
expect(prompts.length, `${mode} produced a system prompt count that differs from 1 + expectedPromptChanges`).toBe(1 + (scenario.expectedPromptChanges ?? 0));
|
|
2567
2639
|
const promptSnapshot = formatSystemPromptSnapshot(prompts[0], prompts.slice(1));
|
|
2568
2640
|
const promptPath = join(snapshotsDir, (promptSourceByClass.get(classOf(scenario)) ?? scenario).name, SYSTEM_PROMPT_SNAPSHOT);
|
|
2569
2641
|
claimSharedSnapshot(promptClaims, promptPath, scenario.name, promptSnapshot);
|
|
2570
2642
|
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);
|
|
2643
|
+
const schemaSets = toolSchemasFrom(pinningHeaderPayloads(primary.content, ctx));
|
|
2644
|
+
expect(schemaSets.length, `${mode} produced a tool-schema count that differs from 1 + expectedHeaderChanges`).toBe(1 + (scenario.expectedHeaderChanges ?? 0));
|
|
2574
2645
|
const toolSchemasSnapshot = formatToolSchemasSnapshot(schemaSets[0], schemaSets.slice(1));
|
|
2575
2646
|
const schemaPath = join(snapshotsDir, (schemaSourceByClass.get(classOf(scenario)) ?? scenario).name, TOOL_SCHEMAS_SNAPSHOT);
|
|
2576
2647
|
claimSharedSnapshot(schemaClaims, schemaPath, scenario.name, toolSchemasSnapshot);
|
|
@@ -2586,7 +2657,7 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2586
2657
|
for (const index of childPromptPins) {
|
|
2587
2658
|
const log = result.sessionLogs[index];
|
|
2588
2659
|
expect(log, `${mode}: no child session log at index ${index} to snapshot a prompt from`).toBeDefined();
|
|
2589
|
-
const prompts =
|
|
2660
|
+
const prompts = normalizedSystemPrompts(log.content, ctx);
|
|
2590
2661
|
expect(prompts.length, `${mode}: child ${index} produced no system prompt to snapshot`).toBeGreaterThan(0);
|
|
2591
2662
|
await writeFile(join(dir, childSystemPromptSnapshot(index)), formatSystemPromptSnapshot(prompts[0]));
|
|
2592
2663
|
}
|
|
@@ -2622,6 +2693,7 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2622
2693
|
const promptSnapshot = await readFile(join(snapshotsDir, promptSource.name, SYSTEM_PROMPT_SNAPSHOT), "utf8");
|
|
2623
2694
|
const initialPromptSnapshot = initialSystemPromptSnapshot(promptSnapshot);
|
|
2624
2695
|
expect(pinned.length, `the pinning fixture (${pinningScenario.name}) has an unexpected request/header count`).toBe(1 + (pinningScenario.expectedHeaderChanges ?? 0));
|
|
2696
|
+
expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `the prompt source (${promptSource.name}) has an unexpected system prompt change count`).toBe(pinningScenario.expectedPromptChanges ?? 0);
|
|
2625
2697
|
const toolSchemasSnapshot = await readFile(join(snapshotsDir, schemaSource.name, TOOL_SCHEMAS_SNAPSHOT), "utf8");
|
|
2626
2698
|
const toolSchemas = parseToolSchemasSnapshot(toolSchemasSnapshot);
|
|
2627
2699
|
const pinnedSchemaSets = [toolSchemas.initial, ...toolSchemas.changes];
|
|
@@ -2636,14 +2708,18 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2636
2708
|
for (const index of childPromptPins) childPinnedPrompts.set(index, await readFile(join(dir, childSystemPromptSnapshot(index)), "utf8"));
|
|
2637
2709
|
for (const [logIndex, log] of result.sessionLogs.entries()) {
|
|
2638
2710
|
const childSchemas = childPinnedSchemas.get(logIndex);
|
|
2639
|
-
const
|
|
2711
|
+
const pinsPrimary = scenario.pinsHeader === true && logIndex === 0;
|
|
2712
|
+
const expectedChanges = pinsPrimary ? scenario.expectedHeaderChanges ?? 0 : 0;
|
|
2713
|
+
const expectedPromptChanges = pinsPrimary ? scenario.expectedPromptChanges ?? 0 : 0;
|
|
2640
2714
|
expect(headerChangeCount(log.content), `session ${log.id}: changed request/header count`).toBe(expectedChanges);
|
|
2641
|
-
const headerEvents = normalizedHeaderEvents(
|
|
2715
|
+
const headerEvents = normalizedHeaderEvents(log.content, ctx);
|
|
2642
2716
|
const headers = headerEvents.map((event) => event.header);
|
|
2643
2717
|
const prompts = normalizedSystemPrompts(log.content, ctx);
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2718
|
+
expect(normalizedToolSchemas(log.content, ctx).length, `session ${log.id}: every request/header must carry an array-valued tools field`).toBe(headers.length);
|
|
2719
|
+
if (headers.length > 0) {
|
|
2720
|
+
expect(systemPromptPrecedesRequests(log.content), `session ${log.id}: a system/message must precede the first request/header`).toBe(true);
|
|
2721
|
+
expect(prompts.length, `session ${log.id}: system/message count`).toBe(1 + expectedPromptChanges);
|
|
2722
|
+
}
|
|
2647
2723
|
if (childSchemas !== void 0) expect(childSchemas.length, `session ${log.id}: ${childToolSchemasSnapshot(logIndex)} has an unexpected tool-schema count`).toBe(1 + headerChangeCount(log.content));
|
|
2648
2724
|
let revision = 0;
|
|
2649
2725
|
for (const [k, header] of headers.entries()) {
|
|
@@ -2654,17 +2730,15 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2654
2730
|
tools: childSchemas[revision]
|
|
2655
2731
|
};
|
|
2656
2732
|
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
2733
|
}
|
|
2663
|
-
if (
|
|
2664
|
-
const
|
|
2665
|
-
const
|
|
2666
|
-
const
|
|
2667
|
-
|
|
2734
|
+
if (expectedPromptChanges === 0) {
|
|
2735
|
+
const childPrompt = childPinnedPrompts.get(logIndex);
|
|
2736
|
+
const promptOrigin = childPrompt === void 0 ? `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}` : childSystemPromptSnapshot(logIndex);
|
|
2737
|
+
for (const [k, prompt] of prompts.entries()) expect(formatSystemPromptSnapshot(prompt), `session ${log.id}: system prompt #${k + 1} diverged from ${promptOrigin}`).toEqual(childPrompt ?? initialPromptSnapshot);
|
|
2738
|
+
}
|
|
2739
|
+
if (pinsPrimary) {
|
|
2740
|
+
const pinningSchemas = toolSchemasFrom(pinningHeaderPayloads(log.content, ctx));
|
|
2741
|
+
expect(formatSystemPromptSnapshot(prompts[0], prompts.slice(1)), `session ${log.id}: changed system prompts diverged from ${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT}`).toEqual(promptSnapshot);
|
|
2668
2742
|
expect(formatToolSchemasSnapshot(pinningSchemas[0], pinningSchemas.slice(1)), `session ${log.id}: changed tool schemas diverged from ${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT}`).toEqual(toolSchemasSnapshot);
|
|
2669
2743
|
}
|
|
2670
2744
|
}
|
|
@@ -2729,6 +2803,8 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2729
2803
|
for (const [index, header] of headers.entries()) expect(() => restorePinnedToolSchemas(header, schemaSets[index]), `${scenario.name}: tools must use the sidecar token`).not.toThrow();
|
|
2730
2804
|
expect(promptSnapshot.length, `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must not be empty`).toBeGreaterThan(0);
|
|
2731
2805
|
expect(promptSnapshot.endsWith("\n"), `${promptSource.name}/${SYSTEM_PROMPT_SNAPSHOT} must end in a newline`).toBe(true);
|
|
2806
|
+
expect(parseSystemPromptSnapshot(promptSnapshot).changes.length, `${promptSource.name}: prompt change count must match ${scenario.name}'s expectedPromptChanges`).toBe(scenario.expectedPromptChanges ?? 0);
|
|
2807
|
+
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
2808
|
expect(toolSchemasSnapshot, `${schemaSource.name}/${TOOL_SCHEMAS_SNAPSHOT} must use canonical JSON formatting`).toBe(formatToolSchemasSnapshot(toolSchemas.initial, toolSchemas.changes));
|
|
2733
2809
|
expect(headerChangeCount(fixture), `${scenario.name}: a pinning fixture must carry exactly its declared changed headers`).toBe(scenario.expectedHeaderChanges ?? 0);
|
|
2734
2810
|
}
|
|
@@ -2765,22 +2841,9 @@ function defineAcpSnapshotSuite(options) {
|
|
|
2765
2841
|
}
|
|
2766
2842
|
});
|
|
2767
2843
|
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
|
-
}
|
|
2844
|
+
for (const scenario of scenarios) await assertSessionFixtureStorage(join(snapshotsDir, scenario.name), scenario.name);
|
|
2782
2845
|
});
|
|
2783
2846
|
});
|
|
2784
2847
|
}
|
|
2785
2848
|
//#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,
|
|
2849
|
+
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 };
|