From 0f2178fd4078d1dd39a8912e08822cf801c59a56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 10:51:10 +0000 Subject: [PATCH] Restore a saved case when deleting it fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit History removed the case locally before the server delete, and a failed delete never threw, so the toast said it was deleted while the row stayed. The delete now waits for the server and puts the case back when that fails. Co-authored-by: Gáborjáni-Szabó Ádám --- src/lib/stores/session-store.ts | 17 +++++- src/test/stores/delete-saved-case.test.ts | 69 +++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 src/test/stores/delete-saved-case.test.ts diff --git a/src/lib/stores/session-store.ts b/src/lib/stores/session-store.ts index dcdea8e78..0865b1b03 100644 --- a/src/lib/stores/session-store.ts +++ b/src/lib/stores/session-store.ts @@ -839,9 +839,20 @@ export const useSessionStore = create()( set((state) => ({ savedCases: [savedItem, ...state.savedCases] })); }, deleteSavedCase: async (id) => { - set((state) => ({ savedCases: state.savedCases.filter((c) => c.id !== id) })); - const userId = await getUserId(); - await applyUserScopedIdFilter(supabase.from('cases').delete(), userId, id); + const previous = get().savedCases; + set({ savedCases: previous.filter((item) => item.id !== id) }); + try { + const userId = await getUserId(); + // A missing session must not report success for a row the server still has. + if (!userId) throw new Error('[SessionStore] Could not confirm the signed-in user to delete a case'); + throwOnWriteError( + await applyUserScopedIdFilter(supabase.from('cases').delete(), userId, id), + 'delete case', + ); + } catch (error) { + set({ savedCases: previous }); + throw error; + } }, loadSavedCase: (id) => { const item = get().savedCases.find((c) => c.id === id); diff --git a/src/test/stores/delete-saved-case.test.ts b/src/test/stores/delete-saved-case.test.ts new file mode 100644 index 000000000..d2cb1f5a4 --- /dev/null +++ b/src/test/stores/delete-saved-case.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { deleteResult, getUser } = vi.hoisted(() => ({ + deleteResult: { error: null as { message: string } | null }, + getUser: vi.fn(async () => ({ data: { user: { id: 'user-1' } as { id: string } | null } })), +})); + +vi.mock('@/lib/supabase/client', () => ({ + supabase: { + auth: { getUser }, + from: () => ({ + delete: () => { + const query = { + eq: () => query, + then: (resolve: (value: { error: { message: string } | null }) => void, reject?: (reason: unknown) => void) => ( + Promise.resolve({ error: deleteResult.error }).then(resolve, reject) + ), + }; + return query; + }, + }), + }, +})); + +import type { GenerateOutput } from '@/lib/ai/schemas/generate'; +import { useSessionStore } from '@/lib/stores/session-store'; + +const savedCase = { + id: 'case-1', + title: 'Saved motion', + createdAt: 1, + debateId: 'debate-1', + data: { + caseStructure: { thesis: 'Thesis', framework: '', keyDefinitions: [] as string[], burdenOfProof: '' }, + contentions: [] as GenerateOutput['contentions'], + predictedOpponentCase: { + likelyArguments: [] as string[], + strategicWeaknesses: [] as string[], + keyClashes: [] as string[], + }, + counters: [] as GenerateOutput['counters'], + metadata: { side: 'pro' as const, format: 'custom', generatedAt: '2026-01-01T00:00:00Z' }, + } satisfies GenerateOutput, +}; + +describe('deleting a saved case', () => { + beforeEach(() => { + deleteResult.error = null; + getUser.mockResolvedValue({ data: { user: { id: 'user-1' } } }); + useSessionStore.setState({ savedCases: [savedCase] }); + }); + + it('removes the case after the server delete succeeds', async () => { + await useSessionStore.getState().deleteSavedCase('case-1'); + expect(useSessionStore.getState().savedCases).toEqual([]); + }); + + it('puts the case back and reports the failure when the server delete fails', async () => { + deleteResult.error = { message: 'permission denied' }; + await expect(useSessionStore.getState().deleteSavedCase('case-1')).rejects.toThrow(/delete case/); + expect(useSessionStore.getState().savedCases).toEqual([savedCase]); + }); + + it('puts the case back when the session cannot be confirmed', async () => { + getUser.mockResolvedValueOnce({ data: { user: null } }); + await expect(useSessionStore.getState().deleteSavedCase('case-1')).rejects.toThrow(/signed-in user/); + expect(useSessionStore.getState().savedCases).toEqual([savedCase]); + }); +});