chore(comments): address PR #143 review (operator doc, badge test, dedup, type)
- [warn 1] Document the is_agent operator setup so it survives plan deletion: added an AI-agent block to .env.example (use a DEDICATED account, set is_agent via SQL, never flag a human/shared account) + a CHANGELOG "Added" entry. - [warn 2] Test the badge deep-link side effects: ai-agent-badge.test.tsx now renders inside an explicit jotai store, clicks the badge, and asserts the active chat id, window-open, cleared draft, closed history modal, AND that stopPropagation keeps a parent onClick from firing. - [suggestion 3] Hoist the window.matchMedia stub into vitest.setup.ts and drop the duplicated beforeAll block from the three test files (ai-agent-badge, comment-list-item, role-cards). - [suggestion 4] Merge the two near-duplicate "non-clickable" cases via it.each. - [follow-up 6] Introduce a single ProvenanceSource = 'user' | 'agent' type in jwt-payload.ts and reference it from AuthProvenanceData, JwtPayload/ JwtCollabPayload, and resolveSource() — so a typo can't slip through as a bare string. (Server auth chain; client IComment mirroring left as a follow-up.) Follow-up 5 (shared agentSourceFields write-stamp helper) is deferred as the review marked it — the 6 REST sites use varied shapes (create-spread vs resolve-conditional-null vs page move), so it's a separate focused refactor. Tests: client badge/comment/role-cards suites 11/11 pass; server auth+comment suites 62 pass; typecheck clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,25 +1,16 @@
|
||||
import { describe, it, expect, vi, beforeAll } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { Provider, createStore } from "jotai";
|
||||
import { AiAgentBadge } from "./ai-agent-badge";
|
||||
import {
|
||||
activeAiChatIdAtom,
|
||||
aiChatWindowOpenAtom,
|
||||
aiChatDraftAtom,
|
||||
} from "@/features/ai-chat/atoms/ai-chat-atom.ts";
|
||||
import { historyAtoms } from "@/features/page-history/atoms/history-atoms.ts";
|
||||
|
||||
// MantineProvider reads window.matchMedia (color scheme) on mount, which jsdom
|
||||
// does not implement. Provide a minimal stub so the provider can render.
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
|
||||
|
||||
function renderBadge(props: { authorName?: string; aiChatId?: string | null }) {
|
||||
return render(
|
||||
@@ -40,21 +31,42 @@ describe("AiAgentBadge", () => {
|
||||
const badge = screen.getByRole("button");
|
||||
expect(badge).toBeDefined();
|
||||
expect(badge.textContent).toContain("AI-agent");
|
||||
// Clicking does not throw — the deep-link handler runs against the default
|
||||
// jotai store. (Asserting the badge exposes an interactive role is the
|
||||
// observable contract; the atom side-effects are covered by the history UI.)
|
||||
fireEvent.click(badge);
|
||||
});
|
||||
|
||||
it("is a plain non-clickable label when aiChatId is null (external MCP agent)", () => {
|
||||
renderBadge({ authorName: "Bot", aiChatId: null });
|
||||
expect(screen.getByText("AI-agent")).toBeDefined();
|
||||
// No interactive role is exposed when there is no chat to deep-link into.
|
||||
expect(screen.queryByRole("button")).toBeNull();
|
||||
it("deep-links on click: sets the active chat, clears the draft, opens the AI-chat window, closes the history modal — and stops propagation", () => {
|
||||
const store = createStore();
|
||||
// Pre-set the state the click must change, so the assertions are meaningful.
|
||||
store.set(historyAtoms, true); // history modal open
|
||||
store.set(aiChatDraftAtom, "leftover draft from another chat");
|
||||
const onParentClick = vi.fn();
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<MantineProvider>
|
||||
{/* Parent click handler must NOT fire — the badge stops propagation. */}
|
||||
<div onClick={onParentClick}>
|
||||
<AiAgentBadge authorName="Bot" aiChatId="chat-1" />
|
||||
</div>
|
||||
</MantineProvider>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button"));
|
||||
|
||||
expect(store.get(activeAiChatIdAtom)).toBe("chat-1");
|
||||
expect(store.get(aiChatWindowOpenAtom)).toBe(true);
|
||||
expect(store.get(aiChatDraftAtom)).toBe(""); // draft cleared
|
||||
expect(store.get(historyAtoms)).toBe(false); // history modal closed
|
||||
expect(onParentClick).not.toHaveBeenCalled(); // stopPropagation contained the click
|
||||
});
|
||||
|
||||
it("is non-clickable when aiChatId is absent", () => {
|
||||
renderBadge({ authorName: "Bot" });
|
||||
expect(screen.queryByRole("button")).toBeNull();
|
||||
});
|
||||
it.each([{ aiChatId: null }, {}])(
|
||||
"is a plain non-clickable label without a chat target (%o)",
|
||||
(props) => {
|
||||
renderBadge({ authorName: "Bot", ...props });
|
||||
expect(screen.getByText("AI-agent")).toBeDefined();
|
||||
// No interactive role is exposed when there is no chat to deep-link into.
|
||||
expect(screen.queryByRole("button")).toBeNull();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,26 +1,10 @@
|
||||
import { describe, it, expect, vi, beforeAll } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import RoleCards from "./role-cards";
|
||||
import { IAiRole } from "@/features/ai-chat/types/ai-chat.types.ts";
|
||||
|
||||
// MantineProvider reads window.matchMedia (color scheme) on mount, which jsdom
|
||||
// does not implement. Provide a minimal stub so the provider can render.
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
|
||||
|
||||
const roles: IAiRole[] = [
|
||||
{
|
||||
|
||||
@@ -1,24 +1,9 @@
|
||||
import { describe, it, expect, vi, beforeAll } from "vitest";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { MantineProvider } from "@mantine/core";
|
||||
import { IComment } from "@/features/comment/types/comment.types";
|
||||
|
||||
// MantineProvider reads window.matchMedia on mount, which jsdom lacks.
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
}),
|
||||
});
|
||||
});
|
||||
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
|
||||
|
||||
// The comment mutation hooks reach out to react-query/network — stub them so the
|
||||
// component renders in isolation. We only assert the AI-badge rendering branch.
|
||||
|
||||
Reference in New Issue
Block a user