UNPKG

testeranto

Version:

the AI powered BDD test framework for typescript projects

778 lines (701 loc) 26.2 kB
// DEPRECATED // /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { ChildProcess, spawn } from "child_process"; import http from "http"; import { RawData, WebSocketServer } from "ws"; import fs from "fs"; import { IBuiltConfig } from "../../Types.js"; import { WebSocketMessage } from "../../PM/types.js"; import { FileService } from "../FileService.js"; import { PM_Base } from "./PM_0.js"; export abstract class PM_WithTCP extends PM_Base implements FileService { wss: WebSocketServer; httpServer: http.Server; clients: Set<any> = new Set(); runningProcesses: Map<string, ChildProcess | Promise<any>> = new Map(); allProcesses: Map< string, { child?: ChildProcess; promise?: Promise<any>; status: "running" | "exited" | "error" | "completed"; exitCode?: number; error?: string; command: string; pid?: number; timestamp: string; type: "process" | "promise"; category: "aider" | "bdd-test" | "build-time" | "other"; testName?: string; platform?: "node" | "web" | "pure" | "python" | "golang"; } > = new Map(); processLogs: Map<string, string[]> = new Map(); constructor(configs: IBuiltConfig, name, mode) { super(configs, name, mode); this.httpServer = http.createServer(this.handleHttpRequest.bind(this)); this.wss = new WebSocketServer({ server: this.httpServer }); this.wss.on("connection", (ws) => { this.clients.add(ws); console.log("Client connected"); ws.on("message", (data) => { try { this.websocket(data, ws); } catch (error) { console.error("Error handling WebSocket message:", error); } }); ws.on("close", () => { this.clients.delete(ws); console.log("Client disconnected"); }); ws.on("error", (error) => { console.error("WebSocket error:", error); this.clients.delete(ws); }); }); const httpPort = Number(process.env.HTTP_PORT) || 3000; this.httpServer.on("error", (error) => { console.error("HTTP server error:", error); if ((error as any).code === "EADDRINUSE") { console.error( `Port ${httpPort} is already in use. Please use a different port.` ); } process.exit(-1); }); this.httpServer.listen(httpPort, "0.0.0.0", () => { console.log(`HTTP server running on http://localhost:${httpPort}`); }); } websocket(data: RawData, ws: any) { try { const message: WebSocketMessage = JSON.parse(data.toString()); if (message.type === "chatMessage") { // Always record the message, even if aider is not available console.log(`Received chat message: ${message.content}`); // Pass to PM_WithHelpo if available if ((this as any).handleChatMessage) { (this as any).handleChatMessage(message.content); } else { console.log("PM_WithHelpo not available - message not processed"); } return; } // Handle file operations via WebSocket if (message.type === "listDirectory") { // this.handleListDirectory(ws, message); } else if (message.type === "readFile") { // this.handleReadFile(ws, message); } else if (message.type === "writeFile") { // this.handleWriteFile(ws, message); } else if (message.type === "createDirectory") { // this.handleCreateDirectory(ws, message); } else if (message.type === "deleteFile") { // this.handleDeleteFile(ws, message); } else if (message.type === "executeCommand") { // const executeMessage = message as ExecuteCommandMessage; if (message.command && message.command.trim().startsWith("aider")) { console.log(`Executing command: ${message.command}`); const processId = Date.now().toString(); const child = spawn(message.command, { shell: true, cwd: process.cwd(), }); this.runningProcesses.set(processId, child); this.allProcesses.set(processId, { child, status: "running", command: message.command, pid: child.pid, timestamp: new Date().toISOString(), type: "process", category: "aider", }); this.processLogs.set(processId, []); this.webSocketBroadcastMessage({ type: "processStarted", processId, command: message.command, timestamp: new Date().toISOString(), logs: [], }); // Capture stdout and stderr child.stdout?.on("data", (data) => { const logData = data.toString(); const logs = this.processLogs.get(processId) || []; logs.push(logData); this.processLogs.set(processId, logs); this.webSocketBroadcastMessage({ type: "processStdout", processId, data: logData, timestamp: new Date().toISOString(), }); }); child.stderr?.on("data", (data) => { const logData = data.toString(); const logs = this.processLogs.get(processId) || []; logs.push(logData); this.processLogs.set(processId, logs); this.webSocketBroadcastMessage({ type: "processStderr", processId, data: logData, timestamp: new Date().toISOString(), }); }); child.on("error", (error) => { console.error(`Failed to execute command: ${error}`); this.runningProcesses.delete(processId); const processInfo = this.allProcesses.get(processId); if (processInfo) { this.allProcesses.set(processId, { ...processInfo, status: "error", error: error.message, }); } this.webSocketBroadcastMessage({ type: "processError", processId, error: error.message, timestamp: new Date().toISOString(), }); }); child.on("exit", (code) => { console.log(`Command exited with code ${code}`); this.runningProcesses.delete(processId); const processInfo = this.allProcesses.get(processId); if (processInfo) { this.allProcesses.set(processId, { ...processInfo, status: "exited", // @ts-ignore exitCode: code, }); } this.webSocketBroadcastMessage({ type: "processExited", processId, exitCode: code, timestamp: new Date().toISOString(), }); }); } else { console.error('Invalid command: must start with "aider"'); } } else if (message.type === "getRunningProcesses") { const processes = Array.from(this.allProcesses.entries()).map( ([id, procInfo]) => ({ processId: id, command: procInfo.command, pid: procInfo.pid, status: procInfo.status, exitCode: procInfo.exitCode, error: procInfo.error, timestamp: procInfo.timestamp, category: procInfo.category, testName: procInfo.testName, platform: procInfo.platform, logs: this.processLogs.get(id) || [], }) ); ws.send( JSON.stringify({ type: "runningProcesses", processes, }) ); } else if (message.type === "getProcess") { const processId = message.processId; const procInfo = this.allProcesses.get(processId); if (procInfo) { ws.send( JSON.stringify({ type: "processData", processId, command: procInfo.command, pid: procInfo.pid, status: procInfo.status, exitCode: procInfo.exitCode, error: procInfo.error, timestamp: procInfo.timestamp, category: procInfo.category, testName: procInfo.testName, platform: procInfo.platform, logs: this.processLogs.get(processId) || [], }) ); } } else if (message.type === "stdin") { const processId = message.processId; const data = message.data; console.log("Received stdin for process", processId, ":", data); const childProcess = this.runningProcesses.get( processId ) as ChildProcess; if (childProcess && childProcess.stdin) { console.log("Writing to process stdin"); childProcess.stdin.write(data); } else { console.log( "Cannot write to stdin - process not found or no stdin:", { processExists: !!childProcess, stdinExists: childProcess?.stdin ? true : false, } ); } } else if (message.type === "killProcess") { const processId = message.processId; console.log("Received killProcess for process", processId); const childProcess = this.runningProcesses.get( processId ) as ChildProcess; if (childProcess) { console.log("Killing process"); childProcess.kill("SIGTERM"); // The process exit handler will update the status and broadcast the change } else { console.log("Cannot kill process - process not found:", { processExists: !!childProcess, }); } } else if (message.type === "getChatHistory") { if ((this as any).getChatHistory) { (this as any) .getChatHistory() .then((history: any) => { ws.send( JSON.stringify({ type: "chatHistory", messages: history, }) ); }) .catch((error: any) => { console.error("Error getting chat history:", error); ws.send( JSON.stringify({ type: "error", message: "Failed to get chat history", }) ); }); } } } catch (error) { console.error("Error handling WebSocket message:", error); } } webSocketBroadcastMessage(message: any) { const data = typeof message === "string" ? message : JSON.stringify(message); this.clients.forEach((client) => { if (client.readyState === 1) { client.send(data); } }); } handleHttpRequest( req: http.IncomingMessage, res: http.ServerResponse<http.IncomingMessage> & { req: http.IncomingMessage; } ) { console.log(req, res); res.writeHead(200, { "Content-Type": "application/json", }); res.end(JSON.stringify({ wtf: "idk" })); } writeFile(path: string, content: string): Promise<void> { throw new Error("Method not implemented."); } readFile(path: string): Promise<string> { throw new Error("Method not implemented."); } createDirectory(path: string): Promise<void> { throw new Error("Method not implemented."); } deleteFile(path: string): Promise<void> { throw new Error("Method not implemented."); } files(path: string): Promise<object> { throw new Error("Method not implemented."); } projects(project: string): Promise<string[]> { throw new Error("Method not implemented."); } tests(project: string): Promise<string[]> { throw new Error("Method not implemented."); } report(project: string, test: string): Promise<object> { throw new Error("Method not implemented."); } // Add connection listener to see if connections are being made // this.httpServer.on("connection", (socket) => { // // console.log( // // "New TCP connection from:", // // socket.remoteAddress, // // socket.remotePort // // ); // // Track when the connection is closed // // socket.on("close", () => { // // console.log( // // "TCP connection closed:", // // socket.remoteAddress, // // socket.remotePort // // ); // // }); // // Track errors on the socket // // socket.on("error", (error) => { // // console.log( // // "TCP connection error from:", // // socket.remoteAddress, // // socket.remotePort, // // error // // ); // // }); // }); // Add request listener to see HTTP requests // this.httpServer.on("request", (req, res) => { // // console.log( // // "HTTP request received:", // // req.method, // // req.url, // // "from", // // req.socket.remoteAddress, // // req.socket.remotePort // // ); // // // Track when the response is finished // // res.on("finish", () => { // // console.log("HTTP response sent:", res.statusCode, req.method, req.url); // // }); // }); // httpRequest(req: http.IncomingMessage, res: http.ServerResponse) { // console.log(`httpRequest method called for ${req.method} ${req.url}`); // res.writeHead(200, { // "Content-Type": "application/json", // }); // // Convert object to JSON string and send // res.end(JSON.stringify({ wtf: "idk" })); // // // Ensure response is always completed // // // const completeResponse = () => { // // // if (!res.writableEnded && !res.headersSent) { // // // res.writeHead(200, { "Content-Type": "text/plain" }); // // // res.end("Default response"); // // // console.log("Complete response sent"); // // // } else { // // // console.log("Response already completed, not sending default"); // // // } // // // }; // // // Handle CORS headers for all responses // // res.setHeader("Access-Control-Allow-Origin", "*"); // // res.setHeader( // // "Access-Control-Allow-Methods", // // "GET, POST, OPTIONS, PUT, DELETE" // // ); // // res.setHeader("Access-Control-Allow-Headers", "Content-Type"); // // // Handle preflight requests // // if (req.method === "OPTIONS") { // // res.writeHead(200); // // res.end(); // // return; // // } // // // Ensure response is always sent // // const sendResponse = ( // // status: number, // // contentType: string, // // body: string | Buffer // // ) => { // // res.writeHead(200, { // // "Content-Type": "application/json", // // }); // // // Convert object to JSON string and send // // res.end(JSON.stringify({ wtf: "idk" })); // // // res.json("Hello from Express!"); // // // console.log('sendResponse called with:', { status, contentType, body: body.toString().substring(0, 50) }); // // // if (!res.headersSent) { // // // try { // // // console.log('About to write headers and end response'); // // // res.writeHead(status, { "Content-Type": contentType }); // // // res.end(body); // // // console.log('Response sent via sendResponse'); // // // } catch (error) { // // // console.error('Error sending response:', error); // // // } // // // } else { // // // console.warn('Headers already sent, cannot send response'); // // // } // // }; // // try { // // console.log(`Processing ${req.method} request for: ${req.url}`); // // console.log("Response writable state:", { // // writable: res.writable, // // destroyed: res.destroyed, // // writableEnded: res.writableEnded, // // writableFinished: res.writableFinished, // // headersSent: res.headersSent, // // }); // // // Handle root path // // if (req.url === "/") { // // sendResponse( // // 200, // // "text/plain", // // "HTTP Server is working! Try /test endpoint" // // ); // // return; // // } // // // Handle test endpoint // // if (req.url === "/test") { // // sendResponse(200, "text/plain", "Test endpoint is working!"); // // return; // // } // // // Handle specific requests // // if (req.url === "/testeranto/projects.html") { // // console.log("Handling projects.html request - before sendResponse"); // // sendResponse( // // 200, // // "text/html", // // "<html><body><h1>Projects</h1></body></html>" // // ); // // console.log("Handling projects.html request - after sendResponse"); // // return; // // } // // // Send a simple response for all other requests // // console.log("Handling other request"); // // sendResponse( // // 200, // // "text/plain", // // `Server is working! Request received for: ${req.url}` // // ); // // return; // // // The rest of the original code is commented out for now // // /* // // const parsedUrl = url.parse(req.url || "/", true); // // const pathname = parsedUrl.pathname || "/"; // // // Handle API requests // // if (pathname?.startsWith("/api/")) { // // console.log("API request received:", pathname); // // // If there's an API handler, use it // // if ((this as any).apiRequest) { // // (this as any).apiRequest(req, res); // // } else { // // sendResponse(404, "application/json", JSON.stringify({ error: "API endpoint not found" })); // // } // // return; // // } // // // Handle root path with a simple response for testing // // if (pathname === "/") { // // sendResponse(200, "text/plain", "HTTP Server is working!"); // // return; // // } // // // Handle static file serving // // console.log("Handling static file serving for path:", pathname); // // const processedPathname = pathname; // // // Remove leading slash to get file path // // let filePath = processedPathname.substring(1); // // console.log("Looking for file:", filePath); // // // Handle special cases // // if (filePath.startsWith("reports/")) { // // filePath = `testeranto/${filePath}`; // // } else if (filePath.startsWith("metafiles/")) { // // filePath = `testeranto/${filePath}`; // // } else if (filePath === "projects.json") { // // // Keep as is // // } else { // // // For frontend assets, try to find the file // // const possiblePaths = [ // // `dist/${filePath}`, // // `testeranto/dist/${filePath}`, // // `../dist/${filePath}`, // // `./${filePath}`, // // ]; // // console.log("Checking possible paths:", possiblePaths); // // // Find the first existing file // // let foundPath = null; // // for (const possiblePath of possiblePaths) { // // try { // // if (fs.existsSync(possiblePath)) { // // foundPath = possiblePath; // // console.log("Found file at:", foundPath); // // break; // // } // // } catch (error) { // // // Continue to next path // // } // // } // // if (foundPath) { // // filePath = foundPath; // // } else { // // console.log("File not found, serving index.html or fallback"); // // // Serve index.html for SPA routing // // const indexPath = this.findIndexHtml(); // // if (indexPath) { // // this.serveFileSync(indexPath, res, true); // // } else { // // sendResponse(200, "text/html", ` // // <html> // // <body> // // <h1>Testeranto is running</h1> // // <p>Frontend files are not built yet. Run 'npm run build' to build the frontend.</p> // // </body> // // </html> // // `); // // } // // return; // // } // // } // // // Check if file exists and serve it using synchronous check // // try { // // console.log("Final file path to serve:", filePath); // // if (!fs.existsSync(filePath)) { // // console.log("File does not exist:", filePath); // // // For SPA routing, serve index.html // // const indexPath = this.findIndexHtml(); // // if (indexPath) { // // this.serveFileSync(indexPath, res, true); // // } else { // // sendResponse(404, "text/plain", "404 Not Found"); // // } // // return; // // } // // console.log("Serving file:", filePath); // // this.serveFileSync(filePath, res, false); // // } catch (error) { // // console.error(`Error checking file existence for ${filePath}:`, error); // // sendResponse(500, "text/plain", "500 Internal Server Error"); // // } // // */ // // } catch (error) { // // console.error("Unexpected error handling request:", error); // // try { // // if (!res.headersSent) { // // sendResponse(500, "text/plain", "Internal Server Error"); // // } else { // // console.log("Headers already sent, cannot send error response"); // // } // // } catch (sendError) { // // console.error("Failed to send error response:", sendError); // // // Try to end the response directly // // if (!res.writableEnded) { // // res.destroy(); // // } // // } // // } finally { // // // Only complete the response if nothing was sent // // console.log("Finally block - response state:", { // // headersSent: res.headersSent, // // writableEnded: res.writableEnded, // // writableFinished: res.writableFinished, // // }); // // if (!res.headersSent && !res.writableEnded) { // // console.log("No response sent, calling completeResponse"); // // completeResponse(); // // } else { // // console.log("Response already handled, skipping completeResponse"); // // } // // } // } // findIndexHtml(): string | null { // const possiblePaths = [ // "dist/index.html", // "testeranto/dist/index.html", // "../dist/index.html", // "./index.html", // ]; // for (const path of possiblePaths) { // try { // if (fs.existsSync(path)) { // return path; // } // } catch (error) { // // Continue to next path // } // } // return null; // } } // private serveFile( // filePath: string, // res: http.ServerResponse, // isSpaFallback: boolean // ) { // // Use the synchronous version // this.serveFileSync(filePath, res, isSpaFallback); // } // private serveFileSync( // filePath: string, // res: http.ServerResponse, // isSpaFallback: boolean // ) { // // Ensure response is always sent // const sendResponse = ( // status: number, // contentType: string, // body: string | Buffer // ) => { // if (!res.headersSent) { // res.writeHead(status, { "Content-Type": contentType }); // res.end(body); // } // }; // // Check if the file exists using synchronous check // try { // if (!fs.existsSync(filePath) && !isSpaFallback) { // // Try to serve index.html as fallback for SPA routing // const indexPath = this.findIndexHtml(); // if (indexPath) { // this.serveFileSync(indexPath, res, true); // return; // } else { // sendResponse(404, "text/plain", "404 Not Found"); // return; // } // } else if (!fs.existsSync(filePath) && isSpaFallback) { // sendResponse(404, "text/plain", "404 Not Found"); // return; // } // // File exists, read and serve it // try { // const data = fs.readFileSync(filePath); // // For HTML files, inject the configuration // if (filePath.endsWith(".html")) { // let content = data.toString(); // // Inject the configuration script before the closing </body> tag // if (content.includes("</body>")) { // const configScript = ` // <script> // window.testerantoConfig = ${JSON.stringify({ // githubOAuth: { // clientId: process.env.GITHUB_CLIENT_ID || "", // }, // serverOrigin: // process.env.SERVER_ORIGIN || "http://localhost:3000", // })}; // </script> // `; // content = content.replace("</body>", `${configScript}</body>`); // } // sendResponse(200, "text/html", content); // } else { // // Get MIME type for other files // const mimeType = mime.lookup(filePath) || "application/octet-stream"; // sendResponse(200, mimeType, data); // } // } catch (error) { // console.error(`Error reading file ${filePath}:`, error); // sendResponse(500, "text/plain", "500 Internal Server Error"); // return; // } // } catch (error) { // console.error(`Error checking file existence for ${filePath}:`, error); // sendResponse(500, "text/plain", "500 Internal Server Error"); // return; // } // }