@liquidmetal-ai/precip 1.0.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.
Files changed (78) hide show
  1. package/.prettierrc +9 -0
  2. package/CHANGELOG.md +8 -0
  3. package/eslint.config.mjs +28 -0
  4. package/package.json +53 -0
  5. package/src/engine/agent.ts +478 -0
  6. package/src/engine/llm-provider.test.ts +275 -0
  7. package/src/engine/llm-provider.ts +330 -0
  8. package/src/engine/stream-parser.ts +170 -0
  9. package/src/index.ts +142 -0
  10. package/src/mounts/mount-manager.test.ts +516 -0
  11. package/src/mounts/mount-manager.ts +327 -0
  12. package/src/mounts/mount-registry.ts +196 -0
  13. package/src/mounts/zod-to-string.test.ts +154 -0
  14. package/src/mounts/zod-to-string.ts +213 -0
  15. package/src/presets/agent-tools.ts +57 -0
  16. package/src/presets/index.ts +5 -0
  17. package/src/sandbox/README.md +1321 -0
  18. package/src/sandbox/bridges/README.md +571 -0
  19. package/src/sandbox/bridges/actor.test.ts +229 -0
  20. package/src/sandbox/bridges/actor.ts +195 -0
  21. package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
  22. package/src/sandbox/bridges/bucket.test.ts +300 -0
  23. package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
  24. package/src/sandbox/bridges/console-multiple.test.ts +187 -0
  25. package/src/sandbox/bridges/console.test.ts +157 -0
  26. package/src/sandbox/bridges/console.ts +122 -0
  27. package/src/sandbox/bridges/fetch.ts +93 -0
  28. package/src/sandbox/bridges/index.ts +78 -0
  29. package/src/sandbox/bridges/readable-stream.ts +323 -0
  30. package/src/sandbox/bridges/response.test.ts +154 -0
  31. package/src/sandbox/bridges/response.ts +123 -0
  32. package/src/sandbox/bridges/review-fixes.test.ts +331 -0
  33. package/src/sandbox/bridges/search.test.ts +475 -0
  34. package/src/sandbox/bridges/search.ts +264 -0
  35. package/src/sandbox/bridges/shared/body-methods.ts +93 -0
  36. package/src/sandbox/bridges/shared/cleanup.ts +112 -0
  37. package/src/sandbox/bridges/shared/convert.ts +76 -0
  38. package/src/sandbox/bridges/shared/headers.ts +181 -0
  39. package/src/sandbox/bridges/shared/index.ts +36 -0
  40. package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
  41. package/src/sandbox/bridges/shared/path-parser.ts +109 -0
  42. package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
  43. package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
  44. package/src/sandbox/bridges/shared/response-object.ts +280 -0
  45. package/src/sandbox/bridges/shared/result-builder.ts +130 -0
  46. package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
  47. package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
  48. package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
  49. package/src/sandbox/bridges/storage.ts +421 -0
  50. package/src/sandbox/bridges/text-decoder.ts +190 -0
  51. package/src/sandbox/bridges/text-encoder.ts +102 -0
  52. package/src/sandbox/bridges/types.ts +39 -0
  53. package/src/sandbox/bridges/utils.ts +123 -0
  54. package/src/sandbox/index.ts +6 -0
  55. package/src/sandbox/quickjs-wasm.d.ts +9 -0
  56. package/src/sandbox/sandbox.test.ts +191 -0
  57. package/src/sandbox/sandbox.ts +831 -0
  58. package/src/sandbox/test-helper.ts +43 -0
  59. package/src/sandbox/test-mocks.ts +154 -0
  60. package/src/sandbox/user-stream.test.ts +77 -0
  61. package/src/skills/frontmatter.test.ts +305 -0
  62. package/src/skills/frontmatter.ts +200 -0
  63. package/src/skills/index.ts +9 -0
  64. package/src/skills/skills-loader.test.ts +237 -0
  65. package/src/skills/skills-loader.ts +200 -0
  66. package/src/tools/actor-storage-tools.ts +250 -0
  67. package/src/tools/code-tools.test.ts +199 -0
  68. package/src/tools/code-tools.ts +444 -0
  69. package/src/tools/file-tools.ts +206 -0
  70. package/src/tools/registry.ts +125 -0
  71. package/src/tools/script-tools.ts +145 -0
  72. package/src/tools/smartbucket-tools.ts +203 -0
  73. package/src/tools/sql-tools.ts +213 -0
  74. package/src/tools/tool-factory.ts +119 -0
  75. package/src/types.ts +512 -0
  76. package/tsconfig.eslint.json +5 -0
  77. package/tsconfig.json +15 -0
  78. package/vitest.config.ts +33 -0
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Tests for bridge fixes: leaks, edge cases, and error handling
3
+ *
4
+ * These tests verify the fixes for issues identified in code review:
5
+ * 1. TextDecoder memory leak (cleanup handler)
6
+ * 2. Bridge installer error handling (try-catch)
7
+ * 3. Promise state check error handling
8
+ * 4. ReadableStream locked property (dynamic getter)
9
+ * 5. Error formatting with circular references (safe stringify)
10
+ * 6. TextDecoder silent fallback (explicit error)
11
+ * 7. CoalescingReader state reset on error
12
+ */
13
+
14
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
15
+ import { executeWithAsyncHost } from '../test-helper.js';
16
+ import type { BridgeInstaller } from './types.js';
17
+ import { sandboxAsync, sandboxSync } from '../../types.js';
18
+ import { createHttpbinMocks } from '../test-mocks.js';
19
+
20
+ let mockFetch: any;
21
+
22
+ function setupMockFetch() {
23
+ const mocks = createHttpbinMocks();
24
+ mockFetch = vi.fn(async (url: string, _init?: RequestInit) => {
25
+ const urlKey = url.split('?')[0];
26
+ const mock = mocks.get(urlKey);
27
+ if (!mock) {
28
+ throw new Error(`No mock for: ${urlKey}`);
29
+ }
30
+ if (mock.delay) {
31
+ await new Promise(resolve => setTimeout(resolve, mock.delay));
32
+ }
33
+ return new Response(mock.body, {
34
+ status: mock.status ?? 200,
35
+ statusText: mock.statusText ?? 'OK',
36
+ headers: new Headers(mock.headers)
37
+ });
38
+ });
39
+ globalThis.fetch = mockFetch;
40
+ }
41
+
42
+ function restoreFetch() {
43
+ vi.restoreAllMocks();
44
+ }
45
+
46
+ describe('TextDecoder Cleanup', () => {
47
+ it('should not leak decoder instances across multiple executions', async () => {
48
+ // Run many iterations using TextDecoder with streaming mode
49
+ // If cleanup isn't working, memory would grow
50
+ for (let i = 0; i < 20; i++) {
51
+ const result = await executeWithAsyncHost(
52
+ `
53
+ const decoder = new TextDecoder();
54
+ // Use streaming mode which creates stateful decoder
55
+ const chunk1 = new Uint8Array([104, 101]); // "he"
56
+ const chunk2 = new Uint8Array([108, 108, 111]); // "llo"
57
+
58
+ const text1 = decoder.decode(chunk1, { stream: true });
59
+ const text2 = decoder.decode(chunk2, { stream: true });
60
+ const text3 = decoder.decode(); // flush
61
+
62
+ return text1 + text2 + text3
63
+ `,
64
+ {}
65
+ );
66
+
67
+ expect(result.success).toBe(true);
68
+ expect(result.result).toBe('hello');
69
+ }
70
+ // If we get here without memory issues, cleanup is working
71
+ });
72
+
73
+ it('should handle multiple TextDecoder instances in single execution', async () => {
74
+ const result = await executeWithAsyncHost(
75
+ `
76
+ const decoders = [];
77
+ for (let i = 0; i < 10; i++) {
78
+ decoders.push(new TextDecoder());
79
+ }
80
+
81
+ // Use all decoders
82
+ const results = decoders.map((d, i) => {
83
+ const bytes = new Uint8Array([48 + i]); // '0' to '9'
84
+ return d.decode(bytes);
85
+ });
86
+
87
+ return results.join('')
88
+ `,
89
+ {}
90
+ );
91
+
92
+ expect(result.success).toBe(true);
93
+ expect(result.result).toBe('0123456789');
94
+ });
95
+ });
96
+
97
+ describe('Bridge Installer Error Handling', () => {
98
+ it('should handle throwing bridge installer gracefully', async () => {
99
+ const throwingInstaller: BridgeInstaller = () => {
100
+ throw new Error('Bridge installer failed');
101
+ };
102
+
103
+ const result = await executeWithAsyncHost(
104
+ 'return 42',
105
+ {},
106
+ { bridgeInstallers: [throwingInstaller] }
107
+ );
108
+
109
+ expect(result.success).toBe(false);
110
+ expect(result.error).toContain('Bridge installer failed');
111
+ });
112
+
113
+ it('should execute installers in order and stop on first error', async () => {
114
+ const installOrder: number[] = [];
115
+
116
+ const installer1: BridgeInstaller = () => {
117
+ installOrder.push(1);
118
+ };
119
+
120
+ const installer2: BridgeInstaller = () => {
121
+ installOrder.push(2);
122
+ throw new Error('Installer 2 failed');
123
+ };
124
+
125
+ const installer3: BridgeInstaller = () => {
126
+ installOrder.push(3); // Should never be called
127
+ };
128
+
129
+ const result = await executeWithAsyncHost(
130
+ 'return 42',
131
+ {},
132
+ { bridgeInstallers: [installer1, installer2, installer3] }
133
+ );
134
+
135
+ expect(result.success).toBe(false);
136
+ expect(installOrder).toEqual([1, 2]); // installer3 should not run
137
+ });
138
+
139
+ it('should cleanup properly after installer error', async () => {
140
+ // Run multiple times to ensure no resource leaks
141
+ for (let i = 0; i < 5; i++) {
142
+ const throwingInstaller: BridgeInstaller = () => {
143
+ throw new Error('Intentional failure');
144
+ };
145
+
146
+ const result = await executeWithAsyncHost(
147
+ 'return 42',
148
+ {},
149
+ { bridgeInstallers: [throwingInstaller] }
150
+ );
151
+
152
+ expect(result.success).toBe(false);
153
+ }
154
+ });
155
+ });
156
+
157
+ describe('Promise State Error Handling', () => {
158
+ it('should handle promise rejection with string error', async () => {
159
+ const result = await executeWithAsyncHost(
160
+ `
161
+ throw new Error('Simple error message')
162
+ `,
163
+ {}
164
+ );
165
+
166
+ expect(result.success).toBe(false);
167
+ expect(result.error).toContain('Simple error message');
168
+ });
169
+
170
+ it('should handle promise rejection with object error', async () => {
171
+ const result = await executeWithAsyncHost(
172
+ `
173
+ const err = { code: 'ERR_001', message: 'Complex error' };
174
+ throw err
175
+ `,
176
+ {}
177
+ );
178
+
179
+ expect(result.success).toBe(false);
180
+ expect(result.error).toBeDefined();
181
+ });
182
+
183
+ it('should handle async function throwing error', async () => {
184
+ const globals = {
185
+ asyncThrow: sandboxAsync(async () => {
186
+ throw new Error('Async error');
187
+ })
188
+ };
189
+
190
+ const result = await executeWithAsyncHost(
191
+ `
192
+ return await asyncThrow()
193
+ `,
194
+ globals
195
+ );
196
+
197
+ expect(result.success).toBe(false);
198
+ expect(result.error).toContain('Async error');
199
+ });
200
+ });
201
+
202
+ describe('Safe Stringify for Error Formatting', () => {
203
+ it('should handle circular references in errors', async () => {
204
+ const result = await executeWithAsyncHost(
205
+ `
206
+ const obj = { a: 1 };
207
+ obj.self = obj; // Create circular reference
208
+ throw obj
209
+ `,
210
+ {}
211
+ );
212
+
213
+ expect(result.success).toBe(false);
214
+ // Should not crash - error should be stringified safely
215
+ expect(result.error).toBeDefined();
216
+ });
217
+
218
+ it('should handle undefined error', async () => {
219
+ const result = await executeWithAsyncHost(
220
+ `
221
+ throw undefined
222
+ `,
223
+ {}
224
+ );
225
+
226
+ expect(result.success).toBe(false);
227
+ expect(result.error).toBeDefined();
228
+ });
229
+
230
+ it('should handle null error', async () => {
231
+ const result = await executeWithAsyncHost(
232
+ `
233
+ throw null
234
+ `,
235
+ {}
236
+ );
237
+
238
+ expect(result.success).toBe(false);
239
+ expect(result.error).toBeDefined();
240
+ });
241
+ });
242
+
243
+ describe('TextDecoder Error Handling', () => {
244
+ it('should handle invalid input to decode()', async () => {
245
+ const result = await executeWithAsyncHost(
246
+ `
247
+ const decoder = new TextDecoder();
248
+ try {
249
+ // Try to decode a non-buffer value
250
+ decoder.decode("not a buffer");
251
+ return "no error"
252
+ } catch (e) {
253
+ return "error caught: " + e.message
254
+ }
255
+ `,
256
+ {}
257
+ );
258
+
259
+ expect(result.success).toBe(true);
260
+ expect(result.result).toContain('error caught');
261
+ });
262
+
263
+ it('should properly decode valid ArrayBuffer', async () => {
264
+ const result = await executeWithAsyncHost(
265
+ `
266
+ const decoder = new TextDecoder();
267
+ const buffer = new ArrayBuffer(5);
268
+ const view = new Uint8Array(buffer);
269
+ view[0] = 104; // 'h'
270
+ view[1] = 101; // 'e'
271
+ view[2] = 108; // 'l'
272
+ view[3] = 108; // 'l'
273
+ view[4] = 111; // 'o'
274
+ return decoder.decode(buffer)
275
+ `,
276
+ {}
277
+ );
278
+
279
+ expect(result.success).toBe(true);
280
+ expect(result.result).toBe('hello');
281
+ });
282
+ });
283
+
284
+ describe('ReadableStream Locked Property', () => {
285
+ beforeEach(() => setupMockFetch());
286
+ afterEach(() => restoreFetch());
287
+
288
+ it('should reflect locked state dynamically after getReader()', async () => {
289
+ const result = await executeWithAsyncHost(
290
+ `
291
+ const response = await fetch('https://httpbin.org/stream-bytes/10');
292
+ const body = response.body;
293
+
294
+ // Check initial state
295
+ const initialLocked = body.locked;
296
+
297
+ // Get reader - should lock the stream
298
+ const reader = body.getReader();
299
+
300
+ // Check locked state after getting reader
301
+ const afterLocked = body.locked;
302
+
303
+ // Read and release
304
+ const { done, value } = await reader.read();
305
+ reader.releaseLock();
306
+
307
+ // Check state after release
308
+ const afterRelease = body.locked;
309
+
310
+ return { initialLocked, afterLocked, afterRelease }
311
+ `,
312
+ {},
313
+ { timeoutMs: 10000 }
314
+ );
315
+
316
+ expect(result.success).toBe(true);
317
+ expect(result.result.initialLocked).toBe(false);
318
+ expect(result.result.afterLocked).toBe(true);
319
+ // Note: afterRelease might still be true if stream is consumed
320
+ });
321
+
322
+ it('should report locked=true when reader is active', async () => {
323
+ const result = await executeWithAsyncHost(
324
+ `
325
+ const response = await fetch('https://httpbin.org/stream-bytes/50');
326
+ const body = response.body;
327
+
328
+ const reader = body.getReader();
329
+ const isLocked = body.locked;
330
+
331
+ // Clean up
332
+ reader.releaseLock();
333
+
334
+ return isLocked
335
+ `,
336
+ {},
337
+ { timeoutMs: 10000 }
338
+ );
339
+
340
+ expect(result.success).toBe(true);
341
+ expect(result.result).toBe(true);
342
+ });
343
+ });
344
+
345
+ describe('Stream Error Handling', () => {
346
+ beforeEach(() => setupMockFetch());
347
+ afterEach(() => restoreFetch());
348
+ it('should handle stream read errors gracefully', async () => {
349
+ // Create a scenario where stream reading might fail
350
+ const globals = {
351
+ createFailingStream: sandboxSync(() => {
352
+ // Return a stream-like object that will fail
353
+ return {
354
+ getReader: () => ({
355
+ read: async () => {
356
+ throw new Error('Stream read failed');
357
+ },
358
+ releaseLock: () => {}
359
+ })
360
+ };
361
+ })
362
+ };
363
+
364
+ const result = await executeWithAsyncHost(
365
+ `
366
+ try {
367
+ const stream = createFailingStream();
368
+ const reader = stream.getReader();
369
+ await reader.read();
370
+ return "no error"
371
+ } catch (e) {
372
+ return "error: " + e.message
373
+ }
374
+ `,
375
+ globals
376
+ );
377
+
378
+ expect(result.success).toBe(true);
379
+ expect(result.result).toContain('error');
380
+ });
381
+ });
382
+
383
+ describe('Cleanup After Errors', () => {
384
+ beforeEach(() => setupMockFetch());
385
+ afterEach(() => restoreFetch());
386
+
387
+ it('should cleanup all resources after timeout', async () => {
388
+ const result = await executeWithAsyncHost(
389
+ `
390
+ // Start a long operation
391
+ while (true) {
392
+ // Infinite loop
393
+ }
394
+ `,
395
+ {},
396
+ { timeoutMs: 100 }
397
+ );
398
+
399
+ expect(result.success).toBe(false);
400
+ expect(result.error).toMatch(/timeout|interrupted/i);
401
+ });
402
+
403
+ it('should cleanup TextEncoder/TextDecoder after error', async () => {
404
+ // Run an execution that creates encoders but fails
405
+ const result = await executeWithAsyncHost(
406
+ `
407
+ const encoder = new TextEncoder();
408
+ const decoder = new TextDecoder();
409
+ const bytes = encoder.encode("test");
410
+ decoder.decode(bytes);
411
+
412
+ // Now fail
413
+ throw new Error('Intentional failure');
414
+ `,
415
+ {}
416
+ );
417
+
418
+ expect(result.success).toBe(false);
419
+
420
+ // Should be able to run another execution without issues
421
+ const result2 = await executeWithAsyncHost(
422
+ `
423
+ const encoder = new TextEncoder();
424
+ return encoder.encode("ok").length
425
+ `,
426
+ {}
427
+ );
428
+
429
+ expect(result2.success).toBe(true);
430
+ expect(result2.result).toBe(2);
431
+ });
432
+
433
+ it('should cleanup with active fetch when error occurs', async () => {
434
+ // This test verifies that starting a fetch and immediately throwing
435
+ // doesn't crash the process. The cleanup now detects lingering promise
436
+ // objects and skips runtime disposal to avoid the GC assertion.
437
+ for (let i = 0; i < 3; i++) {
438
+ const result = await executeWithAsyncHost(
439
+ `
440
+ // Start a fetch but throw before it completes
441
+ const p = fetch('https://httpbin.org/delay/5');
442
+ throw new Error('Interrupting');
443
+ `,
444
+ {},
445
+ { timeoutMs: 1000 }
446
+ );
447
+
448
+ expect(result.success).toBe(false);
449
+ expect(result.error).toContain('Interrupting');
450
+ }
451
+
452
+ // Verify the process still works after the edge case
453
+ const normalResult = await executeWithAsyncHost(`return 42`, {});
454
+ expect(normalResult.success).toBe(true);
455
+ expect(normalResult.result).toBe(42);
456
+ });
457
+ });
458
+
459
+ describe('Multiple Concurrent Async Operations', () => {
460
+ it('should handle rapid promise additions', async () => {
461
+ const code = `
462
+ const promises = [];
463
+ for (let i = 0; i < 50; i++) {
464
+ promises.push(asyncOp(i));
465
+ }
466
+ const results = await Promise.all(promises);
467
+ return results.reduce((a, b) => a + b, 0)
468
+ `;
469
+
470
+ const globals = {
471
+ asyncOp: sandboxAsync(async (n: number) => {
472
+ // Variable delays to create interleaving
473
+ await new Promise(r => setTimeout(r, Math.random() * 5));
474
+ return n;
475
+ })
476
+ };
477
+
478
+ const result = await executeWithAsyncHost(code, globals, { timeoutMs: 5000 });
479
+
480
+ expect(result.success).toBe(true);
481
+ // Sum of 0 to 49 = 1225
482
+ expect(result.result).toBe(1225);
483
+ });
484
+
485
+ it('should handle promise race conditions', async () => {
486
+ const code = `
487
+ const results = [];
488
+
489
+ // Fire off operations that resolve in different orders
490
+ // Using async host functions instead of setTimeout
491
+ const a = Promise.resolve('a');
492
+ const b = delay(10).then(() => 'b');
493
+ const c = Promise.resolve('c');
494
+
495
+ results.push(await a);
496
+ results.push(await c);
497
+ results.push(await b);
498
+
499
+ return results.join('')
500
+ `;
501
+
502
+ const globals = {
503
+ delay: sandboxAsync((ms: number) => new Promise(r => setTimeout(r, ms)))
504
+ };
505
+
506
+ const result = await executeWithAsyncHost(code, globals);
507
+
508
+ expect(result.success).toBe(true);
509
+ expect(result.result).toBe('acb');
510
+ });
511
+ });
512
+
513
+ describe('Resource Leak Prevention', () => {
514
+ beforeEach(() => setupMockFetch());
515
+ afterEach(() => restoreFetch());
516
+
517
+ it('should not leak handles with repeated JSON operations', async () => {
518
+ for (let i = 0; i < 30; i++) {
519
+ const result = await executeWithAsyncHost(
520
+ `
521
+ const data = { i: ${i}, nested: { arr: [1, 2, 3] } };
522
+ const json = JSON.stringify(data);
523
+ const parsed = JSON.parse(json);
524
+ return parsed.i + parsed.nested.arr.length
525
+ `,
526
+ {}
527
+ );
528
+
529
+ expect(result.success).toBe(true);
530
+ expect(result.result).toBe(i + 3);
531
+ }
532
+ });
533
+
534
+ it('should not leak with repeated fetch operations', async () => {
535
+ for (let i = 0; i < 5; i++) {
536
+ const result = await executeWithAsyncHost(
537
+ `
538
+ const response = await fetch('https://httpbin.org/uuid');
539
+ const data = await response.json();
540
+ return typeof data.uuid
541
+ `,
542
+ {},
543
+ { timeoutMs: 10000 }
544
+ );
545
+
546
+ expect(result.success).toBe(true);
547
+ expect(result.result).toBe('string');
548
+ }
549
+ });
550
+ });
551
+
552
+ describe('Edge Cases', () => {
553
+ it('should handle zero timeout gracefully', async () => {
554
+ const result = await executeWithAsyncHost(
555
+ `
556
+ return 42
557
+ `,
558
+ {},
559
+ { timeoutMs: 0 }
560
+ );
561
+
562
+ // With 0 timeout, synchronous code may still succeed since the timeout
563
+ // is checked between async operations, not during sync execution.
564
+ // The important thing is it doesn't crash or hang.
565
+ expect(result.success !== undefined).toBe(true);
566
+ });
567
+
568
+ it('should handle very small memory limit', async () => {
569
+ const result = await executeWithAsyncHost(
570
+ `
571
+ const arr = [];
572
+ for (let i = 0; i < 10000; i++) {
573
+ arr.push({ x: i, y: i * 2, z: "string".repeat(100) });
574
+ }
575
+ return arr.length
576
+ `,
577
+ {},
578
+ { memoryLimitBytes: 1024 * 1024 }
579
+ ); // 1MB
580
+
581
+ // Should either succeed with a smaller allocation or fail gracefully
582
+ expect(result.success !== undefined).toBe(true);
583
+ });
584
+
585
+ it('should handle empty code', async () => {
586
+ const result = await executeWithAsyncHost('', {});
587
+ expect(result.success).toBe(true);
588
+ expect(result.result).toBeUndefined();
589
+ });
590
+
591
+ it('should handle code with only comments', async () => {
592
+ const result = await executeWithAsyncHost(
593
+ `
594
+ // This is a comment
595
+ /* Multi-line
596
+ comment */
597
+ `,
598
+ {}
599
+ );
600
+ expect(result.success).toBe(true);
601
+ });
602
+
603
+ it('should handle code returning undefined', async () => {
604
+ const result = await executeWithAsyncHost('return undefined', {});
605
+ expect(result.success).toBe(true);
606
+ expect(result.result).toBeUndefined();
607
+ });
608
+
609
+ it('should handle code returning null', async () => {
610
+ const result = await executeWithAsyncHost('return null', {});
611
+ expect(result.success).toBe(true);
612
+ expect(result.result).toBeNull();
613
+ });
614
+ });