Files
fusion/packages/dashboard/app/components/__tests__/ChatView.cli-mount.test.tsx
gsxdsm 85f70148a1 feat(dashboard): wire cli-agent chat surface and runner glue; fix stale engine mocks (U12 completion)
Mount CliChatSurface in ChatView for cli-backed chat sessions (sessions carrying
cliExecutorAdapterId): the message-pane + composer region is delegated to the
surface (transcript/raw-terminal toggle for hybrid/native adapters, terminal-only
for the generic adapter), while regular sessions keep the standard composer. The
existing message list and composer JSX are captured once as render thunks and
passed through, so there is no parallel message/composer UI.

Add a narrow telemetry seam: TelemetryHub gains an optional onEvent tap (also
settable post-construction via setEventListener) invoked with each sanitized
event after routing — best-effort, a throwing listener never breaks ingest. This
is the seam the CliChatSessionRunner uses to build the durable transcript from
the same sanitized events the hook route already feeds the hub, without the hub
becoming a general subscriber bus.

Fix the stale @fusion/engine vi.mocks across dashboard tests: object-literal
mocks that fully replace the module now also return listCliAdapterDescriptors
(added by U15's cli-agent-settings route, evaluated at module load). Mocks that
spread importOriginal/importActual already pick it up.

Tests: new ChatView.cli-mount.test.tsx (cli session → CliChatSurface, regular
session → normal composer, generic → terminal-only); telemetry-hub onEvent tap
coverage. chat-attachment-routes, chat-cli-sessions, cli-agent-hooks-route,
ChatView.cli-toggle all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 03:02:27 -07:00

153 lines
5.7 KiB
TypeScript

// ChatView CLI-backed mount test (CLI Agent Executor, U12 completion).
//
// Asserts ChatView delegates the message-pane + composer region to
// <CliChatSurface> when the active chat session carries a `cliExecutorAdapterId`,
// and falls back to the normal provider composer for a regular session.
//
// SessionTerminal is mocked (no xterm / no WS / no PTY / no port 4040) because
// CliChatSurface renders it under the hood.
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { ChatView } from "../ChatView";
import * as useChatModule from "../../hooks/useChat";
import * as useChatRoomsModule from "../../hooks/useChatRooms";
import type { ChatSessionInfo, UseChatReturn } from "../../hooks/useChat";
import type { UseChatRoomsResult } from "../../hooks/useChatRooms";
import { _resetInitialViewportHeight } from "../../hooks/useMobileKeyboard";
Element.prototype.scrollIntoView = vi.fn();
vi.mock("../SessionTerminal", () => ({
SessionTerminal: ({ sessionId }: { sessionId: string }) => (
<div data-testid="session-terminal" data-session-id={sessionId}>
terminal
</div>
),
}));
vi.mock("../../hooks/useChat");
vi.mock("../../hooks/useChatRooms");
vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../hooks/useNavigationHistory")>();
return {
...actual,
useNavigationHistoryContext: () => ({ pushNav: vi.fn(), replaceCurrent: vi.fn() }),
};
});
vi.mock("../../api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../api")>();
return {
...actual,
fetchAgents: vi.fn().mockResolvedValue([]),
fetchDiscoveredSkills: vi.fn().mockResolvedValue([]),
fetchTasks: vi.fn().mockResolvedValue([]),
searchFiles: vi.fn().mockResolvedValue({ files: [] }),
};
});
const mockUseChat = vi.mocked(useChatModule.useChat);
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
function makeSession(overrides: Partial<ChatSessionInfo> = {}): ChatSessionInfo {
return {
id: "sess-1",
agentId: "agent-1",
status: "active",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
function chatState(session: ChatSessionInfo): UseChatReturn {
return {
sessions: [session],
activeSession: session,
sessionsLoading: false,
messages: [],
messagesLoading: false,
isStreaming: false,
streamingText: "",
streamingThinking: "",
streamingToolCalls: [],
selectSession: vi.fn(),
createSession: vi.fn(),
archiveSession: vi.fn(),
deleteSession: vi.fn(),
sendMessage: vi.fn(),
stopStreaming: vi.fn(),
pendingMessage: "",
clearPendingMessage: vi.fn(),
loadMoreMessages: vi.fn(),
hasMoreMessages: false,
searchQuery: "",
setSearchQuery: vi.fn(),
filteredSessions: [session],
refreshSessions: vi.fn(),
agentsMap: new Map(),
};
}
const defaultRoomsState: UseChatRoomsResult = {
rooms: [],
roomsLoading: false,
roomsError: null,
activeRoom: null,
activeRoomMembers: [],
messages: [],
messagesLoading: false,
selectRoom: vi.fn(),
createRoom: vi.fn(),
deleteRoom: vi.fn(),
sendRoomMessage: vi.fn().mockResolvedValue(undefined),
refreshRooms: vi.fn(),
};
describe("ChatView CLI-backed session mount", () => {
beforeEach(() => {
_resetInitialViewportHeight();
vi.clearAllMocks();
mockUseChatRooms.mockReturnValue(defaultRoomsState);
});
it("renders CliChatSurface (transcript/terminal toggle) for a cli-backed session", () => {
mockUseChat.mockReturnValue(
chatState(makeSession({ cliExecutorAdapterId: "claude-code", cliSessionFile: "cli-native-1" })),
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// CliChatSurface renders the transcript/terminal toggle tablist.
expect(screen.getByRole("tab", { name: /transcript/i })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /terminal/i })).toBeInTheDocument();
// The standard provider send button is NOT rendered as a top-level composer
// affordance for the cli surface's default (transcript) view it wraps the
// existing composer, but the distinguishing CLI toggle is present.
});
it("attaches the terminal to the native cli session id linkage", () => {
mockUseChat.mockReturnValue(
chatState(makeSession({ cliExecutorAdapterId: "claude-code", cliSessionFile: "cli-native-1" })),
);
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// Switch to the terminal tab to mount SessionTerminal.
fireEvent.click(screen.getByRole("tab", { name: /terminal/i }));
expect(screen.getByTestId("session-terminal").getAttribute("data-session-id")).toBe("cli-native-1");
});
it("generic-tier cli session renders terminal-only (no toggle)", () => {
mockUseChat.mockReturnValue(chatState(makeSession({ cliExecutorAdapterId: "generic" })));
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
expect(screen.getByTestId("session-terminal")).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: /transcript/i })).toBeNull();
});
it("renders the normal provider composer for a regular (non-cli) session", () => {
mockUseChat.mockReturnValue(chatState(makeSession({ cliExecutorAdapterId: null })));
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
// Normal composer present, CLI toggle absent.
expect(screen.getByPlaceholderText("Type a message...")).toBeInTheDocument();
expect(screen.queryByRole("tab", { name: /transcript/i })).toBeNull();
expect(screen.queryByTestId("session-terminal")).toBeNull();
});
});