Files
supabase/apps/studio/state/ai-assistant-state.test.ts
Ali Waseem 4864031466 fix(assistant): SQL approval buttons do nothing (#48102)
Fixes FE-3954: clicking Skip/Run Query in the AI Assistant did nothing.

## Root cause
`onFinish` synced the AI SDK `Chat` instance's live message array
directly into valtio state (`chat.messages = messages`). valtio's
`proxy()` mutates an object's nested properties in place instead of
cloning them, so this corrupted the SDK's own array with Proxies. The
next approval click hit the SDK's internal `structuredClone()` call and
threw `DOMException: Proxy object could not be cloned` — an unhandled
rejection before any network request, so the buttons silently did
nothing.

## Fix
Assign a sanitized copy of the message array instead of the SDK's live
reference.

## Test plan
- [x] `pnpm --filter studio exec vitest run
state/ai-assistant-state.test.ts` — fails on old code with the exact
DOMException, passes on the fix
- [ ] Manual: approve/skip a suggested query in AI Assistant and confirm
it runs/is skipped

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

## Summary by CodeRabbit

* **Bug Fixes**
* Improved AI assistant chat message synchronization to prevent message
corruption.
* Ensured chat messages remain safely cloneable after approval-related
updates.

* **Tests**
* Added coverage verifying that synchronized AI assistant messages can
be cloned successfully.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-20 16:25:42 +00:00

47 lines
1.7 KiB
TypeScript

import { proxy, ref } from 'valtio/vanilla'
import { describe, expect, it } from 'vitest'
import { sanitizeForCloning } from './ai-assistant-state'
describe('AI assistant chat message sync', () => {
// FE-3954: syncing the live array into valtio corrupted it with Proxies, breaking structuredClone in addToolApprovalResponse
it('does not corrupt the live SDK message array when synced into valtio state', () => {
const state = proxy<{ chats: Record<string, any>; chatInstances: Record<string, any> }>({
chats: {},
chatInstances: {},
})
state.chats['chat-1'] = { id: 'chat-1', messages: [] }
const liveSdkMessages = [
{
id: 'message-1',
role: 'assistant',
parts: [
{ type: 'text', text: "Sure, here's a query" },
{
type: 'tool-execute_sql',
toolCallId: 'tool-1',
state: 'approval-requested',
approval: { id: 'approval-1' },
},
],
},
]
state.chatInstances['chat-1'] = ref({ messages: liveSdkMessages })
const chat = state.chats['chat-1']
chat.messages = liveSdkMessages.map((message) => sanitizeForCloning(message))
// mirrors addToolApprovalResponse's own update logic
const lastMessage = liveSdkMessages[liveSdkMessages.length - 1]
const updatedParts = lastMessage.parts.map((part: any) =>
part.state === 'approval-requested' && part.approval?.id === 'approval-1'
? { ...part, state: 'approval-responded', approval: { id: 'approval-1', approved: true } }
: part
)
const replacedMessage = { ...lastMessage, parts: updatedParts }
expect(() => structuredClone(replacedMessage)).not.toThrow()
})
})