@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,331 @@
1
+ /**
2
+ * Tests for code review fixes:
3
+ * 1. search.ts - use-after-dispose of iterator factory and resultHandle
4
+ * 2. storage.ts - dangling handle in KV json() error path, uses parseJsonInContext
5
+ * 3. headers.ts - callFunction return value leak in forEach
6
+ * 4. sandbox.ts - allHostPromises unbounded growth removed
7
+ * 5. utils.ts - dead code branch in defineClass
8
+ * 6. response-object.ts - bodyUsed property exposed
9
+ */
10
+
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { executeWithAsyncHost } from '../test-helper.js';
13
+ import { sandboxAsync, sandboxSync, sandboxObject } from '../../types.js';
14
+ import { createHttpbinMocks } from '../test-mocks.js';
15
+
16
+ let mockFetch: any;
17
+
18
+ function setupMockFetch() {
19
+ const mocks = createHttpbinMocks();
20
+ mockFetch = vi.fn(async (url: string, _init?: RequestInit) => {
21
+ const urlKey = url.split('?')[0];
22
+ const mock = mocks.get(urlKey);
23
+ if (!mock) {
24
+ throw new Error(`No mock for: ${urlKey}`);
25
+ }
26
+ if (mock.delay) {
27
+ await new Promise(resolve => setTimeout(resolve, mock.delay));
28
+ }
29
+ return new Response(mock.body, {
30
+ status: mock.status ?? 200,
31
+ statusText: mock.statusText ?? 'OK',
32
+ headers: new Headers(mock.headers)
33
+ });
34
+ });
35
+ globalThis.fetch = mockFetch;
36
+ }
37
+
38
+ function restoreFetch() {
39
+ vi.restoreAllMocks();
40
+ }
41
+
42
+ describe('Fix #1-2: Headers forEach handle leak', () => {
43
+ beforeEach(() => setupMockFetch());
44
+ afterEach(() => restoreFetch());
45
+ it('should not leak handles when using headers.forEach repeatedly', async () => {
46
+ // Run many iterations to stress-test handle cleanup in forEach
47
+ for (let i = 0; i < 10; i++) {
48
+ const result = await executeWithAsyncHost(
49
+ `
50
+ const response = await fetch('https://httpbin.org/get');
51
+ const headerNames = [];
52
+ response.headers.forEach((value, name) => {
53
+ headerNames.push(name);
54
+ });
55
+ return headerNames.length > 0
56
+ `,
57
+ {},
58
+ { timeoutMs: 10000 }
59
+ );
60
+
61
+ expect(result.success).toBe(true);
62
+ expect(result.result).toBe(true);
63
+ }
64
+ });
65
+
66
+ it('should handle forEach callback that returns values', async () => {
67
+ const result = await executeWithAsyncHost(
68
+ `
69
+ const response = await fetch('https://httpbin.org/get');
70
+ const entries = [];
71
+ response.headers.forEach((value, name) => {
72
+ entries.push(name + ': ' + value);
73
+ return 'ignored return value'; // This return value was leaking
74
+ });
75
+ return entries.length > 0
76
+ `,
77
+ {},
78
+ { timeoutMs: 10000 }
79
+ );
80
+
81
+ expect(result.success).toBe(true);
82
+ expect(result.result).toBe(true);
83
+ });
84
+ });
85
+
86
+ describe('Fix #3: allHostPromises removed - cleanup still works', () => {
87
+ it('should properly clean up with many async globals', async () => {
88
+ const globals: Record<string, any> = {};
89
+ // Create many async functions to stress the promise tracking
90
+ for (let i = 0; i < 20; i++) {
91
+ globals[`fn${i}`] = sandboxAsync(async (n: number) => n * 2);
92
+ }
93
+
94
+ const calls = Array.from({ length: 20 }, (_, i) => `fn${i}(${i})`).join(', ');
95
+ const code = `
96
+ const results = await Promise.all([${calls}]);
97
+ return results.reduce((a, b) => a + b, 0)
98
+ `;
99
+
100
+ const result = await executeWithAsyncHost(code, globals, { timeoutMs: 5000 });
101
+ expect(result.success).toBe(true);
102
+ // Sum of i*2 for i=0..19 = 2*(0+1+...+19) = 2*190 = 380
103
+ expect(result.result).toBe(380);
104
+ });
105
+
106
+ it('should clean up properly when mixing sync and async globals', async () => {
107
+ for (let i = 0; i < 5; i++) {
108
+ const result = await executeWithAsyncHost(
109
+ `
110
+ const a = syncFn(5);
111
+ const b = await asyncFn(10);
112
+ const c = await obj.method(3);
113
+ return a + b + c
114
+ `,
115
+ {
116
+ syncFn: sandboxSync((n: number) => n * 2),
117
+ asyncFn: sandboxAsync(async (n: number) => n * 3),
118
+ obj: sandboxObject({
119
+ method: sandboxAsync(async (n: number) => n * 4)
120
+ })
121
+ }
122
+ );
123
+
124
+ expect(result.success).toBe(true);
125
+ expect(result.result).toBe(10 + 30 + 12); // 52
126
+ }
127
+ });
128
+ });
129
+
130
+ describe('Fix #4: bodyUsed property exposed on Response', () => {
131
+ beforeEach(() => setupMockFetch());
132
+ afterEach(() => restoreFetch());
133
+
134
+ it('should report bodyUsed=false before consuming body', async () => {
135
+ const result = await executeWithAsyncHost(
136
+ `
137
+ const response = await fetch('https://httpbin.org/get');
138
+ return response.bodyUsed
139
+ `,
140
+ {},
141
+ { timeoutMs: 10000 }
142
+ );
143
+
144
+ expect(result.success).toBe(true);
145
+ expect(result.result).toBe(false);
146
+ });
147
+
148
+ it('should report bodyUsed=true after consuming body with text()', async () => {
149
+ const result = await executeWithAsyncHost(
150
+ `
151
+ const response = await fetch('https://httpbin.org/get');
152
+ const before = response.bodyUsed;
153
+ await response.text();
154
+ const after = response.bodyUsed;
155
+ return { before, after }
156
+ `,
157
+ {},
158
+ { timeoutMs: 10000 }
159
+ );
160
+
161
+ expect(result.success).toBe(true);
162
+ expect(result.result.before).toBe(false);
163
+ expect(result.result.after).toBe(true);
164
+ });
165
+
166
+ it('should report bodyUsed=true after consuming body with json()', async () => {
167
+ const result = await executeWithAsyncHost(
168
+ `
169
+ const response = await fetch('https://httpbin.org/json');
170
+ const before = response.bodyUsed;
171
+ await response.json();
172
+ const after = response.bodyUsed;
173
+ return { before, after }
174
+ `,
175
+ {},
176
+ { timeoutMs: 10000 }
177
+ );
178
+
179
+ expect(result.success).toBe(true);
180
+ expect(result.result.before).toBe(false);
181
+ expect(result.result.after).toBe(true);
182
+ });
183
+ });
184
+
185
+ describe('Fix #5: defineClass dead code removed', () => {
186
+ it('should still define TextEncoder class correctly', async () => {
187
+ const result = await executeWithAsyncHost(
188
+ `
189
+ const enc = new TextEncoder();
190
+ return enc.encoding
191
+ `,
192
+ {}
193
+ );
194
+
195
+ expect(result.success).toBe(true);
196
+ expect(result.result).toBe('utf-8');
197
+ });
198
+
199
+ it('should still define TextDecoder class correctly', async () => {
200
+ const result = await executeWithAsyncHost(
201
+ `
202
+ const dec = new TextDecoder();
203
+ return dec.encoding
204
+ `,
205
+ {}
206
+ );
207
+
208
+ expect(result.success).toBe(true);
209
+ expect(result.result).toBe('utf-8');
210
+ });
211
+ });
212
+
213
+ describe('Fix #6: storage KV json() uses parseJsonInContext', () => {
214
+ it('should handle JSON operations correctly in sandbox', async () => {
215
+ // Test that JSON parsing through the shared utility works
216
+ // This tests the general path since storage KV needs a real KV mount
217
+ const result = await executeWithAsyncHost(
218
+ `
219
+ const obj = { nested: { arr: [1, 2, 3], str: "hello" } };
220
+ const json = JSON.stringify(obj);
221
+ const parsed = JSON.parse(json);
222
+ return parsed.nested.arr.length + parsed.nested.str.length
223
+ `,
224
+ {}
225
+ );
226
+
227
+ expect(result.success).toBe(true);
228
+ expect(result.result).toBe(8); // 3 + 5
229
+ });
230
+
231
+ it('should handle many JSON parse operations without leaks', async () => {
232
+ // Stress test to verify parseJsonInContext caching works
233
+ for (let i = 0; i < 20; i++) {
234
+ const result = await executeWithAsyncHost(
235
+ `
236
+ const objects = [];
237
+ for (let j = 0; j < 50; j++) {
238
+ const obj = { i: ${i}, j, data: "value" };
239
+ const str = JSON.stringify(obj);
240
+ objects.push(JSON.parse(str));
241
+ }
242
+ return objects.length
243
+ `,
244
+ {}
245
+ );
246
+
247
+ expect(result.success).toBe(true);
248
+ expect(result.result).toBe(50);
249
+ }
250
+ });
251
+ });
252
+
253
+ describe('Regression: all existing functionality still works', () => {
254
+ it('should handle async globals with Promise.all', async () => {
255
+ const result = await executeWithAsyncHost(
256
+ `
257
+ const results = await Promise.all([
258
+ asyncAdd(1, 2),
259
+ asyncAdd(3, 4),
260
+ asyncAdd(5, 6)
261
+ ]);
262
+ return results
263
+ `,
264
+ {
265
+ asyncAdd: sandboxAsync(async (a: number, b: number) => a + b)
266
+ }
267
+ );
268
+
269
+ expect(result.success).toBe(true);
270
+ expect(result.result).toEqual([3, 7, 11]);
271
+ });
272
+
273
+ it('should handle object methods with async operations', async () => {
274
+ const result = await executeWithAsyncHost(
275
+ `
276
+ const a = await api.getData();
277
+ const b = await api.processData(a);
278
+ return b
279
+ `,
280
+ {
281
+ api: sandboxObject({
282
+ getData: sandboxAsync(async () => ({ value: 42 })),
283
+ processData: sandboxAsync(async (data: any) => data.value * 2)
284
+ })
285
+ }
286
+ );
287
+
288
+ expect(result.success).toBe(true);
289
+ expect(result.result).toBe(84);
290
+ });
291
+
292
+ it('should properly capture console output', async () => {
293
+ const result = await executeWithAsyncHost(
294
+ `
295
+ console.log("hello");
296
+ console.error("oops");
297
+ console.warn("careful");
298
+ return "done"
299
+ `,
300
+ {}
301
+ );
302
+
303
+ expect(result.success).toBe(true);
304
+ expect(result.result).toBe('done');
305
+ expect(result.consoleOutput).toContain('hello');
306
+ expect(result.consoleOutput).toContain('[ERROR] oops');
307
+ expect(result.consoleOutput).toContain('[WARN] careful');
308
+ });
309
+
310
+ it('should handle errors in async globals cleanly', async () => {
311
+ const result = await executeWithAsyncHost(
312
+ `
313
+ try {
314
+ await failingFn();
315
+ return "no error"
316
+ } catch (e) {
317
+ return "caught: " + e
318
+ }
319
+ `,
320
+ {
321
+ failingFn: sandboxAsync(async () => {
322
+ throw new Error('test failure');
323
+ })
324
+ }
325
+ );
326
+
327
+ expect(result.success).toBe(true);
328
+ expect(result.result).toContain('caught');
329
+ expect(result.result).toContain('test failure');
330
+ });
331
+ });