fix tests
This commit is contained in:
parent
71bdf75f53
commit
2a1b513e7d
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -225,7 +225,7 @@ describe("kill", () => {
|
|||
await sm.kill("app-1", { purgeOpenCode: true });
|
||||
|
||||
expect(existsSync(deleteLogPath)).toBe(false);
|
||||
});
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
describe("cleanup", () => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue