theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

session.test.ts (16287B)


      1 import { describe, it, expect, beforeEach, vi } from "vitest";
      2 import { useSessionStore } from "./session";
      3 
      4 beforeEach(() => {
      5   useSessionStore.setState({
      6     sessions: [],
      7     activeSessionId: null,
      8     loading: false,
      9     error: null,
     10     _allSessions: [],
     11     _topicFilter: null,
     12   });
     13   vi.restoreAllMocks();
     14 });
     15 
     16 describe("session store", () => {
     17   it("has initial empty state", () => {
     18     const state = useSessionStore.getState();
     19     expect(state.sessions).toEqual([]);
     20     expect(state.activeSessionId).toBeNull();
     21     expect(state.loading).toBe(false);
     22     expect(state.error).toBeNull();
     23   });
     24 
     25   it("listSessions fetches and populates sessions", async () => {
     26     vi.stubGlobal(
     27       "fetch",
     28       vi.fn().mockResolvedValue({
     29         ok: true,
     30         json: async () => ({
     31           sessions: [
     32             { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1 },
     33             { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
     34           ],
     35         }),
     36       }),
     37     );
     38 
     39     await useSessionStore.getState().listSessions();
     40 
     41     const { sessions, loading } = useSessionStore.getState();
     42     expect(loading).toBe(false);
     43     expect(sessions).toHaveLength(2);
     44     expect(sessions[0].id).toBe("s2"); // sorted newest first
     45   });
     46 
     47   it("listSessions sets loading to true during fetch", async () => {
     48     let resolveFetch: (v: unknown) => void;
     49     vi.stubGlobal("fetch", vi.fn().mockReturnValue(
     50       new Promise((resolve) => { resolveFetch = resolve; }),
     51     ));
     52 
     53     const promise = useSessionStore.getState().listSessions();
     54     expect(useSessionStore.getState().loading).toBe(true);
     55 
     56     resolveFetch!({ ok: true, json: async () => ({ sessions: [] }) });
     57     await promise;
     58     expect(useSessionStore.getState().loading).toBe(false);
     59   });
     60 
     61   it("createSession adds a new session", async () => {
     62     vi.stubGlobal(
     63       "fetch",
     64       vi.fn().mockResolvedValue({
     65         ok: true,
     66         json: async () => ({ id: "s3", name: "New Chat" }),
     67       }),
     68     );
     69 
     70     await useSessionStore.getState().createSession("New Chat");
     71 
     72     const { _allSessions, sessions } = useSessionStore.getState();
     73     // Session is in _allSessions but filtered from sessions (0 messages)
     74     expect(_allSessions).toHaveLength(1);
     75     expect(_allSessions[0].id).toBe("s3");
     76     expect(_allSessions[0].name).toBe("New Chat");
     77     expect(sessions).toHaveLength(0);
     78   });
     79 
     80   it("createSession with topic_id sends topic in body", async () => {
     81     const mockFetch = vi.fn().mockResolvedValue({
     82       ok: true,
     83       json: async () => ({ id: "s4", name: "Work Chat" }),
     84     });
     85     vi.stubGlobal("fetch", mockFetch);
     86 
     87     await useSessionStore.getState().createSession("Work Chat", "t1");
     88 
     89     const body = JSON.parse(mockFetch.mock.calls[0][1].body);
     90     expect(body.name).toBe("Work Chat");
     91     expect(body.topic_id).toBe("t1");
     92   });
     93 
     94   it("createSession without name sends empty body", async () => {
     95     const mockFetch = vi.fn().mockResolvedValue({
     96       ok: true,
     97       json: async () => ({ id: "s5" }),
     98     });
     99     vi.stubGlobal("fetch", mockFetch);
    100 
    101     await useSessionStore.getState().createSession();
    102 
    103     const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    104     expect(body).toEqual({});
    105   });
    106 
    107   it("createSession sets error on failure", async () => {
    108     vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("create failed")));
    109 
    110     await useSessionStore.getState().createSession("Test");
    111 
    112     const { error } = useSessionStore.getState();
    113     expect(error).toBe("create failed");
    114   });
    115 
    116   it("createSession sets generic error for non-Error rejection", async () => {
    117     vi.stubGlobal("fetch", vi.fn().mockRejectedValue("string error"));
    118 
    119     await useSessionStore.getState().createSession("Test");
    120     expect(useSessionStore.getState().error).toBe("Failed to create session");
    121   });
    122 
    123   it("updateSession renames a session", async () => {
    124     useSessionStore.setState({
    125       sessions: [{ id: "s1", name: "Old Name", created_at: 1000, message_count: 1 }],
    126     });
    127 
    128     vi.stubGlobal(
    129       "fetch",
    130       vi.fn().mockResolvedValue({
    131         ok: true,
    132         json: async () => ({ id: "s1", name: "New Name" }),
    133       }),
    134     );
    135 
    136     await useSessionStore.getState().updateSession("s1", { name: "New Name" });
    137 
    138     const { sessions } = useSessionStore.getState();
    139     expect(sessions[0].name).toBe("New Name");
    140   });
    141 
    142   it("updateSession sets error on failure", async () => {
    143     useSessionStore.setState({
    144       sessions: [{ id: "s1", name: "Old", created_at: 1000, message_count: 1 }],
    145     });
    146     vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("update failed")));
    147 
    148     await useSessionStore.getState().updateSession("s1", { name: "New" });
    149     expect(useSessionStore.getState().error).toBe("update failed");
    150   });
    151 
    152   it("deleteSession removes from list", async () => {
    153     const allSessions = [
    154       { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1 },
    155       { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
    156     ];
    157     useSessionStore.setState({
    158       sessions: allSessions,
    159       _allSessions: allSessions,
    160     });
    161 
    162     vi.stubGlobal(
    163       "fetch",
    164       vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }),
    165     );
    166 
    167     await useSessionStore.getState().deleteSession("s1");
    168     const { sessions } = useSessionStore.getState();
    169     expect(sessions).toHaveLength(1);
    170     expect(sessions[0].id).toBe("s2");
    171   });
    172 
    173   it("deleteSession clears activeSessionId if active session deleted", async () => {
    174     const allSessions = [
    175       { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1 },
    176       { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
    177     ];
    178     useSessionStore.setState({
    179       sessions: allSessions,
    180       _allSessions: allSessions,
    181       activeSessionId: "s1",
    182     });
    183 
    184     vi.stubGlobal(
    185       "fetch",
    186       vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }),
    187     );
    188 
    189     await useSessionStore.getState().deleteSession("s1");
    190     const { activeSessionId } = useSessionStore.getState();
    191     expect(activeSessionId).toBeNull();
    192   });
    193 
    194   it("deleteSession keeps activeSessionId when deleting different session", async () => {
    195     const allSessions = [
    196       { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1 },
    197       { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
    198     ];
    199     useSessionStore.setState({
    200       sessions: allSessions,
    201       _allSessions: allSessions,
    202       activeSessionId: "s1",
    203     });
    204 
    205     vi.stubGlobal(
    206       "fetch",
    207       vi.fn().mockResolvedValue({ ok: true, json: async () => ({}) }),
    208     );
    209 
    210     await useSessionStore.getState().deleteSession("s2");
    211     expect(useSessionStore.getState().activeSessionId).toBe("s1");
    212   });
    213 
    214   it("deleteSession sets error on failure", async () => {
    215     useSessionStore.setState({
    216       sessions: [{ id: "s1", name: "Chat", created_at: 1000, message_count: 1 }],
    217     });
    218     vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("delete failed")));
    219 
    220     await useSessionStore.getState().deleteSession("s1");
    221     expect(useSessionStore.getState().error).toBe("delete failed");
    222   });
    223 
    224   it("setActiveSession updates activeSessionId", () => {
    225     useSessionStore.getState().setActiveSession("s1");
    226     expect(useSessionStore.getState().activeSessionId).toBe("s1");
    227   });
    228 
    229   it("setActiveSession can set to null", () => {
    230     useSessionStore.setState({ activeSessionId: "s1" });
    231     useSessionStore.getState().setActiveSession(null);
    232     expect(useSessionStore.getState().activeSessionId).toBeNull();
    233   });
    234 
    235   it("filterByTopic filters sessions by topic_id", () => {
    236     const allSessions = [
    237       { id: "s1", name: "Chat 1", topic_id: "t1", created_at: 1000, message_count: 1 },
    238       { id: "s2", name: "Chat 2", topic_id: "t2", created_at: 2000, message_count: 1 },
    239       { id: "s3", name: "Chat 3", topic_id: "t1", created_at: 3000, message_count: 1 },
    240     ];
    241     useSessionStore.setState({
    242       sessions: allSessions,
    243       _allSessions: allSessions,
    244     });
    245 
    246     useSessionStore.getState().filterByTopic("t1");
    247 
    248     const { sessions } = useSessionStore.getState();
    249     expect(sessions).toHaveLength(2);
    250     expect(sessions.every((s) => s.topic_id === "t1")).toBe(true);
    251   });
    252 
    253   it("filterByTopic with no matching topic returns empty", () => {
    254     const allSessions = [
    255       { id: "s1", name: "Chat 1", topic_id: "t1", created_at: 1000, message_count: 1 },
    256     ];
    257     useSessionStore.setState({
    258       sessions: allSessions,
    259       _allSessions: allSessions,
    260     });
    261 
    262     useSessionStore.getState().filterByTopic("nonexistent");
    263 
    264     const { sessions } = useSessionStore.getState();
    265     expect(sessions).toHaveLength(0);
    266   });
    267 
    268   it("filterByTopic with null shows all sessions", () => {
    269     const allSessions = [
    270       { id: "s1", name: "Chat 1", topic_id: "t1", created_at: 1000, message_count: 1 },
    271       { id: "s2", name: "Chat 2", topic_id: "t2", created_at: 2000, message_count: 1 },
    272     ];
    273     useSessionStore.setState({
    274       sessions: allSessions,
    275       _allSessions: allSessions,
    276     });
    277 
    278     useSessionStore.getState().filterByTopic(null);
    279 
    280     const { sessions } = useSessionStore.getState();
    281     expect(sessions).toHaveLength(2);
    282   });
    283 
    284   it("clearFilter restores all sessions", () => {
    285     const allSessions = [
    286       { id: "s1", name: "Chat 1", topic_id: "t1", created_at: 1000, message_count: 1 },
    287       { id: "s2", name: "Chat 2", topic_id: "t2", created_at: 2000, message_count: 1 },
    288     ];
    289     useSessionStore.setState({
    290       sessions: allSessions,
    291       _allSessions: allSessions,
    292       _topicFilter: "t1",
    293     });
    294 
    295     useSessionStore.getState().clearFilter();
    296 
    297     const state = useSessionStore.getState();
    298     expect(state.sessions).toHaveLength(2);
    299     expect(state._topicFilter).toBeNull();
    300   });
    301 
    302   it("listSessions sets error on failure", async () => {
    303     vi.stubGlobal(
    304       "fetch",
    305       vi.fn().mockResolvedValue({
    306         ok: false,
    307         json: async () => ({ error: "Server error" }),
    308       }),
    309     );
    310 
    311     await useSessionStore.getState().listSessions();
    312 
    313     const { error, loading } = useSessionStore.getState();
    314     expect(error).toBeTruthy();
    315     expect(loading).toBe(false);
    316   });
    317 
    318   it("listSessions handles non-Error rejection", async () => {
    319     vi.stubGlobal("fetch", vi.fn().mockRejectedValue("string error"));
    320 
    321     await useSessionStore.getState().listSessions();
    322     expect(useSessionStore.getState().error).toBe("Failed to load sessions");
    323     expect(useSessionStore.getState().loading).toBe(false);
    324   });
    325 
    326   it("createSession clears previous error", async () => {
    327     useSessionStore.setState({ error: "old error" });
    328     vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
    329       ok: true,
    330       json: async () => ({ id: "s1" }),
    331     }));
    332 
    333     await useSessionStore.getState().createSession("Test");
    334     expect(useSessionStore.getState().error).toBeNull();
    335   });
    336 
    337   it("deleteSession clears previous error", async () => {
    338     useSessionStore.setState({
    339       sessions: [{ id: "s1", name: "Chat", created_at: 1000, message_count: 1 }],
    340       error: "old error",
    341     });
    342     vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
    343       ok: true, json: async () => ({}),
    344     }));
    345 
    346     await useSessionStore.getState().deleteSession("s1");
    347     expect(useSessionStore.getState().error).toBeNull();
    348   });
    349 
    350   it("archiveSession calls API and updates state", async () => {
    351     const allSessions = [
    352       { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1 },
    353       { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
    354     ];
    355     useSessionStore.setState({
    356       sessions: allSessions,
    357       _allSessions: allSessions,
    358     });
    359 
    360     vi.stubGlobal(
    361       "fetch",
    362       vi.fn().mockResolvedValue({
    363         ok: true,
    364         json: async () => ({ session_id: "s1", archived: true }),
    365       }),
    366     );
    367 
    368     await useSessionStore.getState().archiveSession("s1");
    369     const { sessions, _allSessions, activeSessionId } = useSessionStore.getState();
    370     expect(sessions).toHaveLength(1);
    371     expect(sessions[0].id).toBe("s2");
    372     expect(_allSessions.find((s) => s.id === "s1")?.archived).toBe(true);
    373     expect(activeSessionId).toBeNull();
    374   });
    375 
    376   it("archiveSession clears activeSessionId if active", async () => {
    377     useSessionStore.setState({
    378       sessions: [{ id: "s1", name: "Chat", created_at: 1000, message_count: 1 }],
    379       _allSessions: [{ id: "s1", name: "Chat", created_at: 1000, message_count: 1 }],
    380       activeSessionId: "s1",
    381     });
    382 
    383     vi.stubGlobal(
    384       "fetch",
    385       vi.fn().mockResolvedValue({
    386         ok: true,
    387         json: async () => ({ session_id: "s1", archived: true }),
    388       }),
    389     );
    390 
    391     await useSessionStore.getState().archiveSession("s1");
    392     expect(useSessionStore.getState().activeSessionId).toBeNull();
    393   });
    394 
    395   it("archiveSession sets error on failure", async () => {
    396     useSessionStore.setState({
    397       sessions: [{ id: "s1", name: "Chat", created_at: 1000, message_count: 1 }],
    398       _allSessions: [{ id: "s1", name: "Chat", created_at: 1000, message_count: 1 }],
    399     });
    400     vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("archive failed")));
    401 
    402     await useSessionStore.getState().archiveSession("s1");
    403     expect(useSessionStore.getState().error).toBe("archive failed");
    404   });
    405 
    406   it("unarchiveSession calls API and updates state", async () => {
    407     useSessionStore.setState({
    408       sessions: [],
    409       _allSessions: [
    410         { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1, archived: true },
    411         { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
    412       ],
    413     });
    414 
    415     vi.stubGlobal(
    416       "fetch",
    417       vi.fn().mockResolvedValue({
    418         ok: true,
    419         status: 204,
    420         json: async () => ({}),
    421       }),
    422     );
    423 
    424     await useSessionStore.getState().unarchiveSession("s1");
    425     const { _allSessions, sessions } = useSessionStore.getState();
    426     expect(_allSessions.find((s) => s.id === "s1")?.archived).toBe(false);
    427     // After unarchive + _applyFilter, s1 should appear in sessions
    428     expect(sessions.find((s) => s.id === "s1")).toBeTruthy();
    429   });
    430 
    431   it("unarchiveSession sets error on failure", async () => {
    432     useSessionStore.setState({
    433       _allSessions: [
    434         { id: "s1", name: "Chat", created_at: 1000, message_count: 1, archived: true },
    435       ],
    436     });
    437     vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("unarchive failed")));
    438 
    439     await useSessionStore.getState().unarchiveSession("s1");
    440     expect(useSessionStore.getState().error).toBe("unarchive failed");
    441   });
    442 
    443   it("_applyFilter excludes archived sessions", () => {
    444     const allSessions = [
    445       { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1, archived: true },
    446       { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1 },
    447     ];
    448     useSessionStore.setState({
    449       _allSessions: allSessions,
    450       _topicFilter: null,
    451     });
    452 
    453     useSessionStore.getState()._applyFilter();
    454     const { sessions } = useSessionStore.getState();
    455     expect(sessions).toHaveLength(1);
    456     expect(sessions[0].id).toBe("s2");
    457   });
    458 
    459   it("listSessions uses archived flag from API response", async () => {
    460     vi.stubGlobal(
    461       "fetch",
    462       vi.fn().mockResolvedValue({
    463         ok: true,
    464         json: async () => ({
    465           sessions: [
    466             { id: "s1", name: "Chat 1", created_at: 1000, message_count: 1, archived: true },
    467             { id: "s2", name: "Chat 2", created_at: 2000, message_count: 1, archived: false },
    468           ],
    469         }),
    470       }),
    471     );
    472 
    473     await useSessionStore.getState().listSessions();
    474     const { sessions, _allSessions } = useSessionStore.getState();
    475     // Archived sessions are filtered out of display list
    476     expect(sessions).toHaveLength(1);
    477     expect(sessions[0].id).toBe("s2");
    478     // But present in _allSessions
    479     expect(_allSessions).toHaveLength(2);
    480     expect(_allSessions.find((s) => s.id === "s1")?.archived).toBe(true);
    481   });
    482 
    483   it("_applyFilter returns all when no filter", () => {
    484     const allSessions = [
    485       { id: "s1", name: "Chat 1", topic_id: "t1", created_at: 1000, message_count: 1 },
    486     ];
    487     useSessionStore.setState({
    488       _allSessions: allSessions,
    489       _topicFilter: null,
    490     });
    491 
    492     useSessionStore.getState()._applyFilter();
    493     expect(useSessionStore.getState().sessions).toEqual(allSessions);
    494   });
    495 });