@@ -380,6 +380,8 @@ const { MCP_RECONNECTING_TOOL_ERROR, isDegradedMcpState } =
380380 await import ( "../mcp/plugin.js" ) ;
381381const { mcpNotice } = await import ( "../tui/runtime-notices.js" ) ;
382382const { mcpReconnectDelayMs } = await import ( "./tools.js" ) ;
383+ const { TOOL_SEARCH_PENDING_WAIT_MS , TOOL_SEARCH_RECONNECT_WAIT_MS } =
384+ await import ( "./tool-search.js" ) ;
383385
384386// connectMCPServer is mocked; tests simulate a dead transport by invoking the
385387// onDisconnect hook the real client wires to transport.onclose.
@@ -773,6 +775,71 @@ describe("unintentional disconnect and automatic reconnect", () => {
773775 await toolset . dispose ( ) ;
774776 }
775777 } ) ;
778+
779+ const widgetTool = {
780+ name : "list" ,
781+ description : "List widgets" ,
782+ inputSchema : { type : "object" , required : [ "q" ] } ,
783+ } ;
784+
785+ test ( "reconnect stubs keep the server's name, description, and schema" , async ( ) => {
786+ mock . connectedTools = [ widgetTool ] ;
787+ const toolset = await makeToolset ( ) ;
788+ try {
789+ await toolset . connectMCPServer ( acme , callbacks ( [ ] ) ) ;
790+ killTransport ( ) ;
791+ const stub = toolset . dynamicRunner
792+ . currentDefinitions ( )
793+ . find ( ( d ) => d . name === "mcp__acme__list" ) ;
794+ expect ( stub ) . toEqual ( {
795+ name : "mcp__acme__list" ,
796+ description : "[acme] List widgets" ,
797+ inputSchema : widgetTool . inputSchema ,
798+ } ) ;
799+ } finally {
800+ await toolset . dispose ( ) ;
801+ }
802+ } ) ;
803+
804+ // Resolves to whether tool_search was still waiting once the tier-1 miss
805+ // wait elapsed, i.e. whether a retained tool matched the query.
806+ async function searchWaitsForRedial ( query : string ) : Promise < boolean > {
807+ mock . connectedTools = [ widgetTool ] ;
808+ const toolset = await makeToolset ( ) ;
809+ jest . useFakeTimers ( ) ;
810+ try {
811+ await toolset . connectMCPServer ( acme , callbacks ( [ ] ) ) ;
812+ mock . mode = "deferred" ;
813+ killTransport ( ) ;
814+ await advanceUntil ( ( ) => mock . connectOptions . length === 2 ) ;
815+ let settled = false ;
816+ const search = toolset . dynamicRunner
817+ . run (
818+ { id : "s1" , name : "tool_search" , arguments : { query } } ,
819+ new AbortController ( ) . signal ,
820+ )
821+ . then ( ( ) => {
822+ settled = true ;
823+ } ) ;
824+ await advanceAndFlush ( TOOL_SEARCH_PENDING_WAIT_MS ) ;
825+ const waiting = ! settled ;
826+ await advanceAndFlush ( TOOL_SEARCH_RECONNECT_WAIT_MS ) ;
827+ await search ;
828+ return waiting ;
829+ } finally {
830+ mock . releaseDeferredConnect ?.( ) ;
831+ jest . useRealTimers ( ) ;
832+ await toolset . dispose ( ) ;
833+ }
834+ }
835+
836+ test ( "tool_search waits for a redial whose retained tool matches by description" , async ( ) => {
837+ expect ( await searchWaitsForRedial ( "widgets" ) ) . toBe ( true ) ;
838+ } ) ;
839+
840+ test ( "tool_search does not wait for a redial whose retained tools do not match" , async ( ) => {
841+ expect ( await searchWaitsForRedial ( "zebra" ) ) . toBe ( false ) ;
842+ } ) ;
776843} ) ;
777844
778845describe ( "MCP handshake bounds" , ( ) => {
0 commit comments