mirror of
https://github.com/supabase/supabase.git
synced 2026-09-07 02:20:52 +08:00
## Summary - Plumb pre-update notebook snapshot through `update_notebook` tool response as `previous_content` - Add sanitizers in `tool-sanitizer.ts` to strip snapshot before model sees it - Add client-side stripping in `prepareMessagesForAPI` to avoid re-uploading snapshot on subsequent turns - This is PR 2 of 3 fixing Linear issue FE-4243 (notebook update proposal shows 'unapplyable' error for already-completed updates) - Ships no visible behavior change on its own; enables PR 3 to restore diff preview for completed updates ## Test plan - [x] Unit tests: 80/80 passing across notebook-tools.test.ts, tool-sanitizer.test.ts, generate-assistant-response.utils.test.ts, message-utils.test.ts, and mock-tools.test.ts - [x] Typecheck: clean for all changed files - [x] ESLint: zero errors, lint:ratchet passes (exit 0) - [x] Integration: previous_content is correctly populated with pre-update notebook, stripped before model context, and stripped on client-side re-upload <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Notebook updates now retain previous content for recovery and history. - AI responses expose only the notebook’s ID and name, keeping previous content out of model-visible data. - **Tests** - Added coverage for notebook update results, content sanitization, and message preparation, including cases where previous content is absent or preserved. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
150 lines
4.3 KiB
TypeScript
150 lines
4.3 KiB
TypeScript
import type { ToolUIPart, UIMessage } from 'ai'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { prepareMessagesForModel } from './generate-assistant-response.utils'
|
|
import { encodeNotebookToolError, NotebookToolError } from './tools/notebook-tools'
|
|
|
|
function assistantMessage(parts: UIMessage['parts']): UIMessage {
|
|
return { id: 'msg-1', role: 'assistant', parts }
|
|
}
|
|
|
|
function toolPart(overrides: Partial<ToolUIPart>): ToolUIPart {
|
|
return {
|
|
type: 'tool-update_notebook',
|
|
toolCallId: 'call-1',
|
|
state: 'output-available',
|
|
input: {},
|
|
...overrides,
|
|
} as ToolUIPart
|
|
}
|
|
|
|
describe('prepareMessagesForModel', () => {
|
|
it('filters out a plain output-error tool part', () => {
|
|
const messages = [
|
|
assistantMessage([toolPart({ state: 'output-error', errorText: 'Network error' })]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([])
|
|
})
|
|
|
|
it('keeps a stale-update_notebook conflict and rewrites errorText to the plain message', () => {
|
|
const error = new NotebookToolError('Notebook changed since expected_updated_at', {
|
|
exposeToAssistant: true,
|
|
})
|
|
const messages = [
|
|
assistantMessage([
|
|
toolPart({ state: 'output-error', errorText: encodeNotebookToolError(error)! }),
|
|
]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([
|
|
toolPart({ state: 'output-error', errorText: 'Notebook changed since expected_updated_at' }),
|
|
])
|
|
})
|
|
|
|
it('still filters out an unrelated update_notebook output-error', () => {
|
|
const messages = [
|
|
assistantMessage([
|
|
toolPart({ state: 'output-error', errorText: 'Unexpected upstream failure' }),
|
|
]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([])
|
|
})
|
|
|
|
it('still filters out JSON-shaped output-error text lacking the notebook_tool_error tag', () => {
|
|
const messages = [
|
|
assistantMessage([
|
|
toolPart({
|
|
state: 'output-error',
|
|
errorText: JSON.stringify({
|
|
exposeToAssistant: true,
|
|
message: 'not a real notebook error',
|
|
}),
|
|
}),
|
|
]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([])
|
|
})
|
|
|
|
it('exposes any tool part carrying a validly-tagged NotebookToolError, not just update_notebook', () => {
|
|
const error = new NotebookToolError('Notebook changed since expected_updated_at', {
|
|
exposeToAssistant: true,
|
|
})
|
|
const messages = [
|
|
assistantMessage([
|
|
toolPart({
|
|
type: 'tool-execute_sql',
|
|
state: 'output-error',
|
|
errorText: encodeNotebookToolError(error)!,
|
|
}),
|
|
]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([
|
|
toolPart({
|
|
type: 'tool-execute_sql',
|
|
state: 'output-error',
|
|
errorText: 'Notebook changed since expected_updated_at',
|
|
}),
|
|
])
|
|
})
|
|
|
|
it('still filters out input-streaming, input-available, and approval-requested parts', () => {
|
|
const messages = [
|
|
assistantMessage([
|
|
toolPart({ state: 'input-streaming' }),
|
|
toolPart({ state: 'input-available' }),
|
|
toolPart({ state: 'approval-requested', approval: { id: 'a1' } } as Partial<ToolUIPart>),
|
|
]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([])
|
|
})
|
|
|
|
it('strips update_notebook previous_content before it reaches the model on history replay', () => {
|
|
const messages = [
|
|
assistantMessage([
|
|
toolPart({
|
|
state: 'output-available',
|
|
output: {
|
|
id: 'notebook-1',
|
|
name: 'Signup funnel',
|
|
previous_content: { schema_version: 1, cells: [] },
|
|
},
|
|
}),
|
|
]),
|
|
]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([
|
|
toolPart({
|
|
state: 'output-available',
|
|
output: { id: 'notebook-1', name: 'Signup funnel' },
|
|
}),
|
|
])
|
|
})
|
|
|
|
it('keeps non-tool parts untouched', () => {
|
|
const messages = [assistantMessage([{ type: 'text', text: 'hello' }])]
|
|
|
|
const result = prepareMessagesForModel(messages, 'schema')
|
|
|
|
expect(result[0].parts).toEqual([{ type: 'text', text: 'hello' }])
|
|
})
|
|
})
|