Files
gitmost/apps/client/src/features/ai-chat/components/role-cards.test.tsx
claude code agent 227 0ec0af405a feat(ai-chat): per-role autoStart toggle + custom launchMessage (#149)
Agent role cards always auto-sent a hardcoded "Take a look at the current
document" on pick. Make it configurable per role:
- autoStart (bool, default true): whether picking the role auto-sends a message.
- launchMessage (nullable text): the text sent on auto-start; empty -> the
  built-in default. autoStart=false -> bind the role and send nothing (the user
  types the first message, which still carries the roleId).
Existing roles default to autoStart=true / launchMessage=null => identical old
behavior.

Full-stack:
- migration 20260624T120000 adds `auto_start boolean NOT NULL DEFAULT true` +
  `launch_message text` (additive; down drops both); db.d.ts updated by hand.
- DTO: autoStart (@IsBoolean) + launchMessage (trim @Transform, @MaxLength 2000).
- repo/service: thread + normalize (undefined=unchanged, ""=>null, autoStart??true).
  Both fields exposed in the picker-view for ordinary members (they decide
  whether/what to auto-send); instructions/modelConfig stay ADMIN-ONLY.
- client: IAiRole types, role form (Switch + Textarea, re-hydrated on edit),
  handleRolePick branches on autoStart; i18n en-US + ru-RU.

Review follow-ups folded in: reset the `rolePickedNoSend` flag when the thread
returns to an empty role-less state (the "New chat after autoStart=false pick"
stuck-UI bug — render-phase one-shot reset); made create/update launchMessage
normalization symmetric (raw value, server normalizes ""→null).

Server: 68 role tests pass, tsc clean. Client: tsc clean, role tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 06:28:41 +03:00

60 lines
1.6 KiB
TypeScript

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";
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
const roles: IAiRole[] = [
{
id: "r1",
name: "Pirate",
emoji: "🏴‍☠️",
description: "Talks like a pirate",
enabled: true,
autoStart: true,
launchMessage: null,
},
{
id: "r2",
name: "Grandpa",
emoji: null,
description: null,
enabled: true,
autoStart: true,
launchMessage: null,
},
];
function renderCards(onPick = vi.fn()) {
render(
<MantineProvider>
<RoleCards roles={roles} onPick={onPick} />
</MantineProvider>,
);
return onPick;
}
describe("RoleCards", () => {
it("renders one card per role with name, emoji, and description", () => {
renderCards();
expect(screen.getByText("Pirate")).toBeDefined();
expect(screen.getByText("Talks like a pirate")).toBeDefined();
expect(screen.getByText("Grandpa")).toBeDefined();
// The emoji is shown for the role that has one.
expect(screen.getByText("🏴‍☠️")).toBeDefined();
});
it("does NOT render a Universal assistant card", () => {
renderCards();
expect(screen.queryByText("Universal assistant")).toBeNull();
});
it("calls onPick with the role object when a card is clicked", () => {
const onPick = renderCards();
fireEvent.click(screen.getByText("Pirate"));
expect(onPick).toHaveBeenCalledWith(roles[0]);
});
});