mcp-server-subagent
Version:
MCP server for managing sub-agents
170 lines (169 loc) • 9.63 kB
JavaScript
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { ensureLogDir,
// SUBAGENTS, // No longer needed directly in tests for this pattern
} from "./index.js"; // Assuming functions are exported from index.ts
import { runSubagent } from "./tools/run.js";
import { checkSubagentStatus, updateSubagentStatus } from "./tools/status.js";
import { getSubagentLogs } from "./tools/logs.js";
import { promises as fs } from "fs";
import path from "path";
// Define a helper to delay execution
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Define the log directory, relative to the src directory for consistency with index.ts
const LOG_DIR = path.join(process.cwd(), "logs");
// Define types for our dynamic subagents if not already defined in index.ts
// (Assuming SUBAGENTS values have a specific structure)
// interface SubagentConfig { // This local interface is no longer needed
// name: string; // name is part of the imported SubagentConfig
// command: string;
// getArgs: (input: string) => string[];
// description: string;
// }
describe("Subagent MCP Server Functionality", () => {
const testSubagentName = "test_in_vitest";
const testFailSubagentName = "test_fail_in_vitest";
let runId;
let failRunId;
// Test subagent configurations
let testSubagentConfig;
let testFailSubagentConfig;
beforeAll(async () => {
await ensureLogDir();
testSubagentConfig = {
name: testSubagentName,
command: "echo",
getArgs: () => [],
description: "Test subagent that just echoes input, added by Vitest",
};
testFailSubagentConfig = {
name: testFailSubagentName,
command: "sh",
getArgs: () => [
"-c",
"cat >/dev/null; echo 'Error message for test_fail_in_vitest with full input: TestFailureInput'; echo 'Second error line'; exit 1",
],
description: "Test subagent that intentionally fails, added by Vitest",
};
// We are no longer modifying the global SUBAGENTS from index.ts for tests
// Tests will use their own SubagentConfig instances.
});
afterAll(async () => {
// No cleanup of global SUBAGENTS needed
});
describe("Successful Subagent Operations", () => {
it("should run a subagent and get an initial status", async () => {
console.log(`\n--- Running ${testSubagentConfig.name} ---`);
runId = await runSubagent(testSubagentConfig, "Hello from Vitest!", process.cwd(), LOG_DIR);
console.log(`Subagent ${testSubagentConfig.name} started with run ID: ${runId}`);
expect(runId).toBeTypeOf("string");
expect(runId.length).toBeGreaterThan(0);
await delay(1000);
console.log(`\n--- Checking initial status for ${testSubagentConfig.name} ---`);
const initialStatus = await checkSubagentStatus(runId, LOG_DIR);
console.log("Initial status:", JSON.stringify(initialStatus, null, 2));
expect(initialStatus).toBeDefined();
expect(initialStatus.runId).toBe(runId);
expect(initialStatus.status).toBe("success");
expect(initialStatus.summary).toBeNull(); // Or specific initial summary if set
expect(initialStatus.command).toContain(`cat "`);
expect(initialStatus.command).toContain(`| echo`);
});
it("should update the subagent status with a summary", async () => {
expect(runId, "runId must be set from previous test").toBeDefined();
const summaryText = "The task was completed successfully by Vitest.";
console.log(`\n--- Updating status for ${testSubagentConfig.name} ---`);
const updatedStatus = await updateSubagentStatus(runId, "completed", LOG_DIR, // Pass LOG_DIR
summaryText);
console.log("Updated status:", JSON.stringify(updatedStatus, null, 2));
expect(updatedStatus).toBeDefined();
expect(updatedStatus.runId).toBe(runId);
expect(updatedStatus.status).toBe("completed");
expect(updatedStatus.summary).toBe(summaryText);
expect(updatedStatus.lastUpdated).toBeTypeOf("string");
});
it("should reflect the updated status and summary when checking again", async () => {
expect(runId, "runId must be set from previous test").toBeDefined();
console.log(`\n--- Checking status after update for ${testSubagentConfig.name} ---`);
const finalStatus = await checkSubagentStatus(runId, LOG_DIR);
console.log("Final status:", JSON.stringify(finalStatus, null, 2));
expect(finalStatus).toBeDefined();
expect(finalStatus.runId).toBe(runId);
expect(finalStatus.status).toBe("completed");
expect(finalStatus.summary).toBe("The task was completed successfully by Vitest.");
});
it("should retrieve the logs for the subagent run", async () => {
expect(runId, "runId must be set from previous test").toBeDefined();
console.log(`\n--- Getting logs for ${testSubagentConfig.name} ---`);
const logs = await getSubagentLogs(runId, LOG_DIR);
console.log("Logs:", logs);
expect(logs).toBeTypeOf("string");
expect(logs).toContain("Hello from Vitest!");
expect(logs).toContain("Status updated to: completed");
expect(logs).toContain("Summary: The task was completed successfully by Vitest.");
});
});
describe("Failing Subagent Operations", () => {
it("should not overwrite status if already 'success' or 'error' in metadata before process ends", async () => {
// Custom subagent that just sleeps for a bit
const customSubagentConfig = {
name: "test_status_preservation",
command: "sh",
getArgs: () => ["-c", "sleep 1; echo 'done'"],
description: "Subagent for status preservation test",
};
// Start the subagent
const runId = await runSubagent(customSubagentConfig, "Preserve status test input", process.cwd(), LOG_DIR);
const metaFile = path.join(LOG_DIR, `${runId}.meta.json`);
// Wait a short moment to ensure the process has started and metadata exists
await delay(200);
// Read and update the metadata to set status to 'success' before process ends
let meta = JSON.parse(await fs.readFile(metaFile, "utf-8"));
meta.status = "success";
await fs.writeFile(metaFile, JSON.stringify(meta, null, 2));
// Wait for process to finish
await delay(1200);
// Check that status is still 'success' and not overwritten by exit code logic
const finalStatus = await checkSubagentStatus(runId, LOG_DIR);
expect(finalStatus.status).toBe("success");
// Repeat for 'error' status
const runId2 = await runSubagent(customSubagentConfig, "Preserve error status test input", process.cwd(), LOG_DIR);
const metaFile2 = path.join(LOG_DIR, `${runId2}.meta.json`);
await delay(200);
let meta2 = JSON.parse(await fs.readFile(metaFile2, "utf-8"));
meta2.status = "error";
await fs.writeFile(metaFile2, JSON.stringify(meta2, null, 2));
await delay(1200);
const finalStatus2 = await checkSubagentStatus(runId2, LOG_DIR);
expect(finalStatus2.status).toBe("error");
});
it('should mark a failing subagent run as "error" and capture log tail in summary', async () => {
console.log(`\n--- Running failing subagent ${testFailSubagentConfig.name} ---`);
failRunId = await runSubagent(testFailSubagentConfig, "TestFailureInput", process.cwd(), LOG_DIR);
console.log(`Failing subagent ${testFailSubagentConfig.name} started with run ID: ${failRunId}`);
expect(failRunId).toBeTypeOf("string");
await delay(1000); // Wait for the process to exit and metadata to be updated
console.log(`\n--- Checking status for failing subagent ${testFailSubagentConfig.name} ---`);
const status = await checkSubagentStatus(failRunId, LOG_DIR);
console.log("Failing status:", JSON.stringify(status, null, 2));
expect(status).toBeDefined();
expect(status.runId).toBe(failRunId);
expect(status.status).toBe("error");
expect([1, 127]).toContain(status.exitCode);
expect(status.summary).toBeTypeOf("string");
expect(status.command).toContain(`cat "`);
expect(status.command).toContain(`| sh`);
expect(status.summary).toContain("Second error line");
expect(status.summary).toMatch(/Process exited with code (1|127)/);
});
it("should retrieve logs for the failing subagent run", async () => {
expect(failRunId, "failRunId must be set from previous test").toBeDefined();
console.log(`\n--- Getting logs for failing subagent ${testFailSubagentConfig.name} ---`);
const logs = await getSubagentLogs(failRunId, LOG_DIR);
console.log("Failing logs:", logs);
expect(logs).toBeTypeOf("string");
expect(logs).toContain("TestFailureInput");
expect(logs).toContain("Process exited with code");
});
});
});