165 lines
5.0 KiB
TypeScript
165 lines
5.0 KiB
TypeScript
import { spawn } from "child_process";
|
||
import http from "http";
|
||
|
||
// We will start the runner server on port 3334
|
||
const PORT = 3334;
|
||
const BASE_URL = `http://localhost:${PORT}`;
|
||
|
||
console.log("🧪 Starting AgentRunner Hardening Test Suite...");
|
||
|
||
// Set up environment variables
|
||
const env = {
|
||
...process.env,
|
||
PORT: String(PORT),
|
||
AGENT_RUNNER_SECRET: "test-secret-123",
|
||
GOOGLE_API_KEY: "dummy-key-for-testing", // Pass dummy key to avoid Gemini API initialization crash
|
||
VIBN_API_URL: "http://localhost:3335", // Mock backend
|
||
};
|
||
|
||
// Start mock backend on port 3335 to catch PATCH callbacks and verify headers
|
||
let receivedHeaders: any = null;
|
||
let receivedBody: any = null;
|
||
|
||
const mockBackend = http.createServer((req, res) => {
|
||
receivedHeaders = req.headers;
|
||
let body = "";
|
||
req.on("data", (chunk) => {
|
||
body += chunk;
|
||
});
|
||
req.on("end", () => {
|
||
try {
|
||
receivedBody = JSON.parse(body);
|
||
} catch {
|
||
receivedBody = body;
|
||
}
|
||
res.writeHead(200, { "Content-Type": "application/json" });
|
||
res.end(JSON.stringify({ ok: true }));
|
||
});
|
||
});
|
||
|
||
mockBackend.listen(3335, () => {
|
||
console.log("✓ Mock backend server listening on port 3335");
|
||
});
|
||
|
||
// Spawn the runner server
|
||
const serverProcess = spawn("npx", ["ts-node", "src/server.ts"], {
|
||
env,
|
||
stdio: "pipe",
|
||
});
|
||
|
||
// Wait for server to start
|
||
serverProcess.stdout.on("data", (data) => {
|
||
const output = data.toString();
|
||
console.log(`[Server Out] ${output.trim()}`);
|
||
});
|
||
|
||
serverProcess.stderr.on("data", (data) => {
|
||
console.error(`[Server Err] ${data.toString()}`);
|
||
});
|
||
|
||
// Helper function to sleep
|
||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
||
async function runTests() {
|
||
// Wait 4 seconds for server to boot
|
||
await sleep(4000);
|
||
|
||
let passed = 0;
|
||
let failed = 0;
|
||
|
||
const assert = (condition: boolean, message: string) => {
|
||
if (condition) {
|
||
console.log(` 🟢 PASSED: ${message}`);
|
||
passed++;
|
||
} else {
|
||
console.error(` 🔴 FAILED: ${message}`);
|
||
failed++;
|
||
}
|
||
};
|
||
|
||
try {
|
||
// Test 1: Empty appPath should be accepted and fall back to "."
|
||
console.log("\n1️⃣ Testing appPath empty string fallback...");
|
||
const res1 = await fetch(`${BASE_URL}/agent/execute`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
sessionId: "test-session-1",
|
||
projectId: "test-project-1",
|
||
task: "Test empty appPath",
|
||
appPath: "", // Empty string!
|
||
giteaRepo: "test-repo",
|
||
}),
|
||
});
|
||
|
||
assert(res1.status === 202, `Should return 202, got ${res1.status}`);
|
||
const data1 = (await res1.json()) as any;
|
||
assert(
|
||
data1.sessionId === "test-session-1",
|
||
`Should return correct sessionId, got ${data1.sessionId}`,
|
||
);
|
||
|
||
// Test 2: Missing sessionId should return 400
|
||
console.log("\n2️⃣ Testing missing required parameters (sessionId)...");
|
||
const res2 = await fetch(`${BASE_URL}/agent/execute`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
projectId: "test-project-1",
|
||
task: "Test missing sessionId",
|
||
appPath: ".",
|
||
}),
|
||
});
|
||
assert(res2.status === 400, `Should return 400, got ${res2.status}`);
|
||
|
||
// Test 3: Emergency callback headers should include x-agent-runner-secret
|
||
console.log("\n3️⃣ Testing early failure callback headers...");
|
||
|
||
// Trigger a clone failure by passing a malformed giteaRepo containing slash,
|
||
// which triggers clone instead of default workspace but will fail clone.
|
||
console.log("Triggering clone failure on mock Gitea...");
|
||
const res3 = await fetch(`${BASE_URL}/agent/execute`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({
|
||
sessionId: "test-session-3",
|
||
projectId: "test-project-3",
|
||
task: "Trigger crash",
|
||
appPath: ".",
|
||
giteaRepo: "invalid_owner/invalid_repo",
|
||
}),
|
||
});
|
||
|
||
assert(
|
||
res3.status === 202,
|
||
`Should return 202 Accepted, got ${res3.status}`,
|
||
);
|
||
|
||
// Wait for server to process async task and fail, calling our mock backend PATCH
|
||
console.log("Waiting for runner callback on mock backend...");
|
||
await sleep(4000);
|
||
|
||
assert(receivedHeaders !== null, "Should call mock backend PATCH endpoint");
|
||
if (receivedHeaders) {
|
||
assert(
|
||
receivedHeaders["x-agent-runner-secret"] === "test-secret-123",
|
||
`Callback should include secret header 'test-secret-123', got '${receivedHeaders["x-agent-runner-secret"]}'`,
|
||
);
|
||
assert(
|
||
receivedBody && receivedBody.status === "failed",
|
||
`Callback body should have status 'failed', got '${receivedBody?.status}'`,
|
||
);
|
||
}
|
||
} catch (err) {
|
||
console.error("Test execution failed with exception:", err);
|
||
} finally {
|
||
console.log("\n🧹 Cleaning up test servers...");
|
||
serverProcess.kill();
|
||
mockBackend.close();
|
||
console.log(`\n📊 Tests complete. Passed: ${passed}, Failed: ${failed}`);
|
||
process.exit(failed > 0 ? 1 : 0);
|
||
}
|
||
}
|
||
|
||
runTests();
|