@x1a0f3n9/dsh-session-snapshot 0.1.5-rc.3
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +175 -0
- package/README.zh.md +175 -0
- package/lib/index.js +2849 -0
- package/lib/types/harness.d.ts +240 -0
- package/lib/types/identity.d.ts +8 -0
- package/lib/types/index.d.ts +27 -0
- package/lib/types/launcher.d.ts +84 -0
- package/lib/types/manifest.d.ts +123 -0
- package/lib/types/normalize.d.ts +144 -0
- package/lib/types/session-files.d.ts +104 -0
- package/lib/types/suite.d.ts +430 -0
- package/lib/types/workspace.d.ts +58 -0
- package/package.json +58 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Immutable Session-generation filenames used by recorded-session fixtures. */
|
|
2
|
+
/** One canonical recorded-session fixture filename. */
|
|
3
|
+
export interface SessionFixtureFile {
|
|
4
|
+
/** Parent is `0`; positive values are child/ordinal slots. */
|
|
5
|
+
readonly index: number;
|
|
6
|
+
/** Physical Session format generation; zero is encoded by omission. */
|
|
7
|
+
readonly version: number;
|
|
8
|
+
/** Canonical filename. */
|
|
9
|
+
readonly name: string;
|
|
10
|
+
}
|
|
11
|
+
/** One canonical persistence filename in a Session's own storage directory. */
|
|
12
|
+
export interface PersistedSessionFile {
|
|
13
|
+
/** Physical Session format generation; zero is encoded by omission. */
|
|
14
|
+
readonly version: number;
|
|
15
|
+
/** Physical compression selected by the backend. */
|
|
16
|
+
readonly compression: 'raw' | 'zstd';
|
|
17
|
+
/** Canonical basename. */
|
|
18
|
+
readonly name: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Return the canonical fixture filename for one parent/ordinal and generation.
|
|
22
|
+
*
|
|
23
|
+
* @param index - Parent `0` or a positive child/ordinal slot.
|
|
24
|
+
* @param version - Physical format generation; `0` is omitted.
|
|
25
|
+
* @returns The lowercase canonical JSONL filename.
|
|
26
|
+
*/
|
|
27
|
+
export declare function sessionFixtureName(index: number, version: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Name the native-writer oracle for a retained historical replay role.
|
|
30
|
+
* This expected output never participates in replay generation selection.
|
|
31
|
+
* @param index - Parent `0` or a positive child/ordinal slot.
|
|
32
|
+
* @returns The expected-output JSONL basename.
|
|
33
|
+
*/
|
|
34
|
+
export declare function writerSnapshotName(index: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* Parse one canonical recorded-session fixture filename.
|
|
37
|
+
*
|
|
38
|
+
* @param name - Basename from a scenario directory.
|
|
39
|
+
* @returns Parsed role and generation, or `undefined` for an unrelated file.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseSessionFixtureName(name: string): SessionFixtureFile | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Select the highest generation for every parent/ordinal fixture role.
|
|
44
|
+
* Older generations remain in the directory but do not count as extra Sessions.
|
|
45
|
+
*
|
|
46
|
+
* @param names - File basenames in one scenario directory.
|
|
47
|
+
* @returns Parent first, followed by contiguous child/ordinal roles.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionFixtureFiles(names: readonly string[]): SessionFixtureFile[];
|
|
50
|
+
/**
|
|
51
|
+
* Validate and order a scenario directory's selected Session fixture filenames.
|
|
52
|
+
*
|
|
53
|
+
* @param names - File basenames in one scenario directory.
|
|
54
|
+
* @returns Highest-generation parent and child/ordinal filenames.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sessionFixtureNames(names: readonly string[]): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Read the declared physical generation from one Session JSONL header.
|
|
59
|
+
*
|
|
60
|
+
* @param content - Complete UTF-8 JSONL content.
|
|
61
|
+
* @param label - Diagnostic filename or path.
|
|
62
|
+
* @returns The declared non-negative format generation.
|
|
63
|
+
*/
|
|
64
|
+
export declare function sessionHeaderVersion(content: string, label: string): number;
|
|
65
|
+
/**
|
|
66
|
+
* Require one fixture's canonical filename generation to equal its header.
|
|
67
|
+
*
|
|
68
|
+
* @param name - Canonical fixture basename.
|
|
69
|
+
* @param content - Complete UTF-8 JSONL content.
|
|
70
|
+
* @returns The validated format generation.
|
|
71
|
+
*/
|
|
72
|
+
export declare function assertSessionFixtureVersion(name: string, content: string): number;
|
|
73
|
+
/**
|
|
74
|
+
* Return the canonical persistence basename for one generation and compression.
|
|
75
|
+
*
|
|
76
|
+
* @param version - Physical format generation; `0` is omitted.
|
|
77
|
+
* @param compression - Backend compression mode.
|
|
78
|
+
* @returns The canonical persistence basename.
|
|
79
|
+
*/
|
|
80
|
+
export declare function persistedSessionFilename(version: number, compression?: 'raw' | 'zstd'): string;
|
|
81
|
+
/**
|
|
82
|
+
* Parse a canonical persistence basename from a Session's own directory.
|
|
83
|
+
*
|
|
84
|
+
* @param name - Candidate basename.
|
|
85
|
+
* @returns Its generation and compression, or `undefined` for noise and noncanonical names.
|
|
86
|
+
*/
|
|
87
|
+
export declare function parsePersistedSessionFilename(name: string): PersistedSessionFile | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* Select one highest-generation persistence path per physical Session directory.
|
|
90
|
+
*
|
|
91
|
+
* @param paths - Relative or absolute paths beneath a sessions root.
|
|
92
|
+
* @param compression - Compression selected by the snapshot composition.
|
|
93
|
+
* @returns Stable path order with older generations and filesystem noise omitted.
|
|
94
|
+
*/
|
|
95
|
+
export declare function latestPersistedSessionPaths(paths: readonly string[], compression?: 'raw' | 'zstd'): string[];
|
|
96
|
+
/**
|
|
97
|
+
* Require one persistence basename's generation to equal its Session header.
|
|
98
|
+
*
|
|
99
|
+
* @param name - Canonical persistence basename.
|
|
100
|
+
* @param content - Complete uncompressed UTF-8 JSONL content.
|
|
101
|
+
* @returns The validated generation.
|
|
102
|
+
*/
|
|
103
|
+
export declare function assertPersistedSessionVersion(name: string, content: string): number;
|
|
104
|
+
//# sourceMappingURL=session-files.d.ts.map
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyless-by-default ACP snapshot suite factory. Each scenario drives the real
|
|
3
|
+
* subprocess and compares normalized stdout; comparable session fixtures are
|
|
4
|
+
* both replay input and expected output. Record mode refreshes reproducible
|
|
5
|
+
* model scenarios from the live API, while refresh mode replays committed
|
|
6
|
+
* scripts and rewrites derived artifacts without a key.
|
|
7
|
+
* Replay scenarios run concurrently because each subprocess owns unique temp
|
|
8
|
+
* cwd and persistence roots and reads only committed fixtures. Record and
|
|
9
|
+
* refresh stay serial while writing.
|
|
10
|
+
*
|
|
11
|
+
* Exactly one scenario per header-composition class pins the tokenized header
|
|
12
|
+
* sequence and the tokenized `system/message` sequence. Its prompt and
|
|
13
|
+
* tool-schema sequences live in independent sidecars, each of which may be
|
|
14
|
+
* shared with another class pin when the bytes are identical. Every live
|
|
15
|
+
* header and system prompt is checked against the composed pin, so
|
|
16
|
+
* session-dependent composition must declare a separate class instead of
|
|
17
|
+
* escaping coverage.
|
|
18
|
+
* @module @x1a0f3n9/dsh-session-snapshot/suite
|
|
19
|
+
*/
|
|
20
|
+
import { type AgentUnderTest, type HarvestedLog } from './harness.ts';
|
|
21
|
+
import { type SnapshotSessionFormatManifest } from './manifest.ts';
|
|
22
|
+
import { type CwdPathMode, type NormalizeContext } from './normalize.ts';
|
|
23
|
+
/** A snapshot scenario and how its fixtures are produced. */
|
|
24
|
+
export interface Scenario {
|
|
25
|
+
name: string;
|
|
26
|
+
/** Deployment environment for this scenario's subprocess. */
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
/** Whether the scenario drives at least one model turn (so a JSONL expected output applies). */
|
|
29
|
+
hasModelTurn: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether the run persists a comparable session log to diff against the
|
|
32
|
+
* selected parent Session fixture. Defaults to {@link hasModelTurn} (a model turn
|
|
33
|
+
* always produces a log worth comparing). Set it independently for a scenario
|
|
34
|
+
* that produces a non-trivial durable log without calling the model.
|
|
35
|
+
*/
|
|
36
|
+
comparesLog?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Whether `test:snapshot:record` regenerates this scenario's current-version
|
|
39
|
+
* Session fixtures from the LIVE API. `recorded` scenarios are model-driven and reproducible;
|
|
40
|
+
* `authored` scenarios (fixtures hand-written or hand-harvested — e.g. a
|
|
41
|
+
* provider error or a cancel the live API can't be coaxed into
|
|
42
|
+
* deterministically, a deterministic hook scenario, or a scripted repetition
|
|
43
|
+
* a live model won't reproduce) are NEVER re-recorded.
|
|
44
|
+
*/
|
|
45
|
+
recorded: boolean;
|
|
46
|
+
/** Historical generation retained as a read-only migration fixture. */
|
|
47
|
+
sessionFormat?: SnapshotSessionFormatManifest;
|
|
48
|
+
/**
|
|
49
|
+
* Whether replay is driven by a hand-written `replay.override.json` sidecar
|
|
50
|
+
* (a `ReplayOverrideDoc` that replaces or patches the script derived from
|
|
51
|
+
* selected parent Session fixture) — the throw/hang cases chunks cannot express. The fixture
|
|
52
|
+
* guard requires the sidecar exactly when this is set: the harness forwards
|
|
53
|
+
* the file purely on existence, so an unregistered stray sidecar would
|
|
54
|
+
* silently alter the derived script. The guard fails loud on either
|
|
55
|
+
* mismatch. Defaults to false (replay derives from the fixture's
|
|
56
|
+
* `assistant/chunk` events).
|
|
57
|
+
*/
|
|
58
|
+
overridden?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Whether this scenario is its header class's sole tokenized request-header
|
|
61
|
+
* pin. Prompt and tool-schema sidecars are selected independently, while
|
|
62
|
+
* every classmate is checked for equality with the reconstructed header.
|
|
63
|
+
*/
|
|
64
|
+
pinsHeader?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Header-pinning scenario whose `system-prompt.expected.md` this pin reuses.
|
|
67
|
+
* Defaults to this scenario. The source must own its prompt sidecar and
|
|
68
|
+
* declare the same {@link expectedPromptChanges}; only valid on a pin.
|
|
69
|
+
*/
|
|
70
|
+
systemPromptSource?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Header-pinning scenario whose `tool-schemas.expected.json` this pin reuses.
|
|
73
|
+
* Defaults to this scenario. The source must own its schema sidecar and
|
|
74
|
+
* declare the same {@link expectedHeaderChanges}; only valid on a pin.
|
|
75
|
+
*/
|
|
76
|
+
toolSchemasSource?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Child fixture indices whose own schema sequence is pinned separately,
|
|
79
|
+
* where `1` names `session.1.jsonl` and
|
|
80
|
+
* `tool-schemas.1.expected.json`. The class pin still owns every other
|
|
81
|
+
* request-header field.
|
|
82
|
+
*/
|
|
83
|
+
pinsChildToolSchemas?: readonly number[];
|
|
84
|
+
/**
|
|
85
|
+
* Child fixture indices whose own system prompt is pinned separately, where
|
|
86
|
+
* `1` names `session.1.jsonl` and `system-prompt.1.expected.md`. A child
|
|
87
|
+
* scope that installs its own prompt section (the continuable `report`
|
|
88
|
+
* guidance) composes a prompt the class pin cannot describe.
|
|
89
|
+
*/
|
|
90
|
+
pinsChildSystemPrompts?: readonly number[];
|
|
91
|
+
/**
|
|
92
|
+
* How many changed `request/header` snapshots this PINNING scenario's primary
|
|
93
|
+
* fixture legitimately carries (default 0). Each change owns one more schema
|
|
94
|
+
* set in the structured JSON pin; any other count fails. Only valid on the pin.
|
|
95
|
+
*/
|
|
96
|
+
expectedHeaderChanges?: number;
|
|
97
|
+
/**
|
|
98
|
+
* How many later `system/message` events — replacements of surface node 0
|
|
99
|
+
* or in-history appends — this PINNING scenario's primary fixture
|
|
100
|
+
* legitimately carries after the initial prompt (default 0). Each change's
|
|
101
|
+
* full prompt text is kept in the readable Markdown pin; any other count
|
|
102
|
+
* fails. Only valid on the pin.
|
|
103
|
+
*/
|
|
104
|
+
expectedPromptChanges?: number;
|
|
105
|
+
/**
|
|
106
|
+
* Which header-composition class this scenario belongs to. Scenarios that
|
|
107
|
+
* boot the same config compose the same header; each class has exactly one
|
|
108
|
+
* {@link pinsHeader} scenario, and the uniformity guard compares every
|
|
109
|
+
* other member against ITS class's pin. Defaults to `'default'`; a
|
|
110
|
+
* scenario booting an alternate config ({@link configPath}) whose tool
|
|
111
|
+
* list or prompt sections differ by construction carries its own class.
|
|
112
|
+
*/
|
|
113
|
+
headerClass?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Alternate live profile patch (absolute) this scenario boots instead of
|
|
116
|
+
* {@link AgentUnderTest.configPath}. Its basename must still end in
|
|
117
|
+
* `cordis.yml` so the launcher finds the replay sibling. A scenario whose
|
|
118
|
+
* patch changes the composed header also needs its own
|
|
119
|
+
* {@link headerClass}.
|
|
120
|
+
*/
|
|
121
|
+
configPath?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Parent directory for the generated session cwd. Defaults to the platform
|
|
124
|
+
* temp directory; set this when temp is itself part of the behavior under
|
|
125
|
+
* test and the scenario needs an independent project location.
|
|
126
|
+
*/
|
|
127
|
+
workspaceParent?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Optional final workspace preparation after the committed fixture is
|
|
130
|
+
* copied. Reserve this for paths that Git cannot represent portably; normal
|
|
131
|
+
* scenario files belong under the scenario's `workspace/` directory.
|
|
132
|
+
*/
|
|
133
|
+
prepareWorkspace?: (cwd: string) => void | Promise<void>;
|
|
134
|
+
/**
|
|
135
|
+
* Whether Windows additionally compares stdout with native separators against
|
|
136
|
+
* `stdout.expected.windows.jsonl`. The shared canonical stdout expected output is still
|
|
137
|
+
* compared on every platform, and the fixture guard requires this sidecar
|
|
138
|
+
* exactly when the option is set.
|
|
139
|
+
*/
|
|
140
|
+
pinsNativeWindowsStdout?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Whether the scenario requires a non-Windows host, such as for POSIX process
|
|
143
|
+
* semantics or generated paths Windows cannot represent. The scenario's run
|
|
144
|
+
* test is skipped on Windows; its fixtures stay guarded on every platform.
|
|
145
|
+
*/
|
|
146
|
+
posixOnly?: boolean;
|
|
147
|
+
/**
|
|
148
|
+
* Whether the scenario boots a composition that needs a usable `pwsh`
|
|
149
|
+
* (the pwsh-tool-turn scenario). The run test is skipped when the suite's
|
|
150
|
+
* {@link SnapshotSuiteOptions.hasPwsh} probe is false; fixtures stay guarded
|
|
151
|
+
* on every platform.
|
|
152
|
+
*/
|
|
153
|
+
pwshOnly?: boolean;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether a scenario's run test is skipped for this mode and host: record mode
|
|
157
|
+
* skips authored (non-`recorded`) scenarios and explicit historical Session
|
|
158
|
+
* generations, {@link Scenario.posixOnly} scenarios skip on Windows, and
|
|
159
|
+
* {@link Scenario.pwshOnly} scenarios skip when the caller's `hasPwsh` probe
|
|
160
|
+
* is false.
|
|
161
|
+
*
|
|
162
|
+
* @param scenario The scenario whose run test is being registered.
|
|
163
|
+
* @param recording Whether the suite runs in record mode.
|
|
164
|
+
* @param platform The running Node platform, injectable for unit coverage.
|
|
165
|
+
* @param hasPwsh The caller's pwsh-availability probe; `pwshOnly` scenarios
|
|
166
|
+
* skip unless it is true.
|
|
167
|
+
* @returns True when the scenario's run test must not execute.
|
|
168
|
+
*/
|
|
169
|
+
export declare function scenarioSkipped(scenario: Scenario, recording: boolean, platform?: NodeJS.Platform, hasPwsh?: boolean): boolean;
|
|
170
|
+
/** One stdout expected output selected for a platform run. */
|
|
171
|
+
interface StdoutExpectedVariant {
|
|
172
|
+
file: string;
|
|
173
|
+
cwdPathMode: CwdPathMode;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Select the shared stdout expected output plus any platform-native assertion declared by a scenario.
|
|
177
|
+
*
|
|
178
|
+
* @param scenario The scenario whose stdout contract is being selected.
|
|
179
|
+
* @param platform The running Node platform, injectable for unit coverage.
|
|
180
|
+
* @returns The ordered expected-output variants: shared canonical first, then optional Windows native.
|
|
181
|
+
*/
|
|
182
|
+
export declare function stdoutExpectedVariants(scenario: Scenario, platform?: NodeJS.Platform): StdoutExpectedVariant[];
|
|
183
|
+
/** One suite's inputs: the agent to boot, where its fixtures live, and its scenario table. */
|
|
184
|
+
export interface SnapshotSuiteOptions {
|
|
185
|
+
/** The agent composition every scenario boots. */
|
|
186
|
+
agent: AgentUnderTest;
|
|
187
|
+
/** Absolute path of the suite's `snapshots/` directory (one subdir per scenario). */
|
|
188
|
+
snapshotsDir: string;
|
|
189
|
+
/** The scenario table; exactly one entry per header class must set `pinsHeader`. */
|
|
190
|
+
scenarios: Scenario[];
|
|
191
|
+
/**
|
|
192
|
+
* `replay` (keyless, the default tier), `record` (live API; re-records the
|
|
193
|
+
* `recorded` scenarios' fixtures and refreshes the Vitest expected outputs under
|
|
194
|
+
* `--update`), or `refresh` (keyless replay that rewrites stdout expected outputs and
|
|
195
|
+
* comparable session fixtures from the replay run). The caller derives this
|
|
196
|
+
* from `$DSH_SNAPSHOT` — env reading stays outside this library.
|
|
197
|
+
*/
|
|
198
|
+
mode: 'replay' | 'record' | 'refresh';
|
|
199
|
+
/**
|
|
200
|
+
* Whether a real `pwsh` executable is available on this host (the probe the
|
|
201
|
+
* caller owns; `pwshOnly` scenarios skip when this is not true).
|
|
202
|
+
*/
|
|
203
|
+
hasPwsh?: boolean;
|
|
204
|
+
}
|
|
205
|
+
/** One scenario's generated claim on a shared snapshot file. */
|
|
206
|
+
export interface SharedSnapshotClaim {
|
|
207
|
+
/** Scenario that first generated the snapshot in this suite run. */
|
|
208
|
+
scenario: string;
|
|
209
|
+
/** Complete generated file content. */
|
|
210
|
+
content: string;
|
|
211
|
+
}
|
|
212
|
+
/** One committed snapshot file and its complete content. */
|
|
213
|
+
export interface NamedSnapshotContent {
|
|
214
|
+
/** Diagnostic path of the committed file. */
|
|
215
|
+
path: string;
|
|
216
|
+
/** Complete committed file content. */
|
|
217
|
+
content: string;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Record one scenario's generated content for a shared snapshot source.
|
|
221
|
+
* A later claimant must generate identical bytes; otherwise record/refresh
|
|
222
|
+
* would make the final file depend on scenario order.
|
|
223
|
+
*
|
|
224
|
+
* @param claims Claims already made in this suite run, keyed by source path.
|
|
225
|
+
* @param source The shared snapshot path being claimed.
|
|
226
|
+
* @param scenario The scenario generating the content.
|
|
227
|
+
* @param content The complete content the scenario generated.
|
|
228
|
+
* @returns Nothing.
|
|
229
|
+
*/
|
|
230
|
+
export declare function claimSharedSnapshot(claims: Map<string, SharedSnapshotClaim>, source: string, scenario: string, content: string): void;
|
|
231
|
+
/**
|
|
232
|
+
* Reject byte-identical committed snapshots stored under different paths.
|
|
233
|
+
*
|
|
234
|
+
* @param kind Human-readable snapshot kind for the diagnostic.
|
|
235
|
+
* @param snapshots The committed files to compare.
|
|
236
|
+
* @returns Nothing.
|
|
237
|
+
*/
|
|
238
|
+
export declare function assertUniqueSnapshotContents(kind: string, snapshots: readonly NamedSnapshotContent[]): void;
|
|
239
|
+
/**
|
|
240
|
+
* Derive normalization values from a fixture's own session header. Recorded ids and cwd differ
|
|
241
|
+
* from the live replay run; the non-empty sentinel for missing cwd avoids accidental empty-
|
|
242
|
+
* string replacement.
|
|
243
|
+
*
|
|
244
|
+
* @param fixture The selected committed parent Session fixture content.
|
|
245
|
+
* @returns The fixture's own volatile values, ready for {@link normalizeSessionLog}.
|
|
246
|
+
*/
|
|
247
|
+
export declare function fixtureContext(fixture: string): NormalizeContext;
|
|
248
|
+
/**
|
|
249
|
+
* The `data.header` payload of every `request/header` event in a session
|
|
250
|
+
* JSONL, in log order, with the log's volatile values scrubbed first
|
|
251
|
+
* ({@link normalizeSessionLog}) so headers harvested from different runs —
|
|
252
|
+
* each embedding its own generated cwd — compare on equal footing.
|
|
253
|
+
*
|
|
254
|
+
* @param rawLog The session `.jsonl` content to extract headers from.
|
|
255
|
+
* @param ctx The volatile values of the run that produced it.
|
|
256
|
+
* @returns The normalized `data.header` payloads, in log order.
|
|
257
|
+
*/
|
|
258
|
+
export declare function normalizedHeaders(rawLog: string, ctx: NormalizeContext): unknown[];
|
|
259
|
+
/**
|
|
260
|
+
* The normalized prompt text of every `system/message` event in a session
|
|
261
|
+
* JSONL, in log order: the first is the initial system prompt (surface node 0)
|
|
262
|
+
* and each later one replaced it or, on an in-history route, appended the
|
|
263
|
+
* changed prompt after the cached history. An empty `content` yields `''`; a
|
|
264
|
+
* `system/message` without a text block is omitted.
|
|
265
|
+
*
|
|
266
|
+
* @param rawLog The session `.jsonl` content to inspect.
|
|
267
|
+
* @param ctx The volatile values of the run that produced it.
|
|
268
|
+
* @returns The normalized system prompts, in log order.
|
|
269
|
+
*/
|
|
270
|
+
export declare function normalizedSystemPrompts(rawLog: string, ctx: NormalizeContext): string[];
|
|
271
|
+
/**
|
|
272
|
+
* Whether every model request in a session JSONL is preceded by its system
|
|
273
|
+
* prompt: the log has no `request/header`, or a `system/message` event comes
|
|
274
|
+
* before its first `request/header`.
|
|
275
|
+
*
|
|
276
|
+
* @param rawLog The session `.jsonl` content to inspect.
|
|
277
|
+
* @returns True when the first `request/header` (if any) follows a `system/message`.
|
|
278
|
+
*/
|
|
279
|
+
export declare function systemPromptPrecedesRequests(rawLog: string): boolean;
|
|
280
|
+
/**
|
|
281
|
+
* The normalized tool-schema arrays carried by request headers in a session
|
|
282
|
+
* JSONL, in log order. Headers without an array-valued tools field are omitted
|
|
283
|
+
* so callers can assert one schema set per header explicitly.
|
|
284
|
+
*
|
|
285
|
+
* @param rawLog The session `.jsonl` content to inspect.
|
|
286
|
+
* @param ctx The volatile values of the run that produced it.
|
|
287
|
+
* @returns The normalized initial tool-schema arrays, in header order.
|
|
288
|
+
*/
|
|
289
|
+
export declare function normalizedToolSchemas(rawLog: string, ctx: NormalizeContext): unknown[][];
|
|
290
|
+
/** The structured contents of a tool-schema sidecar. */
|
|
291
|
+
export interface ToolSchemasSnapshot {
|
|
292
|
+
/** The complete tool schemas from the pinned request header. */
|
|
293
|
+
initial: unknown[];
|
|
294
|
+
/** Complete tool schemas from subsequent changed-header snapshots. */
|
|
295
|
+
changes: unknown[][];
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Render the full tool-schema sequence as canonical, readable JSON.
|
|
299
|
+
*
|
|
300
|
+
* @param initial The pinned request header's complete tool schemas.
|
|
301
|
+
* @param changes Complete tool schemas from later changed headers.
|
|
302
|
+
* @returns A pretty-printed JSON snapshot ending in one newline.
|
|
303
|
+
*/
|
|
304
|
+
export declare function formatToolSchemasSnapshot(initial: readonly unknown[], changes?: readonly unknown[][]): string;
|
|
305
|
+
/**
|
|
306
|
+
* Parse and validate the stable top-level fields of a tool-schema sidecar.
|
|
307
|
+
*
|
|
308
|
+
* @param snapshot The JSON sidecar text.
|
|
309
|
+
* @returns Its initial and changed-header schema sets.
|
|
310
|
+
*/
|
|
311
|
+
export declare function parseToolSchemasSnapshot(snapshot: string): ToolSchemasSnapshot;
|
|
312
|
+
/**
|
|
313
|
+
* Restore one sidecar schema set into a tokenized pinned header.
|
|
314
|
+
*
|
|
315
|
+
* @param header The parsed request header carrying `tools: "{{tools}}"`.
|
|
316
|
+
* @param schemas The complete schemas for this full header snapshot.
|
|
317
|
+
* @returns A copy of the header with its complete schemas restored.
|
|
318
|
+
*/
|
|
319
|
+
export declare function restorePinnedToolSchemas(header: unknown, schemas: readonly unknown[]): unknown;
|
|
320
|
+
/**
|
|
321
|
+
* Render a normalized prompt as a repository-friendly Markdown snapshot.
|
|
322
|
+
* Prompt text is unchanged except that a missing terminal newline is added so
|
|
323
|
+
* the committed file follows the repository newline contract.
|
|
324
|
+
*
|
|
325
|
+
* @param prompt The normalized initial system prompt (surface node 0).
|
|
326
|
+
* @param changes Full normalized prompts from later `system/message` events, replacements or in-history appends.
|
|
327
|
+
* @returns Markdown snapshot text ending in a newline.
|
|
328
|
+
*/
|
|
329
|
+
export declare function formatSystemPromptSnapshot(prompt: string, changes?: readonly string[]): string;
|
|
330
|
+
/**
|
|
331
|
+
* Split a prompt sidecar into its initial prompt and each later change, the
|
|
332
|
+
* inverse of {@link formatSystemPromptSnapshot}.
|
|
333
|
+
*
|
|
334
|
+
* @param snapshot The Markdown sidecar text.
|
|
335
|
+
* @returns The initial prompt snapshot plus one entry per later `system/message`.
|
|
336
|
+
*/
|
|
337
|
+
export declare function parseSystemPromptSnapshot(snapshot: string): {
|
|
338
|
+
initial: string;
|
|
339
|
+
changes: string[];
|
|
340
|
+
};
|
|
341
|
+
/**
|
|
342
|
+
* Reject a child prompt sidecar that cannot own distinct, canonical prompt text.
|
|
343
|
+
* @param sidecar - committed child prompt snapshot.
|
|
344
|
+
* @param classPin - initial prompt snapshot owned by the scenario's header class.
|
|
345
|
+
* @param label - repository-relative fixture label for diagnostics.
|
|
346
|
+
*/
|
|
347
|
+
export declare function assertChildSystemPromptSnapshot(sidecar: string, classPin: string, label: string): void;
|
|
348
|
+
/**
|
|
349
|
+
* Count changed `request/header` snapshots in a session JSONL.
|
|
350
|
+
*
|
|
351
|
+
* @param rawLog The session `.jsonl` content.
|
|
352
|
+
* @returns How many headers carry reason `change`.
|
|
353
|
+
*/
|
|
354
|
+
export declare function headerChangeCount(rawLog: string): number;
|
|
355
|
+
/** A literal replacement from a fresh replay-run volatile to its existing fixture value. */
|
|
356
|
+
export interface FixtureReplacement {
|
|
357
|
+
/** The fresh replay run's volatile value. */
|
|
358
|
+
from: string;
|
|
359
|
+
/** The existing fixture value retained during write-back. */
|
|
360
|
+
to: string;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Carry committed UUIDs into unchanged, unambiguous messages in fresh session fixtures.
|
|
364
|
+
*
|
|
365
|
+
* @param logs Fresh fixture-ready session JSONL contents for one scenario.
|
|
366
|
+
* @param fixtures Existing fixture contents in matching order; missing fixtures may be empty strings.
|
|
367
|
+
* @returns The fresh contents with only reusable message UUIDs replaced.
|
|
368
|
+
*/
|
|
369
|
+
export declare function stabilizeFixtureMessageIds(logs: readonly string[], fixtures: readonly string[]): string[];
|
|
370
|
+
/**
|
|
371
|
+
* Find tool calls whose structured result reports `UNKNOWN_TOOL`.
|
|
372
|
+
*
|
|
373
|
+
* Snapshot refresh must not turn a missing registration into accepted behavior;
|
|
374
|
+
* intentional unknown-tool behavior belongs in a focused unit or e2e test.
|
|
375
|
+
*
|
|
376
|
+
* @param rawLog The session JSONL to inspect.
|
|
377
|
+
* @returns The failing call ids in log order, using a diagnostic placeholder when absent.
|
|
378
|
+
*/
|
|
379
|
+
export declare function unknownToolCallIds(rawLog: string): string[];
|
|
380
|
+
/**
|
|
381
|
+
* Build refresh write-back replacements for per-log session ids, cwd values,
|
|
382
|
+
* and spill paths. Durable message ids have a later structural owner.
|
|
383
|
+
*
|
|
384
|
+
* @param logs The freshly harvested logs, in fixture order.
|
|
385
|
+
* @param fixtures The existing fixture contents, in matching order.
|
|
386
|
+
* @returns Literal replacements from fresh values to the fixture's existing values.
|
|
387
|
+
*/
|
|
388
|
+
export declare function refreshFixtureReplacements(logs: HarvestedLog[], fixtures: string[]): FixtureReplacement[];
|
|
389
|
+
/**
|
|
390
|
+
* Rewrite a fresh replay-produced log so repeated refreshes do not churn
|
|
391
|
+
* volatile fixture fields. Meaningful event payloads come from `fresh`; the
|
|
392
|
+
* existing fixture lends normalized-equivalent values, including non-message ids, paths,
|
|
393
|
+
* creation/event times, spill locators, and hook durations, only when the
|
|
394
|
+
* complete record layout aligns and volatile strings form a consistent
|
|
395
|
+
* bijection. Complete durable-message ids are excluded because the later
|
|
396
|
+
* fixture-ready structural pass owns them. Ambiguous layouts or mappings
|
|
397
|
+
* keep fresh strings. Packed timing gaps expand from zero when a projected
|
|
398
|
+
* fixture omits `time0`, so packing does not shift later records;
|
|
399
|
+
* fresh semantic values and fragment arrays remain authoritative.
|
|
400
|
+
*
|
|
401
|
+
* @param fresh The newly harvested session JSONL.
|
|
402
|
+
* @param existing The committed fixture JSONL being refreshed.
|
|
403
|
+
* @param replacements Cross-log literal replacements from {@link refreshFixtureReplacements}.
|
|
404
|
+
* @param freshContext The harvested run's ids, cwd, and every cwd alias.
|
|
405
|
+
* @returns The stabilized JSONL content to write back.
|
|
406
|
+
*/
|
|
407
|
+
export declare function stabilizeRefreshLog(fresh: string, existing: string, replacements: FixtureReplacement[], freshContext: NormalizeContext): string;
|
|
408
|
+
/**
|
|
409
|
+
* Check every selected role for tool/path defects and current generations for canonical prompt/identity storage.
|
|
410
|
+
* Historical roles retain their released bytes, including roles retired by the current writer.
|
|
411
|
+
* @param dir Scenario directory containing canonical Session fixtures.
|
|
412
|
+
* @param scenarioName Scenario name used in failure diagnostics.
|
|
413
|
+
* @returns Resolves when all selected fixtures satisfy the storage checks.
|
|
414
|
+
*/
|
|
415
|
+
export declare function assertSessionFixtureStorage(dir: string, scenarioName: string): Promise<void>;
|
|
416
|
+
/**
|
|
417
|
+
* Register the suite: one test per scenario (the expected-output and log comparisons and
|
|
418
|
+
* the header and prompt uniformity guard) plus the fixture guard block (no orphan
|
|
419
|
+
* scenario dirs, required files present, exactly one pin per header class,
|
|
420
|
+
* shared sidecars unique and well-formed, every JSONL prompt- and
|
|
421
|
+
* schema-scrubbed with a `system/message` before its first request). Must
|
|
422
|
+
* run at vitest collection time — it calls `describe`/`it`. Throws
|
|
423
|
+
* immediately if any header class lacks a pinning scenario or carries two
|
|
424
|
+
* (the uniformity guard needs exactly one comparison anchor per class).
|
|
425
|
+
*
|
|
426
|
+
* @param options The agent, snapshots directory, scenario table, and mode.
|
|
427
|
+
*/
|
|
428
|
+
export declare function defineAcpSnapshotSuite(options: SnapshotSuiteOptions): void;
|
|
429
|
+
export {};
|
|
430
|
+
//# sourceMappingURL=suite.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Capture readable, path-stable workspace state for recorded-session tests. */
|
|
2
|
+
/** Marker that lets Git retain an expected empty directory without becoming expected workspace state. */
|
|
3
|
+
export declare const EMPTY_WORKSPACE_MARKER = ".empty";
|
|
4
|
+
/** One UTF-8 file in a captured workspace. */
|
|
5
|
+
export interface WorkspaceTextFileSnapshot {
|
|
6
|
+
/** Cwd-relative POSIX path. */
|
|
7
|
+
readonly path: string;
|
|
8
|
+
/** Entry discriminator. */
|
|
9
|
+
readonly kind: 'text';
|
|
10
|
+
/** Exact UTF-8 contents. */
|
|
11
|
+
readonly content: string;
|
|
12
|
+
}
|
|
13
|
+
/** One non-text file in a captured workspace. */
|
|
14
|
+
export interface WorkspaceBinaryFileSnapshot {
|
|
15
|
+
/** Cwd-relative POSIX path. */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** Entry discriminator. */
|
|
18
|
+
readonly kind: 'binary';
|
|
19
|
+
/** Exact bytes encoded for deterministic diffs. */
|
|
20
|
+
readonly base64: string;
|
|
21
|
+
}
|
|
22
|
+
/** One symbolic link in a captured workspace. */
|
|
23
|
+
export interface WorkspaceSymlinkSnapshot {
|
|
24
|
+
/** Cwd-relative POSIX path. */
|
|
25
|
+
readonly path: string;
|
|
26
|
+
/** Entry discriminator. */
|
|
27
|
+
readonly kind: 'symlink';
|
|
28
|
+
/** Exact link text without resolving the target. */
|
|
29
|
+
readonly target: string;
|
|
30
|
+
}
|
|
31
|
+
/** One empty directory in a captured workspace. */
|
|
32
|
+
export interface WorkspaceEmptyDirectorySnapshot {
|
|
33
|
+
/** Cwd-relative POSIX path. */
|
|
34
|
+
readonly path: string;
|
|
35
|
+
/** Entry discriminator. */
|
|
36
|
+
readonly kind: 'empty-directory';
|
|
37
|
+
}
|
|
38
|
+
/** Stable complete file, link, and empty-directory state below one workspace root. */
|
|
39
|
+
export type WorkspaceSnapshotEntry = WorkspaceTextFileSnapshot | WorkspaceBinaryFileSnapshot | WorkspaceSymlinkSnapshot | WorkspaceEmptyDirectorySnapshot;
|
|
40
|
+
/** Options for excluding harness-owned root entries from a runtime workspace. */
|
|
41
|
+
export interface CaptureWorkspaceSnapshotOptions {
|
|
42
|
+
/** Exact immediate children of the workspace root to omit. */
|
|
43
|
+
readonly ignoredRootEntries?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Capture one workspace without resolving links or depending on host path separators.
|
|
47
|
+
* @param root - Absolute directory whose user-visible state is captured.
|
|
48
|
+
* @param options - Harness-owned immediate children to omit.
|
|
49
|
+
* @returns Stable entries sorted by relative path.
|
|
50
|
+
*/
|
|
51
|
+
export declare function captureWorkspaceSnapshot(root: string, options?: CaptureWorkspaceSnapshotOptions): Promise<WorkspaceSnapshotEntry[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Capture a committed `workspace.expected/` tree, excluding its Git-only empty marker.
|
|
54
|
+
* @param root - Absolute expected-workspace directory.
|
|
55
|
+
* @returns Stable expected entries.
|
|
56
|
+
*/
|
|
57
|
+
export declare function captureExpectedWorkspaceSnapshot(root: string): Promise<WorkspaceSnapshotEntry[]>;
|
|
58
|
+
//# sourceMappingURL=workspace.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x1a0f3n9/dsh-session-snapshot",
|
|
3
|
+
"description": "Session-log snapshot core with an ACP protocol adapter, expected-output normalization, and fixture invariants",
|
|
4
|
+
"version": "0.1.5-rc.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/test-support/session-snapshot"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./src/*": "./src/*",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib/index.js",
|
|
26
|
+
"lib/types/**/*.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@agentclientprotocol/sdk": "1.4.0",
|
|
31
|
+
"js-yaml": "^4.2.0",
|
|
32
|
+
"vitest": "^4.1.8",
|
|
33
|
+
"@deepseek-ai/cordis-plugin-include": "1.0.7",
|
|
34
|
+
"@x1a0f3n9/dsh-llm-replay": "^0.1.5-rc.3",
|
|
35
|
+
"@x1a0f3n9/dsh-loader-smoke": "0.1.5-rc.3",
|
|
36
|
+
"@x1a0f3n9/dsh-session-format": "^0.1.5-rc.3"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
40
|
+
"@x1a0f3n9/dsh-session": "^0.1.5-rc.3",
|
|
41
|
+
"@x1a0f3n9/dsh-http-proxy": "^0.1.5-rc.3"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/js-yaml": "^4.0.9",
|
|
45
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
46
|
+
"@x1a0f3n9/dsh-agent": "^0.1.5-rc.3",
|
|
47
|
+
"@x1a0f3n9/dsh-agent-loop": "^0.1.5-rc.3",
|
|
48
|
+
"@x1a0f3n9/dsh-compaction": "^0.1.5-rc.3",
|
|
49
|
+
"@x1a0f3n9/dsh-llm": "^0.1.5-rc.3",
|
|
50
|
+
"@x1a0f3n9/dsh-sandbox": "^0.1.5-rc.3",
|
|
51
|
+
"@x1a0f3n9/dsh-sandbox-policy": "^0.1.5-rc.3",
|
|
52
|
+
"@x1a0f3n9/dsh-session": "^0.1.5-rc.3",
|
|
53
|
+
"@x1a0f3n9/dsh-subagent": "^0.1.5-rc.3",
|
|
54
|
+
"@x1a0f3n9/dsh-tools": "^0.1.5-rc.3",
|
|
55
|
+
"@x1a0f3n9/dsh-user-questions": "^0.1.5-rc.3",
|
|
56
|
+
"@x1a0f3n9/dsh-http-proxy": "^0.1.5-rc.3"
|
|
57
|
+
}
|
|
58
|
+
}
|