@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.
- package/.prettierrc +9 -0
- package/CHANGELOG.md +8 -0
- package/eslint.config.mjs +28 -0
- package/package.json +53 -0
- package/src/engine/agent.ts +478 -0
- package/src/engine/llm-provider.test.ts +275 -0
- package/src/engine/llm-provider.ts +330 -0
- package/src/engine/stream-parser.ts +170 -0
- package/src/index.ts +142 -0
- package/src/mounts/mount-manager.test.ts +516 -0
- package/src/mounts/mount-manager.ts +327 -0
- package/src/mounts/mount-registry.ts +196 -0
- package/src/mounts/zod-to-string.test.ts +154 -0
- package/src/mounts/zod-to-string.ts +213 -0
- package/src/presets/agent-tools.ts +57 -0
- package/src/presets/index.ts +5 -0
- package/src/sandbox/README.md +1321 -0
- package/src/sandbox/bridges/README.md +571 -0
- package/src/sandbox/bridges/actor.test.ts +229 -0
- package/src/sandbox/bridges/actor.ts +195 -0
- package/src/sandbox/bridges/bridge-fixes.test.ts +614 -0
- package/src/sandbox/bridges/bucket.test.ts +300 -0
- package/src/sandbox/bridges/cleanup-reproduction.test.ts +225 -0
- package/src/sandbox/bridges/console-multiple.test.ts +187 -0
- package/src/sandbox/bridges/console.test.ts +157 -0
- package/src/sandbox/bridges/console.ts +122 -0
- package/src/sandbox/bridges/fetch.ts +93 -0
- package/src/sandbox/bridges/index.ts +78 -0
- package/src/sandbox/bridges/readable-stream.ts +323 -0
- package/src/sandbox/bridges/response.test.ts +154 -0
- package/src/sandbox/bridges/response.ts +123 -0
- package/src/sandbox/bridges/review-fixes.test.ts +331 -0
- package/src/sandbox/bridges/search.test.ts +475 -0
- package/src/sandbox/bridges/search.ts +264 -0
- package/src/sandbox/bridges/shared/body-methods.ts +93 -0
- package/src/sandbox/bridges/shared/cleanup.ts +112 -0
- package/src/sandbox/bridges/shared/convert.ts +76 -0
- package/src/sandbox/bridges/shared/headers.ts +181 -0
- package/src/sandbox/bridges/shared/index.ts +36 -0
- package/src/sandbox/bridges/shared/json-helpers.ts +77 -0
- package/src/sandbox/bridges/shared/path-parser.ts +109 -0
- package/src/sandbox/bridges/shared/promise-helper.ts +108 -0
- package/src/sandbox/bridges/shared/registry-setup.ts +84 -0
- package/src/sandbox/bridges/shared/response-object.ts +280 -0
- package/src/sandbox/bridges/shared/result-builder.ts +130 -0
- package/src/sandbox/bridges/shared/scope-helpers.ts +44 -0
- package/src/sandbox/bridges/shared/stream-reader.ts +90 -0
- package/src/sandbox/bridges/storage-bridge.test.ts +893 -0
- package/src/sandbox/bridges/storage.ts +421 -0
- package/src/sandbox/bridges/text-decoder.ts +190 -0
- package/src/sandbox/bridges/text-encoder.ts +102 -0
- package/src/sandbox/bridges/types.ts +39 -0
- package/src/sandbox/bridges/utils.ts +123 -0
- package/src/sandbox/index.ts +6 -0
- package/src/sandbox/quickjs-wasm.d.ts +9 -0
- package/src/sandbox/sandbox.test.ts +191 -0
- package/src/sandbox/sandbox.ts +831 -0
- package/src/sandbox/test-helper.ts +43 -0
- package/src/sandbox/test-mocks.ts +154 -0
- package/src/sandbox/user-stream.test.ts +77 -0
- package/src/skills/frontmatter.test.ts +305 -0
- package/src/skills/frontmatter.ts +200 -0
- package/src/skills/index.ts +9 -0
- package/src/skills/skills-loader.test.ts +237 -0
- package/src/skills/skills-loader.ts +200 -0
- package/src/tools/actor-storage-tools.ts +250 -0
- package/src/tools/code-tools.test.ts +199 -0
- package/src/tools/code-tools.ts +444 -0
- package/src/tools/file-tools.ts +206 -0
- package/src/tools/registry.ts +125 -0
- package/src/tools/script-tools.ts +145 -0
- package/src/tools/smartbucket-tools.ts +203 -0
- package/src/tools/sql-tools.ts +213 -0
- package/src/tools/tool-factory.ts +119 -0
- package/src/types.ts +512 -0
- package/tsconfig.eslint.json +5 -0
- package/tsconfig.json +15 -0
- package/vitest.config.ts +33 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test storage bridge with mock bucket
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { executeWithAsyncHost } from '../test-helper.js';
|
|
7
|
+
import { installStorage, type StorageMountInfo } from './storage.js';
|
|
8
|
+
|
|
9
|
+
// Mock Bucket that returns a ReadableStream
|
|
10
|
+
function createMockBucket(files: Record<string, string>) {
|
|
11
|
+
return {
|
|
12
|
+
async get(key: string) {
|
|
13
|
+
const content = files[key];
|
|
14
|
+
if (!content) return null;
|
|
15
|
+
|
|
16
|
+
// Create a ReadableStream from the content
|
|
17
|
+
const encoder = new TextEncoder();
|
|
18
|
+
const data = encoder.encode(content);
|
|
19
|
+
|
|
20
|
+
// Simulate chunked delivery (small chunks to test coalescing)
|
|
21
|
+
const chunkSize = 100; // Small chunks to test coalescing
|
|
22
|
+
let offset = 0;
|
|
23
|
+
|
|
24
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
25
|
+
pull(controller) {
|
|
26
|
+
if (offset >= data.length) {
|
|
27
|
+
controller.close();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const chunk = data.slice(offset, offset + chunkSize);
|
|
31
|
+
controller.enqueue(chunk);
|
|
32
|
+
offset += chunkSize;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
key,
|
|
38
|
+
body: stream,
|
|
39
|
+
size: data.length,
|
|
40
|
+
uploaded: new Date(),
|
|
41
|
+
httpMetadata: { contentType: 'application/json' }
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async put(key: string, content: string) {
|
|
46
|
+
files[key] = content;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async list(options?: { prefix?: string; limit?: number }) {
|
|
50
|
+
const prefix = options?.prefix || '';
|
|
51
|
+
const objects = Object.keys(files)
|
|
52
|
+
.filter(k => k.startsWith(prefix))
|
|
53
|
+
.map(key => ({
|
|
54
|
+
key,
|
|
55
|
+
size: files[key].length,
|
|
56
|
+
uploaded: new Date()
|
|
57
|
+
}));
|
|
58
|
+
return { objects, truncated: false };
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async delete(key: string) {
|
|
62
|
+
delete files[key];
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('Storage Bridge', () => {
|
|
68
|
+
it('should read file with .text()', async () => {
|
|
69
|
+
const files = {
|
|
70
|
+
'test.txt': 'Hello, World!'
|
|
71
|
+
};
|
|
72
|
+
const mockBucket = createMockBucket(files);
|
|
73
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
74
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any }]
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const result = await executeWithAsyncHost(
|
|
78
|
+
`
|
|
79
|
+
const obj = await read("/data/test.txt");
|
|
80
|
+
return await obj.text();
|
|
81
|
+
`,
|
|
82
|
+
{},
|
|
83
|
+
{
|
|
84
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)]
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
console.log('text() result:', result);
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
expect(result.result).toBe('Hello, World!');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should read file with .json()', async () => {
|
|
94
|
+
const files = {
|
|
95
|
+
'data.json': JSON.stringify({ name: 'Test', value: 42 })
|
|
96
|
+
};
|
|
97
|
+
const mockBucket = createMockBucket(files);
|
|
98
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
99
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any }]
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
const result = await executeWithAsyncHost(
|
|
103
|
+
`
|
|
104
|
+
const obj = await read("/data/data.json");
|
|
105
|
+
return await obj.json();
|
|
106
|
+
`,
|
|
107
|
+
{},
|
|
108
|
+
{
|
|
109
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)]
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
console.log('json() result:', result);
|
|
114
|
+
expect(result.success).toBe(true);
|
|
115
|
+
expect(result.result).toEqual({ name: 'Test', value: 42 });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should stream file with body.getReader()', async () => {
|
|
119
|
+
// Create a larger file to test streaming and coalescing
|
|
120
|
+
const largeContent = 'X'.repeat(5000); // 5KB - will be chunked into 50 x 100-byte chunks
|
|
121
|
+
const files = {
|
|
122
|
+
'large.txt': largeContent
|
|
123
|
+
};
|
|
124
|
+
const mockBucket = createMockBucket(files);
|
|
125
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
126
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any }]
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
const result = await executeWithAsyncHost(
|
|
130
|
+
`
|
|
131
|
+
const obj = await read("/data/large.txt");
|
|
132
|
+
const reader = obj.body.getReader();
|
|
133
|
+
const decoder = new TextDecoder();
|
|
134
|
+
|
|
135
|
+
let totalBytes = 0;
|
|
136
|
+
let chunks = 0;
|
|
137
|
+
let content = '';
|
|
138
|
+
|
|
139
|
+
while (true) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done) break;
|
|
142
|
+
totalBytes += value.length;
|
|
143
|
+
chunks++;
|
|
144
|
+
content += decoder.decode(value, { stream: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { totalBytes, chunks, contentLength: content.length, matches: content === '${largeContent}' };
|
|
148
|
+
`,
|
|
149
|
+
{},
|
|
150
|
+
{
|
|
151
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)],
|
|
152
|
+
timeoutMs: 10000
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
console.log('stream result:', result);
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
expect(result.result.totalBytes).toBe(5000);
|
|
159
|
+
expect(result.result.matches).toBe(true);
|
|
160
|
+
|
|
161
|
+
// With 16KB coalescing and 100-byte source chunks, we should get fewer chunks
|
|
162
|
+
// (though 5KB is below the 16KB threshold so might still be multiple)
|
|
163
|
+
console.log(`Chunks: ${result.result.chunks} (coalesced from 50 source chunks)`);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should have Response-like properties', async () => {
|
|
167
|
+
const files = {
|
|
168
|
+
'test.txt': 'Hello'
|
|
169
|
+
};
|
|
170
|
+
const mockBucket = createMockBucket(files);
|
|
171
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
172
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any }]
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const result = await executeWithAsyncHost(
|
|
176
|
+
`
|
|
177
|
+
const obj = await read("/data/test.txt");
|
|
178
|
+
return {
|
|
179
|
+
key: obj.key,
|
|
180
|
+
size: obj.size,
|
|
181
|
+
ok: obj.ok,
|
|
182
|
+
status: obj.status,
|
|
183
|
+
hasBody: obj.body !== null,
|
|
184
|
+
hasText: typeof obj.text === 'function',
|
|
185
|
+
hasJson: typeof obj.json === 'function',
|
|
186
|
+
contentType: obj.headers.get('content-type')
|
|
187
|
+
};
|
|
188
|
+
`,
|
|
189
|
+
{},
|
|
190
|
+
{
|
|
191
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)]
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
console.log('properties result:', result);
|
|
196
|
+
expect(result.success).toBe(true);
|
|
197
|
+
expect(result.result.key).toBe('/data/test.txt');
|
|
198
|
+
expect(result.result.size).toBe(5);
|
|
199
|
+
expect(result.result.ok).toBe(true);
|
|
200
|
+
expect(result.result.status).toBe(200);
|
|
201
|
+
expect(result.result.hasBody).toBe(true);
|
|
202
|
+
expect(result.result.hasText).toBe(true);
|
|
203
|
+
expect(result.result.hasJson).toBe(true);
|
|
204
|
+
expect(result.result.contentType).toBe('application/json');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should handle file not found', async () => {
|
|
208
|
+
const files = {};
|
|
209
|
+
const mockBucket = createMockBucket(files);
|
|
210
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
211
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any }]
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const result = await executeWithAsyncHost(
|
|
215
|
+
`
|
|
216
|
+
try {
|
|
217
|
+
await read("/data/nonexistent.txt");
|
|
218
|
+
return { error: false };
|
|
219
|
+
} catch (e) {
|
|
220
|
+
return { error: true, message: String(e) };
|
|
221
|
+
}
|
|
222
|
+
`,
|
|
223
|
+
{},
|
|
224
|
+
{
|
|
225
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
console.log('not found result:', result);
|
|
230
|
+
expect(result.success).toBe(true);
|
|
231
|
+
expect(result.result.error).toBe(true);
|
|
232
|
+
expect(result.result.message).toContain('not found');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should write and list files', async () => {
|
|
236
|
+
const files: Record<string, string> = {};
|
|
237
|
+
const mockBucket = createMockBucket(files);
|
|
238
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
239
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any, mode: 'rw' }]
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
const result = await executeWithAsyncHost(
|
|
243
|
+
`
|
|
244
|
+
// Write some files
|
|
245
|
+
await write("/data/file1.txt", "content1");
|
|
246
|
+
await write("/data/file2.txt", "content2");
|
|
247
|
+
await write("/data/subdir/file3.txt", "content3");
|
|
248
|
+
|
|
249
|
+
// List all files
|
|
250
|
+
const all = await list("/data/");
|
|
251
|
+
|
|
252
|
+
// List with prefix
|
|
253
|
+
const subdir = await list("/data/subdir/");
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
allCount: all.count,
|
|
257
|
+
subdirCount: subdir.count,
|
|
258
|
+
allKeys: all.files.map(f => f.key)
|
|
259
|
+
};
|
|
260
|
+
`,
|
|
261
|
+
{},
|
|
262
|
+
{
|
|
263
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)]
|
|
264
|
+
}
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
console.log('write/list result:', result);
|
|
268
|
+
expect(result.success).toBe(true);
|
|
269
|
+
expect(result.result.allCount).toBe(3);
|
|
270
|
+
expect(result.result.subdirCount).toBe(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('should reject writes on read-only mounts', async () => {
|
|
274
|
+
const files: Record<string, string> = {};
|
|
275
|
+
const mockBucket = createMockBucket(files);
|
|
276
|
+
const storageMounts = new Map<string, StorageMountInfo>([
|
|
277
|
+
['data', { name: 'data', type: 'bucket', resource: mockBucket as any }] // no mode = read-only
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
const result = await executeWithAsyncHost(
|
|
281
|
+
`
|
|
282
|
+
try {
|
|
283
|
+
await write("/data/test.txt", "content");
|
|
284
|
+
return { error: false };
|
|
285
|
+
} catch (e) {
|
|
286
|
+
return { error: true, message: String(e) };
|
|
287
|
+
}
|
|
288
|
+
`,
|
|
289
|
+
{},
|
|
290
|
+
{
|
|
291
|
+
bridgeInstallers: [ctx => installStorage(ctx, storageMounts)]
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
console.log('read-only write result:', result);
|
|
296
|
+
expect(result.success).toBe(true);
|
|
297
|
+
expect(result.result.error).toBe(true);
|
|
298
|
+
expect(result.result.message).toContain('read-only');
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cleanup handling when sandbox code has unfinished async operations
|
|
3
|
+
*
|
|
4
|
+
* These scenarios previously caused JS_FreeRuntime assertion failures because:
|
|
5
|
+
* 1. A fetch request is in progress
|
|
6
|
+
* 2. The sandbox times out or throws an error
|
|
7
|
+
* 3. The underlying Response and ReadableStream objects are not aborted/closed
|
|
8
|
+
* 4. Internal stream operations leave GC objects alive
|
|
9
|
+
* 5. JS_FreeRuntime() would find non-empty GC object lists
|
|
10
|
+
*
|
|
11
|
+
* The fix: sandbox.ts now detects lingering promise objects in the GC list
|
|
12
|
+
* and skips runtime disposal in those cases, accepting a small memory leak
|
|
13
|
+
* instead of triggering the assertion.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
17
|
+
import { executeWithAsyncHost } from '../test-helper.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('Cleanup - Reproduce Assertion Failure', () => {
|
|
47
|
+
beforeEach(() => setupMockFetch());
|
|
48
|
+
afterEach(() => restoreFetch());
|
|
49
|
+
|
|
50
|
+
it('should handle timeout during fetch with active stream reader', async () => {
|
|
51
|
+
// This simulates a slow fetch that times out while reading
|
|
52
|
+
const code = `
|
|
53
|
+
const response = await fetch('https://httpbin.org/delay/10');
|
|
54
|
+
const reader = response.body.getReader();
|
|
55
|
+
|
|
56
|
+
let chunks = 0;
|
|
57
|
+
while (true) {
|
|
58
|
+
const { done, value } = await reader.read();
|
|
59
|
+
if (done) break;
|
|
60
|
+
chunks++;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return chunks
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const result = await executeWithAsyncHost(
|
|
67
|
+
code,
|
|
68
|
+
{},
|
|
69
|
+
{
|
|
70
|
+
timeoutMs: 500 // Much shorter than the 10s delay
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// The execution should fail due to timeout
|
|
75
|
+
expect(result.success).toBe(false);
|
|
76
|
+
expect(result.error).toMatch(/timeout|interrupted/);
|
|
77
|
+
|
|
78
|
+
// The cleanup now detects lingering GC objects and skips runtime disposal
|
|
79
|
+
// to avoid the assertion (accepting a memory leak in this edge case)
|
|
80
|
+
}, 10000);
|
|
81
|
+
|
|
82
|
+
it('should handle error during stream read', async () => {
|
|
83
|
+
// This uses a small timeout but still starts reading
|
|
84
|
+
const code = `
|
|
85
|
+
const response = await fetch('https://httpbin.org/delay/5');
|
|
86
|
+
const reader = response.body.getReader();
|
|
87
|
+
|
|
88
|
+
// Start reading but never finish due to timeout
|
|
89
|
+
const { value } = await reader.read();
|
|
90
|
+
return value.length
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const result = await executeWithAsyncHost(
|
|
94
|
+
code,
|
|
95
|
+
{},
|
|
96
|
+
{
|
|
97
|
+
timeoutMs: 500
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(result.success).toBe(false);
|
|
102
|
+
expect(result.error).toBeDefined();
|
|
103
|
+
}, 10000);
|
|
104
|
+
|
|
105
|
+
it('should handle multiple concurrent fetches with timeout', async () => {
|
|
106
|
+
const code = `
|
|
107
|
+
// Start multiple slow fetches
|
|
108
|
+
const promises = [
|
|
109
|
+
fetch('https://httpbin.org/delay/5'),
|
|
110
|
+
fetch('https://httpbin.org/delay/5'),
|
|
111
|
+
fetch('https://httpbin.org/delay/5')
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// Timeout before any complete
|
|
115
|
+
const results = await Promise.all(promises);
|
|
116
|
+
return results.length
|
|
117
|
+
`;
|
|
118
|
+
|
|
119
|
+
const result = await executeWithAsyncHost(
|
|
120
|
+
code,
|
|
121
|
+
{},
|
|
122
|
+
{
|
|
123
|
+
timeoutMs: 1000
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(result.success).toBe(false);
|
|
128
|
+
expect(result.error).toBeDefined();
|
|
129
|
+
}, 15000);
|
|
130
|
+
|
|
131
|
+
it('should handle partial stream read then timeout', async () => {
|
|
132
|
+
const code = `
|
|
133
|
+
const response = await fetch('https://httpbin.org/stream-bytes/10000');
|
|
134
|
+
const reader = response.body.getReader();
|
|
135
|
+
|
|
136
|
+
let total = 0;
|
|
137
|
+
// Read a few chunks
|
|
138
|
+
for (let i = 0; i < 5; i++) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
if (done) break;
|
|
141
|
+
total += value.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Now simulate a delay (in real scenario, could be slow processing)
|
|
145
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
146
|
+
|
|
147
|
+
return total
|
|
148
|
+
`;
|
|
149
|
+
|
|
150
|
+
const result = await executeWithAsyncHost(
|
|
151
|
+
code,
|
|
152
|
+
{},
|
|
153
|
+
{
|
|
154
|
+
timeoutMs: 1000
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(result.success).toBe(false);
|
|
159
|
+
}, 15000);
|
|
160
|
+
|
|
161
|
+
it('should properly clean up after successful fetch', async () => {
|
|
162
|
+
// This test should pass - normal case where fetch completes successfully
|
|
163
|
+
const code = `
|
|
164
|
+
const response = await fetch('https://httpbin.org/uuid');
|
|
165
|
+
const reader = response.body.getReader();
|
|
166
|
+
|
|
167
|
+
const chunks = [];
|
|
168
|
+
while (true) {
|
|
169
|
+
const { done, value } = await reader.read();
|
|
170
|
+
if (done) break;
|
|
171
|
+
chunks.push(value);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Convert Uint8Array to string
|
|
175
|
+
let text = '';
|
|
176
|
+
for (const chunk of chunks) {
|
|
177
|
+
text += new TextDecoder().decode(chunk);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return text
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
const result = await executeWithAsyncHost(
|
|
184
|
+
code,
|
|
185
|
+
{},
|
|
186
|
+
{
|
|
187
|
+
timeoutMs: 10000
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
expect(result.success).toBe(true);
|
|
192
|
+
expect(result.result).toBeDefined();
|
|
193
|
+
expect(typeof result.result).toBe('string');
|
|
194
|
+
}, 15000);
|
|
195
|
+
|
|
196
|
+
it('should handle many rapid executions without leaks', async () => {
|
|
197
|
+
// Run the sandbox multiple times to check for cumulative leaks
|
|
198
|
+
const iterations = 50;
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < iterations; i++) {
|
|
201
|
+
const code = `
|
|
202
|
+
const response = await fetch('https://httpbin.org/uuid');
|
|
203
|
+
return await response.text()
|
|
204
|
+
`;
|
|
205
|
+
|
|
206
|
+
const result = await executeWithAsyncHost(
|
|
207
|
+
code,
|
|
208
|
+
{},
|
|
209
|
+
{
|
|
210
|
+
timeoutMs: 5000
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (i === 0) {
|
|
215
|
+
// First one might succeed
|
|
216
|
+
// Subsequent ones might hit rate limits or time out
|
|
217
|
+
}
|
|
218
|
+
// All should complete without crashing (even if they fail)
|
|
219
|
+
expect(result.success !== undefined).toBe(true);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If we get here without assertion error, cleanup is working
|
|
223
|
+
expect(true).toBe(true);
|
|
224
|
+
}, 60000);
|
|
225
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { executeWithAsyncHost } from '../test-helper.js';
|
|
3
|
+
|
|
4
|
+
describe('Console Bridge - Multiple Calls', () => {
|
|
5
|
+
it('should capture ALL console.log calls (10 times)', async () => {
|
|
6
|
+
const code = `
|
|
7
|
+
console.log('Line 1');
|
|
8
|
+
console.log('Line 2');
|
|
9
|
+
console.log('Line 3');
|
|
10
|
+
console.log('Line 4');
|
|
11
|
+
console.log('Line 5');
|
|
12
|
+
console.log('Line 6');
|
|
13
|
+
console.log('Line 7');
|
|
14
|
+
console.log('Line 8');
|
|
15
|
+
console.log('Line 9');
|
|
16
|
+
console.log('Line 10');
|
|
17
|
+
return 'done'
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
const result = await executeWithAsyncHost(code, {});
|
|
21
|
+
|
|
22
|
+
expect(result.success).toBe(true);
|
|
23
|
+
console.log('Console output:', result.consoleOutput);
|
|
24
|
+
expect(result.consoleOutput).toBeDefined();
|
|
25
|
+
expect(result.consoleOutput).toHaveLength(10);
|
|
26
|
+
for (let i = 1; i <= 10; i++) {
|
|
27
|
+
expect(result.consoleOutput).toContain(`Line ${i}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should capture console.log in a loop (20 iterations)', async () => {
|
|
32
|
+
const code = `
|
|
33
|
+
for (let i = 0; i < 20; i++) {
|
|
34
|
+
console.log('Iteration', i);
|
|
35
|
+
}
|
|
36
|
+
return 'done'
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const result = await executeWithAsyncHost(code, {});
|
|
40
|
+
|
|
41
|
+
expect(result.success).toBe(true);
|
|
42
|
+
expect(result.consoleOutput).toBeDefined();
|
|
43
|
+
expect(result.consoleOutput).toHaveLength(20);
|
|
44
|
+
for (let i = 0; i < 20; i++) {
|
|
45
|
+
expect(result.consoleOutput[i]).toContain('Iteration');
|
|
46
|
+
expect(result.consoleOutput[i]).toContain(String(i));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should capture console.log with different data types', async () => {
|
|
51
|
+
const code = `
|
|
52
|
+
console.log('String:', 'hello');
|
|
53
|
+
console.log('Number:', 42);
|
|
54
|
+
console.log('Boolean:', true);
|
|
55
|
+
console.log('Object:', { a: 1, b: 2 });
|
|
56
|
+
console.log('Array:', [1, 2, 3]);
|
|
57
|
+
console.log('Null:', null);
|
|
58
|
+
console.log('Undefined:', undefined);
|
|
59
|
+
return 'done'
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const result = await executeWithAsyncHost(code, {});
|
|
63
|
+
|
|
64
|
+
expect(result.success).toBe(true);
|
|
65
|
+
expect(result.consoleOutput).toBeDefined();
|
|
66
|
+
expect(result.consoleOutput).toHaveLength(7);
|
|
67
|
+
|
|
68
|
+
expect(result.consoleOutput[0]).toContain('String:');
|
|
69
|
+
expect(result.consoleOutput[0]).toContain('hello');
|
|
70
|
+
|
|
71
|
+
expect(result.consoleOutput[1]).toContain('Number:');
|
|
72
|
+
expect(result.consoleOutput[1]).toContain('42');
|
|
73
|
+
|
|
74
|
+
expect(result.consoleOutput[2]).toContain('Boolean:');
|
|
75
|
+
expect(result.consoleOutput[2]).toContain('true');
|
|
76
|
+
|
|
77
|
+
expect(result.consoleOutput[3]).toContain('Object:');
|
|
78
|
+
expect(result.consoleOutput[3]).toContain('a');
|
|
79
|
+
expect(result.consoleOutput[3]).toContain('1');
|
|
80
|
+
|
|
81
|
+
expect(result.consoleOutput[4]).toContain('Array:');
|
|
82
|
+
|
|
83
|
+
expect(result.consoleOutput[5]).toContain('Null:');
|
|
84
|
+
expect(result.consoleOutput[5]).toContain('null');
|
|
85
|
+
|
|
86
|
+
expect(result.consoleOutput[6]).toContain('Undefined:');
|
|
87
|
+
expect(result.consoleOutput[6]).toContain('undefined');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should capture console.log in async function with multiple calls', async () => {
|
|
91
|
+
const code = `
|
|
92
|
+
async function process() {
|
|
93
|
+
console.log('Step 1: Start');
|
|
94
|
+
await Promise.resolve();
|
|
95
|
+
console.log('Step 2: Middle');
|
|
96
|
+
await Promise.resolve();
|
|
97
|
+
console.log('Step 3: End');
|
|
98
|
+
return 'complete';
|
|
99
|
+
}
|
|
100
|
+
return await process()
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const result = await executeWithAsyncHost(code, {});
|
|
104
|
+
|
|
105
|
+
expect(result.success).toBe(true);
|
|
106
|
+
expect(result.consoleOutput).toBeDefined();
|
|
107
|
+
expect(result.consoleOutput).toHaveLength(3);
|
|
108
|
+
expect(result.consoleOutput).toContain('Step 1: Start');
|
|
109
|
+
expect(result.consoleOutput).toContain('Step 2: Middle');
|
|
110
|
+
expect(result.consoleOutput).toContain('Step 3: End');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should capture console.log interleaved with code', async () => {
|
|
114
|
+
const code = `
|
|
115
|
+
let sum = 0;
|
|
116
|
+
console.log('Starting calculation');
|
|
117
|
+
for (let i = 1; i <= 5; i++) {
|
|
118
|
+
sum += i;
|
|
119
|
+
console.log('Added', i, 'Sum is now', sum);
|
|
120
|
+
}
|
|
121
|
+
console.log('Final sum:', sum);
|
|
122
|
+
return sum
|
|
123
|
+
`;
|
|
124
|
+
|
|
125
|
+
const result = await executeWithAsyncHost(code, {});
|
|
126
|
+
|
|
127
|
+
expect(result.success).toBe(true);
|
|
128
|
+
expect(result.consoleOutput).toBeDefined();
|
|
129
|
+
expect(result.consoleOutput).toHaveLength(7);
|
|
130
|
+
|
|
131
|
+
expect(result.consoleOutput[0]).toContain('Starting calculation');
|
|
132
|
+
|
|
133
|
+
// Check that we logged each iteration
|
|
134
|
+
const logs = result.consoleOutput.join('\n');
|
|
135
|
+
expect(logs).toContain('Added 1');
|
|
136
|
+
expect(logs).toContain('Added 2');
|
|
137
|
+
expect(logs).toContain('Added 3');
|
|
138
|
+
expect(logs).toContain('Added 4');
|
|
139
|
+
expect(logs).toContain('Added 5');
|
|
140
|
+
|
|
141
|
+
expect(result.consoleOutput[6]).toContain('Final sum:');
|
|
142
|
+
expect(result.consoleOutput[6]).toContain('15');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should preserve order of console.log calls', async () => {
|
|
146
|
+
const code = `
|
|
147
|
+
console.log('First');
|
|
148
|
+
console.log('Second');
|
|
149
|
+
console.log('Third');
|
|
150
|
+
console.log('Fourth');
|
|
151
|
+
console.log('Fifth');
|
|
152
|
+
return 'done'
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const result = await executeWithAsyncHost(code, {});
|
|
156
|
+
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
expect(result.consoleOutput).toBeDefined();
|
|
159
|
+
expect(result.consoleOutput).toHaveLength(5);
|
|
160
|
+
|
|
161
|
+
expect(result.consoleOutput[0]).toBe('First');
|
|
162
|
+
expect(result.consoleOutput[1]).toBe('Second');
|
|
163
|
+
expect(result.consoleOutput[2]).toBe('Third');
|
|
164
|
+
expect(result.consoleOutput[3]).toBe('Fourth');
|
|
165
|
+
expect(result.consoleOutput[4]).toBe('Fifth');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should capture 100 console.log calls', async () => {
|
|
169
|
+
const code = `
|
|
170
|
+
for (let i = 0; i < 100; i++) {
|
|
171
|
+
console.log('Log entry', i);
|
|
172
|
+
}
|
|
173
|
+
return 100
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const result = await executeWithAsyncHost(code, {});
|
|
177
|
+
|
|
178
|
+
expect(result.success).toBe(true);
|
|
179
|
+
expect(result.consoleOutput).toBeDefined();
|
|
180
|
+
expect(result.consoleOutput).toHaveLength(100);
|
|
181
|
+
|
|
182
|
+
// Check a few entries
|
|
183
|
+
expect(result.consoleOutput[0]).toContain('Log entry 0');
|
|
184
|
+
expect(result.consoleOutput[49]).toContain('Log entry 49');
|
|
185
|
+
expect(result.consoleOutput[99]).toContain('Log entry 99');
|
|
186
|
+
});
|
|
187
|
+
});
|