chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,405 @@
/**
* Tests for copilot feedback API route
*
* @vitest-environment node
*/
import {
copilotHttpMock,
copilotHttpMockFns,
dbChainMock,
dbChainMockFns,
resetDbChainMock,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@sim/db', () => dbChainMock)
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
import { GET, POST } from '@/app/api/copilot/feedback/route'
function createMockRequest(method: string, body: Record<string, unknown>): NextRequest {
return new NextRequest('http://localhost:3000/api/copilot/feedback', {
method,
body: JSON.stringify(body),
headers: { 'Content-Type': 'application/json' },
})
}
describe('Copilot Feedback API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I create a workflow?',
agentResponse: 'You can create a workflow by...',
isPositiveFeedback: true,
})
const response = await POST(req)
expect(response.status).toBe(401)
const responseData = await response.json()
expect(responseData).toEqual({ error: 'Unauthorized' })
})
it('should successfully submit positive feedback', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const feedbackRecord = {
feedbackId: 'feedback-123',
userId: 'user-123',
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I create a workflow?',
agentResponse: 'You can create a workflow by...',
isPositive: true,
feedback: null,
workflowYaml: null,
createdAt: new Date('2024-01-01'),
}
dbChainMockFns.returning.mockResolvedValueOnce([feedbackRecord])
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I create a workflow?',
agentResponse: 'You can create a workflow by...',
isPositiveFeedback: true,
})
const response = await POST(req)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData.success).toBe(true)
expect(responseData.feedbackId).toBe('feedback-123')
expect(responseData.message).toBe('Feedback submitted successfully')
})
it('should successfully submit negative feedback with text', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const feedbackRecord = {
feedbackId: 'feedback-456',
userId: 'user-123',
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I deploy?',
agentResponse: 'Here is how to deploy...',
isPositive: false,
feedback: 'The response was not helpful',
workflowYaml: null,
createdAt: new Date('2024-01-01'),
}
dbChainMockFns.returning.mockResolvedValueOnce([feedbackRecord])
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I deploy?',
agentResponse: 'Here is how to deploy...',
isPositiveFeedback: false,
feedback: 'The response was not helpful',
})
const response = await POST(req)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData.success).toBe(true)
expect(responseData.feedbackId).toBe('feedback-456')
})
it('should successfully submit feedback with workflow YAML', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const workflowYaml = `
blocks:
- id: starter
type: starter
- id: agent
type: agent
edges:
- source: starter
target: agent
`
const feedbackRecord = {
feedbackId: 'feedback-789',
userId: 'user-123',
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'Build a simple agent workflow',
agentResponse: 'I created a workflow for you.',
isPositive: true,
feedback: null,
workflowYaml: workflowYaml,
createdAt: new Date('2024-01-01'),
}
dbChainMockFns.returning.mockResolvedValueOnce([feedbackRecord])
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'Build a simple agent workflow',
agentResponse: 'I created a workflow for you.',
isPositiveFeedback: true,
workflowYaml: workflowYaml,
})
const response = await POST(req)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData.success).toBe(true)
expect(dbChainMockFns.values).toHaveBeenCalledWith(
expect.objectContaining({
workflowYaml: workflowYaml,
})
)
})
it('should return 400 for invalid chatId format', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
chatId: 'not-a-uuid',
userQuery: 'How do I create a workflow?',
agentResponse: 'You can create a workflow by...',
isPositiveFeedback: true,
})
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 empty userQuery', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: '',
agentResponse: 'You can create a workflow by...',
isPositiveFeedback: true,
})
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 empty agentResponse', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I create a workflow?',
agentResponse: '',
isPositiveFeedback: true,
})
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 missing isPositiveFeedback', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I create a workflow?',
agentResponse: 'You can create a workflow by...',
})
const response = await POST(req)
expect(response.status).toBe(400)
const responseData = await response.json()
expect(responseData.error).toContain('Invalid request data')
})
it('should handle database errors gracefully', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
dbChainMockFns.returning.mockRejectedValueOnce(new Error('Database connection failed'))
const req = createMockRequest('POST', {
chatId: '550e8400-e29b-41d4-a716-446655440000',
userQuery: 'How do I create a workflow?',
agentResponse: 'You can create a workflow by...',
isPositiveFeedback: true,
})
const response = await POST(req)
expect(response.status).toBe(500)
const responseData = await response.json()
expect(responseData.error).toBe('Failed to submit feedback')
})
it('should handle JSON parsing errors in request body', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const req = new NextRequest('http://localhost:3000/api/copilot/feedback', {
method: 'POST',
body: '{invalid-json',
headers: {
'Content-Type': 'application/json',
},
})
const response = await POST(req)
expect(response.status).toBe(500)
})
})
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)
expect(response.status).toBe(401)
const responseData = await response.json()
expect(responseData).toEqual({ error: 'Unauthorized' })
})
it('should return empty feedback array when no feedback exists', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
dbChainMockFns.where.mockResolvedValueOnce([])
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData.success).toBe(true)
expect(responseData.feedback).toEqual([])
})
it('should only return feedback records for the authenticated user', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
const mockFeedback = [
{
feedbackId: 'feedback-1',
userId: 'user-123',
chatId: 'chat-1',
userQuery: 'Query 1',
agentResponse: 'Response 1',
isPositive: true,
feedback: null,
workflowYaml: null,
createdAt: new Date('2024-01-01'),
},
]
dbChainMockFns.where.mockResolvedValueOnce(mockFeedback)
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData.success).toBe(true)
expect(responseData.feedback).toHaveLength(1)
expect(responseData.feedback[0].feedbackId).toBe('feedback-1')
expect(responseData.feedback[0].userId).toBe('user-123')
const { eq } = await import('drizzle-orm')
expect(dbChainMockFns.where).toHaveBeenCalled()
expect(eq).toHaveBeenCalledWith('userId', 'user-123')
})
it('should handle database errors gracefully', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
dbChainMockFns.where.mockRejectedValueOnce(new Error('Database connection failed'))
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)
expect(response.status).toBe(500)
const responseData = await response.json()
expect(responseData.error).toBe('Failed to retrieve feedback')
})
it('should return metadata with response', async () => {
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
dbChainMockFns.where.mockResolvedValueOnce([])
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)
expect(response.status).toBe(200)
const responseData = await response.json()
expect(responseData.metadata).toBeDefined()
expect(responseData.metadata.requestId).toBeDefined()
expect(responseData.metadata.duration).toBeDefined()
})
})
})
+162
View File
@@ -0,0 +1,162 @@
import { db } from '@sim/db'
import { copilotFeedback } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { submitCopilotFeedbackContract } from '@/lib/api/contracts'
import { parseRequest, validationErrorResponse } from '@/lib/api/server'
import {
authenticateCopilotRequestSessionOnly,
createInternalServerErrorResponse,
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { captureServerEvent } from '@/lib/posthog/server'
const logger = createLogger('CopilotFeedbackAPI')
/**
* POST /api/copilot/feedback
* Submit feedback for a copilot interaction
*/
export const POST = withRouteHandler(async (req: NextRequest) => {
const tracker = createRequestTracker()
try {
// Authenticate user using the same pattern as other copilot routes
const { userId: authenticatedUserId, isAuthenticated } =
await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !authenticatedUserId) {
return createUnauthorizedResponse()
}
const parsed = await parseRequest(
submitCopilotFeedbackContract,
req,
{},
{
invalidJson: 'throw',
validationErrorResponse: (error) => {
logger.error(`[${tracker.requestId}] Validation error:`, {
duration: tracker.getDuration(),
errors: error.issues,
})
return validationErrorResponse(error, 'Invalid request data')
},
}
)
if (!parsed.success) return parsed.response
const { chatId, userQuery, agentResponse, isPositiveFeedback, feedback, workflowYaml } =
parsed.data.body
logger.info(`[${tracker.requestId}] Processing copilot feedback submission`, {
userId: authenticatedUserId,
chatId,
isPositiveFeedback,
userQueryLength: userQuery.length,
agentResponseLength: agentResponse.length,
hasFeedback: !!feedback,
hasWorkflowYaml: !!workflowYaml,
workflowYamlLength: workflowYaml?.length || 0,
})
// Insert feedback into the database
const [feedbackRecord] = await db
.insert(copilotFeedback)
.values({
userId: authenticatedUserId,
chatId,
userQuery,
agentResponse,
isPositive: isPositiveFeedback,
feedback: feedback || null,
workflowYaml: workflowYaml || null,
})
.returning()
logger.info(`[${tracker.requestId}] Successfully saved copilot feedback`, {
feedbackId: feedbackRecord.feedbackId,
userId: authenticatedUserId,
isPositive: isPositiveFeedback,
duration: tracker.getDuration(),
})
captureServerEvent(authenticatedUserId, 'copilot_feedback_submitted', {
is_positive: isPositiveFeedback,
has_text_feedback: !!feedback,
has_workflow_yaml: !!workflowYaml,
})
return NextResponse.json({
success: true,
feedbackId: feedbackRecord.feedbackId,
message: 'Feedback submitted successfully',
metadata: {
requestId: tracker.requestId,
duration: tracker.getDuration(),
},
})
} catch (error) {
const duration = tracker.getDuration()
logger.error(`[${tracker.requestId}] Error submitting copilot feedback:`, {
duration,
error: getErrorMessage(error, 'Unknown error'),
stack: error instanceof Error ? error.stack : undefined,
})
return createInternalServerErrorResponse('Failed to submit feedback')
}
})
/**
* GET /api/copilot/feedback
* Get feedback records for the authenticated user
*/
export const GET = withRouteHandler(async (req: NextRequest) => {
const tracker = createRequestTracker()
try {
// Authenticate user
const { userId: authenticatedUserId, isAuthenticated } =
await authenticateCopilotRequestSessionOnly()
if (!isAuthenticated || !authenticatedUserId) {
return createUnauthorizedResponse()
}
// Get feedback records for the authenticated user only
const feedbackRecords = await db
.select({
feedbackId: copilotFeedback.feedbackId,
userId: copilotFeedback.userId,
chatId: copilotFeedback.chatId,
userQuery: copilotFeedback.userQuery,
agentResponse: copilotFeedback.agentResponse,
isPositive: copilotFeedback.isPositive,
feedback: copilotFeedback.feedback,
workflowYaml: copilotFeedback.workflowYaml,
createdAt: copilotFeedback.createdAt,
})
.from(copilotFeedback)
.where(eq(copilotFeedback.userId, authenticatedUserId))
logger.info(`[${tracker.requestId}] Retrieved ${feedbackRecords.length} feedback records`)
return NextResponse.json({
success: true,
feedback: feedbackRecords,
metadata: {
requestId: tracker.requestId,
duration: tracker.getDuration(),
},
})
} catch (error) {
logger.error(`[${tracker.requestId}] Error retrieving copilot feedback:`, error)
return createInternalServerErrorResponse('Failed to retrieve feedback')
}
})