fix(webui): derive temporary chats from session policy
This commit is contained in:
@@ -20,6 +20,7 @@ const updateUrlSpy = vi.fn();
|
||||
const attachSpy = vi.fn();
|
||||
const setSidebarStateSpy = vi.fn();
|
||||
const discardTemporaryChatSpy = vi.fn();
|
||||
const newTemporaryChatSpy = vi.fn<() => Promise<string>>();
|
||||
const sendMessageSpy = vi.fn();
|
||||
const statusHandlers = new Set<(status: ConnectionStatus) => void>();
|
||||
const runStatusHandlers = new Set<(chatId: string, startedAt: number | null) => void>();
|
||||
@@ -238,6 +239,7 @@ vi.mock("@/lib/nanobot-client", async (importOriginal) => {
|
||||
getGoalState = () => undefined;
|
||||
sendMessage = sendMessageSpy;
|
||||
newChat = vi.fn();
|
||||
newTemporaryChat = newTemporaryChatSpy;
|
||||
attach = attachSpy;
|
||||
setSidebarState = setSidebarStateSpy;
|
||||
discardTemporaryChat = discardTemporaryChatSpy;
|
||||
@@ -270,6 +272,10 @@ describe("App layout", () => {
|
||||
attachSpy.mockReset();
|
||||
setSidebarStateSpy.mockReset();
|
||||
discardTemporaryChatSpy.mockReset();
|
||||
let temporaryChatCounter = 0;
|
||||
newTemporaryChatSpy.mockImplementation(async () => (
|
||||
`00000000-0000-4000-8000-${String(++temporaryChatCounter).padStart(12, "0")}`
|
||||
));
|
||||
sendMessageSpy.mockReset();
|
||||
statusHandlers.clear();
|
||||
runStatusHandlers.clear();
|
||||
@@ -425,9 +431,9 @@ describe("App layout", () => {
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
|
||||
const firstHash = window.location.hash;
|
||||
expect(firstHash).toMatch(/^#\/temporary\/temporary-/);
|
||||
expect(firstHash).toMatch(/^#\/temporary\/[0-9a-f-]+$/);
|
||||
expect(screen.queryByRole("button", { name: "Temporary chat" })).not.toBeInTheDocument();
|
||||
expect(createChatSpy).not.toHaveBeenCalled();
|
||||
|
||||
@@ -441,9 +447,9 @@ describe("App layout", () => {
|
||||
target: { value: "second private message" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
|
||||
const secondHash = window.location.hash;
|
||||
expect(secondHash).toMatch(/^#\/temporary\/temporary-/);
|
||||
expect(secondHash).toMatch(/^#\/temporary\/[0-9a-f-]+$/);
|
||||
expect(secondHash).not.toBe(firstHash);
|
||||
expect(screen.queryByRole("button", { name: "Temporary chat" })).not.toBeInTheDocument();
|
||||
expect(discardTemporaryChatSpy).not.toHaveBeenCalled();
|
||||
@@ -481,8 +487,8 @@ describe("App layout", () => {
|
||||
const discardedChatIds = discardTemporaryChatSpy.mock.calls.map(([chatId]) => chatId);
|
||||
expect(new Set(discardedChatIds).size).toBe(2);
|
||||
expect(discardedChatIds).toEqual([
|
||||
expect.stringMatching(/^temporary-/),
|
||||
expect.stringMatching(/^temporary-/),
|
||||
"00000000-0000-4000-8000-000000000001",
|
||||
"00000000-0000-4000-8000-000000000002",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -544,7 +550,7 @@ describe("App layout", () => {
|
||||
target: { value: "start temporary chat" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
|
||||
|
||||
expect(screen.queryByText("Not saved")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Clear temporary chat" })).not.toBeInTheDocument();
|
||||
@@ -560,7 +566,7 @@ describe("App layout", () => {
|
||||
target: { value: "do not lose this" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
|
||||
|
||||
const beforeUnload = new Event("beforeunload", { cancelable: true });
|
||||
act(() => window.dispatchEvent(beforeUnload));
|
||||
@@ -579,7 +585,7 @@ describe("App layout", () => {
|
||||
target: { value: "connection-sensitive message" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/temporary-/));
|
||||
await waitFor(() => expect(window.location.hash).toMatch(/^#\/temporary\/[0-9a-f-]+$/));
|
||||
|
||||
act(() => {
|
||||
statusHandlers.forEach((handler) => handler("reconnecting"));
|
||||
|
||||
@@ -71,19 +71,25 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("NanobotClient", () => {
|
||||
it("keeps temporary chats out of attachment and reconnect state", () => {
|
||||
it("keeps temporary chats out of attachment and reconnect state", async () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
reconnect: false,
|
||||
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
|
||||
});
|
||||
const chatId = "temporary-test";
|
||||
const chatId = "temp-server-id";
|
||||
client.connect();
|
||||
lastSocket().fakeOpen();
|
||||
const creation = client.newTemporaryChat();
|
||||
expect(JSON.parse(lastSocket().sent.at(-1) as string)).toEqual({
|
||||
type: "new_temporary_chat",
|
||||
});
|
||||
lastSocket().fakeMessage({ event: "attached", chat_id: chatId, temporary: true });
|
||||
await expect(creation).resolves.toBe(chatId);
|
||||
lastSocket().sent = [];
|
||||
client.onChat(chatId, vi.fn());
|
||||
client.sendMessage(chatId, "hello", undefined, { turnId: "turn-1" });
|
||||
|
||||
lastSocket().fakeOpen();
|
||||
|
||||
expect(lastSocket().sent.map((raw) => JSON.parse(raw))).toEqual([
|
||||
{
|
||||
type: "message",
|
||||
@@ -101,6 +107,30 @@ describe("NanobotClient", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for the temporary attachment when creating a temporary chat", async () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
reconnect: false,
|
||||
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
|
||||
});
|
||||
client.connect();
|
||||
lastSocket().fakeOpen();
|
||||
|
||||
const creation = client.newTemporaryChat();
|
||||
let resolved = false;
|
||||
void creation.then(() => { resolved = true; });
|
||||
lastSocket().fakeMessage({ event: "attached", chat_id: "ordinary-chat" });
|
||||
await Promise.resolve();
|
||||
expect(resolved).toBe(false);
|
||||
|
||||
lastSocket().fakeMessage({
|
||||
event: "attached",
|
||||
chat_id: "server-temporary-chat",
|
||||
temporary: true,
|
||||
});
|
||||
await expect(creation).resolves.toBe("server-temporary-chat");
|
||||
});
|
||||
|
||||
it("forgets every temporary chat when the socket drops", async () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
@@ -112,20 +142,37 @@ describe("NanobotClient", () => {
|
||||
const secondHandler = vi.fn();
|
||||
client.connect();
|
||||
lastSocket().fakeOpen();
|
||||
client.onChat("temporary-drop-a", firstHandler);
|
||||
client.onChat("temporary-drop-b", secondHandler);
|
||||
const firstCreation = client.newTemporaryChat();
|
||||
lastSocket().fakeMessage({
|
||||
event: "attached",
|
||||
chat_id: "temp-drop-a",
|
||||
temporary: true,
|
||||
});
|
||||
await firstCreation;
|
||||
const secondCreation = client.newTemporaryChat();
|
||||
lastSocket().fakeMessage({
|
||||
event: "attached",
|
||||
chat_id: "temp-drop-b",
|
||||
temporary: true,
|
||||
});
|
||||
await secondCreation;
|
||||
lastSocket().sent = [];
|
||||
client.onChat("temp-drop-a", firstHandler);
|
||||
client.onChat("temp-drop-b", secondHandler);
|
||||
firstHandler.mockClear();
|
||||
secondHandler.mockClear();
|
||||
lastSocket().close();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
lastSocket().fakeOpen();
|
||||
lastSocket().fakeMessage({
|
||||
event: "message",
|
||||
chat_id: "temporary-drop-a",
|
||||
chat_id: "temp-drop-a",
|
||||
text: "stale first chat",
|
||||
});
|
||||
lastSocket().fakeMessage({
|
||||
event: "message",
|
||||
chat_id: "temporary-drop-b",
|
||||
chat_id: "temp-drop-b",
|
||||
text: "stale second chat",
|
||||
});
|
||||
|
||||
|
||||
@@ -2944,7 +2944,7 @@ describe("ThreadComposer", () => {
|
||||
onSend={onSend}
|
||||
onStop={vi.fn()}
|
||||
isStreaming
|
||||
pendingQueueKey="temporary-private"
|
||||
pendingQueueKey={null}
|
||||
placeholder="Type your message..."
|
||||
/>,
|
||||
);
|
||||
@@ -2966,7 +2966,7 @@ describe("ThreadComposer", () => {
|
||||
onSend={onSend}
|
||||
onStop={vi.fn()}
|
||||
isStreaming
|
||||
pendingQueueKey="temporary-private"
|
||||
pendingQueueKey={null}
|
||||
placeholder="Type your message..."
|
||||
/>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user