272 lines
9.3 KiB
TypeScript
272 lines
9.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
import { useSessionContextWindow } from "@/hooks/domains/session/use-session-context-window";
|
|
import {
|
|
ClarificationEscapeGuardProvider,
|
|
type ClarificationEscapeGuardEntry,
|
|
type ClarificationEscapeGuardRegistry,
|
|
} from "@/hooks/use-clarification-escape-guard";
|
|
import { isContextWindowReliable, TokenUsageDisplay } from "./token-usage-display";
|
|
|
|
const TOOLTIP_ROOT_TESTID = "tooltip-root";
|
|
|
|
vi.mock("@/hooks/domains/session/use-session-context-window", () => ({
|
|
useSessionContextWindow: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@kandev/ui/tooltip", () => ({
|
|
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
Tooltip: ({ children, open }: { children: React.ReactNode; open?: boolean }) => (
|
|
<div data-testid={TOOLTIP_ROOT_TESTID} data-open={open}>
|
|
{children}
|
|
</div>
|
|
),
|
|
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function withEscapeGuardRegistry(node: React.ReactNode) {
|
|
const holder: { entry: ClarificationEscapeGuardEntry } = { entry: null };
|
|
const registry: ClarificationEscapeGuardRegistry = {
|
|
register: (_id, predicate) => {
|
|
holder.entry = { test: predicate };
|
|
},
|
|
unregister: () => {
|
|
holder.entry = null;
|
|
},
|
|
};
|
|
render(
|
|
<ClarificationEscapeGuardProvider value={registry}>{node}</ClarificationEscapeGuardProvider>,
|
|
);
|
|
return holder;
|
|
}
|
|
|
|
describe("isContextWindowReliable", () => {
|
|
it("accepts normal usage under the window", () => {
|
|
expect(isContextWindowReliable(200_000, 56_047)).toBe(true);
|
|
});
|
|
|
|
it("accepts exactly-full context (100%)", () => {
|
|
expect(isContextWindowReliable(200_000, 200_000)).toBe(true);
|
|
});
|
|
|
|
it("rejects impossible usage (used > size) — the wrong-window bug", () => {
|
|
expect(isContextWindowReliable(200_000, 233_900)).toBe(false);
|
|
});
|
|
|
|
it("rejects a zero/absent window size", () => {
|
|
expect(isContextWindowReliable(0, 0)).toBe(false);
|
|
});
|
|
|
|
it("accepts a correct large window", () => {
|
|
expect(isContextWindowReliable(1_000_000, 233_900)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("TokenUsageDisplay", () => {
|
|
it("transitions only the context ring arc", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 56_047,
|
|
remaining: 143_953,
|
|
efficiency: 28,
|
|
compactionCount: 0,
|
|
});
|
|
|
|
const { container } = render(<TokenUsageDisplay sessionId="sess-1" />);
|
|
const circles = container.querySelectorAll("circle");
|
|
const usageCircle = circles[1];
|
|
|
|
expect(usageCircle).toBeDefined();
|
|
expect(usageCircle.getAttribute("class")).toContain("transition-[stroke-dashoffset]");
|
|
expect(usageCircle.getAttribute("class")).not.toContain("transition-all");
|
|
});
|
|
|
|
it("renders nothing when used exceeds size (wrong-window bug)", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 233_900,
|
|
remaining: -33_900,
|
|
efficiency: 117,
|
|
compactionCount: 0,
|
|
});
|
|
|
|
const { container } = render(<TokenUsageDisplay sessionId="sess-1" />);
|
|
|
|
expect(container.firstChild).toBeNull();
|
|
});
|
|
|
|
it("opens the tooltip when the context ring is tapped", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 56_047,
|
|
remaining: 143_953,
|
|
efficiency: 28,
|
|
compactionCount: 0,
|
|
});
|
|
|
|
const { getByRole, getByTestId } = render(<TokenUsageDisplay sessionId="sess-1" />);
|
|
|
|
fireEvent.click(getByRole("button", { name: "Context window: 28% used" }));
|
|
|
|
expect(getByTestId(TOOLTIP_ROOT_TESTID).getAttribute("data-open")).toBe("true");
|
|
const compactionRow = getByTestId("context-window-compactions-row");
|
|
expect(compactionRow.textContent).toContain("Compactions");
|
|
expect(compactionRow.textContent).toContain("0");
|
|
});
|
|
|
|
it("renders an unmeasured pending state when usage is zero", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 0,
|
|
remaining: 200_000,
|
|
efficiency: 0,
|
|
compactionCount: 2,
|
|
source: "acp",
|
|
});
|
|
|
|
const { getByRole, getByTestId } = render(<TokenUsageDisplay sessionId="sess-1" />);
|
|
|
|
const trigger = getByRole("button", { name: "Context window: usage not measured" });
|
|
fireEvent.click(trigger);
|
|
|
|
expect(getByTestId(TOOLTIP_ROOT_TESTID).getAttribute("data-open")).toBe("true");
|
|
const usage = getByTestId("context-window-usage");
|
|
expect(usage.textContent).toContain("N/A%");
|
|
expect(usage.textContent).toContain("N/A of 200.0K tokens");
|
|
expect(usage.textContent).toContain("Usage data appears after the first completed turn.");
|
|
expect(usage.textContent).not.toContain("0%");
|
|
expect(usage.textContent).not.toContain("0 of");
|
|
expect(usage.textContent).toContain("ACP");
|
|
expect(usage.textContent).toContain("Compactions");
|
|
});
|
|
|
|
it("closes a tapped tooltip when Escape is pressed", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 56_047,
|
|
remaining: 143_953,
|
|
efficiency: 28,
|
|
compactionCount: 0,
|
|
});
|
|
|
|
const { getByRole, getByTestId } = render(<TokenUsageDisplay sessionId="sess-1" />);
|
|
|
|
fireEvent.click(getByRole("button", { name: "Context window: 28% used" }));
|
|
fireEvent.keyDown(document, { key: "Escape" });
|
|
|
|
expect(getByTestId(TOOLTIP_ROOT_TESTID).getAttribute("data-open")).toBe("false");
|
|
});
|
|
|
|
it("claims Escape at dialog capture time while the tooltip is pinned", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 56_047,
|
|
remaining: 143_953,
|
|
efficiency: 28,
|
|
compactionCount: 0,
|
|
});
|
|
|
|
const holder = withEscapeGuardRegistry(<TokenUsageDisplay sessionId="sess-1" />);
|
|
const trigger = screen.getByRole("button", { name: "Context window: 28% used" });
|
|
|
|
fireEvent.click(trigger);
|
|
|
|
expect(holder.entry?.test(new KeyboardEvent("keydown", { key: "Escape" }))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("TokenUsageDisplay context source", () => {
|
|
it("labels agent-reported ACP data", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 56_047,
|
|
remaining: 143_953,
|
|
efficiency: 28,
|
|
compactionCount: 2,
|
|
source: "acp",
|
|
});
|
|
|
|
const { container, getAllByTestId, getByText, getByLabelText } = render(
|
|
<TokenUsageDisplay sessionId="sess-1" />,
|
|
);
|
|
|
|
const tokenCount = getByText("56.0K of 200.0K tokens");
|
|
const source = getByText("ACP");
|
|
|
|
expect(container.querySelector(".cursor-help")).not.toBeNull();
|
|
expect(container.querySelector("svg")).not.toBeNull();
|
|
expect(tokenCount.closest('[data-testid="context-window-token-row"]')).toBe(
|
|
source.closest('[data-testid="context-window-token-row"]'),
|
|
);
|
|
expect(getAllByTestId(TOOLTIP_ROOT_TESTID)).toHaveLength(1);
|
|
expect(getByLabelText("About context window source")).toBeDefined();
|
|
expect(getByText(/ACP is the active session's effective window/i)).toBeDefined();
|
|
});
|
|
|
|
it("shows the inferred compaction count and its accuracy disclosure", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 200_000,
|
|
used: 56_047,
|
|
remaining: 143_953,
|
|
efficiency: 28,
|
|
compactionCount: 2,
|
|
source: "acp",
|
|
});
|
|
|
|
const { container, getByLabelText, getByText } = render(
|
|
<TokenUsageDisplay sessionId="sess-1" />,
|
|
);
|
|
|
|
expect(getByText("Compactions")).toBeDefined();
|
|
expect(getByText("2", { selector: "span" })).toBeDefined();
|
|
expect(getByLabelText("About inferred compaction count")).toBeDefined();
|
|
expect(container.textContent).toContain(
|
|
"Kandev infers this count from observed context usage drops",
|
|
);
|
|
|
|
const helpButton = getByLabelText("About inferred compaction count");
|
|
const helpId = helpButton.getAttribute("aria-describedby");
|
|
if (!helpId) throw new Error("Expected compaction help to be described");
|
|
const help = document.getElementById(helpId);
|
|
if (!help) throw new Error("Expected compaction help element");
|
|
expect(help.className).toContain("opacity-0");
|
|
|
|
fireEvent.click(helpButton);
|
|
expect(help.className).toContain("opacity-100");
|
|
fireEvent.click(helpButton);
|
|
expect(help.className).toContain("opacity-0");
|
|
});
|
|
|
|
it("labels model API fallback data", () => {
|
|
vi.mocked(useSessionContextWindow).mockReturnValue({
|
|
size: 128_000,
|
|
used: 64_000,
|
|
remaining: 64_000,
|
|
efficiency: 50,
|
|
compactionCount: 0,
|
|
source: "api",
|
|
});
|
|
|
|
const { getByText, getByLabelText } = render(<TokenUsageDisplay sessionId="sess-1" />);
|
|
|
|
expect(getByText("API")).toBeDefined();
|
|
const helpButton = getByLabelText("About context window source");
|
|
const helpId = helpButton.getAttribute("aria-describedby");
|
|
if (!helpId) throw new Error("Expected source help to be described");
|
|
const help = document.getElementById(helpId);
|
|
if (!help) throw new Error("Expected source help element");
|
|
|
|
fireEvent.click(helpButton);
|
|
expect(help.className).toContain("opacity-100");
|
|
fireEvent.click(helpButton);
|
|
expect(help.className).toContain("opacity-0");
|
|
expect(getByText(/model's advertised maximum from the catalogue/i)).toBeDefined();
|
|
});
|
|
});
|