@node-llm/testing 0.1.0 → 0.2.0

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.
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Handles serialization and deserialization of complex types that JSON.stringify/parse
3
+ * natively lose (Date, Map, Set, RegExp, Error, Infinity, NaN, Buffer).
4
+ */
5
+ export class Serializer {
6
+ public static serialize(data: unknown, space?: string | number): string {
7
+ return JSON.stringify(data, Serializer.replacer, space);
8
+ }
9
+
10
+ public static deserialize<T = unknown>(json: string): T {
11
+ return JSON.parse(json, Serializer.reviver);
12
+ }
13
+
14
+ /**
15
+ * Deep clone that preserves types better than JSON.parse(JSON.stringify(x))
16
+ * Uses structuredClone if available (Node 17+), otherwise falls back to serialization.
17
+ */
18
+ public static clone<T>(data: T): T {
19
+ if (typeof globalThis.structuredClone === "function") {
20
+ try {
21
+ return globalThis.structuredClone(data);
22
+ } catch (err) {
23
+ // Fallback for types structuredClone might not handle or implementation quirks
24
+ }
25
+ }
26
+ return Serializer.deserialize(Serializer.serialize(data));
27
+ }
28
+
29
+ private static replacer(this: any, key: string, value: unknown): unknown {
30
+ const originalValue = this[key];
31
+
32
+ if (originalValue === Infinity) return { $type: "Infinity" };
33
+ if (originalValue === -Infinity) return { $type: "-Infinity" };
34
+ if (Number.isNaN(originalValue)) return { $type: "NaN" };
35
+
36
+ if (originalValue instanceof Date) {
37
+ return { $type: "Date", value: originalValue.toISOString() };
38
+ }
39
+
40
+ if (originalValue instanceof RegExp) {
41
+ return { $type: "RegExp", source: originalValue.source, flags: originalValue.flags };
42
+ }
43
+
44
+ if (originalValue instanceof Map) {
45
+ return { $type: "Map", value: Array.from(originalValue.entries()) };
46
+ }
47
+
48
+ if (originalValue instanceof Set) {
49
+ return { $type: "Set", value: Array.from(originalValue.values()) };
50
+ }
51
+
52
+ if (originalValue instanceof Error) {
53
+ return {
54
+ ...originalValue, // Capture literal properties attached to the error
55
+ $type: "Error",
56
+ name: originalValue.name,
57
+ message: originalValue.message,
58
+ stack: originalValue.stack,
59
+ cause: (originalValue as any).cause
60
+ };
61
+ }
62
+
63
+ // Handle Buffers (Node.js)
64
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(originalValue)) {
65
+ return { $type: "Buffer", value: originalValue.toString("base64") };
66
+ }
67
+
68
+ return value;
69
+ }
70
+
71
+ private static reviver(key: string, value: unknown): unknown {
72
+ if (value && typeof value === "object" && "$type" in value) {
73
+ const typedValue = value as { $type: string; value?: any; [key: string]: any };
74
+
75
+ switch (typedValue.$type) {
76
+ case "Date":
77
+ return new Date(typedValue.value);
78
+ case "RegExp":
79
+ return new RegExp(typedValue.source, typedValue.flags);
80
+ case "Map":
81
+ return new Map(typedValue.value);
82
+ case "Set":
83
+ return new Set(typedValue.value);
84
+ case "Infinity":
85
+ return Infinity;
86
+ case "-Infinity":
87
+ return -Infinity;
88
+ case "NaN":
89
+ return NaN;
90
+ case "Error": {
91
+ const err = new Error(typedValue.message);
92
+ err.name = typedValue.name;
93
+ if (typedValue.stack) err.stack = typedValue.stack;
94
+ if (typedValue.cause) (err as any).cause = typedValue.cause;
95
+ // Restore other properties
96
+ for (const k in typedValue) {
97
+ if (["$type", "name", "message", "stack", "cause"].includes(k)) continue;
98
+ (err as any)[k] = typedValue[k];
99
+ }
100
+ return err;
101
+ }
102
+ case "Buffer":
103
+ if (typeof Buffer !== "undefined") {
104
+ return Buffer.from(typedValue.value, "base64");
105
+ }
106
+ return typedValue.value;
107
+ default:
108
+ return value;
109
+ }
110
+ }
111
+ return value;
112
+ }
113
+ }
package/src/vcr.ts CHANGED
@@ -2,12 +2,13 @@ import { Provider, providerRegistry } from "@node-llm/core";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { Scrubber } from "./Scrubber.js";
5
+ import { Serializer } from "./Serializer.js";
5
6
 
6
7
  // Internal state for nested scoping (Feature 12)
7
8
  const currentVCRScopes: string[] = [];
8
9
 
9
10
  // Try to import Vitest's expect to get test state
10
- let vitestExpect: any;
11
+ let vitestExpect: { getState?: () => { currentTestName?: string } } | undefined;
11
12
  try {
12
13
  // @ts-ignore
13
14
  import("vitest").then((m) => {
@@ -21,9 +22,9 @@ export type VCRMode = "record" | "replay" | "auto" | "passthrough";
21
22
 
22
23
  export interface VCRInteraction {
23
24
  method: string;
24
- request: any;
25
- response: any;
26
- chunks?: any[];
25
+ request: unknown;
26
+ response: unknown;
27
+ chunks?: unknown[];
27
28
  }
28
29
 
29
30
  export interface VCRCassette {
@@ -40,11 +41,13 @@ export interface VCRCassette {
40
41
 
41
42
  export interface VCROptions {
42
43
  mode?: VCRMode;
43
- scrub?: (data: any) => any;
44
+ scrub?: (data: unknown) => unknown;
44
45
  cassettesDir?: string;
45
46
  scope?: string | string[]; // Allow single or multiple scopes
46
47
  sensitivePatterns?: RegExp[];
47
48
  sensitiveKeys?: string[];
49
+ /** @internal - For testing VCR library itself. Bypasses CI recording check. */
50
+ _allowRecordingInCI?: boolean;
48
51
  }
49
52
 
50
53
  export class VCR {
@@ -95,9 +98,10 @@ export class VCR {
95
98
  const initialMode = mergedOptions.mode || (process.env.VCR_MODE as VCRMode) || "auto";
96
99
  const isCI = !!process.env.CI;
97
100
  const exists = fs.existsSync(this.filePath);
101
+ const allowRecordingInCI = mergedOptions._allowRecordingInCI === true;
98
102
 
99
- // CI Enforcement
100
- if (isCI) {
103
+ // CI Enforcement (can be bypassed for VCR library's own unit tests)
104
+ if (isCI && !allowRecordingInCI) {
101
105
  if (initialMode === "record") {
102
106
  throw new Error(`VCR[${name}]: Recording cassettes is not allowed in CI.`);
103
107
  }
@@ -136,7 +140,7 @@ export class VCR {
136
140
  if (!exists) {
137
141
  throw new Error(`VCR[${name}]: Cassette not found at ${this.filePath}`);
138
142
  }
139
- this.cassette = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
143
+ this.cassette = Serializer.deserialize(fs.readFileSync(this.filePath, "utf-8"));
140
144
  } else {
141
145
  this.cassette = {
142
146
  name,
@@ -167,7 +171,7 @@ export class VCR {
167
171
  this.cassette.metadata.duration = duration;
168
172
  }
169
173
 
170
- fs.writeFileSync(this.filePath, JSON.stringify(this.cassette, null, 2));
174
+ fs.writeFileSync(this.filePath, Serializer.serialize(this.cassette, 2));
171
175
  }
172
176
  providerRegistry.setInterceptor(undefined);
173
177
  }
@@ -178,9 +182,9 @@ export class VCR {
178
182
 
179
183
  public async execute(
180
184
  method: string,
181
- originalMethod: (...args: any[]) => Promise<any>,
182
- request: any
183
- ): Promise<any> {
185
+ originalMethod: (...args: unknown[]) => Promise<unknown>,
186
+ request: unknown
187
+ ): Promise<unknown> {
184
188
  if (this.mode === "replay") {
185
189
  const interaction = this.cassette.interactions[this.interactionIndex++];
186
190
  if (!interaction) {
@@ -206,9 +210,9 @@ export class VCR {
206
210
 
207
211
  public async *executeStream(
208
212
  method: string,
209
- originalMethod: (...args: any[]) => AsyncIterable<any>,
210
- request: any
211
- ): AsyncIterable<any> {
213
+ originalMethod: (...args: unknown[]) => AsyncIterable<unknown>,
214
+ request: unknown
215
+ ): AsyncIterable<unknown> {
212
216
  if (this.mode === "replay") {
213
217
  const interaction = this.cassette.interactions[this.interactionIndex++];
214
218
  if (!interaction || !interaction.chunks) {
@@ -221,7 +225,7 @@ export class VCR {
221
225
  }
222
226
 
223
227
  const stream = originalMethod(request);
224
- const chunks: any[] = [];
228
+ const chunks: unknown[] = [];
225
229
 
226
230
  for await (const chunk of stream) {
227
231
  if (this.mode === "record") chunks.push(this.clone(chunk));
@@ -240,12 +244,8 @@ export class VCR {
240
244
  }
241
245
  }
242
246
 
243
- private clone(obj: any) {
244
- try {
245
- return JSON.parse(JSON.stringify(obj));
246
- } catch {
247
- return obj;
248
- }
247
+ private clone(obj: unknown): unknown {
248
+ return Serializer.clone(obj);
249
249
  }
250
250
 
251
251
  private slugify(text: string): string {
@@ -271,7 +271,7 @@ export function setupVCR(name: string, options: VCROptions = {}) {
271
271
  const method = prop.toString();
272
272
 
273
273
  if (typeof originalValue === "function" && EXECUTION_METHODS.includes(method)) {
274
- return function (...args: any[]) {
274
+ return function (...args: unknown[]) {
275
275
  if (method === "stream") {
276
276
  return vcr.executeStream(method, originalValue.bind(target), args[0]);
277
277
  }
@@ -297,14 +297,20 @@ export function withVCR(
297
297
  options: VCROptions,
298
298
  fn: () => Promise<void>
299
299
  ): () => Promise<void>;
300
- export function withVCR(...args: any[]): () => Promise<void> {
300
+ export function withVCR(
301
+ ...args: [
302
+ (() => Promise<void>) | string | VCROptions,
303
+ ((() => Promise<void>) | VCROptions)?,
304
+ (() => Promise<void>)?
305
+ ]
306
+ ): () => Promise<void> {
301
307
  // Capture scopes at initialization time
302
308
  const capturedScopes = [...currentVCRScopes];
303
309
 
304
310
  return async function () {
305
311
  let name: string | undefined;
306
312
  let options: VCROptions = {};
307
- let fn: () => Promise<void>;
313
+ let fn: (() => Promise<void>) | undefined;
308
314
 
309
315
  if (typeof args[0] === "function") {
310
316
  fn = args[0];
@@ -318,7 +324,11 @@ export function withVCR(...args: any[]): () => Promise<void> {
318
324
  }
319
325
  } else {
320
326
  options = args[0] || {};
321
- fn = args[1];
327
+ fn = args[1] as (() => Promise<void>) | undefined;
328
+ }
329
+
330
+ if (!fn) {
331
+ throw new Error("VCR: No test function provided.");
322
332
  }
323
333
 
324
334
  // Pass captured inherited scopes if not explicitly overridden
@@ -326,7 +336,7 @@ export function withVCR(...args: any[]): () => Promise<void> {
326
336
  options.scope = capturedScopes;
327
337
  }
328
338
 
329
- if (!name && vitestExpect) {
339
+ if (!name && vitestExpect?.getState) {
330
340
  const state = vitestExpect.getState();
331
341
  name = state.currentTestName || "unnamed-test";
332
342
  }
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "Handles Rich Types",
3
+ "version": "1.0",
4
+ "metadata": {
5
+ "recordedAt": "2026-01-26T14:26:32.372Z",
6
+ "duration": 5
7
+ },
8
+ "interactions": [
9
+ {
10
+ "method": "chat",
11
+ "request": {
12
+ "model": "mock-model",
13
+ "messages": [
14
+ {
15
+ "role": "user",
16
+ "content": "Hello"
17
+ }
18
+ ],
19
+ "max_tokens": 4096,
20
+ "headers": {},
21
+ "requestTimeout": 30000,
22
+ "createdAt": {},
23
+ "meta": {
24
+ "$type": "Map",
25
+ "value": [["key", "value"]]
26
+ }
27
+ },
28
+ "response": {
29
+ "content": "Response to Hello",
30
+ "usage": {
31
+ "input_tokens": 10,
32
+ "output_tokens": 10,
33
+ "total_tokens": 20
34
+ }
35
+ }
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,12 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`Mocker Feature: Prompt Snapshots > Snapshots chat messages 1`] = `
4
+ [
5
+ {
6
+ "content": "Hello",
7
+ "role": "user",
8
+ },
9
+ ]
10
+ `;
11
+
12
+ exports[`Mocker Feature: Prompt Snapshots > Snapshots embedding input 1`] = `"text"`;
@@ -70,7 +70,7 @@ describe("VCR Feature 5 & 6: DX Sugar & Auto-Naming", () => {
70
70
  configureVCR({ cassettesDir: GLOBAL_DIR });
71
71
 
72
72
  // 2. Run a test relying on global dir (explicit record mode to create cassette)
73
- await withVCR("global-dir-test", { mode: "record" }, async () => {
73
+ await withVCR("global-dir-test", { mode: "record", _allowRecordingInCI: true }, async () => {
74
74
  const llm = NodeLLM.withProvider("mock-provider");
75
75
  await llm.chat().ask("global dir test");
76
76
  })();
@@ -0,0 +1,115 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "vitest";
2
+ import { mockLLM } from "../../src/Mocker.js";
3
+ import { NodeLLM, providerRegistry, Provider } from "@node-llm/core";
4
+ import { MockProvider } from "../helpers/MockProvider.js";
5
+
6
+ describe("Mocker Feature: Call History", () => {
7
+ let mocker: ReturnType<typeof mockLLM>;
8
+
9
+ beforeEach(() => {
10
+ providerRegistry.register("mock-provider", () => new MockProvider() as unknown as Provider);
11
+ mocker = mockLLM();
12
+ });
13
+
14
+ afterEach(() => {
15
+ mocker.clear();
16
+ });
17
+
18
+ test("Records chat calls in history", async () => {
19
+ mocker.chat("Hello").respond("Hi");
20
+ mocker.chat("Bye").respond("Goodbye");
21
+
22
+ const llm = NodeLLM.withProvider("mock-provider");
23
+ await llm.chat().ask("Hello");
24
+ await llm.chat().ask("Bye");
25
+
26
+ expect(mocker.history.length).toBe(2);
27
+ expect(mocker.history[0]!.method).toBe("chat");
28
+ expect((mocker.history[0]!.args[0] as any).messages[0].content).toBe("Hello");
29
+ expect((mocker.history[1]!.args[0] as any).messages[0].content).toBe("Bye");
30
+ });
31
+
32
+ test("Records streaming calls in history", async () => {
33
+ mocker.chat("Stream me").stream(["chunk1", "chunk2"]);
34
+ const llm = NodeLLM.withProvider("mock-provider");
35
+
36
+ const stream = llm.chat().stream("Stream me");
37
+ for await (const _ of stream) {
38
+ /* consume */
39
+ }
40
+
41
+ expect(mocker.history.length).toBe(1);
42
+ expect(mocker.history[0]!.method).toBe("stream");
43
+ expect((mocker.history[0]!.args[0] as any).messages[0].content).toBe("Stream me");
44
+ });
45
+
46
+ test("getCalls filters by method", async () => {
47
+ mocker.chat("ChatCall").respond("OK");
48
+ mocker.embed("EmbedCall").respond({ vectors: [[0.1]] });
49
+
50
+ const llm = NodeLLM.withProvider("mock-provider");
51
+ await llm.chat().ask("ChatCall");
52
+ await llm.embed("EmbedCall");
53
+
54
+ const chatCalls = mocker.getCalls("chat");
55
+ const embedCalls = mocker.getCalls("embed");
56
+
57
+ expect(chatCalls.length).toBe(1);
58
+ expect(chatCalls[0]!.method).toBe("chat");
59
+
60
+ expect(embedCalls.length).toBe(1);
61
+ expect(embedCalls[0]!.method).toBe("embed");
62
+
63
+ expect(mocker.history.length).toBe(2);
64
+ });
65
+
66
+ test("getLastCall retrieves the most recent call", async () => {
67
+ mocker.chat("First").respond("1");
68
+ mocker.chat("Second").respond("2");
69
+
70
+ const llm = NodeLLM.withProvider("mock-provider");
71
+ await llm.chat().ask("First");
72
+ await llm.chat().ask("Second");
73
+
74
+ const last = mocker.getLastCall();
75
+ expect(last).toBeDefined();
76
+ expect((last?.args[0] as any).messages[0].content).toBe("Second");
77
+ });
78
+
79
+ test("getLastCall with method filtering checks types", async () => {
80
+ mocker.embed("Embed").respond({ vectors: [[1]] });
81
+ mocker.chat("Chat").respond("Hi");
82
+
83
+ const llm = NodeLLM.withProvider("mock-provider");
84
+ await llm.embed("Embed");
85
+ await llm.chat().ask("Chat");
86
+
87
+ const lastEmbed = mocker.getLastCall("embed");
88
+ expect(lastEmbed).toBeDefined();
89
+ expect(lastEmbed?.method).toBe("embed");
90
+
91
+ const lastChat = mocker.getLastCall("chat");
92
+ expect(lastChat).toBeDefined();
93
+ expect(lastChat?.method).toBe("chat");
94
+ });
95
+
96
+ test("resetHistory clears history but preserves mocks", async () => {
97
+ mocker.chat("Preserved").respond("Still here");
98
+
99
+ const llm = NodeLLM.withProvider("mock-provider");
100
+ await llm.chat().ask("Preserved");
101
+
102
+ expect(mocker.history.length).toBe(1);
103
+
104
+ mocker.resetHistory();
105
+
106
+ expect(mocker.history.length).toBe(0);
107
+
108
+ // Mocks should still work
109
+ const res = await llm.chat().ask("Preserved");
110
+ expect(res.content).toBe("Still here");
111
+
112
+ // New history recorded
113
+ expect(mocker.history.length).toBe(1);
114
+ });
115
+ });
@@ -0,0 +1,48 @@
1
+ import { test, expect, describe, afterEach, beforeEach } from "vitest";
2
+ import { mockLLM } from "../../src/Mocker.js";
3
+ import { NodeLLM, providerRegistry, Provider } from "@node-llm/core";
4
+ import { MockProvider } from "../helpers/MockProvider.js";
5
+
6
+ describe("Mocker Feature: Prompt Snapshots", () => {
7
+ let mocker: ReturnType<typeof mockLLM>;
8
+
9
+ beforeEach(() => {
10
+ providerRegistry.register("mock-provider", () => new MockProvider() as unknown as Provider);
11
+ mocker = mockLLM();
12
+ });
13
+
14
+ afterEach(() => {
15
+ mocker.clear();
16
+ });
17
+
18
+ test("Snapshots chat messages", async () => {
19
+ mocker.chat("Hello").respond("Hi");
20
+
21
+ // Test basic chat snapshot
22
+ const llm = NodeLLM.withProvider("mock-provider");
23
+ await llm.chat().ask("Hello");
24
+
25
+ const lastCall = mocker.getLastCall();
26
+ expect(lastCall).toBeDefined();
27
+ // Snapshot should contain just the user message
28
+ expect(lastCall!.prompt).toMatchSnapshot();
29
+ });
30
+
31
+ test("Snapshots embedding input", async () => {
32
+ mocker.embed("text").respond({ vectors: [[0.1]] });
33
+
34
+ const llm = NodeLLM.withProvider("mock-provider");
35
+ await llm.embed("text");
36
+
37
+ expect(mocker.getLastCall()?.prompt).toMatchSnapshot();
38
+ });
39
+
40
+ test("Snapshots paint prompt", async () => {
41
+ mocker.paint("A sunset").respond({ url: "img.png" });
42
+
43
+ const llm = NodeLLM.withProvider("mock-provider");
44
+ await llm.paint("A sunset");
45
+
46
+ expect(mocker.getLastCall()?.prompt).toBe("A sunset");
47
+ });
48
+ });
@@ -29,10 +29,14 @@ describe("VCR Feature 12: Hierarchical Scoping", () => {
29
29
  await describeVCR(LEVEL_1, () => {
30
30
  return describeVCR(LEVEL_2, async () => {
31
31
  // Use explicit mode: "record" to test cassette creation in temp dir
32
- const testFn = withVCR(TEST_NAME, { mode: "record" }, async () => {
33
- const llm = NodeLLM.withProvider("mock-provider");
34
- await llm.chat().ask("Trigger record");
35
- });
32
+ const testFn = withVCR(
33
+ TEST_NAME,
34
+ { mode: "record", _allowRecordingInCI: true },
35
+ async () => {
36
+ const llm = NodeLLM.withProvider("mock-provider");
37
+ await llm.chat().ask("Trigger record");
38
+ }
39
+ );
36
40
 
37
41
  await testFn();
38
42
  });
@@ -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
+ });