@@ -13,6 +13,8 @@ import {
1313 type ExecutionContext ,
1414 type MockFetchResponse ,
1515} from '@sim/testing'
16+ import { DEFAULT_EXECUTION_TIMEOUT_MS } from '@/lib/core/execution-limits'
17+ import * as securityValidation from '@/lib/core/security/input-validation.server'
1618import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
1719
1820// Hoisted mock state - these are available to vi.mock factories
@@ -437,6 +439,113 @@ describe('executeTool Function', () => {
437439 } )
438440} )
439441
442+ describe ( 'Internal Tool Timeout Behavior' , ( ) => {
443+ let cleanupEnvVars : ( ) => void
444+
445+ beforeEach ( ( ) => {
446+ process . env . NEXT_PUBLIC_APP_URL = 'http://localhost:3000'
447+ cleanupEnvVars = setupEnvVars ( { NEXT_PUBLIC_APP_URL : 'http://localhost:3000' } )
448+ } )
449+
450+ afterEach ( ( ) => {
451+ vi . restoreAllMocks ( )
452+ cleanupEnvVars ( )
453+ } )
454+
455+ it ( 'should pass explicit timeout to secureFetchWithPinnedIP for internal routes' , async ( ) => {
456+ const expectedTimeout = 600000
457+
458+ const secureFetchSpy = vi
459+ . spyOn ( securityValidation , 'secureFetchWithPinnedIP' )
460+ . mockResolvedValue ( {
461+ ok : true ,
462+ status : 200 ,
463+ statusText : 'OK' ,
464+ headers : new securityValidation . SecureFetchHeaders ( { 'content-type' : 'application/json' } ) ,
465+ text : async ( ) => JSON . stringify ( { success : true } ) ,
466+ json : async ( ) => ( { success : true } ) ,
467+ arrayBuffer : async ( ) => new TextEncoder ( ) . encode ( JSON . stringify ( { success : true } ) ) . buffer ,
468+ } )
469+
470+ const originalFunctionTool = { ...tools . function_execute }
471+ tools . function_execute = {
472+ ...tools . function_execute ,
473+ transformResponse : vi . fn ( ) . mockResolvedValue ( {
474+ success : true ,
475+ output : { result : 'executed' } ,
476+ } ) ,
477+ }
478+
479+ try {
480+ const result = await executeTool (
481+ 'function_execute' ,
482+ {
483+ code : 'return 1' ,
484+ timeout : expectedTimeout ,
485+ } ,
486+ true
487+ )
488+
489+ expect ( result . success ) . toBe ( true )
490+ expect ( secureFetchSpy ) . toHaveBeenCalled ( )
491+ expect ( secureFetchSpy ) . toHaveBeenCalledWith (
492+ expect . stringContaining ( '/api/function/execute' ) ,
493+ '127.0.0.1' ,
494+ expect . objectContaining ( {
495+ timeout : expectedTimeout ,
496+ } )
497+ )
498+ } finally {
499+ tools . function_execute = originalFunctionTool
500+ }
501+ } )
502+
503+ it ( 'should use DEFAULT_EXECUTION_TIMEOUT_MS when timeout is not provided' , async ( ) => {
504+ const secureFetchSpy = vi
505+ . spyOn ( securityValidation , 'secureFetchWithPinnedIP' )
506+ . mockResolvedValue ( {
507+ ok : true ,
508+ status : 200 ,
509+ statusText : 'OK' ,
510+ headers : new securityValidation . SecureFetchHeaders ( { 'content-type' : 'application/json' } ) ,
511+ text : async ( ) => JSON . stringify ( { success : true } ) ,
512+ json : async ( ) => ( { success : true } ) ,
513+ arrayBuffer : async ( ) => new TextEncoder ( ) . encode ( JSON . stringify ( { success : true } ) ) . buffer ,
514+ } )
515+
516+ const originalFunctionTool = { ...tools . function_execute }
517+ tools . function_execute = {
518+ ...tools . function_execute ,
519+ transformResponse : vi . fn ( ) . mockResolvedValue ( {
520+ success : true ,
521+ output : { result : 'executed' } ,
522+ } ) ,
523+ }
524+
525+ try {
526+ const result = await executeTool (
527+ 'function_execute' ,
528+ {
529+ code : 'return 1' ,
530+ } ,
531+ true
532+ )
533+
534+ expect ( result . success ) . toBe ( true )
535+ expect ( secureFetchSpy ) . toHaveBeenCalled ( )
536+ expect ( secureFetchSpy ) . toHaveBeenCalledWith (
537+ expect . stringContaining ( '/api/function/execute' ) ,
538+ '127.0.0.1' ,
539+ expect . objectContaining ( {
540+ timeout : DEFAULT_EXECUTION_TIMEOUT_MS ,
541+ } )
542+ )
543+ } finally {
544+ tools . function_execute = originalFunctionTool
545+ }
546+ } )
547+ } )
548+
440549describe ( 'Automatic Internal Route Detection' , ( ) => {
441550 let cleanupEnvVars : ( ) => void
442551
0 commit comments