mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-16 09:26:18 -05:00
* improvement(tools): use react query to fetch child workflow schema, avoid refetch and duplicated utils * consolidated utils & testing mocks
378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
/**
|
|
* Tests for copilot confirm API route
|
|
*
|
|
* @vitest-environment node
|
|
*/
|
|
import { createMockRequest, mockAuth, mockCryptoUuid, setupCommonApiMocks } from '@sim/testing'
|
|
import { NextRequest } from 'next/server'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
describe('Copilot Confirm API Route', () => {
|
|
const mockRedisExists = vi.fn()
|
|
const mockRedisSet = vi.fn()
|
|
const mockGetRedisClient = vi.fn()
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
setupCommonApiMocks()
|
|
mockCryptoUuid()
|
|
|
|
const mockRedisClient = {
|
|
exists: mockRedisExists,
|
|
set: mockRedisSet,
|
|
}
|
|
|
|
mockGetRedisClient.mockReturnValue(mockRedisClient)
|
|
mockRedisExists.mockResolvedValue(1)
|
|
mockRedisSet.mockResolvedValue('OK')
|
|
|
|
vi.doMock('@/lib/core/config/redis', () => ({
|
|
getRedisClient: mockGetRedisClient,
|
|
}))
|
|
|
|
vi.spyOn(global, 'setTimeout').mockImplementation((callback, _delay) => {
|
|
if (typeof callback === 'function') {
|
|
setImmediate(callback)
|
|
}
|
|
return setTimeout(() => {}, 0) as any
|
|
})
|
|
|
|
let mockTime = 1640995200000
|
|
vi.spyOn(Date, 'now').mockImplementation(() => {
|
|
mockTime += 10000
|
|
return mockTime
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('POST', () => {
|
|
it('should return 401 when user is not authenticated', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setUnauthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(401)
|
|
const responseData = await response.json()
|
|
expect(responseData).toEqual({ error: 'Unauthorized' })
|
|
})
|
|
|
|
it('should return 400 for invalid request body - missing toolCallId', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toContain('Required')
|
|
})
|
|
|
|
it('should return 400 for invalid request body - missing status', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
// Missing status
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toContain('Invalid request data')
|
|
})
|
|
|
|
it('should return 400 for invalid status value', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
status: 'invalid-status',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toContain('Invalid notification status')
|
|
})
|
|
|
|
it('should successfully confirm tool call with success status', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
status: 'success',
|
|
message: 'Tool executed successfully',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const responseData = await response.json()
|
|
expect(responseData).toEqual({
|
|
success: true,
|
|
message: 'Tool executed successfully',
|
|
toolCallId: 'tool-call-123',
|
|
status: 'success',
|
|
})
|
|
|
|
expect(mockRedisExists).toHaveBeenCalled()
|
|
expect(mockRedisSet).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should successfully confirm tool call with error status', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-456',
|
|
status: 'error',
|
|
message: 'Tool execution failed',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const responseData = await response.json()
|
|
expect(responseData).toEqual({
|
|
success: true,
|
|
message: 'Tool execution failed',
|
|
toolCallId: 'tool-call-456',
|
|
status: 'error',
|
|
})
|
|
|
|
expect(mockRedisSet).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should successfully confirm tool call with accepted status', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-789',
|
|
status: 'accepted',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const responseData = await response.json()
|
|
expect(responseData).toEqual({
|
|
success: true,
|
|
message: 'Tool call tool-call-789 has been accepted',
|
|
toolCallId: 'tool-call-789',
|
|
status: 'accepted',
|
|
})
|
|
|
|
expect(mockRedisSet).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should successfully confirm tool call with rejected status', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-101',
|
|
status: 'rejected',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const responseData = await response.json()
|
|
expect(responseData).toEqual({
|
|
success: true,
|
|
message: 'Tool call tool-call-101 has been rejected',
|
|
toolCallId: 'tool-call-101',
|
|
status: 'rejected',
|
|
})
|
|
})
|
|
|
|
it('should successfully confirm tool call with background status', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-bg',
|
|
status: 'background',
|
|
message: 'Moved to background execution',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const responseData = await response.json()
|
|
expect(responseData).toEqual({
|
|
success: true,
|
|
message: 'Moved to background execution',
|
|
toolCallId: 'tool-call-bg',
|
|
status: 'background',
|
|
})
|
|
})
|
|
|
|
it('should return 400 when Redis client is not available', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
mockGetRedisClient.mockReturnValue(null)
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toBe('Failed to update tool call status or tool call not found')
|
|
})
|
|
|
|
it('should return 400 when tool call is not found in Redis', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
mockRedisExists.mockResolvedValue(0)
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'non-existent-tool',
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toBe('Failed to update tool call status or tool call not found')
|
|
}, 10000) // 10 second timeout for this specific test
|
|
|
|
it('should handle Redis errors gracefully', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
mockRedisExists.mockRejectedValue(new Error('Redis connection failed'))
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toBe('Failed to update tool call status or tool call not found')
|
|
})
|
|
|
|
it('should handle Redis set operation failure', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
mockRedisExists.mockResolvedValue(1)
|
|
mockRedisSet.mockRejectedValue(new Error('Redis set failed'))
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: 'tool-call-123',
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toBe('Failed to update tool call status or tool call not found')
|
|
})
|
|
|
|
it('should handle JSON parsing errors in request body', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = new NextRequest('http://localhost:3000/api/copilot/confirm', {
|
|
method: 'POST',
|
|
body: '{invalid-json',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(500)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toContain('JSON')
|
|
})
|
|
|
|
it('should validate empty toolCallId', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: '',
|
|
status: 'success',
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(400)
|
|
const responseData = await response.json()
|
|
expect(responseData.error).toContain('Tool call ID is required')
|
|
})
|
|
|
|
it('should handle all valid status types', async () => {
|
|
const authMocks = mockAuth()
|
|
authMocks.setAuthenticated()
|
|
|
|
const validStatuses = ['success', 'error', 'accepted', 'rejected', 'background']
|
|
|
|
for (const status of validStatuses) {
|
|
const req = createMockRequest('POST', {
|
|
toolCallId: `tool-call-${status}`,
|
|
status,
|
|
})
|
|
|
|
const { POST } = await import('@/app/api/copilot/confirm/route')
|
|
const response = await POST(req)
|
|
|
|
expect(response.status).toBe(200)
|
|
const responseData = await response.json()
|
|
expect(responseData.success).toBe(true)
|
|
expect(responseData.status).toBe(status)
|
|
expect(responseData.toolCallId).toBe(`tool-call-${status}`)
|
|
}
|
|
})
|
|
})
|
|
})
|