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:
Waleed Latif
2025-06-24 17:44:30 -07:00
committed by GitHub
parent 37786d371e
commit 76df2b9cd9
399 changed files with 60804 additions and 12234 deletions

View 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('')
})
})

View File

@@ -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
}