fix(import): fixed trigger save on export/import flow (#2239)

* fix(import): fixed trigger save on export/import flow

* optimized test runners

* ack PR comments
This commit is contained in:
Waleed
2025-12-07 23:00:28 -08:00
committed by GitHub
parent 434d12956e
commit d09fd6cf92
71 changed files with 877 additions and 1010 deletions

View File

@@ -1,47 +1,60 @@
import { NextRequest } from 'next/server'
/**
* Tests for function execution API route
*
* @vitest-environment node
*/
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
const mockCreateContext = vi.fn()
const mockRunInContext = vi.fn()
const mockLogger = {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}
const mockScript = vi.fn()
const mockExecuteInE2B = vi.fn()
vi.mock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
})),
}))
vi.mock('vm', () => ({
createContext: vi.fn(),
Script: vi.fn(),
}))
vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
}))
import { createContext, Script } from 'vm'
import { validateProxyUrl } from '@/lib/core/security/input-validation'
import { executeInE2B } from '@/lib/execution/e2b'
import { createLogger } from '@/lib/logs/console/logger'
import { POST } from './route'
const mockedCreateContext = vi.mocked(createContext)
const mockedScript = vi.mocked(Script)
const mockedExecuteInE2B = vi.mocked(executeInE2B)
const mockedCreateLogger = vi.mocked(createLogger)
describe('Function Execute API Route', () => {
beforeEach(() => {
vi.resetModules()
vi.resetAllMocks()
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: vi.fn().mockImplementation(() => ({
runInContext: mockRunInContext,
})),
}))
vi.doMock('@/lib/logs/console/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn().mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
sandboxId: 'test-sandbox-id',
}),
}))
vi.clearAllMocks()
mockedCreateContext.mockReturnValue({})
mockRunInContext.mockResolvedValue('vm success')
mockCreateContext.mockReturnValue({})
mockedScript.mockImplementation((): any => ({
runInContext: mockRunInContext,
}))
mockedExecuteInE2B.mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
sandboxId: 'test-sandbox-id',
})
})
afterEach(() => {
@@ -54,20 +67,17 @@ describe('Function Execute API Route', () => {
code: 'return "test"',
})
const { POST } = await import('@/app/api/function/execute/route')
await POST(req)
expect(mockCreateContext).toHaveBeenCalled()
const contextArgs = mockCreateContext.mock.calls[0][0]
expect(mockedCreateContext).toHaveBeenCalled()
const contextArgs = mockedCreateContext.mock.calls[0][0]
expect(contextArgs).toHaveProperty('fetch')
expect(typeof contextArgs.fetch).toBe('function')
expect(typeof (contextArgs as any).fetch).toBe('function')
expect(contextArgs.fetch.name).toBe('secureFetch')
expect((contextArgs as any).fetch?.name).toBe('secureFetch')
})
it.concurrent('should block SSRF attacks through secure fetch wrapper', async () => {
const { validateProxyUrl } = await import('@/lib/core/security/input-validation')
expect(validateProxyUrl('http://169.254.169.254/latest/meta-data/').isValid).toBe(false)
expect(validateProxyUrl('http://127.0.0.1:8080/admin').isValid).toBe(false)
expect(validateProxyUrl('http://192.168.1.1/config').isValid).toBe(false)
@@ -75,16 +85,12 @@ describe('Function Execute API Route', () => {
})
it.concurrent('should allow legitimate external URLs', async () => {
const { validateProxyUrl } = await import('@/lib/core/security/input-validation')
expect(validateProxyUrl('https://api.github.com/user').isValid).toBe(true)
expect(validateProxyUrl('https://httpbin.org/get').isValid).toBe(true)
expect(validateProxyUrl('https://example.com/api').isValid).toBe(true)
})
it.concurrent('should block dangerous protocols', async () => {
const { validateProxyUrl } = await import('@/lib/core/security/input-validation')
expect(validateProxyUrl('file:///etc/passwd').isValid).toBe(false)
expect(validateProxyUrl('ftp://internal.server/files').isValid).toBe(false)
expect(validateProxyUrl('gopher://old.server/menu').isValid).toBe(false)
@@ -98,7 +104,6 @@ describe('Function Execute API Route', () => {
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -113,7 +118,6 @@ describe('Function Execute API Route', () => {
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -127,12 +131,11 @@ describe('Function Execute API Route', () => {
code: 'return "test"',
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
// The logger now logs execution success, not the request details
expect(mockLogger.info).toHaveBeenCalled()
expect(data.success).toBe(true)
})
})
@@ -145,11 +148,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// The code should be resolved to: return "secret-key-123"
})
it.concurrent('should resolve tag variables with <tag_name> syntax', async () => {
@@ -160,11 +161,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// The code should be resolved with the email object
})
it.concurrent('should NOT treat email addresses as template variables', async () => {
@@ -178,11 +177,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// Should not try to replace <waleed@sim.ai> as a template variable
})
it.concurrent('should only match valid variable names in angle brackets', async () => {
@@ -194,11 +191,9 @@ describe('Function Execute API Route', () => {
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// Should replace <validVar> and <another_valid> but not <invalid@email.com>
})
})
@@ -230,7 +225,6 @@ describe('Function Execute API Route', () => {
params: gmailData,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
@@ -255,7 +249,6 @@ describe('Function Execute API Route', () => {
params: complexEmailData,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
@@ -273,11 +266,9 @@ describe('Function Execute API Route', () => {
isCustomTool: true,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// For custom tools, parameters should be directly accessible as variables
})
})
@@ -289,7 +280,6 @@ describe('Function Execute API Route', () => {
headers: { 'Content-Type': 'application/json' },
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(500)
@@ -301,15 +291,11 @@ describe('Function Execute API Route', () => {
timeout: 10000,
})
const { POST } = await import('@/app/api/function/execute/route')
await POST(req)
const response = await POST(req)
const data = await response.json()
expect(mockLogger.info).toHaveBeenCalledWith(
expect.stringMatching(/\[.*\] Function execution request/),
expect.objectContaining({
timeout: 10000,
})
)
expect(response.status).toBe(200)
expect(data.success).toBe(true)
})
it.concurrent('should handle empty parameters object', async () => {
@@ -318,7 +304,6 @@ describe('Function Execute API Route', () => {
params: {},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
@@ -327,31 +312,25 @@ describe('Function Execute API Route', () => {
describe('Enhanced Error Handling', () => {
it('should provide detailed syntax error with line content', async () => {
// Mock VM Script to throw a syntax error
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Invalid or unexpected token')
error.name = 'SyntaxError'
error.stack = `user-function.js:5
const syntaxError = new Error('Invalid or unexpected token')
syntaxError.name = 'SyntaxError'
syntaxError.stack = `user-function.js:5
description: "This has a missing closing quote
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: Invalid or unexpected token
at new Script (node:vm:117:7)
at POST (/path/to/route.ts:123:24)`
throw error
})
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw syntaxError
})
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test",\n description: "This has a missing closing quote\n};\nreturn obj;',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -363,7 +342,6 @@ SyntaxError: Invalid or unexpected token
expect(data.error).toContain('Invalid or unexpected token')
expect(data.error).toContain('(Check for missing quotes, brackets, or semicolons)')
// Check debug information
expect(data.debug).toBeDefined()
expect(data.debug.line).toBe(3)
expect(data.debug.errorType).toBe('SyntaxError')
@@ -371,7 +349,6 @@ SyntaxError: Invalid or unexpected token
})
it('should provide detailed runtime error with line and column', async () => {
// Create the error object first
const runtimeError = new Error("Cannot read properties of null (reading 'someMethod')")
runtimeError.name = 'TypeError'
runtimeError.stack = `TypeError: Cannot read properties of null (reading 'someMethod')
@@ -379,22 +356,13 @@ SyntaxError: Invalid or unexpected token
at user-function.js:9:3
at Script.runInContext (node:vm:147:14)`
// Mock successful script creation but runtime error
const mockScript = vi.fn().mockImplementation(() => ({
runInContext: vi.fn().mockRejectedValue(runtimeError),
}))
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockRunInContext.mockRejectedValueOnce(runtimeError)
const req = createMockRequest('POST', {
code: 'const obj = null;\nreturn obj.someMethod();',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -405,7 +373,6 @@ SyntaxError: Invalid or unexpected token
expect(data.error).toContain('return obj.someMethod();')
expect(data.error).toContain('Cannot read properties of null')
// Check debug information
expect(data.debug).toBeDefined()
expect(data.debug.line).toBe(2)
expect(data.debug.column).toBe(16)
@@ -414,28 +381,19 @@ SyntaxError: Invalid or unexpected token
})
it('should handle ReferenceError with enhanced details', async () => {
// Create the error object first
const referenceError = new Error('undefinedVariable is not defined')
referenceError.name = 'ReferenceError'
referenceError.stack = `ReferenceError: undefinedVariable is not defined
at user-function.js:4:8
at Script.runInContext (node:vm:147:14)`
const mockScript = vi.fn().mockImplementation(() => ({
runInContext: vi.fn().mockRejectedValue(referenceError),
}))
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockRunInContext.mockRejectedValueOnce(referenceError)
const req = createMockRequest('POST', {
code: 'const x = 42;\nreturn undefinedVariable + x;',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -448,24 +406,18 @@ SyntaxError: Invalid or unexpected token
})
it('should handle errors without line content gracefully', async () => {
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Generic error without stack trace')
error.name = 'Error'
// No stack trace
throw error
})
const genericError = new Error('Generic error without stack trace')
genericError.name = 'Error'
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw genericError
})
const req = createMockRequest('POST', {
code: 'return "test";',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -473,7 +425,6 @@ SyntaxError: Invalid or unexpected token
expect(data.success).toBe(false)
expect(data.error).toBe('Generic error without stack trace')
// Should still have debug info, but without line details
expect(data.debug).toBeDefined()
expect(data.debug.errorType).toBe('Error')
expect(data.debug.line).toBeUndefined()
@@ -481,58 +432,47 @@ SyntaxError: Invalid or unexpected token
})
it('should extract line numbers from different stack trace formats', async () => {
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Test error')
error.name = 'Error'
error.stack = `Error: Test error
const testError = new Error('Test error')
testError.name = 'Error'
testError.stack = `Error: Test error
at user-function.js:7:25
at async function
at Script.runInContext (node:vm:147:14)`
throw error
})
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw testError
})
const req = createMockRequest('POST', {
code: 'const a = 1;\nconst b = 2;\nconst c = 3;\nconst d = 4;\nreturn a + b + c + d;',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.success).toBe(false)
// Line 7 in VM should map to line 5 in user code (7 - 3 + 1 = 5)
expect(data.debug.line).toBe(5)
expect(data.debug.column).toBe(25)
expect(data.debug.lineContent).toBe('return a + b + c + d;')
})
it.concurrent('should provide helpful suggestions for common syntax errors', async () => {
const mockScript = vi.fn().mockImplementation(() => {
const error = new Error('Unexpected end of input')
error.name = 'SyntaxError'
error.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
throw error
})
const syntaxError = new Error('Unexpected end of input')
syntaxError.name = 'SyntaxError'
syntaxError.stack = 'user-function.js:4\nSyntaxError: Unexpected end of input'
vi.doMock('vm', () => ({
createContext: mockCreateContext,
Script: mockScript,
}))
mockedScript.mockImplementationOnce(() => {
throw syntaxError
})
const req = createMockRequest('POST', {
code: 'const obj = {\n name: "test"\n// Missing closing brace',
timeout: 5000,
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
const data = await response.json()
@@ -546,7 +486,6 @@ SyntaxError: Invalid or unexpected token
describe('Utility Functions', () => {
it.concurrent('should properly escape regex special characters', async () => {
// This tests the escapeRegExp function indirectly
const req = createMockRequest('POST', {
code: 'return {{special.chars+*?}}',
envVars: {
@@ -554,15 +493,12 @@ SyntaxError: Invalid or unexpected token
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)
// Should handle special regex characters in variable names
})
it.concurrent('should handle JSON serialization edge cases', async () => {
// Test with complex but not circular data first
const req = createMockRequest('POST', {
code: 'return <complexData>',
params: {
@@ -578,7 +514,6 @@ SyntaxError: Invalid or unexpected token
},
})
const { POST } = await import('@/app/api/function/execute/route')
const response = await POST(req)
expect(response.status).toBe(200)