feat(webui): add initial webui with websocket chat flow
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
import type { ReactNode } from "react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { useSessions } from "@/hooks/useSessions";
|
||||
import * as api from "@/lib/api";
|
||||
import { ClientProvider } from "@/providers/ClientProvider";
|
||||
|
||||
vi.mock("@/lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/lib/api")>();
|
||||
return {
|
||||
...actual,
|
||||
listSessions: vi.fn(),
|
||||
deleteSession: vi.fn(),
|
||||
fetchSessionMessages: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
function fakeClient() {
|
||||
return {
|
||||
status: "open" as const,
|
||||
defaultChatId: null as string | null,
|
||||
onStatus: () => () => {},
|
||||
onChat: () => () => {},
|
||||
sendMessage: vi.fn(),
|
||||
newChat: vi.fn(),
|
||||
attach: vi.fn(),
|
||||
connect: vi.fn(),
|
||||
close: vi.fn(),
|
||||
updateUrl: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
function wrap(client: ReturnType<typeof fakeClient>) {
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ClientProvider
|
||||
client={client as unknown as import("@/lib/nanobot-client").NanobotClient}
|
||||
token="tok"
|
||||
>
|
||||
{children}
|
||||
</ClientProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe("useSessions", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(api.listSessions).mockReset();
|
||||
vi.mocked(api.deleteSession).mockReset();
|
||||
vi.mocked(api.fetchSessionMessages).mockReset();
|
||||
});
|
||||
|
||||
it("removes a session from the local list after delete succeeds", async () => {
|
||||
vi.mocked(api.listSessions).mockResolvedValue([
|
||||
{
|
||||
key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chatId: "chat-a",
|
||||
createdAt: "2026-04-16T10:00:00Z",
|
||||
updatedAt: "2026-04-16T10:00:00Z",
|
||||
preview: "Alpha",
|
||||
},
|
||||
{
|
||||
key: "websocket:chat-b",
|
||||
channel: "websocket",
|
||||
chatId: "chat-b",
|
||||
createdAt: "2026-04-16T11:00:00Z",
|
||||
updatedAt: "2026-04-16T11:00:00Z",
|
||||
preview: "Beta",
|
||||
},
|
||||
]);
|
||||
vi.mocked(api.deleteSession).mockResolvedValue(true);
|
||||
|
||||
const { result } = renderHook(() => useSessions(), {
|
||||
wrapper: wrap(fakeClient()),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.sessions).toHaveLength(2));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.deleteChat("websocket:chat-a");
|
||||
});
|
||||
|
||||
expect(api.deleteSession).toHaveBeenCalledWith("tok", "websocket:chat-a");
|
||||
expect(result.current.sessions.map((s) => s.key)).toEqual(["websocket:chat-b"]);
|
||||
});
|
||||
|
||||
it("keeps the session in the list when delete fails", async () => {
|
||||
vi.mocked(api.listSessions).mockResolvedValue([
|
||||
{
|
||||
key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chatId: "chat-a",
|
||||
createdAt: "2026-04-16T10:00:00Z",
|
||||
updatedAt: "2026-04-16T10:00:00Z",
|
||||
preview: "Alpha",
|
||||
},
|
||||
]);
|
||||
vi.mocked(api.deleteSession).mockRejectedValue(new Error("boom"));
|
||||
|
||||
const { result } = renderHook(() => useSessions(), {
|
||||
wrapper: wrap(fakeClient()),
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.sessions).toHaveLength(1));
|
||||
|
||||
await expect(
|
||||
act(async () => {
|
||||
await result.current.deleteChat("websocket:chat-a");
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
|
||||
expect(result.current.sessions.map((s) => s.key)).toEqual(["websocket:chat-a"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user