studio-tools.test.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { safeSql } from '@supabase/pg-meta'
  2. import { beforeEach, describe, expect, it, vi } from 'vitest'
  3. import { getStudioTools } from './studio-tools'
  4. import { executeSql } from '@/data/sql/execute-sql-query'
  5. import { NO_DATA_PERMISSIONS } from '@/lib/ai/tools/tool-sanitizer'
  6. vi.mock('@/data/sql/execute-sql-query', () => ({
  7. executeSql: vi.fn(),
  8. }))
  9. describe('ai/tools/studio-tools', () => {
  10. beforeEach(() => {
  11. vi.mocked(executeSql).mockReset()
  12. })
  13. describe('getStudioTools', () => {
  14. it('should return an object with tool definitions', () => {
  15. const tools = getStudioTools()
  16. expect(tools).toBeDefined()
  17. expect(typeof tools).toBe('object')
  18. })
  19. it('should include execute_sql tool', () => {
  20. const tools = getStudioTools()
  21. expect(tools.execute_sql).toBeDefined()
  22. expect(tools.execute_sql.description).toContain('execute a SQL statement')
  23. })
  24. it('should include deploy_edge_function tool', () => {
  25. const tools = getStudioTools()
  26. expect(tools.deploy_edge_function).toBeDefined()
  27. expect(tools.deploy_edge_function.description).toContain('deploy a Briven Edge Function')
  28. })
  29. it('should include rename_chat tool', () => {
  30. const tools = getStudioTools()
  31. expect(tools.rename_chat).toBeDefined()
  32. expect(tools.rename_chat.description).toContain('Rename the current chat session')
  33. })
  34. it('should have exactly 4 tools', () => {
  35. const tools = getStudioTools()
  36. const toolNames = Object.keys(tools)
  37. expect(toolNames).toHaveLength(4)
  38. expect(toolNames).toContain('load_knowledge')
  39. expect(toolNames).toContain('execute_sql')
  40. expect(toolNames).toContain('deploy_edge_function')
  41. expect(toolNames).toContain('rename_chat')
  42. })
  43. it('should have execute_sql with correct input schema fields', () => {
  44. const tools = getStudioTools()
  45. const executeSqlTool = tools.execute_sql
  46. // Check that the tool has an input schema
  47. expect(executeSqlTool.inputSchema).toBeDefined()
  48. // Verify the schema exists and is a Zod object
  49. const schema = executeSqlTool.inputSchema
  50. expect(schema).toBeDefined()
  51. expect((schema as any)._def.typeName).toBe('ZodObject')
  52. })
  53. it('should have deploy_edge_function with input schema', () => {
  54. const tools = getStudioTools()
  55. const deployTool = tools.deploy_edge_function
  56. expect(deployTool.inputSchema).toBeDefined()
  57. // Verify the schema exists and is a Zod object
  58. expect(deployTool.inputSchema).toBeDefined()
  59. expect((deployTool.inputSchema as any)._def.typeName).toBe('ZodObject')
  60. })
  61. it('should have rename_chat with execute function', async () => {
  62. const tools = getStudioTools()
  63. const renameTool = tools.rename_chat
  64. expect(renameTool.execute).toBeDefined()
  65. expect(typeof renameTool.execute).toBe('function')
  66. // Test the execute function
  67. if (!renameTool.execute) throw new Error('execute is undefined')
  68. const result = await renameTool.execute(
  69. { newName: 'Test Chat' },
  70. { toolCallId: 'test', messages: [] }
  71. )
  72. expect(result).toEqual({ status: 'Chat request sent to client' })
  73. })
  74. it('should validate execute_sql input schema correctly', () => {
  75. const tools = getStudioTools()
  76. const schema = tools.execute_sql.inputSchema
  77. // Check if schema is a Zod schema with safeParse
  78. if ('safeParse' in schema) {
  79. // Valid input
  80. const validInput = {
  81. sql: safeSql`SELECT * FROM users`,
  82. label: 'Get users',
  83. chartConfig: { view: 'table' as const },
  84. isWriteQuery: false,
  85. }
  86. expect(schema.safeParse(validInput).success).toBe(true)
  87. // Valid chart config
  88. const validChartInput = {
  89. sql: safeSql`SELECT count(*) FROM users`,
  90. label: 'User count',
  91. chartConfig: { view: 'chart' as const, xAxis: 'date', yAxis: 'count' },
  92. isWriteQuery: false,
  93. }
  94. expect(schema.safeParse(validChartInput).success).toBe(true)
  95. // Missing required field
  96. const invalidInput = {
  97. sql: safeSql`SELECT * FROM users`,
  98. // missing label, chartConfig, isWriteQuery
  99. }
  100. expect(schema.safeParse(invalidInput).success).toBe(false)
  101. } else {
  102. // Skip test if schema doesn't have safeParse
  103. expect(schema).toBeDefined()
  104. }
  105. })
  106. it('should require approval for read and write SQL queries', () => {
  107. const tools = getStudioTools()
  108. expect(tools.execute_sql.needsApproval).toBe(true)
  109. })
  110. it('should return execute_sql rows to the UI and sanitize model output without data opt-in', async () => {
  111. const rows = [{ email: 'test@example.com' }]
  112. vi.mocked(executeSql).mockResolvedValue({ result: rows })
  113. const tools = getStudioTools({
  114. projectRef: 'test-project',
  115. connectionString: 'encrypted-connection-string',
  116. aiOptInLevel: 'schema',
  117. })
  118. if (!tools.execute_sql.execute) throw new Error('execute is undefined')
  119. const result = await tools.execute_sql.execute(
  120. {
  121. sql: 'SELECT email FROM users',
  122. label: 'Get emails',
  123. chartConfig: { view: 'table' },
  124. isWriteQuery: false,
  125. },
  126. { toolCallId: 'test', messages: [] }
  127. )
  128. expect(executeSql).toHaveBeenCalledWith(
  129. {
  130. projectRef: 'test-project',
  131. connectionString: 'encrypted-connection-string',
  132. sql: 'SELECT email FROM users',
  133. },
  134. undefined,
  135. undefined
  136. )
  137. expect(result).toEqual(rows)
  138. expect((tools.execute_sql as any).toModelOutput({ output: result })).toEqual({
  139. type: 'text',
  140. value: NO_DATA_PERMISSIONS,
  141. })
  142. })
  143. it('should return execute_sql rows with data opt-in', async () => {
  144. const rows = [{ email: 'test@example.com' }]
  145. vi.mocked(executeSql).mockResolvedValue({ result: rows })
  146. const tools = getStudioTools({
  147. projectRef: 'test-project',
  148. connectionString: 'encrypted-connection-string',
  149. aiOptInLevel: 'schema_and_log_and_data',
  150. })
  151. if (!tools.execute_sql.execute) throw new Error('execute is undefined')
  152. const result = await tools.execute_sql.execute(
  153. {
  154. sql: 'SELECT email FROM users',
  155. label: 'Get emails',
  156. chartConfig: { view: 'table' },
  157. isWriteQuery: false,
  158. },
  159. { toolCallId: 'test', messages: [] }
  160. )
  161. expect(executeSql).toHaveBeenCalledWith(
  162. {
  163. projectRef: 'test-project',
  164. connectionString: 'encrypted-connection-string',
  165. sql: 'SELECT email FROM users',
  166. },
  167. undefined,
  168. undefined
  169. )
  170. expect(result).toEqual(rows)
  171. expect((tools.execute_sql as any).toModelOutput({ output: result })).toEqual({
  172. type: 'json',
  173. value: rows,
  174. })
  175. })
  176. it('should validate rename_chat input schema correctly', () => {
  177. const tools = getStudioTools()
  178. const schema = tools.rename_chat.inputSchema
  179. // Check if schema is a Zod schema with safeParse
  180. if ('safeParse' in schema) {
  181. // Valid input
  182. expect(schema.safeParse({ newName: 'My Chat' }).success).toBe(true)
  183. // Invalid input - missing newName
  184. expect(schema.safeParse({}).success).toBe(false)
  185. // Invalid input - wrong type
  186. expect(schema.safeParse({ newName: 123 }).success).toBe(false)
  187. } else {
  188. // Skip test if schema doesn't have safeParse
  189. expect(schema).toBeDefined()
  190. }
  191. })
  192. })
  193. })