testeranto
Version:
the AI powered BDD test framework for typescript projects
778 lines (701 loc) • 26.2 kB
text/typescript
// 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;
// }
// }