@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,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TextEncoder bridge - Encodes strings to Uint8Array
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { QuickJSHandle } from 'quickjs-emscripten-core';
|
|
6
|
+
import type { BridgeContext } from './types.js';
|
|
7
|
+
import { defineClass } from './utils.js';
|
|
8
|
+
import { unwrapResult } from './shared/index.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Install TextEncoder in the sandbox
|
|
12
|
+
* Bridges to runtime's native TextEncoder
|
|
13
|
+
*/
|
|
14
|
+
export function installTextEncoder(ctx: BridgeContext): void {
|
|
15
|
+
const { context, logger } = ctx;
|
|
16
|
+
|
|
17
|
+
// Create cached Uint8Array wrapper function once - avoids evalCode on every encode()
|
|
18
|
+
// Store in global so it persists and can be used by the host function
|
|
19
|
+
{
|
|
20
|
+
using _uint8ArrayWrapper = unwrapResult(
|
|
21
|
+
context.evalCode('(globalThis.__uint8ArrayWrapper = function(ab) { return new Uint8Array(ab); })'),
|
|
22
|
+
context
|
|
23
|
+
);
|
|
24
|
+
// wrapper persists in globalThis; our reference is disposed by `using`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create host function that does the actual encoding
|
|
28
|
+
// Reusable native encoder (stateless)
|
|
29
|
+
const nativeEncoder = new TextEncoder();
|
|
30
|
+
{
|
|
31
|
+
using hostEncodeFunction = context.newFunction('__hostEncode', strHandle => {
|
|
32
|
+
try {
|
|
33
|
+
const str = context.dump(strHandle);
|
|
34
|
+
if (typeof str !== 'string') {
|
|
35
|
+
return context.newError('encode() requires a string');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const uint8Array = nativeEncoder.encode(str);
|
|
39
|
+
|
|
40
|
+
// Create a Uint8Array in QuickJS from the ArrayBuffer
|
|
41
|
+
// Use slice to handle any Uint8Array views into larger ArrayBuffers
|
|
42
|
+
const buf =
|
|
43
|
+
uint8Array.byteOffset === 0 && uint8Array.byteLength === uint8Array.buffer.byteLength
|
|
44
|
+
? uint8Array.buffer
|
|
45
|
+
: uint8Array.buffer.slice(
|
|
46
|
+
uint8Array.byteOffset,
|
|
47
|
+
uint8Array.byteOffset + uint8Array.byteLength
|
|
48
|
+
);
|
|
49
|
+
using arrayBufferHandle = context.newArrayBuffer(buf);
|
|
50
|
+
|
|
51
|
+
// Get the cached wrapper from global
|
|
52
|
+
using wrapperHandle = context.getProp(context.global, '__uint8ArrayWrapper');
|
|
53
|
+
|
|
54
|
+
// Use cached wrapper function instead of evalCode on every encode
|
|
55
|
+
return unwrapResult(
|
|
56
|
+
context.callFunction(wrapperHandle, context.undefined, arrayBufferHandle),
|
|
57
|
+
context
|
|
58
|
+
);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
logger?.error?.('TextEncoder.encode error', { error: e });
|
|
61
|
+
return context.newError(e instanceof Error ? e.message : String(e));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
context.setProp(context.global, '__hostEncode', hostEncodeFunction);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Define TextEncoder class in QuickJS
|
|
69
|
+
const classCode = `
|
|
70
|
+
class TextEncoder {
|
|
71
|
+
constructor() {
|
|
72
|
+
this.encoding = 'utf-8';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
encode(str) {
|
|
76
|
+
return __hostEncode(str);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const classHandle = defineClass(ctx, classCode, 'TextEncoder');
|
|
82
|
+
if (classHandle) {
|
|
83
|
+
using handle = classHandle;
|
|
84
|
+
context.setProp(context.global, 'TextEncoder', handle);
|
|
85
|
+
|
|
86
|
+
// Clean up internal globals — make non-enumerable/writable to hide from sandbox
|
|
87
|
+
// NOTE: The sandbox is not a hard security boundary. Internal helpers are hidden via
|
|
88
|
+
// enumerable: false but can still be called by name. This is acceptable because the bridges
|
|
89
|
+
// are the primary security boundary, not QuickJS containment. See Sandbox.ts for details.
|
|
90
|
+
const hideResult = context.evalCode(`(function() {
|
|
91
|
+
try { Object.defineProperty(globalThis, '__hostEncode', { enumerable: false, writable: false, configurable: false }); } catch(e) {}
|
|
92
|
+
try { Object.defineProperty(globalThis, '__uint8ArrayWrapper', { enumerable: false, writable: false, configurable: false }); } catch(e) {}
|
|
93
|
+
})()`);
|
|
94
|
+
if (hideResult.error) {
|
|
95
|
+
hideResult.error.dispose();
|
|
96
|
+
} else {
|
|
97
|
+
(hideResult as { value: QuickJSHandle }).value.dispose();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger?.info?.('[Bridge] TextEncoder installed');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for QuickJS bridge utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { QuickJSContext, QuickJSRuntime, QuickJSDeferredPromise } from 'quickjs-emscripten-core';
|
|
6
|
+
import type { Logger } from '../../types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Promise tracker for async operations
|
|
10
|
+
*/
|
|
11
|
+
export interface PromiseTracker {
|
|
12
|
+
pendingPromises: Set<Promise<void>>;
|
|
13
|
+
notifyNewPromise: () => void;
|
|
14
|
+
newPromiseSignal: Promise<void>;
|
|
15
|
+
deferredPromises: Set<QuickJSDeferredPromise>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bridge context containing QuickJS context and shared resources
|
|
20
|
+
*/
|
|
21
|
+
export interface BridgeContext {
|
|
22
|
+
context: QuickJSContext;
|
|
23
|
+
runtime: QuickJSRuntime;
|
|
24
|
+
tracker: PromiseTracker;
|
|
25
|
+
logger?: Logger;
|
|
26
|
+
cleanupHandlers: Array<() => void | Promise<void>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for bridge installation
|
|
31
|
+
*/
|
|
32
|
+
export interface BridgeOptions {
|
|
33
|
+
logger?: Logger;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A bridge installer function
|
|
38
|
+
*/
|
|
39
|
+
export type BridgeInstaller = (ctx: BridgeContext) => void | (() => void);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for creating QuickJS bridges
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { QuickJSHandle } from 'quickjs-emscripten-core';
|
|
6
|
+
import type { BridgeContext } from './types.js';
|
|
7
|
+
import { convertToHandle } from './shared/convert.js';
|
|
8
|
+
|
|
9
|
+
// Re-export convertToHandle from the shared module (single source of truth)
|
|
10
|
+
export { convertToHandle } from './shared/convert.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a synchronous function that bridges to a sync host function
|
|
14
|
+
*/
|
|
15
|
+
export function createSyncBridge(
|
|
16
|
+
ctx: BridgeContext,
|
|
17
|
+
name: string,
|
|
18
|
+
hostFn: (...args: any[]) => any
|
|
19
|
+
): QuickJSHandle {
|
|
20
|
+
const { context, logger } = ctx;
|
|
21
|
+
|
|
22
|
+
return context.newFunction(name, (...args: QuickJSHandle[]) => {
|
|
23
|
+
const jsArgs = args.map(arg => context.dump(arg));
|
|
24
|
+
const result = hostFn(...jsArgs);
|
|
25
|
+
|
|
26
|
+
logger?.debug?.(`[Bridge] Sync bridge called: ${name}`);
|
|
27
|
+
|
|
28
|
+
return convertToHandle(context, result);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a promise-returning function that bridges to an async host function
|
|
34
|
+
*/
|
|
35
|
+
export function createAsyncBridge(
|
|
36
|
+
ctx: BridgeContext,
|
|
37
|
+
name: string,
|
|
38
|
+
hostFn: (...args: any[]) => Promise<any>
|
|
39
|
+
): QuickJSHandle {
|
|
40
|
+
const { context, runtime, tracker, logger } = ctx;
|
|
41
|
+
|
|
42
|
+
return context.newFunction(name, (...args: QuickJSHandle[]) => {
|
|
43
|
+
const jsArgs = args.map(arg => context.dump(arg));
|
|
44
|
+
const deferred = context.newPromise();
|
|
45
|
+
tracker.deferredPromises.add(deferred);
|
|
46
|
+
|
|
47
|
+
const hostPromise = hostFn(...jsArgs)
|
|
48
|
+
.then((result: any) => {
|
|
49
|
+
try {
|
|
50
|
+
const handle = convertToHandle(context, result);
|
|
51
|
+
deferred.resolve(handle);
|
|
52
|
+
handle.dispose();
|
|
53
|
+
runtime.executePendingJobs();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
try {
|
|
56
|
+
const errHandle = context.newString(String(e));
|
|
57
|
+
deferred.reject(errHandle);
|
|
58
|
+
errHandle.dispose();
|
|
59
|
+
runtime.executePendingJobs();
|
|
60
|
+
} catch {
|
|
61
|
+
// Context truly disposed — deferred will be cleaned up by sandbox cleanup
|
|
62
|
+
}
|
|
63
|
+
logger?.warn?.(`[Bridge] ${name}: error after async completion: ${e}`);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.catch((err: Error) => {
|
|
67
|
+
try {
|
|
68
|
+
const errHandle = context.newString(err.message || String(err));
|
|
69
|
+
deferred.reject(errHandle);
|
|
70
|
+
errHandle.dispose();
|
|
71
|
+
runtime.executePendingJobs();
|
|
72
|
+
} catch (e2) {
|
|
73
|
+
// Second-level catch: context disposed or string creation failed
|
|
74
|
+
// Try one more time with a minimal error before giving up
|
|
75
|
+
try {
|
|
76
|
+
const fallbackHandle = context.newString('Async bridge error');
|
|
77
|
+
deferred.reject(fallbackHandle);
|
|
78
|
+
fallbackHandle.dispose();
|
|
79
|
+
runtime.executePendingJobs();
|
|
80
|
+
} catch {
|
|
81
|
+
// Context truly disposed
|
|
82
|
+
}
|
|
83
|
+
logger?.warn?.(`[Bridge] ${name}: error handling failed: ${err.message}, fallback error: ${e2}`);
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
.finally(() => {
|
|
87
|
+
tracker.pendingPromises.delete(hostPromise);
|
|
88
|
+
tracker.deferredPromises.delete(deferred);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
tracker.pendingPromises.add(hostPromise);
|
|
92
|
+
tracker.notifyNewPromise();
|
|
93
|
+
|
|
94
|
+
logger?.debug?.(`[Bridge] Created async bridge for ${name}`);
|
|
95
|
+
|
|
96
|
+
return deferred.handle;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Define an ES6 class in the sandbox using evalCode
|
|
102
|
+
* Returns the class handle or null if there was an error
|
|
103
|
+
*/
|
|
104
|
+
export function defineClass(
|
|
105
|
+
ctx: BridgeContext,
|
|
106
|
+
classCode: string,
|
|
107
|
+
className: string
|
|
108
|
+
): QuickJSHandle | null {
|
|
109
|
+
const { context, logger } = ctx;
|
|
110
|
+
|
|
111
|
+
const classDef = context.evalCode(`(${classCode})`);
|
|
112
|
+
|
|
113
|
+
if (classDef.error) {
|
|
114
|
+
const error = context.dump(classDef.error);
|
|
115
|
+
classDef.error.dispose();
|
|
116
|
+
logger?.error?.(`Failed to define ${className} class`, { error });
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (classDef as any).value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
6
|
+
import { Sandbox } from './index.js';
|
|
7
|
+
|
|
8
|
+
describe('Sandbox', () => {
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await Sandbox.reset();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await Sandbox.reset();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should create singleton instance', async () => {
|
|
18
|
+
const sandbox1 = await Sandbox.configure();
|
|
19
|
+
const sandbox2 = await Sandbox.getInstance();
|
|
20
|
+
|
|
21
|
+
expect(sandbox1).toBe(sandbox2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should return existing instance when configure is called twice', async () => {
|
|
25
|
+
const sandbox1 = await Sandbox.configure();
|
|
26
|
+
const sandbox2 = await Sandbox.configure();
|
|
27
|
+
expect(sandbox2).toBe(sandbox1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should reject getInstance when not configured', async () => {
|
|
31
|
+
await expect(Sandbox.getInstance()).rejects.toThrow('not been configured');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should execute simple code', async () => {
|
|
35
|
+
const sandbox = await Sandbox.configure();
|
|
36
|
+
const result = await sandbox.execute('const x = 1 + 2; return x;', {}, { logger: console });
|
|
37
|
+
|
|
38
|
+
if (!result.success) {
|
|
39
|
+
console.error('Execution failed:', result.error);
|
|
40
|
+
console.error('Full result:', JSON.stringify(result, null, 2));
|
|
41
|
+
}
|
|
42
|
+
expect(result.success).toBe(true);
|
|
43
|
+
expect(result.result).toBe(3);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should handle async operations', async () => {
|
|
47
|
+
const sandbox = await Sandbox.configure();
|
|
48
|
+
const result = await sandbox.execute(
|
|
49
|
+
`
|
|
50
|
+
const p = Promise.resolve('done');
|
|
51
|
+
return await p;
|
|
52
|
+
`,
|
|
53
|
+
{},
|
|
54
|
+
{ logger: console }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
console.error('Execution failed:', result.error);
|
|
59
|
+
console.error('Full result:', JSON.stringify(result, null, 2));
|
|
60
|
+
}
|
|
61
|
+
expect(result.success).toBe(true);
|
|
62
|
+
expect(result.result).toBe('done');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should queue concurrent executions', async () => {
|
|
66
|
+
const sandbox = await Sandbox.configure();
|
|
67
|
+
const order: number[] = [];
|
|
68
|
+
|
|
69
|
+
// Each execution records its completion order via .then() callback.
|
|
70
|
+
// The queue serializes them, so they complete in submission order.
|
|
71
|
+
// Use computation (not setTimeout) to create varying execution times.
|
|
72
|
+
const promise1 = sandbox
|
|
73
|
+
.execute(
|
|
74
|
+
`
|
|
75
|
+
// Heavier computation to take longer
|
|
76
|
+
let sum = 0;
|
|
77
|
+
for (let i = 0; i < 10000; i++) sum += i;
|
|
78
|
+
return 1;
|
|
79
|
+
`
|
|
80
|
+
)
|
|
81
|
+
.then((result) => {
|
|
82
|
+
if (!result.success) console.error('Execution 1 failed:', result.error);
|
|
83
|
+
expect(result.success).toBe(true);
|
|
84
|
+
order.push(result.result as number);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const promise2 = sandbox
|
|
88
|
+
.execute(
|
|
89
|
+
`
|
|
90
|
+
let sum = 0;
|
|
91
|
+
for (let i = 0; i < 100; i++) sum += i;
|
|
92
|
+
return 2;
|
|
93
|
+
`
|
|
94
|
+
)
|
|
95
|
+
.then((result) => {
|
|
96
|
+
expect(result.success).toBe(true);
|
|
97
|
+
order.push(result.result as number);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const promise3 = sandbox
|
|
101
|
+
.execute(
|
|
102
|
+
`
|
|
103
|
+
return 3;
|
|
104
|
+
`
|
|
105
|
+
)
|
|
106
|
+
.then((result) => {
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
order.push(result.result as number);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await Promise.all([promise1, promise2, promise3]);
|
|
112
|
+
|
|
113
|
+
// Executions are serialized by the queue — they must complete in submission order
|
|
114
|
+
// even though execution 2 and 3 are faster computations than execution 1
|
|
115
|
+
expect(order).toEqual([1, 2, 3]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should inject globals', async () => {
|
|
119
|
+
const sandbox = await Sandbox.configure();
|
|
120
|
+
const result = await sandbox.execute('return myGlobal;', {
|
|
121
|
+
myGlobal: 'hello world'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
expect(result.result).toBe('hello world');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle errors', async () => {
|
|
129
|
+
const sandbox = await Sandbox.configure();
|
|
130
|
+
const result = await sandbox.execute('throw new Error("test error");', {}, { logger: console });
|
|
131
|
+
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
console.error('Error result:', result.error);
|
|
134
|
+
}
|
|
135
|
+
expect(result.success).toBe(false);
|
|
136
|
+
expect(result.error).toContain('test error');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should enforce timeout', async () => {
|
|
140
|
+
const sandbox = await Sandbox.configure();
|
|
141
|
+
const result = await sandbox.execute(
|
|
142
|
+
'await new Promise(() => {});',
|
|
143
|
+
{},
|
|
144
|
+
{
|
|
145
|
+
timeoutMs: 100
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(result.success).toBe(false);
|
|
150
|
+
expect(result.error).toContain('timeout');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should capture console output', async () => {
|
|
154
|
+
const sandbox = await Sandbox.configure();
|
|
155
|
+
const result = await sandbox.execute('console.log("hello"); return "done";');
|
|
156
|
+
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
expect(result.consoleOutput).toContain('hello');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should dispose correctly', async () => {
|
|
162
|
+
const sandbox = await Sandbox.configure();
|
|
163
|
+
await sandbox.dispose();
|
|
164
|
+
|
|
165
|
+
// After dispose, configure should create a new instance
|
|
166
|
+
const sandbox2 = await Sandbox.configure();
|
|
167
|
+
expect(sandbox2).not.toBe(sandbox);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should reject queued executions after dispose', async () => {
|
|
171
|
+
const sandbox = await Sandbox.configure();
|
|
172
|
+
|
|
173
|
+
// Start a long execution
|
|
174
|
+
const longPromise = sandbox.execute(`
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
176
|
+
return 'done';
|
|
177
|
+
`);
|
|
178
|
+
|
|
179
|
+
// Queue another execution
|
|
180
|
+
const queuedPromise = sandbox.execute('return "queued";');
|
|
181
|
+
|
|
182
|
+
// Dispose while first is still running
|
|
183
|
+
await sandbox.dispose();
|
|
184
|
+
|
|
185
|
+
// Queued execution should be rejected
|
|
186
|
+
await expect(queuedPromise).rejects.toThrow('disposed');
|
|
187
|
+
|
|
188
|
+
// Clean up long promise
|
|
189
|
+
await longPromise.catch(() => {});
|
|
190
|
+
});
|
|
191
|
+
});
|