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:
@@ -1,9 +1,10 @@
|
||||
/**
|
||||
* @file ChatInput.test.tsx
|
||||
* @description Tests for ChatInput: model selector, temperature/params, localStorage persistence
|
||||
* @description Tests for ChatInput: model selector, temperature/params, localStorage persistence,
|
||||
* and command autocomplete.
|
||||
*/
|
||||
|
||||
import { render, screen, fireEvent, waitFor, within } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor, within, act } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
ChatInput,
|
||||
@@ -291,3 +292,195 @@ describe("ChatInput — send behavior", () => {
|
||||
expect(onStop).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatInput — command autocomplete", () => {
|
||||
it("shows no autocomplete for regular text", () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "hello world" } });
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows autocomplete dropdown when user types /", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows all commands when only / is typed", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const dropdown = screen.getByTestId("command-autocomplete");
|
||||
expect(dropdown).toHaveTextContent("/status");
|
||||
expect(dropdown).toHaveTextContent("/agents");
|
||||
expect(dropdown).toHaveTextContent("/jobs");
|
||||
expect(dropdown).toHaveTextContent("/pause");
|
||||
expect(dropdown).toHaveTextContent("/resume");
|
||||
expect(dropdown).toHaveTextContent("/help");
|
||||
});
|
||||
});
|
||||
|
||||
it("filters commands by typed prefix", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/ag" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const dropdown = screen.getByTestId("command-autocomplete");
|
||||
expect(dropdown).toHaveTextContent("/agents");
|
||||
expect(dropdown).not.toHaveTextContent("/status");
|
||||
expect(dropdown).not.toHaveTextContent("/pause");
|
||||
});
|
||||
});
|
||||
|
||||
it("dismisses autocomplete on Escape key", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(textarea, { key: "Escape" });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts first command on Tab key", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/stat" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(textarea, { key: "Tab" });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("/status ");
|
||||
});
|
||||
|
||||
it("navigates with ArrowDown key", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(textarea, { key: "ArrowDown" });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const options = screen.getAllByRole("option");
|
||||
// Second item should be selected after ArrowDown
|
||||
expect(options[1]).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
});
|
||||
|
||||
it("fills command when clicking a suggestion", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on /agents option
|
||||
const options = screen.getAllByRole("option");
|
||||
const agentsOption = options.find((o) => o.textContent.includes("/agents"));
|
||||
if (!agentsOption) throw new Error("Could not find /agents option");
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(agentsOption);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect((textarea as HTMLTextAreaElement).value).toBe("/agents ");
|
||||
});
|
||||
|
||||
it("shows command descriptions", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const dropdown = screen.getByTestId("command-autocomplete");
|
||||
expect(dropdown).toHaveTextContent("Show orchestrator health");
|
||||
expect(dropdown).toHaveTextContent("Pause the job queue");
|
||||
});
|
||||
});
|
||||
|
||||
it("hides autocomplete when input no longer starts with /", async () => {
|
||||
render(<ChatInput onSend={vi.fn()} />);
|
||||
const textarea = screen.getByLabelText(/message input/i);
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "/" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("command-autocomplete")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.change(textarea, { target: { value: "" } });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("command-autocomplete")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user