mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-11 07:04:58 -05:00
fix(sockets): added throttling, refactor entire socket server, added tests (#534)
* refactor(kb): use chonkie locally (#475) * feat(parsers): text and markdown parsers (#473) * feat: text and markdown parsers * fix: don't readfile on buffer, convert buffer to string instead * fix(knowledge-wh): fixed authentication error on webhook trigger fix(knowledge-wh): fixed authentication error on webhook trigger * feat(tools): add huggingface tools/blcok (#472) * add hugging face tool * docs: add Hugging Face tool documentation * fix: format and lint Hugging Face integration files * docs: add manual intro section to Hugging Face documentation * feat: replace Record<string, any> with proper HuggingFaceRequestBody interface * accidental local files added * restore some docs * make layout full for model field * change huggingface logo * add manual content * fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> * fix(knowledge-ux): fixed ux for knowledge base (#478) fix(knowledge-ux): fixed ux for knowledge base (#478) * fix(billing): bump better-auth version & fix existing subscription issue when adding seats (#484) * bump better-auth version & fix existing subscription issue Bwhen adding seats * ack PR comments * fix(env): added NEXT_PUBLIC_APP_URL to .env.example (#485) * feat(subworkflows): workflows as a block within workflows (#480) * feat(subworkflows) workflows in workflows * revert sync changes * working output vars * fix greptile comments * add cycle detection * add tests * working tests * works * fix formatting * fix input var handling * add images --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> * fix(kb): fixed kb race condition resulting in no chunks found (#487) * fix: added all blocks activeExecutionPath (#486) * refactor(chunker): replace chonkie with custom TextChunker (#479) * refactor(chunker): replace chonkie with custom TextChunker implementation and update document processing logic * chore: cleanup unimplemented types * fix: KB tests updated * fix(tab-sync): sync between tabs on change (#489) * fix(tab-sync): sync between tabs on change * refactor: optimize JSON.stringify operations that are redundant * fix(file-upload): upload presigned url to kb for file upload instead of the whole file, circumvents 4.5MB serverless func limit (#491) * feat(folders): folders to manage workflows (#490) * feat(subworkflows) workflows in workflows * revert sync changes * working output vars * fix greptile comments * add cycle detection * add tests * working tests * works * fix formatting * fix input var handling * fix(tab-sync): sync between tabs on change * feat(folders): folders to organize workflows * address comments * change schema types * fix lint error * fix typing error * fix race cond * delete unused files * improved UI * updated naming conventions * revert unrelated changes to db schema * fixed collapsed sidebar subfolders * add logs filters for folders --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> Co-authored-by: Waleed Latif <walif6@gmail.com> * revert tab sync * improvement(folders): added multi-select for moving folders (#493) * added multi-select for folders * allow drag into root * remove extraneous comments * instantly create worfklow on plus * styling improvements, fixed flicker * small improvement to dragover container * ack PR comments * fix(deployed-chat): made the chat mobile friendly (#494) * improvement(ui/ux): chat deploy (#496) * improvement(ui/ux): chat deploy experience * improvement(ui/ux): chat fontweight * feat(gmail): added option to access raw gmail from gmail polling service (#495) * added option to grab raw gmail from gmail polling service * safe json parse for function block execution to prevent vars in raw email from being resolved as sim studio vars * added tests * remove extraneous comments * fix(ui): fix the UI for folder deletion, huggingface icon, workflow block icon, standardized alert dialog (#498) * fixed folder delete UI * fixed UI for workflow block, huggingface, & added alert dialog for deleting folders * consistently style all alert dialogs * fix(reset-data): remove reset all data button from settings modal along with logic (#499) * fix(airtable): fixed airtable oauth token refresh, added tests (#502) * fixed airtable token refresh, added tests * added helpers for refreshOAuthToken function * feat(registration): disable registration + handle env booleans (#501) * feat: disable registration + handle env booleans * chore: removing pre-process because we need to use util * chore: format * feat(providers): added azure openai (#503) * added azure openai * fix request params being passed through agent block for azure * remove o1 from azure-openai models list * fix: add vscode settings to gitignore * feat(file-upload): generalized storage to support azure blob, enhanced error logging in kb, added xlsx parser (#506) * added blob storage option for azure, refactored storage client to be provider agnostic, tested kb & file upload and s3 is undisrupted, still have to test blob * updated CORS policy for blob, added azure blob-specific headers * remove extraneous comments * add file size limit and timeout * added some extra error handling in kb add documents * grouped envvars * ack PR comments * added sheetjs and xlsx parser * fix(folders): modified folder deletion to delete subfolders & workflows in it instead of moving to root (#508) * modified folder deletion to delete subfolders & workflows in it instead of moving to root * added additional testing utils * ack PR comments * feat: api response block and implementation * improvement(local-storage): remove use of local storage except for oauth and last active workspace id (#497) * remove local storage usage * remove migration for last active workspace id * Update apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx Add fallback for required scopes Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * add url builder util * fi * fix lint * lint * modify pre commit hook * fix oauth * get last active workspace working again * new workspace logic works * fetch locks * works now * remove empty useEffect * fix loading issue * skip empty workflow syncs * use isWorkspace in transition flag * add logging * add data initialized flag * fix lint * fix: build error by create a server-side utils * remove migration snapshots * reverse search for workspace based on workflow id * fix lint * improvement: loading check and animation * remove unused utils * remove console logs --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan> * feat(multi-select): simplified chat to always return readable stream, can select multiple outputs and get response streamed back in chat panel & deployed chat (#507) * improvement: all workflow executions return ReadableStream & use sse to support multiple streamed outputs in chats * fixed build * remove extraneous comments * general improvemetns * ack PR comments * fixed built * improvement(workflow-state): split workflow state into separate tables (#511) * new tables to track workflow state * fix lint * refactor into separate tables * fix typing * fix lint * add tests * fix lint * add correct foreign key constraint * add self ref * remove unused checks * fix types * fix type --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> * feat(models): added new openai models, updated model pricing, added new groq model (#513) * fix(autocomplete): fixed extra closing tag on tag dropdown autocomplete (#514) * chore: enable input format again * fix: process the input made on api calls with proper extraction * feat: add json-object for ai generation for response block and others * chore: add documentation for response block * chore: rollback temp fix and uncomment original input handler * chore: add missing mock for response handler * chore: add missing mock * chore: greptile recommendations * added cost tracking for router & evaluator blocks, consolidated model information into a single file, hosted keys for evaluator & router, parallelized unit tests (#516) * fix(deployState): deploy not persisting bug (#518) * fix(undeploy-bug): fix deployment persistence failing bug * fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> * fix decimal entry issues * remove unused files * fix(db): decimal position entry issues (#520) * fix decimal entry issues * remove unused files --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> * fix lint * fix test * improvement(kb): added configurability for chunks, query across multiple knowledge bases (#512) * refactor: consolidate create modal file * fix: identify dead processes * fix: mark failed in DB after processing timeout * improvement: added overlap chunks and fixed modal UI * feat: multiselect logic * fix: biome changes for css ordering warn instead of error * improvement: create chunk ui * fix: removed unused schema columns * fix: removed references to deleted columns * improvement: sped up vector search time * feat: multi-kb search * add bulk endpoint to disable/delete multiple chunks * add bulk endpoint to disable/delete multiple chunks * fix: removed unused schema columns * fix: removed references to deleted columns * made endpoints for knowledge more RESTful, added tests * added batch operations for delete/enable/disable docs, alr have this for chunks * added migrations * added migrations --------- Co-authored-by: Waleed Latif <walif6@gmail.com> * fix(models): remove temp from models that don't support it * feat(sdk): added ts and python SDKs + docs (#524) * added ts & python sdk, renamed cli from simstudio to cli * added docs * ack PR comments * improvements * fixed issue where it goes to random workspace when you click reload fixed lint issue * feat: better response builder + doc update * fix(auth): added preview URLs to list of trusted origins (#525) * trusted origins * lint error * removed localhost * ran lint --------- Co-authored-by: Waleed Latif <walif6@gmail.com> * fix(sdk): remove dev script from SDK * PR: changes for migration * add changes on top of db migration changes * fix: allow removing single input field * improvement(permissions): workspace permissions improvements, added provider and reduced API calls by 85% (#530) * improved permissions UI & access patterns, show outstanding invites * added logger * added provider for workspace permissions, 85% reduction in API calls to get user permissions and improved performance for invitations * ack PR comments * cleanup * fix disabled tooltips * improvement(tests): parallelized tests and build fixes (#531) * added provider for workspace permissions, 85% reduction in API calls to get user permissions and improved performance for invitations * parallelized more tests, fixed test warnings * removed waitlist verification route, use more utils in tests * fixed build * ack PR comments * fix * fix(kb): reduced params in kb block, added advanced mode to starter block, updated docs * feat(realtime): sockets + normalized tables + deprecate sync (#523) * feat: implement real-time collaborative workflow editing with Socket.IO - Add Socket.IO server with room-based architecture for workflow collaboration - Implement socket context for client-side real-time communication - Add collaborative workflow hook for synchronized state management - Update CSP to allow socket connections to localhost:3002 - Add fallback authentication for testing collaborative features - Enable real-time broadcasting of workflow operations between tabs - Support multi-user editing of blocks, edges, and workflow state Key components: - socket-server/: Complete Socket.IO server with authentication and room management - contexts/socket-context.tsx: Client-side socket connection and state management - hooks/use-collaborative-workflow.ts: Hook for collaborative workflow operations - Workflow store integration for real-time state synchronization Status: Basic collaborative features working, authentication bypass enabled for testing * feat: complete collaborative subblock editing implementation ✅ All collaborative features now working perfectly: - Real-time block movement and positioning - Real-time subblock value editing (text fields, inputs) - Real-time edge operations and parent updates - Multi-user workflow rooms with proper broadcasting - Socket.IO server with room-based architecture - Permission bypass system for testing 🔧 Technical improvements: - Modified useSubBlockValue hook to use collaborative event system - All subblock setValue calls now dispatch 'update-subblock-value' events - Collaborative workflow hook handles all real-time operations - Socket server processes and persists all operations to database - Clean separation between local and collaborative state management 🧪 Tested and verified: - Multiple browser tabs with different fallback users - Block dragging and positioning updates in real-time - Subblock text editing reflects immediately across tabs - Workflow room management and user presence - Database persistence of all collaborative operations Status: Full collaborative workflow editing working with fallback authentication * feat: implement proper authentication for collaborative Socket.IO server ✅ **Authentication System Complete**: - Removed all fallback authentication code and bypasses - Socket server now requires valid Better Auth session cookies - Proper session validation using auth.api.getSession() - Authentication errors properly handled and logged - User info extracted from session: userId, userName, email, organizationId 🔧 **Technical Implementation**: - Updated CSP to allow WebSocket connections (ws://localhost:3002) - Socket authentication middleware validates session tokens - Proper error handling for missing/invalid sessions - Permission system enforces workflow access controls - Clean separation between authenticated and unauthenticated states 🧪 **Testing Status**: - Socket server properly rejects unauthenticated connections - Authentication errors logged with clear messages - CSP updated to allow both HTTP and WebSocket protocols - Ready for testing with authenticated users Status: Production-ready collaborative authentication system * feat: complete authentication integration for collaborative Socket.IO system 🎉 **PRODUCTION-READY COLLABORATIVE SYSTEM** ✅ **Authentication Integration Complete**: - Fixed Socket.IO client to send credentials (withCredentials: true) - Updated server CORS to accept credentials with specific origin - Removed all fallback authentication bypasses - Proper Better Auth session validation working 🔧 **Technical Fixes**: - Socket client: Enable withCredentials for cookie transmission - Socket server: Accept credentials with origin 'http://localhost:3000' - Better Auth cookie utility integration for session parsing - Comprehensive authentication middleware with proper error handling 🧪 **Verified Working Features**: - ✅ Real user authentication (Vikhyath Mondreti authenticated) - ✅ Multi-user workflow rooms (2+ users in same workflow) - ✅ Permission system enforcing workflow access controls - ✅ Real-time subblock editing across browser tabs - ✅ Block movement and positioning updates - ✅ Automatic room cleanup and management - ✅ Database persistence of all collaborative operations 🚀 **Status**: Complete enterprise-grade collaborative workflow editing system - No more fallback users - production authentication - Multi-tab collaboration working perfectly - Secure access control with Better Auth integration - Real-time updates for all workflow operations * remove sync system and move to server side * fix lint * delete unused file * added socketio dep * fix subblock persistence bug * working deletion of workflows * fix lint * added railway * add debug logging for railway deployment * improve typing * fix lint * working subflow persistence * fix lint * working cascade deletion * fix lint * working subflow inside subflow * works * fix lint * prevent subflow in subflow * fix lint * add additional logs, add localhost as allowedOrigin * add additional logs, add localhost as allowedOrigin * fix type error * remove unused code * fix lint * fix tests * fix lint * fix build error * workign folder updates * fix typing issue * fix lint * fix typing issues * lib/ * fix tests * added old presence component back, updated to use one-time-token better auth plugin for socket server auth, tested * fix errors * fix bugs * add migration scripts to run * fix lint * fix deploy tests * fix lint * fix minor issues * fix lint * fix migration script * allow comma separateds id file input to migration script * fix lint * fixed * fix lint * fix fallback case * fix type errors * address greptile comments * fix lint * fix script to generate new block ids * fix lint --------- Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan> Co-authored-by: Waleed Latif <walif6@gmail.com> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> * fix(sockets): updated CSP * remove unecessary logs * fix lint * added throttling, refactor entire socket server, added tests * improvements * remove self monitoring func, add block name event * working isWide, isAdvanced toggles with sockets * fix lint * fix duplicate key issue for user avatar * fix lint * fix user presence * working parallel badges / loop badges updates * working connection output persistence * fix lint * fix build errors * fix lint * logs removed * fix cascade var name update bug * works * fix lint * fix parallel blocks * fix placeholder * fix test * fixed tests --------- Co-authored-by: Aditya Tripathi <aditya@climactic.co> Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-MacBook-Air.local> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@Vikhyaths-Air.attlocal.net> Co-authored-by: Emir Karabeg <emirkarabeg@berkeley.edu> Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Vikhyath Mondreti <vikhyathmondreti@vikhyaths-air.lan> Co-authored-by: Ajit Kadaveru <ajit.kadaveru@berkeley.edu>
This commit is contained in:
544
apps/sim/app/api/function/execute/route.test.ts
Normal file
544
apps/sim/app/api/function/execute/route.test.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
/**
|
||||
* Tests for function execution API route
|
||||
*
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createMockRequest } from '@/app/api/__test-utils__/utils'
|
||||
|
||||
const mockFreestyleExecuteScript = vi.fn()
|
||||
const mockCreateContext = vi.fn()
|
||||
const mockRunInContext = vi.fn()
|
||||
const mockLogger = {
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}
|
||||
|
||||
describe('Function Execute API Route', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.resetAllMocks()
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: vi.fn().mockImplementation(() => ({
|
||||
runInContext: mockRunInContext,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.doMock('freestyle-sandboxes', () => ({
|
||||
FreestyleSandboxes: vi.fn().mockImplementation(() => ({
|
||||
executeScript: mockFreestyleExecuteScript,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
FREESTYLE_API_KEY: 'test-freestyle-key',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
mockFreestyleExecuteScript.mockResolvedValue({
|
||||
result: 'freestyle success',
|
||||
logs: [],
|
||||
})
|
||||
|
||||
mockRunInContext.mockResolvedValue('vm success')
|
||||
mockCreateContext.mockReturnValue({})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Basic Function Execution', () => {
|
||||
it('should execute simple JavaScript code successfully', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "Hello World"',
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.output).toHaveProperty('result')
|
||||
expect(data.output).toHaveProperty('executionTime')
|
||||
})
|
||||
|
||||
it('should handle missing code parameter', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
expect(data.success).toBe(false)
|
||||
expect(data).toHaveProperty('error')
|
||||
})
|
||||
|
||||
it('should use default timeout when not provided', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\[.*\] Function execution request/),
|
||||
expect.objectContaining({
|
||||
timeout: 5000, // default timeout
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Template Variable Resolution', () => {
|
||||
it('should resolve environment variables with {{var_name}} syntax', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{API_KEY}}',
|
||||
envVars: {
|
||||
API_KEY: 'secret-key-123',
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// The code should be resolved to: return "secret-key-123"
|
||||
})
|
||||
|
||||
it('should resolve tag variables with <tag_name> syntax', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <email>',
|
||||
params: {
|
||||
email: { id: '123', subject: 'Test Email' },
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// The code should be resolved with the email object
|
||||
})
|
||||
|
||||
it('should NOT treat email addresses as template variables', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "Email sent to user"',
|
||||
params: {
|
||||
email: {
|
||||
from: 'Waleed Latif <waleed@simstudio.ai>',
|
||||
to: 'User <user@example.com>',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// Should not try to replace <waleed@simstudio.ai> as a template variable
|
||||
})
|
||||
|
||||
it('should only match valid variable names in angle brackets', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <validVar> + "<invalid@email.com>" + <another_valid>',
|
||||
params: {
|
||||
validVar: 'hello',
|
||||
another_valid: 'world',
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// Should replace <validVar> and <another_valid> but not <invalid@email.com>
|
||||
})
|
||||
})
|
||||
|
||||
describe('Gmail Email Data Handling', () => {
|
||||
it('should handle Gmail webhook data with email addresses containing angle brackets', async () => {
|
||||
const gmailData = {
|
||||
email: {
|
||||
id: '123',
|
||||
from: 'Waleed Latif <waleed@simstudio.ai>',
|
||||
to: 'User <user@example.com>',
|
||||
subject: 'Test Email',
|
||||
bodyText: 'Hello world',
|
||||
},
|
||||
rawEmail: {
|
||||
id: '123',
|
||||
payload: {
|
||||
headers: [
|
||||
{ name: 'From', value: 'Waleed Latif <waleed@simstudio.ai>' },
|
||||
{ name: 'To', value: 'User <user@example.com>' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <email>',
|
||||
params: gmailData,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.success).toBe(true)
|
||||
})
|
||||
|
||||
it('should properly serialize complex email objects with special characters', async () => {
|
||||
const complexEmailData = {
|
||||
email: {
|
||||
from: 'Test User <test@example.com>',
|
||||
bodyHtml: '<div>HTML content with "quotes" and \'apostrophes\'</div>',
|
||||
bodyText: 'Text with\nnewlines\tand\ttabs',
|
||||
},
|
||||
}
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <email>',
|
||||
params: complexEmailData,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Freestyle Execution', () => {
|
||||
it('should use Freestyle when API key is available', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "freestyle test"',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
await POST(req)
|
||||
|
||||
expect(mockFreestyleExecuteScript).toHaveBeenCalled()
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\[.*\] Using Freestyle for code execution/)
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle Freestyle errors and fallback to VM', async () => {
|
||||
mockFreestyleExecuteScript.mockRejectedValueOnce(new Error('Freestyle API error'))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "fallback test"',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(mockFreestyleExecuteScript).toHaveBeenCalled()
|
||||
expect(mockRunInContext).toHaveBeenCalled()
|
||||
expect(mockLogger.error).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\[.*\] Freestyle API call failed, falling back to VM:/),
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle Freestyle script errors', async () => {
|
||||
mockFreestyleExecuteScript.mockResolvedValueOnce({
|
||||
result: null,
|
||||
logs: [{ type: 'error', message: 'ReferenceError: undefined variable' }],
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return undefinedVariable',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
const data = await response.json()
|
||||
expect(data.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('VM Execution', () => {
|
||||
it('should use VM when Freestyle API key is not available', async () => {
|
||||
// Mock no Freestyle API key
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
FREESTYLE_API_KEY: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "vm test"',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
await POST(req)
|
||||
|
||||
expect(mockFreestyleExecuteScript).not.toHaveBeenCalled()
|
||||
expect(mockRunInContext).toHaveBeenCalled()
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/\[.*\] Using VM for code execution \(no Freestyle API key available\)/
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle VM execution errors', async () => {
|
||||
// Mock no Freestyle API key so it uses VM
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
FREESTYLE_API_KEY: undefined,
|
||||
},
|
||||
}))
|
||||
|
||||
mockRunInContext.mockRejectedValueOnce(new Error('VM execution error'))
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return invalidCode(',
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
const data = await response.json()
|
||||
expect(data.success).toBe(false)
|
||||
expect(data.error).toContain('VM execution error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom Tools', () => {
|
||||
it('should handle custom tool execution with direct parameter access', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return location + " weather is sunny"',
|
||||
params: {
|
||||
location: 'San Francisco',
|
||||
},
|
||||
isCustomTool: true,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// For custom tools, parameters should be directly accessible as variables
|
||||
})
|
||||
})
|
||||
|
||||
describe('Security and Edge Cases', () => {
|
||||
it('should handle malformed JSON in request body', async () => {
|
||||
const req = new NextRequest('http://localhost:3000/api/function/execute', {
|
||||
method: 'POST',
|
||||
body: 'invalid json{',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(500)
|
||||
})
|
||||
|
||||
it('should handle timeout parameter', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "test"',
|
||||
timeout: 10000,
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
await POST(req)
|
||||
|
||||
expect(mockLogger.info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/\[.*\] Function execution request/),
|
||||
expect.objectContaining({
|
||||
timeout: 10000,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle empty parameters object', async () => {
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return "no params"',
|
||||
params: {},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
it('should properly escape regex special characters', async () => {
|
||||
// This tests the escapeRegExp function indirectly
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{special.chars+*?}}',
|
||||
envVars: {
|
||||
'special.chars+*?': 'escaped-value',
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
// Should handle special regex characters in variable names
|
||||
})
|
||||
|
||||
it('should handle JSON serialization edge cases', async () => {
|
||||
// Test with complex but not circular data first
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return <complexData>',
|
||||
params: {
|
||||
complexData: {
|
||||
special: 'chars"with\'quotes',
|
||||
unicode: '🎉 Unicode content',
|
||||
nested: {
|
||||
deep: {
|
||||
value: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Function Execute API - Template Variable Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules()
|
||||
vi.resetAllMocks()
|
||||
|
||||
vi.doMock('@/lib/logs/console-logger', () => ({
|
||||
createLogger: vi.fn().mockReturnValue(mockLogger),
|
||||
}))
|
||||
|
||||
vi.doMock('@/lib/env', () => ({
|
||||
env: {
|
||||
FREESTYLE_API_KEY: 'test-freestyle-key',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.doMock('vm', () => ({
|
||||
createContext: mockCreateContext,
|
||||
Script: vi.fn().mockImplementation(() => ({
|
||||
runInContext: mockRunInContext,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.doMock('freestyle-sandboxes', () => ({
|
||||
FreestyleSandboxes: vi.fn().mockImplementation(() => ({
|
||||
executeScript: mockFreestyleExecuteScript,
|
||||
})),
|
||||
}))
|
||||
|
||||
mockFreestyleExecuteScript.mockResolvedValue({
|
||||
result: 'freestyle success',
|
||||
logs: [],
|
||||
})
|
||||
|
||||
mockRunInContext.mockResolvedValue('vm success')
|
||||
mockCreateContext.mockReturnValue({})
|
||||
})
|
||||
|
||||
it('should handle nested template variables', async () => {
|
||||
mockFreestyleExecuteScript.mockResolvedValueOnce({
|
||||
result: 'environment-valueparam-value',
|
||||
logs: [],
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{outer}} + <inner>',
|
||||
envVars: {
|
||||
outer: 'environment-value',
|
||||
},
|
||||
params: {
|
||||
inner: 'param-value',
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.output.result).toBe('environment-valueparam-value')
|
||||
})
|
||||
|
||||
it('should prioritize environment variables over params for {{}} syntax', async () => {
|
||||
mockFreestyleExecuteScript.mockResolvedValueOnce({
|
||||
result: 'env-wins',
|
||||
logs: [],
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{conflictVar}}',
|
||||
envVars: {
|
||||
conflictVar: 'env-wins',
|
||||
},
|
||||
params: {
|
||||
conflictVar: 'param-loses',
|
||||
},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
// Environment variable should take precedence
|
||||
expect(data.output.result).toBe('env-wins')
|
||||
})
|
||||
|
||||
it('should handle missing template variables gracefully', async () => {
|
||||
mockFreestyleExecuteScript.mockResolvedValueOnce({
|
||||
result: '',
|
||||
logs: [],
|
||||
})
|
||||
|
||||
const req = createMockRequest('POST', {
|
||||
code: 'return {{nonexistent}} + <alsoMissing>',
|
||||
envVars: {},
|
||||
params: {},
|
||||
})
|
||||
|
||||
const { POST } = await import('./route')
|
||||
const response = await POST(req)
|
||||
const data = await response.json()
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data.success).toBe(true)
|
||||
expect(data.output.result).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,39 @@ const logger = createLogger('FunctionExecuteAPI')
|
||||
* @param envVars - Environment variables from the workflow
|
||||
* @returns Resolved code
|
||||
*/
|
||||
/**
|
||||
* Safely serialize a value to JSON string with proper escaping
|
||||
* This prevents JavaScript syntax errors when the serialized data is injected into code
|
||||
*/
|
||||
function safeJSONStringify(value: any): string {
|
||||
try {
|
||||
// Use JSON.stringify with proper escaping
|
||||
// The key is to let JSON.stringify handle the escaping properly
|
||||
return JSON.stringify(value)
|
||||
} catch (error) {
|
||||
// If JSON.stringify fails (e.g., circular references), return a safe fallback
|
||||
try {
|
||||
// Try to create a safe representation by removing circular references
|
||||
const seen = new WeakSet()
|
||||
const cleanValue = JSON.parse(
|
||||
JSON.stringify(value, (key, val) => {
|
||||
if (typeof val === 'object' && val !== null) {
|
||||
if (seen.has(val)) {
|
||||
return '[Circular Reference]'
|
||||
}
|
||||
seen.add(val)
|
||||
}
|
||||
return val
|
||||
})
|
||||
)
|
||||
return JSON.stringify(cleanValue)
|
||||
} catch {
|
||||
// If that also fails, return a safe string representation
|
||||
return JSON.stringify(String(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCodeVariables(
|
||||
code: string,
|
||||
params: Record<string, any>,
|
||||
@@ -29,21 +62,34 @@ function resolveCodeVariables(
|
||||
const varName = match.slice(2, -2).trim()
|
||||
// Priority: 1. Environment variables from workflow, 2. Params
|
||||
const varValue = envVars[varName] || params[varName] || ''
|
||||
// Wrap the value in quotes to ensure it's treated as a string literal
|
||||
resolvedCode = resolvedCode.replace(match, JSON.stringify(varValue))
|
||||
// Use safe JSON stringify to prevent syntax errors
|
||||
resolvedCode = resolvedCode.replace(
|
||||
new RegExp(escapeRegExp(match), 'g'),
|
||||
safeJSONStringify(varValue)
|
||||
)
|
||||
}
|
||||
|
||||
// Resolve tags with <tag_name> syntax
|
||||
const tagMatches = resolvedCode.match(/<([^>]+)>/g) || []
|
||||
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_]*)>/g) || []
|
||||
for (const match of tagMatches) {
|
||||
const tagName = match.slice(1, -1).trim()
|
||||
const tagValue = params[tagName] || ''
|
||||
resolvedCode = resolvedCode.replace(match, JSON.stringify(tagValue))
|
||||
resolvedCode = resolvedCode.replace(
|
||||
new RegExp(escapeRegExp(match), 'g'),
|
||||
safeJSONStringify(tagValue)
|
||||
)
|
||||
}
|
||||
|
||||
return resolvedCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special regex characters in a string
|
||||
*/
|
||||
function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const startTime = Date.now()
|
||||
@@ -61,18 +107,18 @@ export async function POST(req: NextRequest) {
|
||||
isCustomTool = false,
|
||||
} = body
|
||||
|
||||
// Extract internal parameters that shouldn't be passed to the execution context
|
||||
const executionParams = { ...params }
|
||||
executionParams._context = undefined
|
||||
|
||||
logger.info(`[${requestId}] Function execution request`, {
|
||||
hasCode: !!code,
|
||||
paramsCount: Object.keys(params).length,
|
||||
paramsCount: Object.keys(executionParams).length,
|
||||
timeout,
|
||||
workflowId,
|
||||
isCustomTool,
|
||||
})
|
||||
|
||||
// Extract internal parameters that shouldn't be passed to the execution context
|
||||
const executionParams = { ...params }
|
||||
executionParams._context = undefined
|
||||
|
||||
// Resolve variables in the code with workflow environment variables
|
||||
const resolvedCode = resolveCodeVariables(code, executionParams, envVars)
|
||||
|
||||
@@ -115,7 +161,7 @@ export async function POST(req: NextRequest) {
|
||||
? `export default async () => {
|
||||
// For custom tools, directly declare parameters as variables
|
||||
${Object.entries(executionParams)
|
||||
.map(([key, value]) => `const ${key} = ${JSON.stringify(value)};`)
|
||||
.map(([key, value]) => `const ${key} = ${safeJSONStringify(value)};`)
|
||||
.join('\n ')}
|
||||
${resolvedCode}
|
||||
}`
|
||||
@@ -152,7 +198,10 @@ export async function POST(req: NextRequest) {
|
||||
errorMessage,
|
||||
stdout,
|
||||
})
|
||||
throw errorMessage
|
||||
// Create a proper Error object to be caught by the outer handler
|
||||
const scriptError = new Error(errorMessage)
|
||||
scriptError.name = 'FreestyleScriptError'
|
||||
throw scriptError
|
||||
}
|
||||
|
||||
// If no errors, execution was successful
|
||||
@@ -163,7 +212,7 @@ export async function POST(req: NextRequest) {
|
||||
})
|
||||
} catch (error: any) {
|
||||
// Check if the error came from our explicit throw above due to script errors
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'FreestyleScriptError') {
|
||||
throw error // Re-throw to be caught by the outer handler
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user