Files
stack/apps/web/src/hooks/useOrchestratorCommands.test.ts
Jason Woltje b110c469c4
All checks were successful
ci/woodpecker/push/web Pipeline was successful
feat(web): add orchestrator command system in chat interface (#521)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-02-26 03:39:00 +00:00

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();
});
});
});
});