All checks were successful
ci/woodpecker/push/web Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
/**
|
|
* Tests for useOrchestratorCommands hook
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook, act } from "@testing-library/react";
|
|
import { useOrchestratorCommands } from "./useOrchestratorCommands";
|
|
import type { Message } from "./useChat";
|
|
|
|
// Mock fetch globally
|
|
const mockFetch = vi.fn();
|
|
global.fetch = mockFetch;
|
|
|
|
function makeOkResponse(data: unknown): Response {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: () => Promise.resolve(data),
|
|
text: () => Promise.resolve(JSON.stringify(data)),
|
|
} as unknown as Response;
|
|
}
|
|
|
|
/** Run executeCommand and return the result synchronously after act() */
|
|
async function runCommand(
|
|
executeCommand: (content: string) => Promise<Message | null>,
|
|
content: string
|
|
): Promise<Message | null> {
|
|
let msg: Message | null = null;
|
|
await act(async () => {
|
|
msg = await executeCommand(content);
|
|
});
|
|
return msg;
|
|
}
|
|
|
|
describe("useOrchestratorCommands", () => {
|
|
beforeEach(() => {
|
|
mockFetch.mockReset();
|
|
});
|
|
|
|
describe("isCommand", () => {
|
|
it("returns true for messages starting with /", () => {
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
expect(result.current.isCommand("/status")).toBe(true);
|
|
expect(result.current.isCommand("/agents")).toBe(true);
|
|
expect(result.current.isCommand("/help")).toBe(true);
|
|
expect(result.current.isCommand(" /status")).toBe(true);
|
|
});
|
|
|
|
it("returns false for regular messages", () => {
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
expect(result.current.isCommand("hello")).toBe(false);
|
|
expect(result.current.isCommand("tell me about /status")).toBe(false);
|
|
expect(result.current.isCommand("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("executeCommand", () => {
|
|
describe("/help", () => {
|
|
it("returns help message without network calls", async () => {
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/help");
|
|
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
expect(msg).not.toBeNull();
|
|
expect(msg?.role).toBe("assistant");
|
|
expect(msg?.content).toContain("/status");
|
|
expect(msg?.content).toContain("/agents");
|
|
expect(msg?.content).toContain("/jobs");
|
|
expect(msg?.content).toContain("/pause");
|
|
expect(msg?.content).toContain("/resume");
|
|
});
|
|
|
|
it("returns message with id and createdAt", async () => {
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/help");
|
|
|
|
expect(msg?.id).toBeDefined();
|
|
expect(msg?.createdAt).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("/status", () => {
|
|
it("calls /api/orchestrator/health and returns formatted status", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
makeOkResponse({ status: "ready", version: "1.2.3", uptime: 3661 })
|
|
);
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/status");
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/health", { method: "GET" });
|
|
expect(msg?.role).toBe("assistant");
|
|
expect(msg?.content).toContain("Ready");
|
|
expect(msg?.content).toContain("1.2.3");
|
|
expect(msg?.content).toContain("1h");
|
|
});
|
|
|
|
it("shows Not Ready when status is not ready", async () => {
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse({ status: "not-ready" }));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/status");
|
|
|
|
expect(msg?.content).toContain("Not Ready");
|
|
});
|
|
|
|
it("handles network error gracefully", async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error("Connection refused"));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/status");
|
|
|
|
expect(msg?.role).toBe("assistant");
|
|
expect(msg?.content).toContain("Error");
|
|
expect(msg?.content).toContain("Connection refused");
|
|
});
|
|
|
|
it("shows error from API response", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
makeOkResponse({ error: "ORCHESTRATOR_API_KEY is not configured" })
|
|
);
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/status");
|
|
|
|
expect(msg?.content).toContain("Not reachable");
|
|
});
|
|
});
|
|
|
|
describe("/agents", () => {
|
|
it("calls /api/orchestrator/agents and returns agent table", async () => {
|
|
const agents = [
|
|
{ id: "agent-1", status: "active", type: "codex", startedAt: "2026-02-25T10:00:00Z" },
|
|
{
|
|
id: "agent-2",
|
|
agentStatus: "TERMINATED",
|
|
channel: "claude",
|
|
startedAt: "2026-02-25T09:00:00Z",
|
|
},
|
|
];
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse(agents));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/agents");
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/agents", { method: "GET" });
|
|
expect(msg?.content).toContain("agent-1");
|
|
expect(msg?.content).toContain("agent-2");
|
|
expect(msg?.content).toContain("TERMINATED");
|
|
});
|
|
|
|
it("handles empty agent list", async () => {
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse([]));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/agents");
|
|
|
|
expect(msg?.content).toContain("No agents currently running");
|
|
});
|
|
|
|
it("handles agents in nested object", async () => {
|
|
const data = {
|
|
agents: [{ id: "agent-nested", status: "active" }],
|
|
};
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse(data));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/agents");
|
|
|
|
expect(msg?.content).toContain("agent-nested");
|
|
});
|
|
|
|
it("handles network error gracefully", async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error("Timeout"));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/agents");
|
|
|
|
expect(msg?.content).toContain("Error");
|
|
expect(msg?.content).toContain("Timeout");
|
|
});
|
|
});
|
|
|
|
describe("/jobs", () => {
|
|
it("calls /api/orchestrator/queue/stats", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
makeOkResponse({ pending: 3, active: 1, completed: 42, failed: 0 })
|
|
);
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/jobs");
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/stats", { method: "GET" });
|
|
expect(msg?.content).toContain("3");
|
|
expect(msg?.content).toContain("42");
|
|
expect(msg?.content).toContain("Pending");
|
|
expect(msg?.content).toContain("Completed");
|
|
});
|
|
|
|
it("/queue is an alias for /jobs", async () => {
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse({ pending: 0, active: 0 }));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/queue");
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/stats", { method: "GET" });
|
|
expect(msg?.role).toBe("assistant");
|
|
});
|
|
|
|
it("shows paused indicator when queue is paused", async () => {
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse({ pending: 0, active: 0, paused: true }));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/jobs");
|
|
|
|
expect(msg?.content).toContain("paused");
|
|
});
|
|
});
|
|
|
|
describe("/pause", () => {
|
|
it("calls POST /api/orchestrator/queue/pause", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
makeOkResponse({ success: true, message: "Queue paused." })
|
|
);
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/pause");
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/pause", {
|
|
method: "POST",
|
|
});
|
|
expect(msg?.content).toContain("paused");
|
|
});
|
|
|
|
it("handles API error response", async () => {
|
|
mockFetch.mockResolvedValueOnce(makeOkResponse({ error: "Already paused." }));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/pause");
|
|
|
|
expect(msg?.content).toContain("failed");
|
|
expect(msg?.content).toContain("Already paused");
|
|
});
|
|
|
|
it("handles network error", async () => {
|
|
mockFetch.mockRejectedValueOnce(new Error("Network failure"));
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/pause");
|
|
|
|
expect(msg?.content).toContain("Error");
|
|
});
|
|
});
|
|
|
|
describe("/resume", () => {
|
|
it("calls POST /api/orchestrator/queue/resume", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
makeOkResponse({ success: true, message: "Queue resumed." })
|
|
);
|
|
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/resume");
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith("/api/orchestrator/queue/resume", {
|
|
method: "POST",
|
|
});
|
|
expect(msg?.content).toContain("resumed");
|
|
});
|
|
});
|
|
|
|
describe("unknown command", () => {
|
|
it("returns help hint for unknown commands", async () => {
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "/unknown-command");
|
|
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
expect(msg?.content).toContain("Unknown command");
|
|
expect(msg?.content).toContain("/unknown-command");
|
|
expect(msg?.content).toContain("/help");
|
|
});
|
|
});
|
|
|
|
describe("non-command input", () => {
|
|
it("returns null for regular messages", async () => {
|
|
const { result } = renderHook(() => useOrchestratorCommands());
|
|
const msg = await runCommand(result.current.executeCommand, "hello world");
|
|
|
|
expect(msg).toBeNull();
|
|
expect(mockFetch).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
});
|