- Switch conversation agent to xiaomi/mimo-v2.5 (multimodal: text + image) - Render native reasoning parts as live 'Thinking trace' (streaming open, collapsed after completion); inline <think> extraction for streaming models - Image attachments: picker (up to 4, 10MB each), base64 to Flue AgentPromptImage, authenticated blob-URL replay for historical images - Mobile keyboard viewport fix: visual-viewport hook, fixed shell, interactive-widget=resizes-content, header pinned, composer follows keyboard - Conversation to Signal to proposed Work: Convex persistence, Effect validation in @code/work-os, Work cards with exact source provenance - Streamdown markdown + Mermaid chart rendering in chat messages - Flue tool turns hidden, reasoning-containing turns remain visible - Frontend regression tests: keyboard viewport, responsive shell, attachment overflow, authenticated images, reasoning traces, transforms - .env.example updated to xiaomi/mimo-v2.5 config
80 lines
2.1 KiB
TypeScript
80 lines
2.1 KiB
TypeScript
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
import { useChatAgent } from "./use-chat-agent";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
agent: {
|
|
error: undefined,
|
|
historyReady: true,
|
|
messages: [],
|
|
sendMessage: vi.fn(() => Promise.resolve()),
|
|
status: "idle" as const,
|
|
},
|
|
organization: {} as { error?: Error; organizationId?: string },
|
|
useFlueAgent: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@flue/react", () => ({
|
|
useFlueAgent: mocks.useFlueAgent,
|
|
}));
|
|
vi.mock("@/hooks/use-personal-organization", () => ({
|
|
usePersonalOrganization: () => mocks.organization,
|
|
}));
|
|
|
|
describe("useChatAgent", () => {
|
|
beforeEach(() => {
|
|
mocks.organization = {};
|
|
mocks.agent.sendMessage.mockClear();
|
|
mocks.useFlueAgent.mockReset();
|
|
mocks.useFlueAgent.mockReturnValue(mocks.agent);
|
|
});
|
|
|
|
test("does not use the agent before organization bootstrap completes", async () => {
|
|
const chat = useChatAgent();
|
|
|
|
expect(mocks.useFlueAgent).toHaveBeenCalledWith({
|
|
id: undefined,
|
|
live: "sse",
|
|
name: "zopu",
|
|
});
|
|
expect(chat.status).toBe("connecting");
|
|
await expect(chat.sendMessage("too early")).rejects.toThrow(
|
|
"Personal organization is still being prepared"
|
|
);
|
|
expect(mocks.agent.sendMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("uses the ensured organization id for the agent session", async () => {
|
|
mocks.organization = { organizationId: "org-a" };
|
|
|
|
const chat = useChatAgent();
|
|
expect(mocks.useFlueAgent).toHaveBeenCalledWith({
|
|
id: "org-a",
|
|
live: "sse",
|
|
name: "zopu",
|
|
});
|
|
|
|
await chat.sendMessage("ready");
|
|
expect(mocks.agent.sendMessage).toHaveBeenCalledWith("ready", undefined);
|
|
});
|
|
|
|
test("forwards image attachments through to the Flue agent", async () => {
|
|
mocks.organization = { organizationId: "org-a" };
|
|
const images = [
|
|
{
|
|
data: "abc",
|
|
filename: "a.png",
|
|
mimeType: "image/png",
|
|
type: "image" as const,
|
|
},
|
|
];
|
|
|
|
const chat = useChatAgent();
|
|
await chat.sendMessage("describe this", { images });
|
|
|
|
expect(mocks.agent.sendMessage).toHaveBeenCalledWith("describe this", {
|
|
images,
|
|
});
|
|
});
|
|
});
|