@@ -1901,5 +1901,317 @@ describe('AgentBlockHandler', () => {
19011901
19021902 expect ( discoveryCalls [ 0 ] . url ) . toContain ( 'serverId=mcp-legacy-server' )
19031903 } )
1904+
1905+ describe ( 'customToolId resolution - DB as source of truth' , ( ) => {
1906+ const staleInlineSchema = {
1907+ function : {
1908+ name : 'buttonTemplate' ,
1909+ description : 'Creates a button template' ,
1910+ parameters : {
1911+ type : 'object' ,
1912+ properties : {
1913+ sender_id : { type : 'string' , description : 'Sender ID' } ,
1914+ header_value : { type : 'string' , description : 'Header text' } ,
1915+ body_value : { type : 'string' , description : 'Body text' } ,
1916+ button_array : {
1917+ type : 'array' ,
1918+ items : { type : 'string' } ,
1919+ description : 'Button labels' ,
1920+ } ,
1921+ } ,
1922+ required : [ 'sender_id' , 'header_value' , 'body_value' , 'button_array' ] ,
1923+ } ,
1924+ } ,
1925+ }
1926+
1927+ const dbSchema = {
1928+ function : {
1929+ name : 'buttonTemplate' ,
1930+ description : 'Creates a button template' ,
1931+ parameters : {
1932+ type : 'object' ,
1933+ properties : {
1934+ sender_id : { type : 'string' , description : 'Sender ID' } ,
1935+ header_value : { type : 'string' , description : 'Header text' } ,
1936+ body_value : { type : 'string' , description : 'Body text' } ,
1937+ button_array : {
1938+ type : 'array' ,
1939+ items : { type : 'string' } ,
1940+ description : 'Button labels' ,
1941+ } ,
1942+ channel : { type : 'string' , description : 'Channel name' } ,
1943+ } ,
1944+ required : [ 'sender_id' , 'header_value' , 'body_value' , 'button_array' , 'channel' ] ,
1945+ } ,
1946+ } ,
1947+ }
1948+
1949+ const staleInlineCode =
1950+ 'return JSON.stringify({ type: "button", phone: sender_id, header: header_value, body: body_value, buttons: button_array });'
1951+ const dbCode =
1952+ 'if (channel === "whatsapp") { return JSON.stringify({ type: "button", phone: sender_id, header: header_value, body: body_value, buttons: button_array }); }'
1953+
1954+ function mockFetchForCustomTool ( toolId : string ) {
1955+ mockFetch . mockImplementation ( ( url : string ) => {
1956+ if ( typeof url === 'string' && url . includes ( '/api/tools/custom' ) ) {
1957+ return Promise . resolve ( {
1958+ ok : true ,
1959+ headers : { get : ( ) => null } ,
1960+ json : ( ) =>
1961+ Promise . resolve ( {
1962+ data : [
1963+ {
1964+ id : toolId ,
1965+ title : 'buttonTemplate' ,
1966+ schema : dbSchema ,
1967+ code : dbCode ,
1968+ } ,
1969+ ] ,
1970+ } ) ,
1971+ } )
1972+ }
1973+ return Promise . resolve ( {
1974+ ok : true ,
1975+ headers : { get : ( ) => null } ,
1976+ json : ( ) => Promise . resolve ( { } ) ,
1977+ } )
1978+ } )
1979+ }
1980+
1981+ function mockFetchFailure ( ) {
1982+ mockFetch . mockImplementation ( ( url : string ) => {
1983+ if ( typeof url === 'string' && url . includes ( '/api/tools/custom' ) ) {
1984+ return Promise . resolve ( {
1985+ ok : false ,
1986+ status : 500 ,
1987+ headers : { get : ( ) => null } ,
1988+ json : ( ) => Promise . resolve ( { } ) ,
1989+ } )
1990+ }
1991+ return Promise . resolve ( {
1992+ ok : true ,
1993+ headers : { get : ( ) => null } ,
1994+ json : ( ) => Promise . resolve ( { } ) ,
1995+ } )
1996+ } )
1997+ }
1998+
1999+ beforeEach ( ( ) => {
2000+ Object . defineProperty ( global , 'window' , {
2001+ value : undefined ,
2002+ writable : true ,
2003+ configurable : true ,
2004+ } )
2005+ } )
2006+
2007+ it ( 'should always fetch latest schema from DB when customToolId is present' , async ( ) => {
2008+ const toolId = 'custom-tool-123'
2009+ mockFetchForCustomTool ( toolId )
2010+
2011+ const inputs = {
2012+ model : 'gpt-4o' ,
2013+ userPrompt : 'Send a button template' ,
2014+ apiKey : 'test-api-key' ,
2015+ tools : [
2016+ {
2017+ type : 'custom-tool' ,
2018+ customToolId : toolId ,
2019+ title : 'buttonTemplate' ,
2020+ schema : staleInlineSchema ,
2021+ code : staleInlineCode ,
2022+ usageControl : 'auto' as const ,
2023+ } ,
2024+ ] ,
2025+ }
2026+
2027+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
2028+
2029+ await handler . execute ( mockContext , mockBlock , inputs )
2030+
2031+ expect ( mockExecuteProviderRequest ) . toHaveBeenCalled ( )
2032+ const providerCall = mockExecuteProviderRequest . mock . calls [ 0 ]
2033+ const tools = providerCall [ 1 ] . tools
2034+
2035+ expect ( tools . length ) . toBe ( 1 )
2036+ // DB schema wins over stale inline — includes channel param
2037+ expect ( tools [ 0 ] . parameters . required ) . toContain ( 'channel' )
2038+ expect ( tools [ 0 ] . parameters . properties ) . toHaveProperty ( 'channel' )
2039+ } )
2040+
2041+ it ( 'should fetch from DB when customToolId has no inline schema' , async ( ) => {
2042+ const toolId = 'custom-tool-123'
2043+ mockFetchForCustomTool ( toolId )
2044+
2045+ const inputs = {
2046+ model : 'gpt-4o' ,
2047+ userPrompt : 'Send a button template' ,
2048+ apiKey : 'test-api-key' ,
2049+ tools : [
2050+ {
2051+ type : 'custom-tool' ,
2052+ customToolId : toolId ,
2053+ usageControl : 'auto' as const ,
2054+ } ,
2055+ ] ,
2056+ }
2057+
2058+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
2059+
2060+ await handler . execute ( mockContext , mockBlock , inputs )
2061+
2062+ expect ( mockExecuteProviderRequest ) . toHaveBeenCalled ( )
2063+ const providerCall = mockExecuteProviderRequest . mock . calls [ 0 ]
2064+ const tools = providerCall [ 1 ] . tools
2065+
2066+ expect ( tools . length ) . toBe ( 1 )
2067+ expect ( tools [ 0 ] . name ) . toBe ( 'buttonTemplate' )
2068+ expect ( tools [ 0 ] . parameters . required ) . toContain ( 'channel' )
2069+ } )
2070+
2071+ it ( 'should fall back to inline schema when DB fetch fails and inline exists' , async ( ) => {
2072+ mockFetchFailure ( )
2073+
2074+ const inputs = {
2075+ model : 'gpt-4o' ,
2076+ userPrompt : 'Send a button template' ,
2077+ apiKey : 'test-api-key' ,
2078+ tools : [
2079+ {
2080+ type : 'custom-tool' ,
2081+ customToolId : 'custom-tool-123' ,
2082+ title : 'buttonTemplate' ,
2083+ schema : staleInlineSchema ,
2084+ code : staleInlineCode ,
2085+ usageControl : 'auto' as const ,
2086+ } ,
2087+ ] ,
2088+ }
2089+
2090+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
2091+
2092+ await handler . execute ( mockContext , mockBlock , inputs )
2093+
2094+ expect ( mockExecuteProviderRequest ) . toHaveBeenCalled ( )
2095+ const providerCall = mockExecuteProviderRequest . mock . calls [ 0 ]
2096+ const tools = providerCall [ 1 ] . tools
2097+
2098+ // Falls back to inline schema (4 params, no channel)
2099+ expect ( tools . length ) . toBe ( 1 )
2100+ expect ( tools [ 0 ] . name ) . toBe ( 'buttonTemplate' )
2101+ expect ( tools [ 0 ] . parameters . required ) . not . toContain ( 'channel' )
2102+ } )
2103+
2104+ it ( 'should return null when DB fetch fails and no inline schema exists' , async ( ) => {
2105+ mockFetchFailure ( )
2106+
2107+ const inputs = {
2108+ model : 'gpt-4o' ,
2109+ userPrompt : 'Send a button template' ,
2110+ apiKey : 'test-api-key' ,
2111+ tools : [
2112+ {
2113+ type : 'custom-tool' ,
2114+ customToolId : 'custom-tool-123' ,
2115+ usageControl : 'auto' as const ,
2116+ } ,
2117+ ] ,
2118+ }
2119+
2120+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
2121+
2122+ await handler . execute ( mockContext , mockBlock , inputs )
2123+
2124+ expect ( mockExecuteProviderRequest ) . toHaveBeenCalled ( )
2125+ const providerCall = mockExecuteProviderRequest . mock . calls [ 0 ]
2126+ const tools = providerCall [ 1 ] . tools
2127+
2128+ expect ( tools . length ) . toBe ( 0 )
2129+ } )
2130+
2131+ it ( 'should use DB code for executeFunction when customToolId resolves' , async ( ) => {
2132+ const toolId = 'custom-tool-123'
2133+ mockFetchForCustomTool ( toolId )
2134+
2135+ let capturedTools : any [ ] = [ ]
2136+ Promise . all = vi . fn ( ) . mockImplementation ( ( promises : Promise < any > [ ] ) => {
2137+ const result = originalPromiseAll . call ( Promise , promises )
2138+ result . then ( ( tools : any [ ] ) => {
2139+ if ( tools ?. length ) {
2140+ capturedTools = tools . filter ( ( t ) => t !== null )
2141+ }
2142+ } )
2143+ return result
2144+ } )
2145+
2146+ const inputs = {
2147+ model : 'gpt-4o' ,
2148+ userPrompt : 'Send a button template' ,
2149+ apiKey : 'test-api-key' ,
2150+ tools : [
2151+ {
2152+ type : 'custom-tool' ,
2153+ customToolId : toolId ,
2154+ title : 'buttonTemplate' ,
2155+ schema : staleInlineSchema ,
2156+ code : staleInlineCode ,
2157+ usageControl : 'auto' as const ,
2158+ } ,
2159+ ] ,
2160+ }
2161+
2162+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
2163+
2164+ await handler . execute ( mockContext , mockBlock , inputs )
2165+
2166+ expect ( capturedTools . length ) . toBe ( 1 )
2167+ expect ( typeof capturedTools [ 0 ] . executeFunction ) . toBe ( 'function' )
2168+
2169+ await capturedTools [ 0 ] . executeFunction ( { sender_id : '123' , channel : 'whatsapp' } )
2170+
2171+ // Should use DB code, not stale inline code
2172+ expect ( mockExecuteTool ) . toHaveBeenCalledWith (
2173+ 'function_execute' ,
2174+ expect . objectContaining ( {
2175+ code : dbCode ,
2176+ } ) ,
2177+ false ,
2178+ expect . any ( Object )
2179+ )
2180+ } )
2181+
2182+ it ( 'should not fetch from DB when no customToolId is present' , async ( ) => {
2183+ const inputs = {
2184+ model : 'gpt-4o' ,
2185+ userPrompt : 'Use the tool' ,
2186+ apiKey : 'test-api-key' ,
2187+ tools : [
2188+ {
2189+ type : 'custom-tool' ,
2190+ title : 'inlineTool' ,
2191+ schema : staleInlineSchema ,
2192+ code : staleInlineCode ,
2193+ usageControl : 'auto' as const ,
2194+ } ,
2195+ ] ,
2196+ }
2197+
2198+ mockGetProviderFromModel . mockReturnValue ( 'openai' )
2199+
2200+ await handler . execute ( mockContext , mockBlock , inputs )
2201+
2202+ const customToolFetches = mockFetch . mock . calls . filter (
2203+ ( call : any [ ] ) => typeof call [ 0 ] === 'string' && call [ 0 ] . includes ( '/api/tools/custom' )
2204+ )
2205+ expect ( customToolFetches . length ) . toBe ( 0 )
2206+
2207+ expect ( mockExecuteProviderRequest ) . toHaveBeenCalled ( )
2208+ const providerCall = mockExecuteProviderRequest . mock . calls [ 0 ]
2209+ const tools = providerCall [ 1 ] . tools
2210+
2211+ expect ( tools . length ) . toBe ( 1 )
2212+ expect ( tools [ 0 ] . name ) . toBe ( 'buttonTemplate' )
2213+ expect ( tools [ 0 ] . parameters . required ) . not . toContain ( 'channel' )
2214+ } )
2215+ } )
19042216 } )
19052217} )
0 commit comments