@node-llm/testing 0.1.0 → 0.2.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/CHANGELOG.md +22 -1
- package/LICENSE +21 -0
- package/README.md +42 -3
- package/dist/Mocker.d.ts +27 -2
- package/dist/Mocker.d.ts.map +1 -1
- package/dist/Mocker.js +43 -3
- package/dist/Scrubber.d.ts.map +1 -1
- package/dist/Scrubber.js +25 -0
- package/dist/Serializer.d.ts +16 -0
- package/dist/Serializer.d.ts.map +1 -0
- package/dist/Serializer.js +107 -0
- package/dist/vcr.d.ts +8 -6
- package/dist/vcr.d.ts.map +1 -1
- package/dist/vcr.js +11 -11
- package/package.json +8 -8
- package/src/Mocker.ts +67 -3
- package/src/Scrubber.ts +30 -0
- package/src/Serializer.ts +113 -0
- package/src/vcr.ts +37 -27
- package/test/cassettes/handles-rich-types.json +38 -0
- package/test/unit/__snapshots__/mocker_snapshots.test.ts.snap +12 -0
- package/test/unit/dx.test.ts +1 -1
- package/test/unit/mocker_history.test.ts +115 -0
- package/test/unit/mocker_snapshots.test.ts +48 -0
- package/test/unit/scoping.test.ts +8 -4
- package/test/unit/scrubbing.test.ts +9 -2
- package/test/unit/serializer.test.ts +81 -0
- package/test/unit/vcr-global-config.test.ts +15 -3
- package/test/unit/vcr-mismatch.test.ts +2 -1
- package/test/unit/vcr_advanced_types.test.ts +72 -0
|
@@ -17,7 +17,7 @@ describe("VCR Feature 4: Automatic Scrubbing", () => {
|
|
|
17
17
|
CASSETTE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "vcr-scrub-test-"));
|
|
18
18
|
CASSETTE_PATH = path.join(CASSETTE_DIR, `${CASSETTE_NAME}.json`);
|
|
19
19
|
mock = new MockProvider();
|
|
20
|
-
providerRegistry.register("mock-provider", () => mock);
|
|
20
|
+
providerRegistry.register("mock-provider", () => mock as any);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
afterEach(() => {
|
|
@@ -29,7 +29,11 @@ describe("VCR Feature 4: Automatic Scrubbing", () => {
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
test("Automatically scrubs API keys and sensitive JSON keys", async () => {
|
|
32
|
-
const vcr = setupVCR(CASSETTE_NAME, {
|
|
32
|
+
const vcr = setupVCR(CASSETTE_NAME, {
|
|
33
|
+
mode: "record",
|
|
34
|
+
cassettesDir: CASSETTE_DIR,
|
|
35
|
+
_allowRecordingInCI: true
|
|
36
|
+
});
|
|
33
37
|
const llm = NodeLLM.withProvider("mock-provider");
|
|
34
38
|
|
|
35
39
|
// 1. Trigger request with secrets
|
|
@@ -50,6 +54,7 @@ describe("VCR Feature 4: Automatic Scrubbing", () => {
|
|
|
50
54
|
const vcr = setupVCR(CASSETTE_NAME, {
|
|
51
55
|
mode: "record",
|
|
52
56
|
cassettesDir: CASSETTE_DIR,
|
|
57
|
+
_allowRecordingInCI: true,
|
|
53
58
|
scrub: (data: unknown) => {
|
|
54
59
|
// Deep string replacement on the whole interaction object
|
|
55
60
|
return JSON.parse(JSON.stringify(data).replace(/sensitive-info/g, "XXXX"));
|
|
@@ -69,6 +74,7 @@ describe("VCR Feature 4: Automatic Scrubbing", () => {
|
|
|
69
74
|
const vcr = setupVCR("custom-scrub-config", {
|
|
70
75
|
mode: "record",
|
|
71
76
|
cassettesDir: CASSETTE_DIR,
|
|
77
|
+
_allowRecordingInCI: true,
|
|
72
78
|
sensitiveKeys: ["user_email", "internal_id"],
|
|
73
79
|
sensitivePatterns: [/secret-project-[a-z]+/g]
|
|
74
80
|
});
|
|
@@ -90,6 +96,7 @@ describe("VCR Feature 4: Automatic Scrubbing", () => {
|
|
|
90
96
|
const vcr = setupVCR("defaults-plus-custom", {
|
|
91
97
|
mode: "record",
|
|
92
98
|
cassettesDir: CASSETTE_DIR,
|
|
99
|
+
_allowRecordingInCI: true,
|
|
93
100
|
sensitiveKeys: ["custom_field"]
|
|
94
101
|
});
|
|
95
102
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import { Serializer } from "../../src/Serializer.js";
|
|
3
|
+
|
|
4
|
+
describe("Serializer", () => {
|
|
5
|
+
test("Handles Date objects", () => {
|
|
6
|
+
const data = { date: new Date("2023-01-01T00:00:00.000Z") };
|
|
7
|
+
const serialized = Serializer.serialize(data);
|
|
8
|
+
const deserialized = Serializer.deserialize(serialized);
|
|
9
|
+
expect(deserialized).toEqual(data);
|
|
10
|
+
expect((deserialized as any).date).toBeInstanceOf(Date);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("Handles Map objects", () => {
|
|
14
|
+
const data = {
|
|
15
|
+
map: new Map<string, string | number>([
|
|
16
|
+
["key", "value"],
|
|
17
|
+
["num", 123]
|
|
18
|
+
])
|
|
19
|
+
};
|
|
20
|
+
const serialized = Serializer.serialize(data);
|
|
21
|
+
const deserialized = Serializer.deserialize(serialized);
|
|
22
|
+
expect(deserialized).toEqual(data);
|
|
23
|
+
expect((deserialized as any).map).toBeInstanceOf(Map);
|
|
24
|
+
expect((deserialized as any).map.get("key")).toBe("value");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("Handles Set objects", () => {
|
|
28
|
+
const data = { set: new Set([1, 2, 3, "four"]) };
|
|
29
|
+
const serialized = Serializer.serialize(data);
|
|
30
|
+
const deserialized = Serializer.deserialize(serialized);
|
|
31
|
+
expect(deserialized).toEqual(data);
|
|
32
|
+
expect((deserialized as any).set).toBeInstanceOf(Set);
|
|
33
|
+
expect((deserialized as any).set.has("four")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("Handles RegExp objects", () => {
|
|
37
|
+
const data = { regex: /abc/gi };
|
|
38
|
+
const serialized = Serializer.serialize(data);
|
|
39
|
+
const deserialized = Serializer.deserialize(serialized);
|
|
40
|
+
expect(deserialized).toEqual(data);
|
|
41
|
+
expect((deserialized as any).regex).toBeInstanceOf(RegExp);
|
|
42
|
+
expect((deserialized as any).regex.flags).toContain("g");
|
|
43
|
+
expect((deserialized as any).regex.flags).toContain("i");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Handles Error objects", () => {
|
|
47
|
+
const err = new Error("Test error");
|
|
48
|
+
err.name = "CustomError";
|
|
49
|
+
(err as any).customProp = "customValue";
|
|
50
|
+
|
|
51
|
+
const data = { error: err };
|
|
52
|
+
const serialized = Serializer.serialize(data);
|
|
53
|
+
const deserialized: any = Serializer.deserialize(serialized);
|
|
54
|
+
|
|
55
|
+
expect(deserialized.error).toBeInstanceOf(Error);
|
|
56
|
+
expect(deserialized.error.message).toBe("Test error");
|
|
57
|
+
expect(deserialized.error.name).toBe("CustomError");
|
|
58
|
+
expect(deserialized.error.customProp).toBe("customValue");
|
|
59
|
+
expect(deserialized.error.stack).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("Handles Buffer objects", () => {
|
|
63
|
+
if (typeof Buffer === "undefined") return; // Skip in browser-like envs if any
|
|
64
|
+
const data = { buf: Buffer.from("Hello World") };
|
|
65
|
+
const serialized = Serializer.serialize(data);
|
|
66
|
+
const deserialized: any = Serializer.deserialize(serialized);
|
|
67
|
+
|
|
68
|
+
expect(deserialized.buf).toBeInstanceOf(Buffer);
|
|
69
|
+
expect(deserialized.buf.toString()).toBe("Hello World");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("Handles Infinity and NaN", () => {
|
|
73
|
+
const data = { inf: Infinity, negInf: -Infinity, nan: NaN };
|
|
74
|
+
const serialized = Serializer.serialize(data);
|
|
75
|
+
const deserialized: any = Serializer.deserialize(serialized);
|
|
76
|
+
|
|
77
|
+
expect(deserialized.inf).toBe(Infinity);
|
|
78
|
+
expect(deserialized.negInf).toBe(-Infinity);
|
|
79
|
+
expect(Number.isNaN(deserialized.nan)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -33,7 +33,11 @@ describe("VCR: Global Configuration", () => {
|
|
|
33
33
|
const CASSETTE_NAME = "global-config-keys";
|
|
34
34
|
const CASSETTE_PATH = path.join(CASSETTE_DIR, `${CASSETTE_NAME}.json`);
|
|
35
35
|
|
|
36
|
-
const vcr = setupVCR(CASSETTE_NAME, {
|
|
36
|
+
const vcr = setupVCR(CASSETTE_NAME, {
|
|
37
|
+
mode: "record",
|
|
38
|
+
cassettesDir: CASSETTE_DIR,
|
|
39
|
+
_allowRecordingInCI: true
|
|
40
|
+
});
|
|
37
41
|
const llm = NodeLLM.withProvider("mock-provider");
|
|
38
42
|
|
|
39
43
|
await llm.chat().ask("regular question");
|
|
@@ -53,7 +57,11 @@ describe("VCR: Global Configuration", () => {
|
|
|
53
57
|
const CASSETTE_NAME = "global-config-patterns";
|
|
54
58
|
const CASSETTE_PATH = path.join(CASSETTE_DIR, `${CASSETTE_NAME}.json`);
|
|
55
59
|
|
|
56
|
-
const vcr = setupVCR(CASSETTE_NAME, {
|
|
60
|
+
const vcr = setupVCR(CASSETTE_NAME, {
|
|
61
|
+
mode: "record",
|
|
62
|
+
cassettesDir: CASSETTE_DIR,
|
|
63
|
+
_allowRecordingInCI: true
|
|
64
|
+
});
|
|
57
65
|
const llm = NodeLLM.withProvider("mock-provider");
|
|
58
66
|
|
|
59
67
|
await llm.chat().ask("Status of custom-secret-omega");
|
|
@@ -74,7 +82,11 @@ describe("VCR: Global Configuration", () => {
|
|
|
74
82
|
const CASSETTE_NAME = "global-config-reset";
|
|
75
83
|
const CASSETTE_PATH = path.join(CASSETTE_DIR, `${CASSETTE_NAME}.json`);
|
|
76
84
|
|
|
77
|
-
const vcr = setupVCR(CASSETTE_NAME, {
|
|
85
|
+
const vcr = setupVCR(CASSETTE_NAME, {
|
|
86
|
+
mode: "record",
|
|
87
|
+
cassettesDir: CASSETTE_DIR,
|
|
88
|
+
_allowRecordingInCI: true
|
|
89
|
+
});
|
|
78
90
|
const llm = NodeLLM.withProvider("mock-provider");
|
|
79
91
|
|
|
80
92
|
await llm.chat().ask("to_reset should not be redacted");
|
|
@@ -34,7 +34,8 @@ describe("VCR: Interaction Mismatch Detection", () => {
|
|
|
34
34
|
// First: Record with specific request
|
|
35
35
|
const vcrRecord = setupVCR(CASSETTE_NAME, {
|
|
36
36
|
mode: "record",
|
|
37
|
-
cassettesDir: CASSETTE_DIR
|
|
37
|
+
cassettesDir: CASSETTE_DIR,
|
|
38
|
+
_allowRecordingInCI: true
|
|
38
39
|
});
|
|
39
40
|
const llmRecord = NodeLLM.withProvider("mock-provider");
|
|
40
41
|
await llmRecord.chat().ask("Record this question");
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { test, expect, describe, afterEach } from "vitest";
|
|
2
|
+
import { withVCR, describeVCR } from "../../src/vcr.js";
|
|
3
|
+
import { NodeLLM, providerRegistry } from "@node-llm/core";
|
|
4
|
+
import { MockProvider } from "../helpers/MockProvider.js";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Serializer } from "../../src/Serializer.js";
|
|
8
|
+
|
|
9
|
+
describe("VCR Advanced Types Persistence", () => {
|
|
10
|
+
const cassettePath = "test/cassettes/vcr-advanced-types-persistence/handles-rich-types.json";
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
if (fs.existsSync(cassettePath)) {
|
|
14
|
+
// Clean up file
|
|
15
|
+
fs.rmSync(cassettePath);
|
|
16
|
+
// Try to clean up parent dir if empty (optional but good)
|
|
17
|
+
try {
|
|
18
|
+
fs.rmdirSync(path.dirname(cassettePath));
|
|
19
|
+
} catch {
|
|
20
|
+
/* ignore */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("Persists Date and Map in cassettes", async () => {
|
|
26
|
+
const date = new Date("2024-01-01T00:00:00.000Z");
|
|
27
|
+
const map = new Map<string, string>([["key", "value"]]);
|
|
28
|
+
|
|
29
|
+
await describeVCR("VCR Advanced Types Persistence", async () => {
|
|
30
|
+
providerRegistry.register("mock-provider", () => new MockProvider() as any);
|
|
31
|
+
|
|
32
|
+
// 1. Record Phase
|
|
33
|
+
await withVCR(
|
|
34
|
+
"Handles Rich Types",
|
|
35
|
+
{ mode: "record", _allowRecordingInCI: true },
|
|
36
|
+
async () => {
|
|
37
|
+
const llm = NodeLLM.withProvider("mock-provider");
|
|
38
|
+
|
|
39
|
+
// Pass rich types in params
|
|
40
|
+
await llm
|
|
41
|
+
.chat()
|
|
42
|
+
.withParams({
|
|
43
|
+
createdAt: date,
|
|
44
|
+
meta: map
|
|
45
|
+
})
|
|
46
|
+
.ask("Hello");
|
|
47
|
+
}
|
|
48
|
+
)();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 2. Verify Disk Content (Serialization)
|
|
52
|
+
expect(fs.existsSync(cassettePath)).toBe(true);
|
|
53
|
+
const rawContent = fs.readFileSync(cassettePath, "utf-8");
|
|
54
|
+
|
|
55
|
+
// Should NOT contain raw ISO string only, but the typed wrapper
|
|
56
|
+
expect(rawContent).toContain('"$type": "Date"');
|
|
57
|
+
expect(rawContent).toContain('"value": "2024-01-01T00:00:00.000Z"');
|
|
58
|
+
expect(rawContent).toContain('"$type": "Map"');
|
|
59
|
+
|
|
60
|
+
// 3. Replay/Load Phase (Deserialization)
|
|
61
|
+
// We manually load to verify the deserialization logic
|
|
62
|
+
const cassette = Serializer.deserialize<any>(rawContent);
|
|
63
|
+
const request = cassette.interactions[0].request;
|
|
64
|
+
|
|
65
|
+
// Check that params are restored as real instances
|
|
66
|
+
expect(request.createdAt).toBeInstanceOf(Date);
|
|
67
|
+
expect(request.createdAt.toISOString()).toBe(date.toISOString());
|
|
68
|
+
|
|
69
|
+
expect(request.meta).toBeInstanceOf(Map);
|
|
70
|
+
expect(request.meta.get("key")).toBe("value");
|
|
71
|
+
});
|
|
72
|
+
});
|