test(canvas/ChatTab): add abilities banner coverage #1364
@@ -0,0 +1,155 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// Tests for the talk_to_user_enabled banner in ChatTab.
|
||||
//
|
||||
// The banner appears when the agent's workspace has talkToUserEnabled=false.
|
||||
// It shows an informational message and an "Enable" button that calls:
|
||||
// PATCH /workspaces/:id/abilities { talk_to_user_enabled: true }
|
||||
// and updates the canvas store on success.
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
||||
import React from "react";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// ── API mock ──────────────────────────────────────────────────────────
|
||||
|
||||
const apiPatch = vi.fn<[_path: string, _body: unknown], Promise<void>>();
|
||||
vi.mock("@/lib/api", () => ({
|
||||
api: {
|
||||
get: vi.fn(() => Promise.resolve([])),
|
||||
post: vi.fn(() => Promise.resolve({})),
|
||||
patch: (path: string, body: unknown) => apiPatch(path, body),
|
||||
del: vi.fn(),
|
||||
put: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// ── Store mock ────────────────────────────────────────────────────────
|
||||
// Must include agentMessages / consumeAgentMessages — used by useChatSocket
|
||||
// inside ChatTab even when the banner is shown. Also provides getState()
|
||||
// so ChatTab can call useCanvasStore.getState().updateNodeData() directly.
|
||||
const mockState = {
|
||||
updateNodeData: vi.fn(),
|
||||
agentMessages: {} as Record<string, unknown[]>,
|
||||
consumeAgentMessages: vi.fn(() => []),
|
||||
};
|
||||
vi.mock("@/store/canvas", () => ({
|
||||
useCanvasStore: Object.assign(
|
||||
(selector?: (s: typeof mockState) => unknown) =>
|
||||
selector ? selector(mockState) : mockState,
|
||||
{ getState: () => mockState },
|
||||
) as unknown as typeof import("@/store/canvas").useCanvasStore,
|
||||
}));
|
||||
|
||||
// ── Upload mock ───────────────────────────────────────────────────────
|
||||
|
||||
vi.mock("../chat/uploads", async () => {
|
||||
const actual = await vi.importActual<typeof import("../chat/uploads")>("../chat/uploads");
|
||||
return { ...actual, downloadChatFile: vi.fn() };
|
||||
});
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
const minimalData = {
|
||||
status: "online" as const,
|
||||
runtime: "claude-code",
|
||||
currentTask: null,
|
||||
} as unknown as import("@/store/canvas").WorkspaceNodeData;
|
||||
|
||||
beforeEach(() => {
|
||||
apiPatch.mockClear();
|
||||
mockState.updateNodeData.mockClear();
|
||||
Element.prototype.scrollIntoView = vi.fn();
|
||||
class FakeIO {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
}
|
||||
(window as unknown as { IntersectionObserver: unknown }).IntersectionObserver = FakeIO;
|
||||
(globalThis as unknown as { IntersectionObserver: unknown }).IntersectionObserver = FakeIO;
|
||||
});
|
||||
|
||||
import { ChatTab } from "../ChatTab";
|
||||
|
||||
// ─── Banner renders ───────────────────────────────────────────────────────────
|
||||
|
||||
describe("ChatTab — talk_to_user_enabled banner", () => {
|
||||
it("shows the banner when talkToUserEnabled is false", () => {
|
||||
const data = { ...minimalData, talkToUserEnabled: false };
|
||||
render(<ChatTab workspaceId="ws-disabled" data={data} />);
|
||||
|
||||
// screen.getByText throws if not found, so absence of error = found
|
||||
expect(screen.getByText((content: string) =>
|
||||
content.includes("Agent is not enabled to chat with you"),
|
||||
)).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: /enable/i })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does NOT show the banner when talkToUserEnabled is true", () => {
|
||||
const data = { ...minimalData, talkToUserEnabled: true };
|
||||
render(<ChatTab workspaceId="ws-enabled" data={data} />);
|
||||
|
||||
expect(screen.queryByText((content: string) =>
|
||||
content.includes("Agent is not enabled to chat with you"),
|
||||
)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("does NOT show the banner when talkToUserEnabled is absent (undefined)", () => {
|
||||
const data = { ...minimalData };
|
||||
render(<ChatTab workspaceId="ws-unset" data={data} />);
|
||||
|
||||
expect(screen.queryByText((content: string) =>
|
||||
content.includes("Agent is not enabled to chat with you"),
|
||||
)).toBeFalsy();
|
||||
});
|
||||
|
||||
// ─── Enable button ────────────────────────────────────────────────────────────
|
||||
|
||||
it('Enable button calls PATCH /workspaces/:id/abilities with talk_to_user_enabled: true', async () => {
|
||||
const data = { ...minimalData, talkToUserEnabled: false };
|
||||
apiPatch.mockResolvedValue(undefined);
|
||||
|
||||
render(<ChatTab workspaceId="ws-banner-1" data={data} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /enable/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiPatch).toHaveBeenCalledOnce();
|
||||
expect(apiPatch).toHaveBeenCalledWith(
|
||||
"/workspaces/ws-banner-1/abilities",
|
||||
{ talk_to_user_enabled: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("Enable button updates the store on success", async () => {
|
||||
const data = { ...minimalData, talkToUserEnabled: false };
|
||||
apiPatch.mockResolvedValue(undefined);
|
||||
|
||||
render(<ChatTab workspaceId="ws-banner-2" data={data} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /enable/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledOnce();
|
||||
expect(mockState.updateNodeData).toHaveBeenCalledWith("ws-banner-2", { talkToUserEnabled: true });
|
||||
});
|
||||
});
|
||||
|
||||
it("Enable button does NOT throw when PATCH fails — error is silently swallowed", async () => {
|
||||
const data = { ...minimalData, talkToUserEnabled: false };
|
||||
apiPatch.mockRejectedValue(new Error("network error"));
|
||||
|
||||
render(<ChatTab workspaceId="ws-banner-3" data={data} />);
|
||||
|
||||
// Should not throw — the catch block swallows the error
|
||||
fireEvent.click(screen.getByRole("button", { name: /enable/i }));
|
||||
|
||||
// Store update should NOT have been called since the API call failed
|
||||
await waitFor(() => {
|
||||
expect(mockState.updateNodeData).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user