Files
supabase/apps/studio/lib/ai/tools/tool-sanitizer.test.ts
626eb30e77 Assistant action orientated approach (#38806)
* update onboarding

* update model and fix part issue

* action orientated assistant

* fix tool

* lock

* remove unused filter

* fix tests

* fix again

* update package

* update container

* fix tests

* refactor(ai assistant): break out message markdown and profile picture

* wip

* refactor(ai assistant): break up message component

* refactor: break ai assistant message down into multiple files

* refactor: simplify ReportBlock state

* fix: styling of draggable report block header

When the drag handle is showing, it overlaps with the block header.
Decrease the opacity of the header so the handle can be seen and the two
can be distinguished.

* fix: minor tweaks to tool ui

* refactor: simplify DisplayBlockRenderer state

* fix: remove double deploy button in edge function block

When the confirm footer is shown, the deploy button on the top right should be
hidden (not just disabled) to avoid confusion.

* refactor, test: message sanitization by opt-in level

Refactor the message sanitization to have more type safety and be more testable.
Add tests to ensure:

- Message sanitization always runs on generate-v4
- Message sanitization correctly works by opt-in level

* Fix conflicts in pnpm lock

* Couple of nits and refactors

* Revert casing for report block snippet

* adjust sanitised prompt

* Fix tests

---------

Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2025-09-29 03:57:36 +00:00

176 lines
5.4 KiB
TypeScript

import type { ToolUIPart } from 'ai'
import { describe, expect, test } from 'vitest'
// End of third-party imports
import { prepareMessagesForAPI } from '../message-utils'
import {
createAssistantMessageWithExecuteSqlTool,
createAssistantMessageWithMultipleTools,
createLongConversation,
} from '../test-fixtures'
import { NO_DATA_PERMISSIONS, sanitizeMessagePart } from './tool-sanitizer'
describe('messages are sanitized based on opt-in level', () => {
test('messages are sanitized at disabled level', () => {
const messages = [
createAssistantMessageWithExecuteSqlTool('SELECT email FROM users', [
{ email: 'test@example.com' },
]),
]
// Prepare messages as frontend would
const preparedMessages = prepareMessagesForAPI(messages)
// Sanitize messages as API endpoint would
const processedMessages = preparedMessages.map((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const processedParts = msg.parts.map((part) => {
return sanitizeMessagePart(part, 'disabled')
})
return { ...msg, parts: processedParts }
}
return msg
})
const output = (processedMessages[0].parts[1] as ToolUIPart).output
expect(output).toMatch(NO_DATA_PERMISSIONS)
})
test('messages are sanitized at schema level', () => {
const messages = [
createAssistantMessageWithExecuteSqlTool('SELECT email FROM users', [
{ email: 'test@example.com' },
]),
]
// Prepare messages as frontend would
const preparedMessages = prepareMessagesForAPI(messages)
// Sanitize messages as API endpoint would
const processedMessages = preparedMessages.map((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const processedParts = msg.parts.map((part) => {
return sanitizeMessagePart(part, 'schema')
})
return { ...msg, parts: processedParts }
}
return msg
})
const output = (processedMessages[0].parts[1] as ToolUIPart).output
expect(output).toMatch(NO_DATA_PERMISSIONS)
})
test('messages are sanitized at schema and log level', () => {
const messages = [
createAssistantMessageWithExecuteSqlTool('SELECT email FROM users', [
{ email: 'test@example.com' },
]),
]
// Prepare messages as frontend would
const preparedMessages = prepareMessagesForAPI(messages)
// Sanitize messages as API endpoint would
const processedMessages = preparedMessages.map((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const processedParts = msg.parts.map((part) => {
return sanitizeMessagePart(part, 'schema_and_log')
})
return { ...msg, parts: processedParts }
}
return msg
})
const output = (processedMessages[0].parts[1] as ToolUIPart).output
expect(output).toMatch(NO_DATA_PERMISSIONS)
})
test('messages are not sanitized at data level', () => {
const messages = [
createAssistantMessageWithExecuteSqlTool('SELECT email FROM users', [
{ email: 'test@example.com' },
]),
]
// Prepare messages as frontend would
const preparedMessages = prepareMessagesForAPI(messages)
// Sanitize messages as API endpoint would
const processedMessages = preparedMessages.map((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const processedParts = msg.parts.map((part) => {
return sanitizeMessagePart(part, 'schema_and_log_and_data')
})
return { ...msg, parts: processedParts }
}
return msg
})
const output = (processedMessages[0].parts[1] as ToolUIPart).output
expect(output).toEqual([{ email: 'test@example.com' }])
})
test('multiple tool parts in message are sanitized', () => {
const messages = [createAssistantMessageWithMultipleTools()]
// Prepare messages as frontend would
const preparedMessages = prepareMessagesForAPI(messages)
// Sanitize messages as API endpoint would
const processedMessages = preparedMessages.map((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const processedParts = msg.parts.map((part) => {
return sanitizeMessagePart(part, 'schema')
})
return { ...msg, parts: processedParts }
}
return msg
})
const parts = processedMessages[0].parts
parts.forEach((part) => {
if (part.type.startsWith('tool')) {
const tool = part as ToolUIPart
expect(tool.output).toMatch(NO_DATA_PERMISSIONS)
}
})
})
test('long message chain is sanitized', () => {
const messages = createLongConversation()
// Prepare messages as frontend would
const preparedMessages = prepareMessagesForAPI(messages)
// Sanitize messages as API endpoint would
const processedMessages = preparedMessages.map((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const processedParts = msg.parts.map((part) => {
return sanitizeMessagePart(part, 'schema')
})
return { ...msg, parts: processedParts }
}
return msg
})
processedMessages.forEach((msg) => {
if (msg.role === 'assistant' && msg.parts) {
const parts = msg.parts
parts.forEach((part) => {
if (part.type.startsWith('tool')) {
const tool = part as ToolUIPart
expect(tool.output).toMatch(NO_DATA_PERMISSIONS)
}
})
}
})
})
})