Compare commits

...

1 Commits

Author SHA1 Message Date
fullstack-engineer c0e60da238 test(canvas/ChatTab): add abilities banner coverage
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 27s
CI / Detect changes (pull_request) Successful in 34s
E2E API Smoke Test / detect-changes (pull_request) Successful in 31s
E2E Chat / detect-changes (pull_request) Successful in 32s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 31s
Harness Replays / detect-changes (pull_request) Successful in 18s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 31s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 22s
qa-review / approved (pull_request) Successful in 33s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m39s
security-review / approved (pull_request) Successful in 32s
CI / Canvas (Next.js) (pull_request) Successful in 25m37s
CI / Platform (Go) (pull_request) Successful in 28m25s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 9s
CI / Python Lint & Test (pull_request) Successful in 9s
E2E Chat / E2E Chat (pull_request) Failing after 9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 14s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 16s
Harness Replays / Harness Replays (pull_request) Successful in 14s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 13s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / all-required (pull_request) Successful in 9s
gate-check-v3 / gate-check (pull_request) Successful in 3s
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request) Successful in 3s
sop-tier-check / tier-check (pull_request) Successful in 3s
Tests the talk_to_user_enabled=false banner and Enable button in ChatTab:
- Banner renders when talkToUserEnabled=false
- Banner absent when true or undefined
- Enable button calls PATCH /workspaces/:id/abilities { talk_to_user_enabled: true }
- Store updated on success
- Errors silently swallowed (no crash on network failure)

Refs: #1312
2026-05-16 16:21:42 +00:00
@@ -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();
});
});
});