Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/chats/getChatsHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions lib/mcp/tools/chats/registerGetChatsTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type GetChatsArgs = z.infer<typeof getChatsSchema>;
* 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
Expand Down
13 changes: 12 additions & 1 deletion lib/supabase/chats/__tests__/selectChatsWithSessions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 });
Expand Down Expand Up @@ -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 });

Expand Down
9 changes: 6 additions & 3 deletions lib/supabase/chats/selectChatsWithSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
Expand Down
Loading