mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix(studio): invalidate notebook cache after assistant delete (#49496)
Stacked on supabase/supabase#49415. ## Summary - collect completed assistant delete_notebook tool calls as deleted cache effects - evict deleted notebooks from the React Query cache and notebook state - cover collector, cache eviction, and the open-tab Notebook not found state ## Verification - pnpm test:studio -- notebook-cache-invalidation ExplorerNotebookTab.assistant-cache-invalidation - pnpm --filter studio typecheck - touched-file ESLint and Prettier checks <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved notebook deletion handling so removed notebooks no longer remain visible after deletion. * Navigation and cached notebook data now update promptly when a notebook is deleted. * Attempting to access a deleted notebook now displays a clear “Notebook not found” message. * **Tests** * Added coverage for notebook deletion, cache invalidation, navigation updates, and the resulting not-found state. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
5f50338a2f
commit
40d9a2e929
4 files changed
+101
-7
No files matched your search
+34
-2
@@ -10,12 +10,15 @@ import {
|
||||
applyNotebookCacheEffects,
|
||||
collectNotebookCacheEffects,
|
||||
} from '@/lib/ai/notebook-cache-invalidation'
|
||||
import { createAssistantMessageWithUpdateNotebookTool } from '@/lib/ai/test-fixtures'
|
||||
import {
|
||||
createAssistantMessageWithDeleteNotebookTool,
|
||||
createAssistantMessageWithUpdateNotebookTool,
|
||||
} from '@/lib/ai/test-fixtures'
|
||||
import { notebooksState } from '@/state/notebooks/notebooks-state'
|
||||
import type { Notebook } from '@/state/notebooks/types'
|
||||
import { createTabsState, TabsStateContext } from '@/state/tabs'
|
||||
import { customRender } from '@/tests/lib/custom-render'
|
||||
import { addAPIMock } from '@/tests/lib/msw'
|
||||
import { addAPIMock, type APIErrorBody } from '@/tests/lib/msw'
|
||||
import { setupSqlEditorMocks } from '@/tests/lib/sql-editor-test-utils'
|
||||
|
||||
const PROJECT_REF = 'default'
|
||||
@@ -173,4 +176,33 @@ describe('ExplorerNotebookTab — assistant cache invalidation', () => {
|
||||
expect(await screen.findByText('Updated by assistant')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Original content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the not-found state after an assistant delete_notebook tool call completes', async () => {
|
||||
setupSqlEditorMocks()
|
||||
seedNotebook()
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
queryClient.setQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK_ID), { id: NOTEBOOK_ID })
|
||||
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/projects/:ref/content/item/:id',
|
||||
response: () =>
|
||||
HttpResponse.json<APIErrorBody>({ message: 'Notebook not found' }, { status: 404 }),
|
||||
})
|
||||
|
||||
renderNotebookTab(queryClient)
|
||||
|
||||
expect(await screen.findByText('Original content')).toBeInTheDocument()
|
||||
|
||||
const message = createAssistantMessageWithDeleteNotebookTool({
|
||||
id: NOTEBOOK_ID,
|
||||
name: 'Test notebook',
|
||||
})
|
||||
const effects = collectNotebookCacheEffects([message], new Set())
|
||||
await applyNotebookCacheEffects({ queryClient, projectRef: PROJECT_REF, effects })
|
||||
|
||||
expect(await screen.findByText('Notebook not found')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Original content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from './notebook-cache-invalidation'
|
||||
import {
|
||||
createAssistantMessageWithCreateNotebookTool,
|
||||
createAssistantMessageWithDeleteNotebookTool,
|
||||
createAssistantMessageWithUpdateNotebookTool,
|
||||
createAssistantTextMessage,
|
||||
createUserMessage,
|
||||
@@ -50,6 +51,14 @@ describe('collectNotebookCacheEffects', () => {
|
||||
expect(effects).toEqual([{ _tag: 'upserted', toolCallId: 'call-notebook-1', id: 'notebook-1' }])
|
||||
})
|
||||
|
||||
it('collects a delete_notebook output-available part', () => {
|
||||
const messages = [createAssistantMessageWithDeleteNotebookTool()]
|
||||
|
||||
const effects = collectNotebookCacheEffects(messages, new Set())
|
||||
|
||||
expect(effects).toEqual([{ _tag: 'deleted', toolCallId: 'call-notebook-1', id: 'notebook-1' }])
|
||||
})
|
||||
|
||||
it('ignores non-terminal tool states', () => {
|
||||
const messages = [
|
||||
assistantMessage([
|
||||
@@ -138,6 +147,29 @@ describe('applyNotebookCacheEffects', () => {
|
||||
expect(notebooksState.notebooks[NOTEBOOK.id]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('invalidates the nav list and evicts a deleted notebook from the cache', async () => {
|
||||
notebooksState.setNotebook({ projectRef: PROJECT_REF, notebook: NOTEBOOK })
|
||||
const queryClient = new QueryClient()
|
||||
queryClient.setQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK.id), { id: NOTEBOOK.id })
|
||||
queryClient.setQueryData(contentKeys.allContentLists(PROJECT_REF), [])
|
||||
queryClient.setQueryData(contentKeys.infiniteList(PROJECT_REF), {})
|
||||
|
||||
await applyNotebookCacheEffects({
|
||||
queryClient,
|
||||
projectRef: PROJECT_REF,
|
||||
effects: [{ _tag: 'deleted', toolCallId: 'call-1', id: NOTEBOOK.id }],
|
||||
})
|
||||
|
||||
expect(queryClient.getQueryState(contentKeys.allContentLists(PROJECT_REF))?.isInvalidated).toBe(
|
||||
true
|
||||
)
|
||||
expect(queryClient.getQueryState(contentKeys.infiniteList(PROJECT_REF))?.isInvalidated).toBe(
|
||||
true
|
||||
)
|
||||
expect(queryClient.getQueryData(contentKeys.resource(PROJECT_REF, NOTEBOOK.id))).toBeUndefined()
|
||||
expect(notebooksState.notebooks[NOTEBOOK.id]).toBeUndefined()
|
||||
})
|
||||
|
||||
it('leaves a dirty (unsaved) notebook in the store untouched', async () => {
|
||||
notebooksState.addNotebook({ projectRef: PROJECT_REF, notebook: NOTEBOOK })
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
@@ -10,10 +10,14 @@ export type NotebookCacheEffect =
|
||||
| { _tag: 'upserted'; toolCallId: string; id: string }
|
||||
| { _tag: 'deleted'; toolCallId: string; id: string }
|
||||
|
||||
const NOTEBOOK_UPSERT_TOOL_TYPES = new Set(['tool-create_notebook', 'tool-update_notebook'])
|
||||
const NOTEBOOK_MUTATION_TOOL_TYPES = new Set([
|
||||
'tool-create_notebook',
|
||||
'tool-update_notebook',
|
||||
'tool-delete_notebook',
|
||||
])
|
||||
|
||||
function isNotebookUpsertPart(part: UIMessage['parts'][number]): part is ToolUIPart {
|
||||
return NOTEBOOK_UPSERT_TOOL_TYPES.has(part.type)
|
||||
function isNotebookMutationPart(part: UIMessage['parts'][number]): part is ToolUIPart {
|
||||
return NOTEBOOK_MUTATION_TOOL_TYPES.has(part.type)
|
||||
}
|
||||
|
||||
export function collectNotebookCacheEffects(
|
||||
@@ -26,14 +30,18 @@ export function collectNotebookCacheEffects(
|
||||
if (message.role !== 'assistant') continue
|
||||
|
||||
for (const part of message.parts ?? []) {
|
||||
if (!isNotebookUpsertPart(part)) continue
|
||||
if (!isNotebookMutationPart(part)) continue
|
||||
if (part.state !== 'output-available') continue
|
||||
if (processed.has(part.toolCallId)) continue
|
||||
|
||||
const result = notebookToolOutputSchema.safeParse(part.output)
|
||||
if (!result.success) continue
|
||||
|
||||
effects.push({ _tag: 'upserted', toolCallId: part.toolCallId, id: result.data.id })
|
||||
effects.push({
|
||||
_tag: part.type === 'tool-delete_notebook' ? 'deleted' : 'upserted',
|
||||
toolCallId: part.toolCallId,
|
||||
id: result.data.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,28 @@ export function createAssistantMessageWithCreateNotebookTool(
|
||||
}
|
||||
}
|
||||
|
||||
export function createAssistantMessageWithDeleteNotebookTool(
|
||||
output: Record<string, unknown> = {
|
||||
id: 'notebook-1',
|
||||
name: 'Signup funnel',
|
||||
},
|
||||
id = 'assistant-notebook-msg-1'
|
||||
): UIMessage {
|
||||
return {
|
||||
id,
|
||||
role: 'assistant',
|
||||
parts: [
|
||||
{
|
||||
type: 'tool-delete_notebook',
|
||||
state: 'output-available',
|
||||
toolCallId: 'call-notebook-1',
|
||||
input: { id: 'notebook-1' },
|
||||
output,
|
||||
} satisfies ToolUIPart,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function createAssistantMessageWithMultipleTools(
|
||||
id = 'assistant-multi-tool-msg-1'
|
||||
): UIMessage {
|
||||
|
||||
Reference in new issue
Block a user