@ricsam/isolate-test-utils 0.1.4 → 0.1.6

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @ricsam/isolate-test-utils
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - new version
8
+ - Updated dependencies
9
+ - @ricsam/isolate-console@0.1.6
10
+ - @ricsam/isolate-core@0.1.6
11
+ - @ricsam/isolate-fetch@0.1.6
12
+ - @ricsam/isolate-fs@0.1.6
13
+ - @ricsam/isolate-runtime@0.1.7
14
+
15
+ ## 0.1.5
16
+
17
+ ### Patch Changes
18
+
19
+ - new API
20
+ - Updated dependencies
21
+ - @ricsam/isolate-runtime@0.1.5
22
+ - @ricsam/isolate-console@0.1.5
23
+ - @ricsam/isolate-core@0.1.5
24
+ - @ricsam/isolate-fetch@0.1.5
25
+ - @ricsam/isolate-fs@0.1.5
26
+
3
27
  ## 0.1.4
4
28
 
5
29
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @ricsam/isolate-test-utils
2
+
3
+ Testing utilities for isolated-vm V8 sandbox development.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm add @ricsam/isolate-test-utils
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { createRuntimeTestContext } from "@ricsam/isolate-test-utils";
15
+
16
+ // Create a test context with all APIs set up
17
+ const ctx = await createRuntimeTestContext({ fs: true });
18
+
19
+ // Set up mock response for fetch
20
+ ctx.setMockResponse({ status: 200, body: '{"data": "test"}' });
21
+
22
+ // Run code
23
+ await ctx.context.eval(`
24
+ (async () => {
25
+ console.log("Starting fetch...");
26
+ const response = await fetch("https://api.example.com/data");
27
+ const data = await response.json();
28
+ console.log("Got data:", data);
29
+ })()
30
+ `, { promise: true });
31
+
32
+ // Check captured logs
33
+ console.log(ctx.logs);
34
+ // [{ level: "log", args: ["Starting fetch..."] }, ...]
35
+
36
+ // Check captured fetch calls
37
+ console.log(ctx.fetchCalls);
38
+ // [{ url: "https://api.example.com/data", method: "GET", headers: [...] }]
39
+
40
+ // Cleanup
41
+ ctx.dispose();
42
+ ```
43
+
44
+ ## RuntimeTestContext
45
+
46
+ ```typescript
47
+ interface RuntimeTestContext {
48
+ isolate: ivm.Isolate;
49
+ context: ivm.Context;
50
+ tick(ms?: number): Promise<void>;
51
+ dispose(): void;
52
+ logs: Array<{ level: string; args: unknown[] }>;
53
+ fetchCalls: Array<{ url: string; method: string; headers: [string, string][] }>;
54
+ setMockResponse(response: MockResponse): void;
55
+ mockFs: MockFileSystem;
56
+ }
57
+
58
+ interface MockResponse {
59
+ status?: number;
60
+ body?: string;
61
+ headers?: Record<string, string>;
62
+ }
63
+ ```
64
+
65
+ ## Options
66
+
67
+ ```typescript
68
+ interface RuntimeTestContextOptions {
69
+ fs?: boolean; // Enable file system APIs with mock file system
70
+ }
71
+ ```
72
+
73
+ ## License
74
+
75
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/isolate-test-utils",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
package/src/index.test.ts CHANGED
@@ -328,16 +328,17 @@ describe("createFsTestContext", () => {
328
328
  describe("createRuntimeTestContext", () => {
329
329
  let ctx: RuntimeTestContext | undefined;
330
330
 
331
- afterEach(() => {
332
- ctx?.dispose();
331
+ afterEach(async () => {
332
+ await ctx?.dispose();
333
333
  ctx = undefined;
334
334
  });
335
335
 
336
336
  test("creates full runtime context", async () => {
337
337
  ctx = await createRuntimeTestContext();
338
- assert.ok(ctx.isolate);
339
- assert.ok(ctx.context);
340
- assert.ok(typeof ctx.tick === "function");
338
+ assert.ok(typeof ctx.eval === "function");
339
+ assert.ok(typeof ctx.clearTimers === "function");
340
+ assert.ok(typeof ctx.dispatchRequest === "function");
341
+ assert.ok(typeof ctx.dispose === "function");
341
342
  assert.ok(Array.isArray(ctx.logs));
342
343
  assert.ok(Array.isArray(ctx.fetchCalls));
343
344
  });
@@ -345,14 +346,14 @@ describe("createRuntimeTestContext", () => {
345
346
  test("captures console logs", async () => {
346
347
  ctx = await createRuntimeTestContext();
347
348
 
348
- ctx.context.evalSync('console.log("test message")');
349
- ctx.context.evalSync('console.warn("warning message")');
349
+ await ctx.eval('console.log("test message")');
350
+ await ctx.eval('console.warn("warning message")');
350
351
 
351
352
  assert.strictEqual(ctx.logs.length, 2);
352
- assert.strictEqual(ctx.logs[0].level, "log");
353
- assert.deepStrictEqual(ctx.logs[0].args, ["test message"]);
354
- assert.strictEqual(ctx.logs[1].level, "warn");
355
- assert.deepStrictEqual(ctx.logs[1].args, ["warning message"]);
353
+ assert.strictEqual(ctx.logs[0]!.level, "log");
354
+ assert.deepStrictEqual(ctx.logs[0]!.args, ["test message"]);
355
+ assert.strictEqual(ctx.logs[1]!.level, "warn");
356
+ assert.deepStrictEqual(ctx.logs[1]!.args, ["warning message"]);
356
357
  });
357
358
 
358
359
  test("captures and mocks fetch calls", async () => {
@@ -364,42 +365,40 @@ describe("createRuntimeTestContext", () => {
364
365
  headers: { "Content-Type": "application/json" },
365
366
  });
366
367
 
367
- const result = await ctx.context.eval(
368
- `
369
- (async () => {
370
- const response = await fetch("https://api.example.com/data");
371
- const json = await response.json();
372
- return JSON.stringify({ status: response.status, data: json });
373
- })()
374
- `,
375
- { promise: true }
376
- );
368
+ await ctx.eval(`
369
+ const response = await fetch("https://api.example.com/data");
370
+ const json = await response.json();
371
+ setResult({ status: response.status, data: json });
372
+ `);
377
373
 
378
- const parsed = JSON.parse(result as string);
379
- assert.strictEqual(parsed.status, 200);
380
- assert.deepStrictEqual(parsed.data, { data: "test" });
374
+ const result = ctx.getResult<{ status: number; data: { data: string } }>();
375
+ assert.strictEqual(result?.status, 200);
376
+ assert.deepStrictEqual(result?.data, { data: "test" });
381
377
 
382
378
  assert.strictEqual(ctx.fetchCalls.length, 1);
383
- assert.strictEqual(ctx.fetchCalls[0].url, "https://api.example.com/data");
384
- assert.strictEqual(ctx.fetchCalls[0].method, "GET");
379
+ assert.strictEqual(ctx.fetchCalls[0]!.url, "https://api.example.com/data");
380
+ assert.strictEqual(ctx.fetchCalls[0]!.method, "GET");
385
381
  });
386
382
 
387
- test("tick advances timers", async () => {
383
+ test("timers fire automatically with real time", async () => {
388
384
  ctx = await createRuntimeTestContext();
389
385
 
390
- ctx.context.evalSync(`
386
+ await ctx.eval(`
391
387
  globalThis.timerFired = false;
392
- setTimeout(() => { globalThis.timerFired = true; }, 100);
388
+ setTimeout(() => { globalThis.timerFired = true; }, 20);
393
389
  `);
394
390
 
395
- // Timer should not have fired yet
396
- assert.strictEqual(evalCode<boolean>(ctx.context, "timerFired"), false);
391
+ // Timer should not have fired yet - check via setResult
392
+ await ctx.eval('setResult(globalThis.timerFired)');
393
+ assert.strictEqual(ctx.getResult<boolean>(), false);
394
+ ctx.clearResult();
397
395
 
398
- // Advance time
399
- await ctx.tick(100);
396
+ // Wait for real time to pass
397
+ await new Promise((r) => setTimeout(r, 50));
400
398
 
401
399
  // Timer should have fired
402
- assert.strictEqual(evalCode<boolean>(ctx.context, "timerFired"), true);
400
+ await ctx.eval('setResult(globalThis.timerFired)');
401
+ assert.strictEqual(ctx.getResult<boolean>(), true);
403
402
  });
404
403
  });
405
404
 
@@ -447,10 +446,10 @@ describe("startIntegrationServer", () => {
447
446
 
448
447
  const requests = server.getRequests();
449
448
  assert.strictEqual(requests.length, 1);
450
- assert.strictEqual(requests[0].method, "POST");
451
- assert.strictEqual(requests[0].path, "/api/endpoint");
452
- assert.strictEqual(requests[0].headers["x-custom"], "value");
453
- assert.strictEqual(requests[0].body, "request body");
449
+ assert.strictEqual(requests[0]!.method, "POST");
450
+ assert.strictEqual(requests[0]!.path, "/api/endpoint");
451
+ assert.strictEqual(requests[0]!.headers["x-custom"], "value");
452
+ assert.strictEqual(requests[0]!.body, "request body");
454
453
  });
455
454
 
456
455
  test("clears requests", async () => {
@@ -1,4 +1,3 @@
1
- import type ivm from "isolated-vm";
2
1
  import { MockFileSystem } from "./mock-fs.ts";
3
2
 
4
3
  export interface MockResponse {
@@ -13,11 +12,14 @@ export interface RuntimeTestContextOptions {
13
12
  }
14
13
 
15
14
  export interface RuntimeTestContext {
16
- isolate: ivm.Isolate;
17
- context: ivm.Context;
18
- /** Advance virtual time and process pending timers */
19
- tick(ms?: number): Promise<void>;
20
- dispose(): void;
15
+ /** Execute code in the runtime (ES module mode, supports top-level await) */
16
+ eval(code: string): Promise<void>;
17
+ /** Clear all pending timers */
18
+ clearTimers(): void;
19
+ /** Dispatch an HTTP request to the serve() handler */
20
+ dispatchRequest(request: Request): Promise<Response>;
21
+ /** Dispose all resources */
22
+ dispose(): Promise<void>;
21
23
  /** Captured console.log calls */
22
24
  logs: Array<{ level: string; args: unknown[] }>;
23
25
  /** Captured fetch calls */
@@ -26,6 +28,13 @@ export interface RuntimeTestContext {
26
28
  setMockResponse(response: MockResponse): void;
27
29
  /** Mock file system (only available if fs option is true) */
28
30
  mockFs: MockFileSystem;
31
+ /**
32
+ * Get a result from the isolate. Call `await setResult(value)` in your eval code
33
+ * to pass a value back to the host.
34
+ */
35
+ getResult<T = unknown>(): T | undefined;
36
+ /** Clear the stored result */
37
+ clearResult(): void;
29
38
  }
30
39
 
31
40
  /**
@@ -38,15 +47,17 @@ export interface RuntimeTestContext {
38
47
  * // Set up mock response for fetch
39
48
  * ctx.setMockResponse({ status: 200, body: '{"data": "test"}' });
40
49
  *
41
- * // Run code
42
- * await ctx.context.eval(`
43
- * (async () => {
44
- * console.log("Starting fetch...");
45
- * const response = await fetch("https://api.example.com/data");
46
- * const data = await response.json();
47
- * console.log("Got data:", data);
48
- * })()
49
- * `, { promise: true });
50
+ * // Run code and pass result back via setResult
51
+ * await ctx.eval(`
52
+ * console.log("Starting fetch...");
53
+ * const response = await fetch("https://api.example.com/data");
54
+ * const data = await response.json();
55
+ * console.log("Got data:", data);
56
+ * setResult(data);
57
+ * `);
58
+ *
59
+ * // Get the result
60
+ * console.log(ctx.getResult()); // { data: "test" }
50
61
  *
51
62
  * // Check logs
52
63
  * console.log(ctx.logs); // [{ level: "log", args: ["Starting fetch..."] }, ...]
@@ -54,7 +65,7 @@ export interface RuntimeTestContext {
54
65
  * // Check fetch calls
55
66
  * console.log(ctx.fetchCalls); // [{ url: "https://api.example.com/data", method: "GET", ... }]
56
67
  *
57
- * ctx.dispose();
68
+ * await ctx.dispose();
58
69
  */
59
70
  export async function createRuntimeTestContext(
60
71
  options?: RuntimeTestContextOptions
@@ -75,6 +86,7 @@ export async function createRuntimeTestContext(
75
86
  }> = [];
76
87
 
77
88
  let mockResponse: MockResponse = { status: 200, body: "" };
89
+ let storedResult: unknown = undefined;
78
90
 
79
91
  // Create mock file system
80
92
  const mockFs = new MockFileSystem();
@@ -82,33 +94,43 @@ export async function createRuntimeTestContext(
82
94
  // Create runtime with configured handlers
83
95
  const runtime = await createRuntime({
84
96
  console: {
85
- onLog: (level: string, ...args: unknown[]) => {
86
- logs.push({ level, args });
97
+ onEntry: (entry) => {
98
+ if (entry.type === "output") {
99
+ logs.push({ level: entry.level, args: entry.args });
100
+ } else if (entry.type === "assert") {
101
+ logs.push({ level: "error", args: ["Assertion failed:", ...entry.args] });
102
+ }
87
103
  },
88
104
  },
89
- fetch: {
90
- onFetch: async (request: Request) => {
91
- // Capture fetch call
92
- fetchCalls.push({
93
- url: request.url,
94
- method: request.method,
95
- headers: [...request.headers.entries()],
96
- });
105
+ fetch: async (request: Request) => {
106
+ // Capture fetch call
107
+ fetchCalls.push({
108
+ url: request.url,
109
+ method: request.method,
110
+ headers: [...request.headers.entries()],
111
+ });
97
112
 
98
- // Return mock response
99
- return new Response(mockResponse.body ?? "", {
100
- status: mockResponse.status ?? 200,
101
- headers: mockResponse.headers,
102
- });
103
- },
113
+ // Return mock response
114
+ return new Response(mockResponse.body ?? "", {
115
+ status: mockResponse.status ?? 200,
116
+ headers: mockResponse.headers,
117
+ });
104
118
  },
105
119
  fs: opts.fs ? { getDirectory: async () => mockFs } : undefined,
120
+ customFunctions: {
121
+ setResult: {
122
+ fn: (value: unknown) => {
123
+ storedResult = value;
124
+ },
125
+ type: 'sync',
126
+ },
127
+ },
106
128
  });
107
129
 
108
130
  return {
109
- isolate: runtime.isolate,
110
- context: runtime.context,
111
- tick: runtime.tick.bind(runtime),
131
+ eval: runtime.eval.bind(runtime),
132
+ clearTimers: runtime.timers.clearAll.bind(runtime.timers),
133
+ dispatchRequest: runtime.fetch.dispatchRequest.bind(runtime.fetch),
112
134
  dispose: runtime.dispose.bind(runtime),
113
135
  logs,
114
136
  fetchCalls,
@@ -116,5 +138,11 @@ export async function createRuntimeTestContext(
116
138
  mockResponse = response;
117
139
  },
118
140
  mockFs,
141
+ getResult<T = unknown>(): T | undefined {
142
+ return storedResult as T | undefined;
143
+ },
144
+ clearResult() {
145
+ storedResult = undefined;
146
+ },
119
147
  };
120
148
  }
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "rootDir": "./src"
5
- },
6
- "include": ["src/**/*"],
7
- "exclude": ["node_modules", "dist", "**/*.test.ts"]
8
- }