@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.
@@ -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, { mode: "record", cassettesDir: CASSETTE_DIR });
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, { mode: "record", cassettesDir: CASSETTE_DIR });
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, { mode: "record", cassettesDir: CASSETTE_DIR });
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, { mode: "record", cassettesDir: CASSETTE_DIR });
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
+ });