test(ai-chat): cover the conditional assistant-name signature (#108)

Extract the shared assistant-name predicate (resolveAssistantName: trimmed name
or null) used by typing-indicator + message-item, and unit-test the branches
(name shown; whitespace-only -> 'AI agent' fallback; undefined -> fallback).
Behavior-identical (|| -> ?? since the helper returns null).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-21 05:39:35 +03:00
parent 865d72256e
commit eb17109fe0
4 changed files with 45 additions and 3 deletions

View File

@@ -5,6 +5,7 @@ import type { UIMessage } from "@ai-sdk/react";
import ToolCallCard from "@/features/ai-chat/components/tool-call-card.tsx";
import { ToolUiPart, isToolPart } from "@/features/ai-chat/utils/tool-parts.tsx";
import { renderChatMarkdown } from "@/features/ai-chat/utils/markdown.ts";
import { resolveAssistantName } from "@/features/ai-chat/utils/assistant-name.ts";
import { describeChatError } from "@/features/ai-chat/utils/error-message.ts";
import classes from "@/features/ai-chat/components/ai-chat.module.css";
@@ -67,7 +68,7 @@ export default function MessageItem({
return (
<Box className={classes.messageRow}>
<Text size="xs" c="dimmed" mb={4}>
{assistantName?.trim() || t("AI agent")}
{resolveAssistantName(assistantName) ?? t("AI agent")}
</Text>
{message.parts.map((part, index) => {
if (part.type === "text") {

View File

@@ -1,5 +1,6 @@
import { Box, Group, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { resolveAssistantName } from "@/features/ai-chat/utils/assistant-name.ts";
import classes from "@/features/ai-chat/components/ai-chat.module.css";
interface TypingIndicatorProps {
@@ -23,12 +24,12 @@ interface TypingIndicatorProps {
*/
export default function TypingIndicator({ assistantName }: TypingIndicatorProps) {
const { t } = useTranslation();
const name = assistantName?.trim();
const name = resolveAssistantName(assistantName);
return (
<Box className={classes.messageRow}>
<Text size="xs" c="dimmed" mb={4}>
{name || t("AI agent")}
{name ?? t("AI agent")}
</Text>
<Group gap={8} align="center">
<span className={classes.typingDots} aria-hidden="true">

View File

@@ -0,0 +1,24 @@
import { describe, it, expect } from "vitest";
import { resolveAssistantName } from "./assistant-name";
describe("resolveAssistantName", () => {
it("returns a real name unchanged", () => {
expect(resolveAssistantName("Ada")).toBe("Ada");
});
it("trims surrounding whitespace from a real name", () => {
expect(resolveAssistantName(" Ada ")).toBe("Ada");
});
it("returns null for a whitespace-only name (the reason for .trim())", () => {
expect(resolveAssistantName(" ")).toBeNull();
});
it("returns null when the name is undefined", () => {
expect(resolveAssistantName(undefined)).toBeNull();
});
it("returns null for an empty string", () => {
expect(resolveAssistantName("")).toBeNull();
});
});

View File

@@ -0,0 +1,16 @@
// Pure helper for resolving the assistant's display name. Kept free of React so
// it can be unit-tested in isolation (see assistant-name.test.ts) and shared by
// the components that render the assistant identity (TypingIndicator, MessageItem).
/**
* Resolve the assistant's display name from the optional configured identity.
*
* Returns the trimmed name when it has visible (non-whitespace) characters, or
* `null` when the name is absent or whitespace-only. Callers fall back to a
* generic "AI agent" label on `null`. The `.trim()` is why a name of " " must
* resolve to `null` rather than rendering an empty label.
*/
export function resolveAssistantName(assistantName?: string): string | null {
const name = assistantName?.trim();
return name ? name : null;
}