diff --git a/lib/chats/getChatsHandler.ts b/lib/chats/getChatsHandler.ts index 0f7b254a..1a0c31cd 100644 --- a/lib/chats/getChatsHandler.ts +++ b/lib/chats/getChatsHandler.ts @@ -11,7 +11,9 @@ import { selectChatsWithSessions } from "@/lib/supabase/chats/selectChatsWithSes * Returns chats joined with their owning session so the response carries the * `sessionId`, owning `accountId`, and `artistId` per row, enabling clients * to render canonical `/sessions/{sid}/chats/{cid}` URLs and filter by - * artist context. + * artist context. Chats whose owning session is archived + * (`sessions.status === "archived"`) are excluded — archive is the + * soft-delete path for sessions. * * Scope: * - Personal/org key: chats belonging to the caller's account. diff --git a/lib/mcp/tools/chats/registerGetChatsTool.ts b/lib/mcp/tools/chats/registerGetChatsTool.ts index d667964b..0230d5c8 100644 --- a/lib/mcp/tools/chats/registerGetChatsTool.ts +++ b/lib/mcp/tools/chats/registerGetChatsTool.ts @@ -30,6 +30,7 @@ export type GetChatsArgs = z.infer; * GET /api/chats: personal/org → caller's account; Recoup admin → all; * or a specific account when `account_id` is supplied and the caller * can access it. `artist_account_id` further scopes by artist context. + * Chats whose owning session is archived are excluded. * * Admin status is derived from `account_organization_ids` membership so * that Bearer-authed callers get the same admin scope as x-api-key diff --git a/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts b/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts index 2d9de9af..0c88d411 100644 --- a/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts +++ b/lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts @@ -3,6 +3,7 @@ import { selectChatsWithSessions } from "@/lib/supabase/chats/selectChatsWithSes const fromMock = vi.fn(); const selectMock = vi.fn(); +const neqMock = vi.fn(); const inMock = vi.fn(); const eqMock = vi.fn(); const orderMock = vi.fn(); @@ -15,15 +16,17 @@ vi.mock("@/lib/supabase/serverClient", () => ({ beforeEach(() => { vi.clearAllMocks(); - // Default chain: from().select().in().eq().order() -> resolves to { data, error } + // Default chain: from().select().neq().in().eq().order() -> resolves to { data, error } const builder = { select: selectMock, + neq: neqMock, in: inMock, eq: eqMock, order: orderMock, }; fromMock.mockReturnValue(builder); selectMock.mockReturnValue(builder); + neqMock.mockReturnValue(builder); inMock.mockReturnValue(builder); eqMock.mockReturnValue(builder); orderMock.mockResolvedValue({ data: [], error: null }); @@ -51,13 +54,21 @@ describe("selectChatsWithSessions", () => { expect(String(selectArg)).toContain("session:sessions!inner"); expect(String(selectArg)).toContain("account_id"); expect(String(selectArg)).toContain("artist_id"); + expect(String(selectArg)).toContain("status"); + expect(neqMock).toHaveBeenCalledWith("session.status", "archived"); expect(inMock).toHaveBeenCalledWith("session.account_id", ["acc-1", "acc-2"]); expect(eqMock).not.toHaveBeenCalled(); expect(orderMock).toHaveBeenCalledWith("updated_at", { ascending: false }); expect(result).toEqual(rows); }); + it("always excludes archived sessions, even with no other filters", async () => { + await selectChatsWithSessions({}); + + expect(neqMock).toHaveBeenCalledWith("session.status", "archived"); + }); + it("composes accountIds + artistAccountId — both filters applied", async () => { orderMock.mockResolvedValueOnce({ data: [], error: null }); diff --git a/lib/supabase/chats/selectChatsWithSessions.ts b/lib/supabase/chats/selectChatsWithSessions.ts index 932341ff..ee3d204d 100644 --- a/lib/supabase/chats/selectChatsWithSessions.ts +++ b/lib/supabase/chats/selectChatsWithSessions.ts @@ -18,13 +18,16 @@ export interface SelectChatsWithSessionsParams { const SELECT = ` *, - session:sessions!inner ( id, account_id, artist_id ) + session:sessions!inner ( id, account_id, artist_id, status ) ` as const; /** * Reads chats joined to their owning session, optionally scoped to a set of * account IDs through `sessions.account_id` and/or an artist context through - * `sessions.artist_id`. Ordered by `chats.updated_at` descending so newest + * `sessions.artist_id`. Chats whose session is archived + * (`sessions.status === "archived"`) are excluded — archive is the + * soft-delete path, and archived sessions should not surface in chat + * listings. Results are ordered by `chats.updated_at` descending so newest * activity surfaces first. * * Returns `null` when Supabase reports an error so callers can distinguish a @@ -39,7 +42,7 @@ export async function selectChatsWithSessions(params: SelectChatsWithSessionsPar return []; } - let query = supabase.from("chats").select(SELECT); + let query = supabase.from("chats").select(SELECT).neq("session.status", "archived"); if (accountIds !== undefined) { query = query.in("session.account_id", accountIds); }