feat(FN-1975): stream chat session updates through SSE

- Emit chat:session:updated from ChatStore when message deletion mutates a session and cover deleteMessage false returns
- Pass ChatStore into SSE setup and forward chat session update events to connected clients
- Update useChat to consume SSE updates in real time with safer EnrichedChatSession typing
- Add core and dashboard tests for chat-store emissions, SSE forwarding, route wiring, and hook behavior
This commit is contained in:
Fusion
2026-04-18 00:44:27 -07:00
committed by gsxdsm
parent 03f4e7fe44
commit 430f7cf39d
12 changed files with 692 additions and 7 deletions

View File

@@ -555,6 +555,31 @@ describe("ChatStore", () => {
expect(store.getMessages(session2.id)).toHaveLength(1);
expect(store.getMessages(session2.id)[0].content).toBe("Session 2");
});
it("updates the parent session's updatedAt timestamp", async () => {
const session = createTestSession(store);
store.addMessage(session.id, { role: "user", content: "Hello" });
const originalUpdatedAt = store.getSession(session.id)!.updatedAt;
await new Promise((r) => setTimeout(r, 5));
const msg = store.addMessage(session.id, { role: "assistant", content: "Reply" });
const afterAddUpdatedAt = store.getSession(session.id)!.updatedAt;
await new Promise((r) => setTimeout(r, 5));
store.deleteMessage(msg.id);
const afterDeleteUpdatedAt = store.getSession(session.id)!.updatedAt;
// The updatedAt should be newer after adding and after deleting
expect(new Date(afterAddUpdatedAt).getTime()).toBeGreaterThan(
new Date(originalUpdatedAt).getTime(),
);
expect(new Date(afterDeleteUpdatedAt).getTime()).toBeGreaterThan(
new Date(afterAddUpdatedAt).getTime(),
);
});
});
});
@@ -627,6 +652,20 @@ describe("ChatStore", () => {
expect(handler).toHaveBeenCalledWith(message.id);
});
it("deleteMessage emits chat:session:updated for the parent session", () => {
const handler = vi.fn();
store.on("chat:session:updated", handler);
const session = createTestSession(store);
const message = store.addMessage(session.id, { role: "user", content: "Hello" });
handler.mockClear();
store.deleteMessage(message.id);
expect(handler).toHaveBeenCalledTimes(1);
expect(handler.mock.calls[0][0].id).toBe(session.id);
});
it("deleteMessage does NOT emit for non-existent message", () => {
const handler = vi.fn();
store.on("chat:message:deleted", handler);
@@ -636,6 +675,15 @@ describe("ChatStore", () => {
expect(handler).not.toHaveBeenCalled();
});
it("deleteMessage does NOT emit chat:session:updated for non-existent message", () => {
const handler = vi.fn();
store.on("chat:session:updated", handler);
store.deleteMessage("msg-nonexistent");
expect(handler).not.toHaveBeenCalled();
});
it("archiveSession emits chat:session:updated", () => {
const handler = vi.fn();
store.on("chat:session:updated", handler);