Files
sim/apps/sim/app/api/copilot/stats/route.test.ts
Waleed 4ccb57371b improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern (#3357)
* improvement(tests): speed up unit tests by eliminating vi.resetModules anti-pattern

- convert 51 test files from vi.resetModules/vi.doMock/dynamic import to vi.hoisted/vi.mock/static import
- add global @sim/db mock to vitest.setup.ts
- switch 4 test files from jsdom to node environment
- remove all vi.importActual calls that loaded heavy modules (200+ block files)
- remove slow mockConsoleLogger/mockAuth/setupCommonApiMocks helpers
- reduce real setTimeout delays in engine tests
- mock heavy transitive deps in diff-engine test

test execution time: 34s -> 9s (3.9x faster)
environment time: 2.5s -> 0.6s (4x faster)

* docs(testing): update testing best practices with performance rules

- document vi.hoisted + vi.mock + static import as the standard pattern
- explicitly ban vi.resetModules, vi.doMock, vi.importActual, mockAuth, setupCommonApiMocks
- document global mocks from vitest.setup.ts
- add mock pattern reference for auth, hybrid auth, and database chains
- add performance rules section covering heavy deps, jsdom vs node, real timers

* fix(tests): fix 4 failing test files with missing mocks

- socket/middleware/permissions: add vi.mock for @/lib/auth to prevent transitive getBaseUrl() call
- workflow-handler: add vi.mock for @/executor/utils/http matching executor mock pattern
- evaluator-handler: add db.query.account mock structure before vi.spyOn
- router-handler: same db.query.account fix as evaluator

* fix(tests): replace banned Function type with explicit callback signature
2026-02-26 15:46:49 -08:00

343 lines
10 KiB
TypeScript

/**
* Tests for copilot stats API route
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockAuthenticateCopilotRequestSessionOnly,
mockCreateUnauthorizedResponse,
mockCreateBadRequestResponse,
mockCreateInternalServerErrorResponse,
mockCreateRequestTracker,
mockFetch,
} = vi.hoisted(() => ({
mockAuthenticateCopilotRequestSessionOnly: vi.fn(),
mockCreateUnauthorizedResponse: vi.fn(),
mockCreateBadRequestResponse: vi.fn(),
mockCreateInternalServerErrorResponse: vi.fn(),
mockCreateRequestTracker: vi.fn(),
mockFetch: vi.fn(),
}))
vi.mock('@/lib/copilot/request-helpers', () => ({
authenticateCopilotRequestSessionOnly: mockAuthenticateCopilotRequestSessionOnly,
createUnauthorizedResponse: mockCreateUnauthorizedResponse,
createBadRequestResponse: mockCreateBadRequestResponse,
createInternalServerErrorResponse: mockCreateInternalServerErrorResponse,
createRequestTracker: mockCreateRequestTracker,
}))
vi.mock('@/lib/copilot/constants', () => ({
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
SIM_AGENT_API_URL: 'https://agent.sim.example.com',
}))
vi.mock('@/lib/core/config/env', () => ({
env: {
COPILOT_API_KEY: 'test-api-key',
},
getEnv: vi.fn((key: string) => {
const vals: Record<string, string | undefined> = {
COPILOT_API_KEY: 'test-api-key',
}
return vals[key]
}),
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value),
isFalsy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value.toLowerCase() === 'false' || value === '0' : value === false,
}))
import { POST } from '@/app/api/copilot/stats/route'
describe('Copilot Stats API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
global.fetch = mockFetch
mockCreateUnauthorizedResponse.mockReturnValue(
new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
)
mockCreateBadRequestResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 400 })
)
mockCreateInternalServerErrorResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 500 })
)
mockCreateRequestTracker.mockReturnValue({
requestId: 'test-request-id',
getDuration: vi.fn().mockReturnValue(100),
})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
const req = createMockRequest('POST', {
messageId: 'message-123',
diffCreated: true,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(401)
const responseData = await response.json()
expect(responseData).toEqual({ error: 'Unauthorized' })
})
it('should successfully forward stats to Sim Agent', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
})
const req = createMockRequest('POST', {
messageId: 'message-123',
diffCreated: true,
diffAccepted: true,
})
const response = await POST(req)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData).toEqual({ success: true })
expect(mockFetch).toHaveBeenCalledWith(
'https://agent.sim.example.com/api/stats',
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Content-Type': 'application/json',
'x-api-key': 'test-api-key',
}),
body: JSON.stringify({
messageId: 'message-123',
diffCreated: true,
diffAccepted: true,
}),
})
)
})
it('should return 400 for invalid request body - missing messageId', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
diffCreated: true,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData.error).toBe('Invalid request body for copilot stats')
})
it('should return 400 for invalid request body - missing diffCreated', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
messageId: 'message-123',
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData.error).toBe('Invalid request body for copilot stats')
})
it('should return 400 for invalid request body - missing diffAccepted', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
messageId: 'message-123',
diffCreated: true,
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData.error).toBe('Invalid request body for copilot stats')
})
it('should return 400 when upstream Sim Agent returns error', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockFetch.mockResolvedValueOnce({
ok: false,
json: () => Promise.resolve({ error: 'Invalid message ID' }),
})
const req = createMockRequest('POST', {
messageId: 'invalid-message',
diffCreated: true,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData).toEqual({ success: false, error: 'Invalid message ID' })
})
it('should handle upstream error with message field', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockFetch.mockResolvedValueOnce({
ok: false,
json: () => Promise.resolve({ message: 'Rate limit exceeded' }),
})
const req = createMockRequest('POST', {
messageId: 'message-123',
diffCreated: true,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData).toEqual({ success: false, error: 'Rate limit exceeded' })
})
it('should handle upstream error with no JSON response', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockFetch.mockResolvedValueOnce({
ok: false,
json: () => Promise.reject(new Error('Not JSON')),
})
const req = createMockRequest('POST', {
messageId: 'message-123',
diffCreated: true,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData).toEqual({ success: false, error: 'Upstream error' })
})
it('should handle network errors gracefully', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockFetch.mockRejectedValueOnce(new Error('Network error'))
const req = createMockRequest('POST', {
messageId: 'message-123',
diffCreated: true,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(500)
const responseData = await response.json()
expect(responseData.error).toBe('Failed to forward copilot stats')
})
it('should handle JSON parsing errors in request body', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = new NextRequest('http://localhost:3000/api/copilot/stats', {
method: 'POST',
body: '{invalid-json',
headers: {
'Content-Type': 'application/json',
},
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData.error).toBe('Invalid request body for copilot stats')
})
it('should forward stats with diffCreated=false and diffAccepted=false', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true }),
})
const req = createMockRequest('POST', {
messageId: 'message-456',
diffCreated: false,
diffAccepted: false,
})
const response = await POST(req)
expect(response.status).toBe(200)
expect(mockFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: JSON.stringify({
messageId: 'message-456',
diffCreated: false,
diffAccepted: false,
}),
})
)
})
})
})