fabro/apps/fabro-web/app/lib/chats-store.test.tsx
Bryan Helmkamp 75e5f34c0d
Expose lithos request and response shapes through the API, server, CLI, and web
The OpenAPI spec adopts the lithos request, response, content part,
tool, usage, and cost schemas. The completions endpoint returns the
lithos `Response` JSON verbatim and SSE carries lithos `StreamEvent`s
verbatim. The models and providers endpoints serve the fabro-types
catalog views, and the install and model-test flows probe providers
through fabro-llm.

The CLI builds its catalog from the operator overlay, drives `fabro exec`
through the server gateway adapter, and parses reasoning effort with the
shared controls. The web app reads content parts as lithos-tagged
objects. The TypeScript client is regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 17:26:57 -06:00

95 lines
3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { act } from "react-test-renderer";
import { renderHook, setupReactTestEnv } from "./test-utils";
import {
ChatsProvider,
useChatsActions,
useChatsState,
} from "./chats-store";
function useStore() {
return { ...useChatsActions(), state: useChatsState() };
}
function wrapper({ children }: { children: React.ReactNode }) {
return <ChatsProvider>{children}</ChatsProvider>;
}
describe("chats-store reducer", () => {
let teardown: () => void = () => {};
beforeEach(() => {
teardown = setupReactTestEnv();
});
afterEach(() => {
teardown();
});
test("createChatWithFirstMessage seeds title and user message", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage("Help me with React");
});
const chat = result.current.state.chats[id];
expect(chat?.title).toBe("Help me with React");
expect(chat?.pendingResponse).toBe(true);
expect(chat?.seedMessages).toHaveLength(1);
expect(chat?.seedMessages[0]?.role).toBe("user");
expect(chat?.seedMessages[0]?.content[0]).toEqual({
type: "text",
text: "Help me with React",
});
});
test("title is truncated to 40 chars at word boundary", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage(
"Help me draft a launch announcement email for our new analytics dashboard",
);
});
expect(result.current.state.chats[id]?.title).toBe(
"Help me draft a launch announcement…",
);
});
test("consumePendingResponse clears the flag", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage("hi");
});
expect(result.current.state.chats[id]?.pendingResponse).toBe(true);
act(() => {
result.current.consumePendingResponse(id);
});
expect(result.current.state.chats[id]?.pendingResponse).toBe(false);
});
test("advanceScriptIndex increments by one", () => {
const { result } = renderHook(() => useStore(), { wrapper });
let id = "";
act(() => {
id = result.current.createChatWithFirstMessage("hi");
});
expect(result.current.state.chats[id]?.scriptIndex).toBe(0);
act(() => {
result.current.advanceScriptIndex(id);
});
expect(result.current.state.chats[id]?.scriptIndex).toBe(1);
});
test("seed chats appear in order on mount", () => {
const { result } = renderHook(() => useStore(), { wrapper });
expect(result.current.state.order.length).toBeGreaterThanOrEqual(3);
const titles = result.current.state.order.map(
(id) => result.current.state.chats[id]?.title,
);
expect(titles).toContain("Draft a launch email");
expect(titles).toContain("Refactor a React hook");
expect(titles).toContain("Compare Postgres vs SQLite");
});
});