feat(web): add orchestrator command system in chat interface (#521)
All checks were successful
ci/woodpecker/push/web Pipeline was successful
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>
This commit was merged in pull request #521.
This commit is contained in:
@@ -4,12 +4,13 @@
|
||||
*/
|
||||
|
||||
import { createRef } from "react";
|
||||
import { render } from "@testing-library/react";
|
||||
import { render, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, beforeEach, vi, afterEach, type MockedFunction } from "vitest";
|
||||
import { Chat, type ChatRef } from "./Chat";
|
||||
import * as useChatModule from "@/hooks/useChat";
|
||||
import * as useWebSocketModule from "@/hooks/useWebSocket";
|
||||
import * as authModule from "@/lib/auth/auth-context";
|
||||
import * as orchestratorModule from "@/hooks/useOrchestratorCommands";
|
||||
|
||||
// Mock scrollIntoView (not available in JSDOM)
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
@@ -39,9 +40,14 @@ vi.mock("./ChatInput", () => ({
|
||||
disabled: boolean;
|
||||
inputRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||
}): React.ReactElement => (
|
||||
<button data-testid="chat-input" onClick={(): void => void onSend("test message")}>
|
||||
Send
|
||||
</button>
|
||||
<>
|
||||
<button data-testid="chat-input" onClick={(): void => void onSend("test message")}>
|
||||
Send
|
||||
</button>
|
||||
<button data-testid="chat-input-command" onClick={(): void => void onSend("/status")}>
|
||||
Send Command
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
DEFAULT_TEMPERATURE: 0.7,
|
||||
DEFAULT_MAX_TOKENS: 4096,
|
||||
@@ -54,11 +60,18 @@ vi.mock("./ChatInput", () => ({
|
||||
],
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useOrchestratorCommands", () => ({
|
||||
useOrchestratorCommands: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockUseAuth = authModule.useAuth as MockedFunction<typeof authModule.useAuth>;
|
||||
const mockUseChat = useChatModule.useChat as MockedFunction<typeof useChatModule.useChat>;
|
||||
const mockUseWebSocket = useWebSocketModule.useWebSocket as MockedFunction<
|
||||
typeof useWebSocketModule.useWebSocket
|
||||
>;
|
||||
const mockUseOrchestratorCommands = orchestratorModule.useOrchestratorCommands as MockedFunction<
|
||||
typeof orchestratorModule.useOrchestratorCommands
|
||||
>;
|
||||
|
||||
function createMockUseChatReturn(
|
||||
overrides: Partial<useChatModule.UseChatReturn> = {}
|
||||
@@ -107,6 +120,12 @@ describe("Chat", () => {
|
||||
socket: null,
|
||||
connectionError: null,
|
||||
});
|
||||
|
||||
// Default: no commands intercepted
|
||||
mockUseOrchestratorCommands.mockReturnValue({
|
||||
isCommand: vi.fn().mockReturnValue(false),
|
||||
executeCommand: vi.fn().mockResolvedValue(null),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -160,4 +179,105 @@ describe("Chat", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("orchestrator command routing", () => {
|
||||
it("routes command messages through orchestrator instead of LLM", async () => {
|
||||
const mockSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const mockSetMessages = vi.fn();
|
||||
const mockExecuteCommand = vi.fn().mockResolvedValue({
|
||||
id: "orch-123",
|
||||
role: "assistant" as const,
|
||||
content: "**Orchestrator Status**\n\n| Field | Value |\n|---|---|\n| Status | **Ready** |",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
mockUseChat.mockReturnValue(
|
||||
createMockUseChatReturn({
|
||||
sendMessage: mockSendMessage,
|
||||
setMessages: mockSetMessages,
|
||||
})
|
||||
);
|
||||
|
||||
mockUseOrchestratorCommands.mockReturnValue({
|
||||
isCommand: (content: string) => content.trim().startsWith("/"),
|
||||
executeCommand: mockExecuteCommand,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<Chat />);
|
||||
|
||||
const commandButton = getByTestId("chat-input-command");
|
||||
fireEvent.click(commandButton);
|
||||
|
||||
await waitFor(() => {
|
||||
// executeCommand should have been called with the slash command
|
||||
expect(mockExecuteCommand).toHaveBeenCalledWith("/status");
|
||||
});
|
||||
|
||||
// sendMessage should NOT have been called
|
||||
expect(mockSendMessage).not.toHaveBeenCalled();
|
||||
|
||||
// setMessages should have been called to add user and assistant messages
|
||||
await waitFor(() => {
|
||||
expect(mockSetMessages).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not call orchestrator for regular messages", async () => {
|
||||
const mockSendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const mockExecuteCommand = vi.fn().mockResolvedValue(null);
|
||||
|
||||
mockUseChat.mockReturnValue(createMockUseChatReturn({ sendMessage: mockSendMessage }));
|
||||
|
||||
mockUseOrchestratorCommands.mockReturnValue({
|
||||
isCommand: vi.fn().mockReturnValue(false),
|
||||
executeCommand: mockExecuteCommand,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<Chat />);
|
||||
|
||||
fireEvent.click(getByTestId("chat-input"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSendMessage).toHaveBeenCalledWith("test message");
|
||||
});
|
||||
|
||||
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still adds user message to chat for commands", async () => {
|
||||
const mockSetMessages = vi.fn();
|
||||
const mockExecuteCommand = vi.fn().mockResolvedValue({
|
||||
id: "orch-456",
|
||||
role: "assistant" as const,
|
||||
content: "Help content",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
mockUseChat.mockReturnValue(createMockUseChatReturn({ setMessages: mockSetMessages }));
|
||||
|
||||
mockUseOrchestratorCommands.mockReturnValue({
|
||||
isCommand: (content: string) => content.trim().startsWith("/"),
|
||||
executeCommand: mockExecuteCommand,
|
||||
});
|
||||
|
||||
const { getByTestId } = render(<Chat />);
|
||||
|
||||
fireEvent.click(getByTestId("chat-input-command"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// First setMessages call should add the user message
|
||||
const firstCall = mockSetMessages.mock.calls[0];
|
||||
if (!firstCall) throw new Error("Expected setMessages to have been called");
|
||||
const updater = firstCall[0] as (prev: useChatModule.Message[]) => useChatModule.Message[];
|
||||
const result = updater([]);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
role: "user",
|
||||
content: "/status",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user