Review of #158 (Request changes) — core logic verified correct; addressed the test-coverage + localization items: 1. i18n pluralization: the token-count keys were called with {count} but had one form, so ru-RU always rendered the genitive ("1 токенов"). Added _one/_other (en) and _one/_few/_many (ru: токен/токена/токенов) for both "Thinking… · {{count}} tokens" and "Thinking · {{count}} tokens"; de-duped the PR-added duplicate "Thinking" key. Call sites unchanged. 2. ReasoningBlock: new reasoning-block.test.tsx (4 branches: authoritative count wins / estimate fallback / header-only when count-but-no-text / body render). 3. Reasoning-token attribution: extracted the #151 anti-double-count rule into a pure `reasoningTokensForPart(message)` (single reasoning part -> authoritative turn total; multiple/none -> undefined so each estimates). message-item uses it; removed the now-dead lastReasoningIndex reduce (review #5). Unit-tested. 6. adopt-chat-id.ts: refreshed 3 stale `chatStreamStartMetadata` -> `chatStreamMetadata` comment references. 7. chat-markdown.test.ts: assert the export footer's `reasoning: N` line appears when reasoningTokens>0 and is absent at 0/undefined. Skipped optional #4 (mantine useThrottledCallback): the manual throttle has two distinct exit paths (turn-end revert-to-null + the captured-total trailing emit) with no guarding test; remapping risks the streaming behavior — non-blocking. Client tsc clean; ai-chat suite green (171 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
66 lines
2.7 KiB
TypeScript
66 lines
2.7 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import { render, screen } from "@testing-library/react";
|
|
import { MantineProvider } from "@mantine/core";
|
|
|
|
// Stub react-i18next so `t` returns the key with `{{count}}` interpolated. This
|
|
// keeps the assertions on the component's OWN count logic (authoritative vs
|
|
// estimate) rather than on translation, and mirrors the t-mock pattern used by
|
|
// other component tests in the repo.
|
|
vi.mock("react-i18next", () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string, opts?: { count?: number }) =>
|
|
opts && typeof opts.count === "number"
|
|
? key.replace("{{count}}", String(opts.count))
|
|
: key,
|
|
}),
|
|
}));
|
|
|
|
import ReasoningBlock from "./reasoning-block";
|
|
import { estimateTokens } from "@/features/ai-chat/utils/count-stream-tokens.ts";
|
|
|
|
// matchMedia (read by MantineProvider) is stubbed globally in vitest.setup.ts.
|
|
|
|
function renderBlock(props: { text: string; tokens?: number }) {
|
|
return render(
|
|
<MantineProvider>
|
|
<ReasoningBlock {...props} />
|
|
</MantineProvider>,
|
|
);
|
|
}
|
|
|
|
describe("ReasoningBlock", () => {
|
|
it("shows the authoritative count in the header when tokens > 0", () => {
|
|
// Text "thinking…" estimates to ceil(9/4) = 3, but the authoritative 42
|
|
// must win, so the header shows 42 (and NOT the 3-token estimate).
|
|
renderBlock({ text: "thinking…", tokens: 42 });
|
|
expect(screen.getByText("Thinking · 42 tokens")).toBeDefined();
|
|
expect(screen.queryByText("Thinking · 3 tokens")).toBeNull();
|
|
});
|
|
|
|
it("falls back to the text-length estimate when no authoritative tokens", () => {
|
|
const text = "some reasoning prose that streams in";
|
|
const estimate = estimateTokens(text);
|
|
renderBlock({ text });
|
|
expect(estimate).toBeGreaterThan(0);
|
|
expect(screen.getByText(new RegExp(`${estimate} tokens`))).toBeDefined();
|
|
});
|
|
|
|
it("header-only when text is empty but an authoritative count is present", () => {
|
|
renderBlock({ text: "", tokens: 17 });
|
|
expect(screen.getByText(/17 tokens/)).toBeDefined();
|
|
// No disclosure body to expand: the toggle button is disabled.
|
|
const button = screen.getByRole("button");
|
|
expect((button as HTMLButtonElement).disabled).toBe(true);
|
|
});
|
|
|
|
it("renders the reasoning body (markdown or raw-text fallback)", () => {
|
|
renderBlock({ text: "**bold** reasoning", tokens: 5 });
|
|
// The toggle is enabled because there IS body text to expand.
|
|
const button = screen.getByRole("button");
|
|
expect((button as HTMLButtonElement).disabled).toBe(false);
|
|
// The body prose renders (markdown -> sanitized html, or raw-text fallback);
|
|
// either way the text is present in the document.
|
|
expect(screen.getByText(/reasoning/)).toBeDefined();
|
|
});
|
|
});
|