Files
supabase/apps/studio/data/content/notebooks/notebook-cache.test.ts
Charis eabb87564b fix(studio): resolve dirty notebook save conflicts (#49540)
## Summary
- require an explicit choice before saving a notebook that diverged
while dirty
- let users save over assistant changes or discard their local edits,
with deleted notebooks recreating safely
- keep dismissals side-effect free and close deleted notebook tabs when
edits are discarded

## Testing
- pnpm --filter studio exec vitest run
components/interfaces/Explorer/__tests__/ExplorerNotebookTab.assistant-cache-invalidation.test.tsx
data/content/notebooks/notebook-cache.test.ts --reporter=dot
- pnpm --filter studio typecheck

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added conflict handling when server-side notebook changes overlap with
local edits.
- Users can overwrite, recreate, discard, or dismiss changes through a
confirmation dialog.
- Deleted notebooks can be recreated when saved, while discarded deleted
notebooks are automatically removed from open tabs.
  - Conflict dialogs remain open while an action is in progress.

- **Bug Fixes**
- Improved notebook cache cleanup to remove stale and unsaved notebook
data reliably.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 17:12:10 +08:00

87 lines
2.7 KiB
TypeScript

import { QueryClient } from '@tanstack/react-query'
import { afterEach, describe, expect, it } from 'vitest'
import { evictNotebookFromCaches } from './notebook-cache'
import { contentKeys } from '@/data/content/keys'
import { notebooksState } from '@/state/notebooks/notebooks-state'
import type { Notebook } from '@/state/notebooks/types'
const PROJECT_REF = 'default'
const NOTEBOOK_ID = 'notebook-cache-test'
const NOTEBOOK: Notebook = {
id: NOTEBOOK_ID,
type: 'notebook',
name: 'Test notebook',
visibility: 'project',
favorite: false,
owner_id: 1,
project_id: 1,
content: { schema_version: 1, cells: [] },
}
const seedNotebook = (status: 'new' | 'saved') => {
delete notebooksState.notebooks[NOTEBOOK_ID]
if (status === 'new') {
notebooksState.addNotebook({ projectRef: PROJECT_REF, notebook: NOTEBOOK })
} else {
notebooksState.setNotebook({ projectRef: PROJECT_REF, notebook: NOTEBOOK })
}
}
const seedQueryData = (queryClient: QueryClient) =>
queryClient.setQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK_ID), { id: NOTEBOOK_ID })
afterEach(() => {
delete notebooksState.notebooks[NOTEBOOK_ID]
notebooksState.needsSaving.clear()
})
describe('evictNotebookFromCaches', () => {
it('removes a saved notebook from the store and drops its cache entry', async () => {
seedNotebook('saved')
const queryClient = new QueryClient()
seedQueryData(queryClient)
const evicted = await evictNotebookFromCaches({
queryClient,
projectRef: PROJECT_REF,
id: NOTEBOOK_ID,
})
expect(evicted).toBe(true)
expect(notebooksState.notebooks[NOTEBOOK_ID]).toBeUndefined()
expect(queryClient.getQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK_ID))).toBeUndefined()
})
it('removes an unsaved notebook and its cache entry after its caller confirms discard', async () => {
seedNotebook('new')
const queryClient = new QueryClient()
seedQueryData(queryClient)
const evicted = await evictNotebookFromCaches({
queryClient,
projectRef: PROJECT_REF,
id: NOTEBOOK_ID,
})
expect(evicted).toBe(true)
expect(notebooksState.notebooks[NOTEBOOK_ID]).toBeUndefined()
expect(queryClient.getQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK_ID))).toBeUndefined()
})
it('drops a stale cache entry when the notebook is not present in the store', async () => {
const queryClient = new QueryClient()
seedQueryData(queryClient)
const evicted = await evictNotebookFromCaches({
queryClient,
projectRef: PROJECT_REF,
id: NOTEBOOK_ID,
})
expect(evicted).toBe(true)
expect(queryClient.getQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK_ID))).toBeUndefined()
})
})