feat(FN-4733): complete Step 2 — add chat cleanup store method

Fusion-Task-Id: FN-4733
Fusion-Task-Lineage: 38495ced-c7a0-40aa-be22-7f02fe958806
This commit is contained in:
Fusion (runfusion.ai)
2026-05-16 08:36:57 -07:00
committed by gsxdsm
parent 7ec2711aa8
commit 32291be20a
2 changed files with 101 additions and 0 deletions

View File

@@ -1093,6 +1093,65 @@ describe("ChatStore", () => {
});
});
describe("cleanupOldChats", () => {
it("deletes stale sessions/rooms, cascades messages, and emits deleted events", () => {
startFakeClock();
const deletedSessionEvents: string[] = [];
const deletedRoomEvents: string[] = [];
store.on("chat:session:deleted", (id) => deletedSessionEvents.push(id));
store.on("chat:room:deleted", (id) => deletedRoomEvents.push(id));
const staleSession = createTestSession(store, { title: "stale" });
const staleSessionMessage = store.addMessage(staleSession.id, { role: "user", content: "old session msg" });
const staleRoom = store.createRoom({ name: "old room", projectId: "proj-1" });
const staleRoomMessage = store.addRoomMessage(staleRoom.id, { role: "user", content: "old room msg" });
advanceClock(3 * 24 * 60 * 60 * 1000);
const freshSession = createTestSession(store, { title: "fresh" });
const freshSessionMessage = store.addMessage(freshSession.id, { role: "user", content: "new session msg" });
const freshRoom = store.createRoom({ name: "fresh room", projectId: "proj-1" });
const freshRoomMessage = store.addRoomMessage(freshRoom.id, { role: "user", content: "new room msg" });
const staleTimestamp = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
const freshTimestamp = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString();
db.prepare("UPDATE chat_sessions SET updatedAt = ? WHERE id = ?").run(staleTimestamp, staleSession.id);
db.prepare("UPDATE chat_rooms SET updatedAt = ? WHERE id = ?").run(staleTimestamp, staleRoom.id);
db.prepare("UPDATE chat_sessions SET updatedAt = ? WHERE id = ?").run(freshTimestamp, freshSession.id);
db.prepare("UPDATE chat_rooms SET updatedAt = ? WHERE id = ?").run(freshTimestamp, freshRoom.id);
const result = store.cleanupOldChats(7 * 24 * 60 * 60 * 1000);
expect(result).toEqual({ sessionsDeleted: 1, roomsDeleted: 1 });
expect(store.getSession(staleSession.id)).toBeUndefined();
expect(store.getRoom(staleRoom.id)).toBeUndefined();
expect(store.getSession(freshSession.id)).toBeDefined();
expect(store.getRoom(freshRoom.id)).toBeDefined();
expect(store.getMessage(staleSessionMessage.id)).toBeUndefined();
expect(store.getRoomMessage(staleRoomMessage.id)).toBeUndefined();
expect(store.getMessage(freshSessionMessage.id)).toBeDefined();
expect(store.getRoomMessage(freshRoomMessage.id)).toBeDefined();
expect(deletedSessionEvents).toContain(staleSession.id);
expect(deletedRoomEvents).toContain(staleRoom.id);
expect(deletedSessionEvents).not.toContain(freshSession.id);
expect(deletedRoomEvents).not.toContain(freshRoom.id);
});
it("returns no-op for non-positive maxAgeMs", () => {
const session = createTestSession(store);
const room = store.createRoom({ name: "noop-room", projectId: "proj-1" });
expect(store.cleanupOldChats(0)).toEqual({ sessionsDeleted: 0, roomsDeleted: 0 });
expect(store.cleanupOldChats(-10)).toEqual({ sessionsDeleted: 0, roomsDeleted: 0 });
expect(store.cleanupOldChats(Number.NaN)).toEqual({ sessionsDeleted: 0, roomsDeleted: 0 });
expect(store.getSession(session.id)).toBeDefined();
expect(store.getRoom(room.id)).toBeDefined();
});
});
describe("Test isolation", () => {
it("starts with no leaked sessions from prior tests", () => {
expect(store.listSessions()).toEqual([]);

View File

@@ -823,6 +823,48 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
return true;
}
cleanupOldChats(maxAgeMs: number): { sessionsDeleted: number; roomsDeleted: number } {
if (!Number.isFinite(maxAgeMs) || maxAgeMs <= 0) {
return { sessionsDeleted: 0, roomsDeleted: 0 };
}
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
const result = this.db.transaction(() => {
const staleSessionRows = this.db.prepare("SELECT id FROM chat_sessions WHERE updatedAt < ?").all(cutoff) as Array<{ id: string }>;
const staleRoomRows = this.db.prepare("SELECT id FROM chat_rooms WHERE updatedAt < ?").all(cutoff) as Array<{ id: string }>;
if (staleSessionRows.length > 0) {
this.db.prepare("DELETE FROM chat_sessions WHERE updatedAt < ?").run(cutoff);
}
if (staleRoomRows.length > 0) {
this.db.prepare("DELETE FROM chat_rooms WHERE updatedAt < ?").run(cutoff);
}
return {
staleSessionIds: staleSessionRows.map((row) => row.id),
staleRoomIds: staleRoomRows.map((row) => row.id),
};
});
if (result.staleSessionIds.length === 0 && result.staleRoomIds.length === 0) {
return { sessionsDeleted: 0, roomsDeleted: 0 };
}
this.db.bumpLastModified();
for (const sessionId of result.staleSessionIds) {
this.emit("chat:session:deleted", sessionId);
}
for (const roomId of result.staleRoomIds) {
this.emit("chat:room:deleted", roomId);
}
return {
sessionsDeleted: result.staleSessionIds.length,
roomsDeleted: result.staleRoomIds.length,
};
}
addRoomMember(roomId: string, agentId: string, role: RoomMemberRole = "member"): ChatRoomMember {
const now = new Date().toISOString();
const result = this.db.prepare(`