@alumnium/test 0.0.0-stage → 0.24.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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Alexey Rodionov, Tatiana Shepeleva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # @alumnium/test
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Alumnium test record and replay: agent-agnostic recordings, masking and the test cache. See [the main `alumnium` package page](https://www.npmjs.com/package/alumnium) for more details.
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@alumnium/test",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.24.1",
4
+ "description": "Alumnium test record and replay: agent-agnostic recordings, masking and the test cache",
5
+ "license": "MIT",
6
+ "author": "Alex Rodionov <p0deje@gmail.com>",
7
+ "contributors": [
8
+ "Alex Rodionov <p0deje@gmail.com>",
9
+ "Sasha Koss <koss@nocorp.me>",
10
+ "Tatiana Shepeleva <tati.shep@gmail.com>"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/alumnium-hq/alumnium.git"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "README.md",
19
+ "LICENSE.md"
20
+ ],
21
+ "type": "module",
22
+ "exports": {
23
+ ".": {
24
+ "import": {
25
+ "types": "./src/index.d.ts",
26
+ "default": "./src/index.js"
27
+ },
28
+ "require": {
29
+ "types": "./src/index.d.ts",
30
+ "default": "./src/index.cjs"
31
+ }
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@alumnium/core": "0.24.0",
36
+ "@js-fns/xxhash": "^0.1.0",
37
+ "zod": "^4.4.1"
38
+ }
39
+ }
@@ -0,0 +1,26 @@
1
+ import { Recording } from "./Recording.ts";
2
+ import type { TestCache } from "./TestCache.ts";
3
+ export declare namespace FileTestCache {
4
+ interface Props {
5
+ /** Directory to store recordings in, `<ALUMNIUM_CACHE_PATH>/tests` by default. */
6
+ dir?: string | undefined;
7
+ }
8
+ }
9
+ /**
10
+ * Test cache storing each recording as `<xxh64(text)>.json`, in the `tests`
11
+ * directory of the Alumnium cache.
12
+ */
13
+ export declare class FileTestCache implements TestCache {
14
+ #private;
15
+ constructor(props?: FileTestCache.Props);
16
+ static defaultDir(): string;
17
+ get dir(): string;
18
+ get lookups(): TestCache.Lookups;
19
+ lookup(
20
+ text: string,
21
+ options?: TestCache.LookupOptions,
22
+ ): Promise<Recording.Type | null>;
23
+ update(recording: Recording.Type): Promise<void>;
24
+ save(): Promise<void>;
25
+ discard(): Promise<void>;
26
+ }
@@ -0,0 +1,85 @@
1
+ import { Masker } from "./masker/Masker.ts";
2
+ import { Recording } from "./Recording.ts";
3
+ export declare namespace Recorder {
4
+ interface Props extends Masker.Props {
5
+ text: string;
6
+ agent: string;
7
+ /** See `Recording.Schema` `version`, `Recording.DEFAULT_VERSION` by default. */
8
+ version?: number | undefined;
9
+ /** Name the host gave Alumnium's MCP server, `alumnium` by default. */
10
+ alumniumServer?: string | undefined;
11
+ }
12
+ interface ToolUse {
13
+ id: string;
14
+ /** MCP server the tool belongs to, absent for a host tool (e.g. `Bash`). */
15
+ server?: string | undefined;
16
+ tool: string;
17
+ input: unknown;
18
+ }
19
+ interface ToolResult {
20
+ id: string;
21
+ content: Recording.Content;
22
+ isError?: boolean | undefined;
23
+ }
24
+ interface Narration {
25
+ narration: Recording.NarrationStep["narration"];
26
+ text: string;
27
+ }
28
+ }
29
+ /**
30
+ * Agent-agnostic sink turning the host agent's tool calls and narration into a
31
+ * replayable recording. The host owns the agent loop and reports what happens
32
+ * in it. The recorder masks the values a replay has to refresh or must never
33
+ * see in clear (see `Masker`) and pairs each call with its result.
34
+ */
35
+ export declare class Recorder {
36
+ #private;
37
+ constructor(props: Recorder.Props);
38
+ /**
39
+ * Records a tool call, held until its result arrives.
40
+ *
41
+ * Masked here, on the call, and not once the step is pushed in `onToolResult`.
42
+ * A tool call's output is only registered once its result arrives,
43
+ * so a call can neither mask its own input, nor - when the
44
+ * agent runs tools in parallel - be masked with a value that did not exist
45
+ * yet when it was issued.
46
+ *
47
+ * A tool replay cannot execute is masked too. Its input is
48
+ * never re-run, so nothing gets substituted back into it, but masking still
49
+ * keeps the value a recording happened to produce - often a credential - out
50
+ * of the stored recording.
51
+ *
52
+ * @param use - Tool call as the host agent issued it.
53
+ */
54
+ onToolUse(use: Recorder.ToolUse): void;
55
+ /**
56
+ * Records the result of a tool call, pushing the call as a step.
57
+ *
58
+ * Steps land in the order results arrive. The values a call produced are
59
+ * registered under its id, which later masks refer to.
60
+ *
61
+ * @param toolResult - Result of a call reported with `onToolUse`.
62
+ */
63
+ onToolResult(toolResult: Recorder.ToolResult): void;
64
+ /**
65
+ * Records what the agent said, so that a replay of this recording reads the
66
+ * way the recording did. Nothing is executed from it.
67
+ *
68
+ * Pushed as it arrives, while a tool call is pushed once its result
69
+ * does. Narration therefore lands ahead of the call it introduced, which is
70
+ * the order the agent produced them in.
71
+ *
72
+ * @param narration - Prose as the agent wrote it.
73
+ */
74
+ onNarration(narration: Recorder.Narration): void;
75
+ /**
76
+ * Finishes the recording with the run's verdict. Calls still waiting for a
77
+ * result are dropped, since replay has nothing to compare them against.
78
+ *
79
+ * @param verdict - Verdict the host agent reached.
80
+ * @returns Finished recording.
81
+ * @throws {RecordingLeakError} when a host variable or a secret survived
82
+ * masking, so that the recording is never stored with it.
83
+ */
84
+ finish(verdict: Recording.Verdict): Recording.Type;
85
+ }
@@ -0,0 +1,288 @@
1
+ import z from "zod";
2
+ export declare namespace Recording {
3
+ type Id = z.infer<typeof Recording.Id>;
4
+ type Input = z.infer<typeof Recording.Input>;
5
+ type Content = z.infer<typeof Recording.Content>;
6
+ type ToolCall = z.infer<typeof Recording.ToolCall>;
7
+ type ToolResult = z.infer<typeof Recording.ToolResult>;
8
+ type ToolStep = z.infer<typeof Recording.ToolStep>;
9
+ type NarrationStep = z.infer<typeof Recording.NarrationStep>;
10
+ type Step = z.infer<typeof Recording.Step>;
11
+ type Verdict = z.infer<typeof Recording.Verdict>;
12
+ type Type = z.infer<typeof Recording.Schema>;
13
+ }
14
+ /**
15
+ * A replayable recording of a test run: every tool call the host agent made,
16
+ * paired with its result, and the agent's narration in between.
17
+ *
18
+ * Agent-neutral on purpose. The host (e.g. a test runner or the CLI)
19
+ * owns the agent loop and translates its own messages into the calls and
20
+ * results recorded here, so nothing in a recording is specific to the agent
21
+ * that made it but its name.
22
+ */
23
+ export declare abstract class Recording {
24
+ static Id: z.core.$ZodBranded<z.ZodString, "Recording.Id", "out">;
25
+ static Input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
26
+ /**
27
+ * MCP content blocks a tool returned, or a bare string for a host tool that
28
+ * returns plain output. Only text blocks are ever read, every other block is
29
+ * kept as it was.
30
+ */
31
+ static Content: z.ZodUnion<
32
+ readonly [
33
+ z.ZodString,
34
+ z.ZodArray<
35
+ z.ZodObject<
36
+ {
37
+ type: z.ZodString;
38
+ },
39
+ z.core.$loose
40
+ >
41
+ >,
42
+ ]
43
+ >;
44
+ static ToolCall: z.ZodObject<
45
+ {
46
+ id: z.ZodString;
47
+ server: z.ZodOptional<z.ZodString>;
48
+ tool: z.ZodString;
49
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
50
+ },
51
+ z.core.$strip
52
+ >;
53
+ static ToolResult: z.ZodObject<
54
+ {
55
+ content: z.ZodUnion<
56
+ readonly [
57
+ z.ZodString,
58
+ z.ZodArray<
59
+ z.ZodObject<
60
+ {
61
+ type: z.ZodString;
62
+ },
63
+ z.core.$loose
64
+ >
65
+ >,
66
+ ]
67
+ >;
68
+ isError: z.ZodBoolean;
69
+ },
70
+ z.core.$strip
71
+ >;
72
+ /**
73
+ * A tool call and its result, whether it went to Alumnium's own MCP server,
74
+ * another MCP server or a host tool - `call.server` tells them apart. Replay
75
+ * re-issues every one, since a tool call can produce values later calls
76
+ * consume.
77
+ */
78
+ static ToolStep: z.ZodObject<
79
+ {
80
+ kind: z.ZodLiteral<"tool-call">;
81
+ call: z.ZodObject<
82
+ {
83
+ id: z.ZodString;
84
+ server: z.ZodOptional<z.ZodString>;
85
+ tool: z.ZodString;
86
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
87
+ },
88
+ z.core.$strip
89
+ >;
90
+ result: z.ZodObject<
91
+ {
92
+ content: z.ZodUnion<
93
+ readonly [
94
+ z.ZodString,
95
+ z.ZodArray<
96
+ z.ZodObject<
97
+ {
98
+ type: z.ZodString;
99
+ },
100
+ z.core.$loose
101
+ >
102
+ >,
103
+ ]
104
+ >;
105
+ isError: z.ZodBoolean;
106
+ },
107
+ z.core.$strip
108
+ >;
109
+ },
110
+ z.core.$strip
111
+ >;
112
+ /**
113
+ * Something the agent said rather than did: a piece of its thinking, or a
114
+ * message meant for the person running the test. Recorded so that a replay
115
+ * reads like the recording it came from, and never executed.
116
+ */
117
+ static NarrationStep: z.ZodObject<
118
+ {
119
+ kind: z.ZodLiteral<"narration">;
120
+ narration: z.ZodEnum<{
121
+ assistant: "assistant";
122
+ thinking: "thinking";
123
+ }>;
124
+ text: z.ZodString;
125
+ },
126
+ z.core.$strip
127
+ >;
128
+ static Step: z.ZodUnion<
129
+ readonly [
130
+ z.ZodObject<
131
+ {
132
+ kind: z.ZodLiteral<"tool-call">;
133
+ call: z.ZodObject<
134
+ {
135
+ id: z.ZodString;
136
+ server: z.ZodOptional<z.ZodString>;
137
+ tool: z.ZodString;
138
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
139
+ },
140
+ z.core.$strip
141
+ >;
142
+ result: z.ZodObject<
143
+ {
144
+ content: z.ZodUnion<
145
+ readonly [
146
+ z.ZodString,
147
+ z.ZodArray<
148
+ z.ZodObject<
149
+ {
150
+ type: z.ZodString;
151
+ },
152
+ z.core.$loose
153
+ >
154
+ >,
155
+ ]
156
+ >;
157
+ isError: z.ZodBoolean;
158
+ },
159
+ z.core.$strip
160
+ >;
161
+ },
162
+ z.core.$strip
163
+ >,
164
+ z.ZodObject<
165
+ {
166
+ kind: z.ZodLiteral<"narration">;
167
+ narration: z.ZodEnum<{
168
+ assistant: "assistant";
169
+ thinking: "thinking";
170
+ }>;
171
+ text: z.ZodString;
172
+ },
173
+ z.core.$strip
174
+ >,
175
+ ]
176
+ >;
177
+ static Verdict: z.ZodObject<
178
+ {
179
+ status: z.ZodEnum<{
180
+ failure: "failure";
181
+ success: "success";
182
+ }>;
183
+ details: z.ZodString;
184
+ },
185
+ z.core.$strip
186
+ >;
187
+ static DEFAULT_VERSION: number;
188
+ static Schema: z.ZodObject<
189
+ {
190
+ id: z.core.$ZodBranded<z.ZodString, "Recording.Id", "out">;
191
+ version: z.ZodNumber;
192
+ text: z.ZodString;
193
+ agent: z.ZodString;
194
+ alumnium: z.ZodObject<
195
+ {
196
+ version: z.ZodString;
197
+ },
198
+ z.core.$strip
199
+ >;
200
+ hostVars: z.ZodArray<z.ZodString>;
201
+ steps: z.ZodArray<
202
+ z.ZodUnion<
203
+ readonly [
204
+ z.ZodObject<
205
+ {
206
+ kind: z.ZodLiteral<"tool-call">;
207
+ call: z.ZodObject<
208
+ {
209
+ id: z.ZodString;
210
+ server: z.ZodOptional<z.ZodString>;
211
+ tool: z.ZodString;
212
+ input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
213
+ },
214
+ z.core.$strip
215
+ >;
216
+ result: z.ZodObject<
217
+ {
218
+ content: z.ZodUnion<
219
+ readonly [
220
+ z.ZodString,
221
+ z.ZodArray<
222
+ z.ZodObject<
223
+ {
224
+ type: z.ZodString;
225
+ },
226
+ z.core.$loose
227
+ >
228
+ >,
229
+ ]
230
+ >;
231
+ isError: z.ZodBoolean;
232
+ },
233
+ z.core.$strip
234
+ >;
235
+ },
236
+ z.core.$strip
237
+ >,
238
+ z.ZodObject<
239
+ {
240
+ kind: z.ZodLiteral<"narration">;
241
+ narration: z.ZodEnum<{
242
+ assistant: "assistant";
243
+ thinking: "thinking";
244
+ }>;
245
+ text: z.ZodString;
246
+ },
247
+ z.core.$strip
248
+ >,
249
+ ]
250
+ >
251
+ >;
252
+ verdict: z.ZodObject<
253
+ {
254
+ status: z.ZodEnum<{
255
+ failure: "failure";
256
+ success: "success";
257
+ }>;
258
+ details: z.ZodString;
259
+ },
260
+ z.core.$strip
261
+ >;
262
+ },
263
+ z.core.$strip
264
+ >;
265
+ /**
266
+ * Converts test text to a recording ID using a hash function.
267
+ *
268
+ * @param text - Test text to convert to ID.
269
+ * @returns Recording ID generated from the trimmed text.
270
+ */
271
+ static textToId(text: string): Recording.Id;
272
+ /**
273
+ * Key a tool call is configured by, e.g. in `maskResponsePaths`:
274
+ * `<server>/<tool>` for an MCP tool, the bare tool name for a host tool.
275
+ *
276
+ * @param call - Tool call to get the key of.
277
+ * @returns Tool key.
278
+ */
279
+ static toolKey(call: Pick<Recording.ToolCall, "server" | "tool">): string;
280
+ /**
281
+ * Number of steps a replay executes, which is everything the agent did as
282
+ * opposed to said.
283
+ *
284
+ * @param recording - Recording to count the steps of.
285
+ * @returns Number of executable steps.
286
+ */
287
+ static executableStepsCount(recording: Recording.Type): number;
288
+ }
@@ -0,0 +1,4 @@
1
+ export declare class RecordingLeakError extends Error {
2
+ readonly masks: string[];
3
+ constructor(masks: string[]);
4
+ }
@@ -0,0 +1,38 @@
1
+ import type { Recording } from "./Recording.ts";
2
+ export declare namespace TestCache {
3
+ interface LookupOptions {
4
+ /**
5
+ * Recording version to accept, `Recording.DEFAULT_VERSION` by default. A
6
+ * recording of another version is a miss, see `Recording.Schema` `version`.
7
+ */
8
+ version?: number | undefined;
9
+ }
10
+ interface Lookups {
11
+ hits: number;
12
+ misses: number;
13
+ }
14
+ }
15
+ export interface TestCache {
16
+ readonly lookups: TestCache.Lookups;
17
+ /**
18
+ * Looks up the recording of a test, staged or saved.
19
+ *
20
+ * @param text - Test text.
21
+ * @param options - Lookup options.
22
+ * @returns Recording if found in the accepted version, `null` otherwise.
23
+ */
24
+ lookup(
25
+ text: string,
26
+ options?: TestCache.LookupOptions,
27
+ ): Promise<Recording.Type | null>;
28
+ /**
29
+ * Stages a recording, replacing any other one of the same test.
30
+ *
31
+ * @param recording - Recording to stage.
32
+ */
33
+ update(recording: Recording.Type): Promise<void>;
34
+ /** Persists the staged recordings. */
35
+ save(): Promise<void>;
36
+ /** Drops the staged recordings. */
37
+ discard(): Promise<void>;
38
+ }