fix tests

This commit is contained in:
harshitsinghbhandari 2026-04-04 21:52:50 +05:30
parent 71bdf75f53
commit 2a1b513e7d
6 changed files with 324 additions and 5 deletions

View File

@ -64,6 +64,7 @@ vi.mock("ora", () => ({
stop: vi.fn().mockReturnThis(),
succeed: vi.fn().mockReturnThis(),
fail: vi.fn().mockReturnThis(),
info: vi.fn().mockReturnThis(),
text: "",
}),
}));
@ -861,6 +862,57 @@ describe("start command — orchestrator session strategy display", () => {
expect(output).not.toContain("reused existing session");
},
);
it("handles existing orchestrator sessions by showing info and not spawning new", async () => {
mockConfigRef.current = makeConfig({ "my-app": makeProject() });
// Return an existing orchestrator session
mockSessionManager.list.mockResolvedValue([
{
id: "app-orchestrator",
projectId: "my-app",
metadata: { role: "orchestrator" },
},
]);
await program.parseAsync(["node", "test", "start", "--no-dashboard"]);
const output = getLoggedOutput();
expect(output).toContain("existing sessions found — select one in the dashboard");
// Should NOT spawn a new orchestrator when existing ones exist
expect(mockSessionManager.spawnOrchestrator).not.toHaveBeenCalled();
});
it("fails and cleans up dashboard when orchestrator setup throws", async () => {
mockConfigRef.current = makeConfig({ "my-app": makeProject() });
// Mock findWebDir
const { findWebDir } = await import("../../src/lib/web-dir.js");
vi.mocked(findWebDir).mockReturnValue(tmpDir);
writeFileSync(join(tmpDir, "package.json"), "{}");
const fakeDashboard = {
on: vi.fn(),
kill: vi.fn(),
emit: vi.fn(),
};
mockSpawn.mockReturnValue(fakeDashboard);
mockSessionManager.list.mockResolvedValue([]);
mockSessionManager.spawnOrchestrator.mockRejectedValue(new Error("Spawn failed"));
await expect(program.parseAsync(["node", "test", "start"])).rejects.toThrow("process.exit(1)");
const errors = vi
.mocked(console.error)
.mock.calls.map((c) => c.join(" "))
.join("\n");
expect(errors).toContain("Failed to setup orchestrator: Spawn failed");
// Should have killed the dashboard
expect(fakeDashboard.kill).toHaveBeenCalled();
});
});
// ---------------------------------------------------------------------------

View File

@ -305,7 +305,7 @@ describe("send", () => {
await sm.send("app-1", "confirm via updated timestamp");
const elapsedMs = Date.now() - startedAt;
expect(elapsedMs).toBeLessThan(2_000);
expect(elapsedMs).toBeLessThan(5_000);
expect(readFileSync(listLogPath, "utf-8").trim().split("\n").length).toBeGreaterThanOrEqual(2);
expect(mockRuntime.sendMessage).toHaveBeenCalledWith(
makeHandle("rt-1"),
@ -442,7 +442,7 @@ describe("remap", () => {
expect(mapped).toBe("ses_slow_discovery");
const meta = readMetadataRaw(sessionsDir, "app-1");
expect(meta?.["opencodeSessionId"]).toBe("ses_slow_discovery");
});
}, 20000);
it("throws when OpenCode session id mapping is missing", async () => {
const deleteLogPath = join(tmpDir, "opencode-delete-missing-remap.log");

View File

@ -225,7 +225,7 @@ describe("kill", () => {
await sm.kill("app-1", { purgeOpenCode: true });
expect(existsSync(deleteLogPath)).toBe(false);
});
}, 15000);
});
describe("cleanup", () => {

View File

@ -365,7 +365,7 @@ describe("restore", () => {
expect(restored.status).toBe("spawning");
const meta = readMetadataRaw(sessionsDir, "app-1");
expect(meta?.["opencodeSessionId"]).toBe("ses_restore_discovered");
});
}, 15000);
it("uses orchestratorModel when restoring orchestrator sessions", async () => {
const wsPath = join(tmpDir, "ws-app-orchestrator-restore");

View File

@ -195,7 +195,7 @@ vi.mock("@/lib/services", () => ({
// ── Import routes after mocking ───────────────────────────────────────
import { GET as sessionsGET } from "@/app/api/sessions/route";
import { POST as orchestratorsPOST } from "@/app/api/orchestrators/route";
import { POST as orchestratorsPOST, GET as orchestratorsGET } from "@/app/api/orchestrators/route";
import { POST as spawnPOST } from "@/app/api/spawn/route";
import { POST as sendPOST } from "@/app/api/sessions/[id]/send/route";
import { POST as messagePOST } from "@/app/api/sessions/[id]/message/route";
@ -687,6 +687,52 @@ describe("API Routes", () => {
});
});
describe("GET /api/orchestrators", () => {
it("returns orchestrators for a project", async () => {
const orchestrator = makeSession({
id: "my-app-orchestrator",
projectId: "my-app",
metadata: { role: "orchestrator" },
});
(mockSessionManager.list as ReturnType<typeof vi.fn>).mockResolvedValueOnce([orchestrator]);
const res = await orchestratorsGET(
makeRequest("http://localhost:3000/api/orchestrators?project=my-app"),
);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.orchestrators).toHaveLength(1);
expect(data.orchestrators[0].id).toBe("my-app-orchestrator");
expect(data.projectName).toBe("My App");
});
it("returns 400 when project parameter is missing", async () => {
const res = await orchestratorsGET(makeRequest("http://localhost:3000/api/orchestrators"));
expect(res.status).toBe(400);
const data = await res.json();
expect(data.error).toMatch(/Missing project query parameter/);
});
it("returns 404 for unknown project", async () => {
const res = await orchestratorsGET(
makeRequest("http://localhost:3000/api/orchestrators?project=unknown-app"),
);
expect(res.status).toBe(404);
const data = await res.json();
expect(data.error).toMatch(/Unknown project/);
});
it("returns 500 when list fails", async () => {
(mockSessionManager.list as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error("boom"));
const res = await orchestratorsGET(
makeRequest("http://localhost:3000/api/orchestrators?project=my-app"),
);
expect(res.status).toBe(500);
const data = await res.json();
expect(data.error).toBe("boom");
});
});
// ── POST /api/sessions/:id/send ────────────────────────────────────
describe("POST /api/sessions/:id/send", () => {

View File

@ -0,0 +1,221 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { OrchestratorSelector } from "@/components/OrchestratorSelector";
import OrchestratorsRoute from "@/app/orchestrators/page";
import { getServices } from "@/lib/services";
import { getAllProjects } from "@/lib/project-name";
// ── Mocks ─────────────────────────────────────────────────────────────
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: mockPush,
}),
}));
vi.mock("next/link", () => ({
default: ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href}>{children}</a>
),
}));
vi.mock("@/lib/services", () => ({
getServices: vi.fn(),
}));
vi.mock("@/lib/project-name", () => ({
getAllProjects: vi.fn(),
}));
global.fetch = vi.fn();
// ── Tests ─────────────────────────────────────────────────────────────
describe("OrchestratorSelector Component", () => {
const defaultProps = {
orchestrators: [
{
id: "orch-1",
projectId: "my-app",
projectName: "My App",
status: "working",
activity: "active",
createdAt: new Date(Date.now() - 3600000 * 2).toISOString(), // 2h ago
lastActivityAt: new Date(Date.now() - 60000 * 5).toISOString(), // 5m ago
},
],
projectId: "my-app",
projectName: "My App",
projects: [{ id: "my-app", name: "My App" }],
error: null,
};
beforeEach(() => {
vi.clearAllMocks();
});
it("renders orchestrators and handles spawn success", async () => {
(global.fetch as any).mockResolvedValue({
ok: true,
json: async () => ({ orchestrator: { id: "new-orch" } }),
});
render(<OrchestratorSelector {...defaultProps} />);
expect(screen.getByText("orch-1")).toBeInTheDocument();
expect(screen.getByText(/2h ago/)).toBeInTheDocument();
expect(screen.getByText(/5m ago/)).toBeInTheDocument();
const spawnBtn = screen.getByText("Start New Orchestrator");
fireEvent.click(spawnBtn);
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith("/sessions/new-orch");
});
});
it("covers relative time for days and status colors/labels", () => {
const wideProps = {
...defaultProps,
orchestrators: [
{
id: "orch-2",
projectId: "my-app",
projectName: "My App",
status: "ci_failed",
activity: "waiting_input",
createdAt: new Date(Date.now() - 3600000 * 50).toISOString(), // 2d ago
lastActivityAt: null,
},
{
id: "orch-3",
projectId: "my-app",
projectName: "My App",
status: "killed",
activity: "ready",
createdAt: new Date(Date.now() - 1000).toISOString(), // Just now
lastActivityAt: null,
},
{
id: "orch-4",
projectId: "my-app",
projectName: "My App",
status: "unknown",
activity: "blocked",
createdAt: new Date().toISOString(),
lastActivityAt: null,
},
{
id: "orch-5",
projectId: "my-app",
projectName: "My App",
status: "mergeable",
activity: "exited",
createdAt: new Date().toISOString(),
lastActivityAt: null,
}
],
};
render(<OrchestratorSelector {...wideProps} />);
expect(screen.getByText(/2d ago/)).toBeInTheDocument();
expect(screen.getAllByText(/Just now/i).length).toBeGreaterThan(0);
expect(screen.getByText(/Waiting/)).toBeInTheDocument();
expect(screen.getByText(/Ready/)).toBeInTheDocument();
expect(screen.getByText(/Blocked/)).toBeInTheDocument();
expect(screen.getByText(/Exited/)).toBeInTheDocument();
expect(screen.getByText(/ci failed/i)).toBeInTheDocument();
});
it("handles spawn failure with error message", async () => {
(global.fetch as any).mockResolvedValue({
ok: false,
json: async () => ({ error: "Server error" }),
});
render(<OrchestratorSelector {...defaultProps} orchestrators={[]} />);
fireEvent.click(screen.getByText("Start New Orchestrator"));
await waitFor(() => {
expect(screen.getByText("Server error")).toBeInTheDocument();
});
});
it("renders error state from props", () => {
render(<OrchestratorSelector {...defaultProps} error="Project not found" />);
expect(screen.getByText("Project not found")).toBeInTheDocument();
expect(screen.getByText("Go to Dashboard")).toHaveAttribute("href", "/");
});
});
describe("Orchestrators Page (OrchestratorsRoute)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders the page with searchParams and listed orchestrators", async () => {
const mockSessionManager = {
list: vi.fn().mockResolvedValue([
{
id: "app-orchestrator",
projectId: "my-app",
status: "working",
activity: "active",
createdAt: new Date(),
lastActivityAt: new Date(),
},
]),
};
(getServices as any).mockResolvedValue({
config: {
projects: {
"my-app": { name: "My App", sessionPrefix: "app" },
},
},
sessionManager: mockSessionManager,
});
(getAllProjects as any).mockReturnValue([{ id: "my-app", name: "My App" }]);
const searchParams = Promise.resolve({ project: "my-app" });
const jsx = await OrchestratorsRoute({ searchParams });
render(jsx);
expect(screen.getByText("My App")).toBeInTheDocument();
expect(screen.getByText("app-orchestrator")).toBeInTheDocument();
});
it("shows error when project is missing in searchParams", async () => {
const searchParams = Promise.resolve({});
const jsx = await OrchestratorsRoute({ searchParams });
render(jsx);
expect(screen.getByText("Missing Project")).toBeInTheDocument();
});
it("shows error when project is not found in config", async () => {
(getServices as any).mockResolvedValue({
config: { projects: {} },
sessionManager: { list: vi.fn() },
});
(getAllProjects as any).mockReturnValue([]);
const searchParams = Promise.resolve({ project: "ghost" });
const jsx = await OrchestratorsRoute({ searchParams });
render(jsx);
expect(screen.getByText('Project "ghost" not found')).toBeInTheDocument();
});
it("handles service errors gracefully", async () => {
(getServices as any).mockRejectedValue(new Error("Database down"));
const searchParams = Promise.resolve({ project: "my-app" });
const jsx = await OrchestratorsRoute({ searchParams });
render(jsx);
expect(screen.getByText("Database down")).toBeInTheDocument();
});
});