Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@ rl.on('line', async (line: string) => {
}

result = vm.runInContext(code, context, options);

// If result is a Promise (thenable), await it
if (
result &&
typeof result === 'object' &&
typeof (result as any).then === 'function'
) {
result = await (result as Promise<unknown>);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical timeout bypass: vm.runInContext timeout only applies to sync execution. When awaiting the Promise here, long-running or infinite Promises ignore the timeout entirely.

Wrap in Promise.race:

if (isThenable(result)) {
  if (timeout !== undefined) {
    const timeoutPromise = new Promise((_, reject) => 
      setTimeout(() => reject(new Error('Execution timeout')), timeout)
    );
    result = await Promise.race([result, timeoutPromise]);
  } else {
    result = await result;
  }
}

} catch (error: unknown) {
const err = error as Error;
stderr += err.stack || err.toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ rl.on('line', async (line: string) => {
}

result = vm.runInContext(jsCode, context, options);

// If result is a Promise (thenable), await it
if (
result &&
typeof result === 'object' &&
typeof (result as any).then === 'function'
) {
result = await (result as Promise<unknown>);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same timeout bypass as node_executor.ts:107. Apply Promise.race fix here too.

} catch (error: unknown) {
const err = error as Error;
if (err.message?.includes('Transform failed')) {
Expand Down
184 changes: 184 additions & 0 deletions tests/e2e/code-interpreter-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,190 @@ describe('Code Interpreter Workflow (E2E)', () => {
);
}, 120000);

// ============================================================================
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests cover happy path and error cases well, but missing timeout enforcement tests. Add:

  • Test with long-running async code (10s Promise with 1s timeout)
  • Test with infinite Promise (never resolves)

Without these, the timeout bypass at executor lines 105-107/116-118 goes undetected.

// Async/Await Promise Handling (Issue #206)
// ============================================================================

test('should resolve async IIFE and return the value', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to validate - all the tests are wrapped in an IIFE. Would this change work if the user passed code not wrapped in an IIFE? If not, is the answer then to wrap the underlying implementation in an async IIFE?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ghostwriternr to support top-level await we'd need to wrap the code in an async IIFE, which adds complexity (need to handle "last expression" capture for multi-statement code) and changes. For this PR, I'd suggest keeping as is since it solves the issue. We can open a separate issue for top-level await? or should we just do it properly here?

currentSandboxId = createSandboxId();
const headers = createTestHeaders(currentSandboxId);

// Create JavaScript context
const ctxResponse = await fetch(`${workerUrl}/api/code/context/create`, {
method: 'POST',
headers,
body: JSON.stringify({ language: 'javascript' })
});

const context = await ctxResponse.json();

// Execute async IIFE that returns a value
const execResponse = await fetch(`${workerUrl}/api/code/execute`, {
method: 'POST',
headers,
body: JSON.stringify({
code: '(async () => { return 42; })()',
options: { context }
})
});

expect(execResponse.status).toBe(200);
const execution = (await execResponse.json()) as ExecutionResult;

expect(execution.error).toBeUndefined();
expect(execution.results).toBeDefined();
expect(execution.results.length).toBeGreaterThan(0);
// The result should be 42, not an empty object {}
const resultData = execution.results[0];
expect(resultData.json).toBe(42);
}, 120000);

test('should resolve Promise.resolve() and return the value', async () => {
currentSandboxId = createSandboxId();
const headers = createTestHeaders(currentSandboxId);

// Create JavaScript context
const ctxResponse = await fetch(`${workerUrl}/api/code/context/create`, {
method: 'POST',
headers,
body: JSON.stringify({ language: 'javascript' })
});

const context = await ctxResponse.json();

// Execute Promise.resolve
const execResponse = await fetch(`${workerUrl}/api/code/execute`, {
method: 'POST',
headers,
body: JSON.stringify({
code: 'Promise.resolve({ status: "success", value: 123 })',
options: { context }
})
});

expect(execResponse.status).toBe(200);
const execution = (await execResponse.json()) as ExecutionResult;

expect(execution.error).toBeUndefined();
expect(execution.results).toBeDefined();
expect(execution.results.length).toBeGreaterThan(0);
// The result should be the resolved object, not {}
const resultData = execution.results[0];
expect(resultData.json).toEqual({ status: 'success', value: 123 });
}, 120000);

test('should handle nested async operations', async () => {
currentSandboxId = createSandboxId();
const headers = createTestHeaders(currentSandboxId);

// Create JavaScript context
const ctxResponse = await fetch(`${workerUrl}/api/code/context/create`, {
method: 'POST',
headers,
body: JSON.stringify({ language: 'javascript' })
});

const context = await ctxResponse.json();

// Execute nested async code
const execResponse = await fetch(`${workerUrl}/api/code/execute`, {
method: 'POST',
headers,
body: JSON.stringify({
code: `
(async () => {
const a = await Promise.resolve(10);
const b = await Promise.resolve(20);
return a + b;
})()
`.trim(),
options: { context }
})
});

expect(execResponse.status).toBe(200);
const execution = (await execResponse.json()) as ExecutionResult;

expect(execution.error).toBeUndefined();
expect(execution.results).toBeDefined();
expect(execution.results.length).toBeGreaterThan(0);
// The result should be 30
const resultData = execution.results[0];
expect(resultData.json).toBe(30);
}, 120000);

test('should still handle synchronous code correctly', async () => {
currentSandboxId = createSandboxId();
const headers = createTestHeaders(currentSandboxId);

// Create JavaScript context
const ctxResponse = await fetch(`${workerUrl}/api/code/context/create`, {
method: 'POST',
headers,
body: JSON.stringify({ language: 'javascript' })
});

const context = await ctxResponse.json();

// Execute synchronous code
const execResponse = await fetch(`${workerUrl}/api/code/execute`, {
method: 'POST',
headers,
body: JSON.stringify({
code: '({ sum: 1 + 2 + 3, product: 2 * 3 * 4 })',
options: { context }
})
});

expect(execResponse.status).toBe(200);
const execution = (await execResponse.json()) as ExecutionResult;

expect(execution.error).toBeUndefined();
expect(execution.results).toBeDefined();
expect(execution.results.length).toBeGreaterThan(0);
const resultData = execution.results[0];
expect(resultData.json).toEqual({ sum: 6, product: 24 });
}, 120000);

test('should resolve TypeScript async code', async () => {
currentSandboxId = createSandboxId();
const headers = createTestHeaders(currentSandboxId);

// Create TypeScript context
const ctxResponse = await fetch(`${workerUrl}/api/code/context/create`, {
method: 'POST',
headers,
body: JSON.stringify({ language: 'typescript' })
});

const context = await ctxResponse.json();

// Execute TypeScript async code
const execResponse = await fetch(`${workerUrl}/api/code/execute`, {
method: 'POST',
headers,
body: JSON.stringify({
code: `
(async (): Promise<number> => {
const value: number = await Promise.resolve(100);
return value * 2;
})()
`.trim(),
options: { context }
})
});

expect(execResponse.status).toBe(200);
const execution = (await execResponse.json()) as ExecutionResult;

expect(execution.error).toBeUndefined();
expect(execution.results).toBeDefined();
expect(execution.results.length).toBeGreaterThan(0);
// The result should be 200
const resultData = execution.results[0];
expect(resultData.json).toBe(200);
}, 120000);

// ============================================================================
// Streaming Execution
// ============================================================================
Expand Down
Loading