@@ -25,6 +25,7 @@ import { classifyAgentsFile } from "../../src/commands/initializeProject.js";
2525import { buildStatusDetails , preferredStatusAction } from "../../src/commands/showStatus.js" ;
2626import { buildReplaceQuickAction , retargetQuickAction , withApplyFlag } from "../../src/commands/quickActions.js" ;
2727import { configureMcpTargets , inspectMcpTargets } from "../../src/mcp/config.js" ;
28+ import { performManagedInstall } from "../../src/install/managed.js" ;
2829
2930const execFileAsync = promisify ( execFile ) ;
3031
@@ -620,3 +621,144 @@ describe("patchloom CLI integration", async () => {
620621 } ) ;
621622 } ) ;
622623} ) ;
624+
625+ // --- End-to-end: managed install + MCP server ---
626+ //
627+ // Downloads the real patchloom binary via performManagedInstall (no mocks),
628+ // starts the MCP server, sends JSON-RPC requests, and validates responses.
629+ // This proves the full pipeline works on a clean machine with no pre-installed binary.
630+
631+ describe ( "managed install end-to-end MCP" , { timeout : 120_000 } , async ( ) => {
632+ let installDir : string ;
633+ let binaryPath : string ;
634+
635+ // Install once for all tests in this block
636+ try {
637+ installDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "patchloom-e2e-" ) ) ;
638+ const result = await performManagedInstall ( { installRoot : installDir } ) ;
639+ binaryPath = result . binaryPath ;
640+ } catch ( err ) {
641+ // Network or platform issue; skip all tests in this block
642+ test ( "skipped: managed install failed" , {
643+ skip : `managed install unavailable: ${ err instanceof Error ? err . message : String ( err ) } `
644+ } , ( ) => { } ) ;
645+ return ;
646+ }
647+
648+ // Verify the binary is executable
649+ test ( "managed install produces a runnable binary" , async ( ) => {
650+ const { stdout, stderr } = await execFileAsync ( binaryPath , [ "--version" ] , { timeout : 5000 } ) ;
651+ const output = `${ stdout } ${ stderr } ` . trim ( ) ;
652+ const version = parsePatchloomVersion ( output ) ;
653+ assert . ok ( version , `should parse version from managed binary: ${ output } ` ) ;
654+ assert . match ( version , / ^ \d + \. \d + \. \d + / ) ;
655+ } ) ;
656+
657+ test ( "MCP server responds to initialize" , async ( ) => {
658+ const child = execFile ( binaryPath , [ "mcp-server" ] , { timeout : 15000 } ) ;
659+ let stdout = "" ;
660+ child . stdout ! . on ( "data" , ( data : Buffer ) => { stdout += data . toString ( ) ; } ) ;
661+
662+ const initRequest = JSON . stringify ( {
663+ jsonrpc : "2.0" ,
664+ id : 1 ,
665+ method : "initialize" ,
666+ params : {
667+ protocolVersion : "2024-11-05" ,
668+ capabilities : { } ,
669+ clientInfo : { name : "e2e-test" , version : "0.0.1" }
670+ }
671+ } ) ;
672+ child . stdin ! . write ( initRequest + "\n" ) ;
673+
674+ const deadline = Date . now ( ) + 10000 ;
675+ while ( stdout . length === 0 && Date . now ( ) < deadline ) {
676+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
677+ }
678+
679+ child . kill ( ) ;
680+
681+ assert . ok ( stdout . length > 0 , "mcp-server should produce output" ) ;
682+ const response = JSON . parse ( stdout . trim ( ) . split ( "\n" ) [ 0 ] ) as Record < string , unknown > ;
683+ assert . equal ( response . jsonrpc , "2.0" ) ;
684+ assert . equal ( response . id , 1 ) ;
685+ const responseResult = response . result as Record < string , unknown > ;
686+ assert . ok ( responseResult , "response should have a result" ) ;
687+ const serverInfo = responseResult . serverInfo as Record < string , string > ;
688+ assert . ok ( serverInfo ?. name , "response should include serverInfo.name" ) ;
689+ } ) ;
690+
691+ test ( "MCP server lists available tools" , async ( ) => {
692+ const child = execFile ( binaryPath , [ "mcp-server" ] , { timeout : 15000 } ) ;
693+ let stdout = "" ;
694+ child . stdout ! . on ( "data" , ( data : Buffer ) => { stdout += data . toString ( ) ; } ) ;
695+
696+ // Must initialize first, then list tools
697+ const initRequest = JSON . stringify ( {
698+ jsonrpc : "2.0" ,
699+ id : 1 ,
700+ method : "initialize" ,
701+ params : {
702+ protocolVersion : "2024-11-05" ,
703+ capabilities : { } ,
704+ clientInfo : { name : "e2e-test" , version : "0.0.1" }
705+ }
706+ } ) ;
707+ child . stdin ! . write ( initRequest + "\n" ) ;
708+
709+ // Wait for initialize response
710+ let deadline = Date . now ( ) + 10000 ;
711+ while ( ! stdout . includes ( '"id":1' ) && Date . now ( ) < deadline ) {
712+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
713+ }
714+
715+ // Send initialized notification then tools/list
716+ const initializedNotification = JSON . stringify ( {
717+ jsonrpc : "2.0" ,
718+ method : "notifications/initialized"
719+ } ) ;
720+ child . stdin ! . write ( initializedNotification + "\n" ) ;
721+
722+ const toolsRequest = JSON . stringify ( {
723+ jsonrpc : "2.0" ,
724+ id : 2 ,
725+ method : "tools/list"
726+ } ) ;
727+ child . stdin ! . write ( toolsRequest + "\n" ) ;
728+
729+ // Wait for tools/list response
730+ deadline = Date . now ( ) + 10000 ;
731+ while ( ! stdout . includes ( '"id":2' ) && Date . now ( ) < deadline ) {
732+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) ) ;
733+ }
734+
735+ child . kill ( ) ;
736+
737+ // Parse the tools/list response (second JSON line)
738+ const lines = stdout . trim ( ) . split ( "\n" ) ;
739+ const toolsLine = lines . find ( ( line ) => line . includes ( '"id":2' ) ) ;
740+ assert . ok ( toolsLine , "should have a tools/list response" ) ;
741+
742+ const toolsResponse = JSON . parse ( toolsLine ) as Record < string , unknown > ;
743+ assert . equal ( toolsResponse . jsonrpc , "2.0" ) ;
744+ assert . equal ( toolsResponse . id , 2 ) ;
745+ const toolsResult = toolsResponse . result as Record < string , unknown > ;
746+ assert . ok ( toolsResult , "tools/list should have a result" ) ;
747+ const tools = toolsResult . tools as Array < Record < string , unknown > > ;
748+ assert . ok ( Array . isArray ( tools ) , "result.tools should be an array" ) ;
749+ assert . ok ( tools . length > 0 , "should expose at least one tool" ) ;
750+
751+ // Verify tools have required MCP fields
752+ for ( const tool of tools ) {
753+ assert . ok ( typeof tool . name === "string" && tool . name . length > 0 ,
754+ `tool should have a non-empty name: ${ JSON . stringify ( tool ) } ` ) ;
755+ assert . ok ( tool . inputSchema !== undefined ,
756+ `tool ${ tool . name } should have an inputSchema` ) ;
757+ }
758+ } ) ;
759+
760+ // Cleanup
761+ test ( "cleanup managed install temp directory" , async ( ) => {
762+ await fs . rm ( installDir , { recursive : true , force : true } ) ;
763+ } ) ;
764+ } ) ;
0 commit comments