Skip to content

Commit 989bb26

Browse files
author
Paul Caplan
committed
feat: add environment-controlled mock AI and comprehensive E2E tests
- Add USE_MOCK_AI environment variable to control mock vs real AI responses - Remove streaming complexity, implement simple non-streaming chat - Create comprehensive E2E test suite with 8 passing tests - Configure Playwright for fast tests (1s timeout, Chromium only, no HTML report) - Add env.test file for E2E testing configuration - Clean up environment files and remove unused OPENAI_API_KEY - Update package dependencies and switch to pnpm - All tests passing in under 2 seconds
1 parent 06c1154 commit 989bb26

9 files changed

Lines changed: 810 additions & 75 deletions

File tree

app/api/chat/route.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,34 @@ export async function POST (req: Request): Promise<Response> {
44
try {
55
const { messages } = await req.json()
66

7-
// Simple OpenRouter API call
7+
// Check if we should use mock responses
8+
if (env.USE_MOCK_AI) {
9+
console.log('Using mock AI response')
10+
const mockResponse = {
11+
id: `chatcmpl-mock-${Date.now()}`,
12+
object: 'chat.completion',
13+
created: Date.now(),
14+
model: env.OPENROUTER_MODEL,
15+
choices: [{
16+
index: 0,
17+
message: {
18+
role: 'assistant',
19+
content: "Hello! I'm a mock AI response. The chat interface is working correctly! 🎉 To use real AI responses, set USE_MOCK_AI=false in your environment variables."
20+
},
21+
finish_reason: 'stop'
22+
}]
23+
}
24+
25+
return new Response(JSON.stringify(mockResponse), {
26+
headers: {
27+
'Content-Type': 'application/json',
28+
'Cache-Control': 'no-cache'
29+
}
30+
})
31+
}
32+
33+
// Use real OpenRouter API
34+
console.log('Using real OpenRouter API')
835
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
936
method: 'POST',
1037
headers: {
@@ -17,20 +44,47 @@ export async function POST (req: Request): Promise<Response> {
1744
model: env.OPENROUTER_MODEL,
1845
messages,
1946
temperature: 0.7,
20-
max_tokens: 1000,
21-
stream: true
47+
max_tokens: 1000
2248
})
2349
})
2450

2551
if (!response.ok) {
26-
throw new Error(`OpenRouter API error: ${response.status}`)
52+
const errorText = await response.text()
53+
console.error('OpenRouter API error response:', errorText)
54+
55+
// If API key limit exceeded, return a helpful error message
56+
if (response.status === 403 && errorText.includes('Key limit exceeded')) {
57+
const errorResponse = {
58+
id: `chatcmpl-error-${Date.now()}`,
59+
object: 'chat.completion',
60+
created: Date.now(),
61+
model: env.OPENROUTER_MODEL,
62+
choices: [{
63+
index: 0,
64+
message: {
65+
role: 'assistant',
66+
content: "Sorry, your OpenRouter API key has exceeded its limit. Please check your account at https://openrouter.ai/settings/keys and either add credits or get a new API key. You can also set USE_MOCK_AI=true in your environment variables to use mock responses for testing."
67+
},
68+
finish_reason: 'stop'
69+
}]
70+
}
71+
72+
return new Response(JSON.stringify(errorResponse), {
73+
headers: {
74+
'Content-Type': 'application/json',
75+
'Cache-Control': 'no-cache'
76+
}
77+
})
78+
}
79+
80+
throw new Error(`OpenRouter API error: ${response.status} ${response.statusText} - ${errorText}`)
2781
}
2882

29-
return new Response(response.body, {
83+
const data = await response.json()
84+
return new Response(JSON.stringify(data), {
3085
headers: {
31-
'Content-Type': 'text/plain; charset=utf-8',
32-
'Cache-Control': 'no-cache',
33-
'Connection': 'keep-alive'
86+
'Content-Type': 'application/json',
87+
'Cache-Control': 'no-cache'
3488
}
3589
})
3690
} catch (error) {

app/chat/page.tsx

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,64 @@
11
'use client'
22

3-
// import { useChat } from 'ai/react' // Removed for testing
43
import { useState } from 'react'
54
import ErrorBoundary, { ChatErrorFallback } from '@/components/ErrorBoundary'
65

6+
interface Message {
7+
id: string
8+
role: 'user' | 'assistant'
9+
content: string
10+
}
11+
712
export default function ChatPage () {
8-
const [isLoading, setIsLoading] = useState(false)
13+
const [messages, setMessages] = useState<Message[]>([])
914
const [input, setInput] = useState('')
10-
11-
// Mock chat functionality for testing
12-
const messages: any[] = []
13-
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
14-
setInput(e.target.value)
15-
}
16-
const handleSubmit = async () => {}
17-
const chatLoading = false
15+
const [isLoading, setIsLoading] = useState(false)
1816

1917
const handleFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
18+
e.preventDefault()
19+
if (!input.trim() || isLoading) return
20+
21+
const userMessage: Message = {
22+
id: Date.now().toString(),
23+
role: 'user',
24+
content: input.trim()
25+
}
26+
27+
setMessages(prev => [...prev, userMessage])
28+
setInput('')
2029
setIsLoading(true)
30+
2131
try {
22-
await handleSubmit()
32+
const response = await fetch('/api/chat', {
33+
method: 'POST',
34+
headers: {
35+
'Content-Type': 'application/json'
36+
},
37+
body: JSON.stringify({
38+
messages: [...messages, userMessage]
39+
})
40+
})
41+
42+
if (!response.ok) {
43+
throw new Error(`HTTP error! status: ${response.status}`)
44+
}
45+
46+
const data = await response.json()
47+
const assistantMessage: Message = {
48+
id: (Date.now() + 1).toString(),
49+
role: 'assistant',
50+
content: data.choices[0].message.content
51+
}
52+
53+
setMessages(prev => [...prev, assistantMessage])
54+
} catch (error) {
55+
console.error('Chat error:', error)
56+
const errorMessage: Message = {
57+
id: (Date.now() + 1).toString(),
58+
role: 'assistant',
59+
content: 'Sorry, there was an error processing your message. Please try again.'
60+
}
61+
setMessages(prev => [...prev, errorMessage])
2362
} finally {
2463
setIsLoading(false)
2564
}
@@ -81,7 +120,7 @@ export default function ChatPage () {
81120
</div>
82121
))}
83122

84-
{(chatLoading || isLoading) && (
123+
{isLoading && (
85124
<div className='flex justify-start'>
86125
<div className='bg-gray-200 text-gray-900 rounded-lg px-4 py-2'>
87126
<div className='flex items-center space-x-2'>
@@ -102,17 +141,17 @@ export default function ChatPage () {
102141
<form onSubmit={handleFormSubmit} className='flex gap-2'>
103142
<input
104143
value={input}
105-
onChange={handleInputChange}
144+
onChange={(e) => setInput(e.target.value)}
106145
placeholder='Type your message here...'
107146
className='flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'
108-
disabled={chatLoading || isLoading}
147+
disabled={isLoading}
109148
/>
110149
<button
111150
type='submit'
112-
disabled={chatLoading || isLoading || !input.trim()}
151+
disabled={isLoading || !input.trim()}
113152
className='bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors'
114153
>
115-
{chatLoading || isLoading ? 'Sending...' : 'Send'}
154+
{isLoading ? 'Sending...' : 'Send'}
116155
</button>
117156
</form>
118157

env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ OPENROUTER_API_KEY=your_openrouter_api_key_here
66
# See https://openrouter.ai/models for available models
77
OPENROUTER_MODEL=gpt-4
88

9+
# Mock AI Configuration
10+
# Set to 'true' to use mock responses instead of real API calls
11+
# Useful for testing and development
12+
USE_MOCK_AI=true
13+
914
# Optional: App URL for production deployments
1015
# NEXT_PUBLIC_APP_URL=https://your-domain.com
1116

env.test

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Test Environment Configuration
2+
# This file is used for end-to-end testing
3+
4+
# OpenRouter Configuration (test values)
5+
OPENROUTER_API_KEY=test-api-key
6+
OPENROUTER_MODEL=test-model
7+
8+
# Mock AI Configuration
9+
# Always use mock responses for E2E tests
10+
USE_MOCK_AI=true
11+
12+
# Test App URL
13+
NEXT_PUBLIC_APP_URL=http://localhost:3000
14+
15+
# Node environment
16+
NODE_ENV=test

lib/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const envSchema = z.object({
66
OPENROUTER_API_KEY: z.string().min(1, 'OpenRouter API key is required'),
77
OPENROUTER_MODEL: z.string().min(1, 'OpenRouter model is required'),
88

9+
// Mock AI configuration
10+
USE_MOCK_AI: z.string().transform(val => val === 'true').default('false'),
11+
912
// Optional: Add more environment variables as needed
1013
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
1114
NEXT_PUBLIC_APP_URL: z.string().url().optional()
@@ -25,6 +28,7 @@ function validateEnv () {
2528
return {
2629
OPENROUTER_API_KEY: 'build-time-default',
2730
OPENROUTER_MODEL: 'gpt-4',
31+
USE_MOCK_AI: false,
2832
NODE_ENV: 'production' as const
2933
}
3034
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"test:e2e:headed": "playwright test --headed"
1717
},
1818
"dependencies": {
19+
"@ai-sdk/openai": "^2.0.30",
20+
"ai": "^3.0.0",
1921
"next": "^14.0.0",
2022
"react": "^18.0.0",
2123
"react-dom": "^18.0.0",
@@ -30,6 +32,7 @@
3032
"@types/react-dom": "^18.0.0",
3133
"@vitejs/plugin-react": "^4.0.0",
3234
"autoprefixer": "^10.0.0",
35+
"dotenv": "^17.2.2",
3336
"eslint": "^8.0.0",
3437
"jsdom": "^24.0.0",
3538
"postcss": "^8.0.0",

playwright.config.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,34 @@ export default defineConfig({
66
forbidOnly: !!process.env.CI,
77
retries: process.env.CI ? 2 : 0,
88
workers: process.env.CI ? 1 : undefined,
9-
reporter: 'html',
9+
reporter: 'list',
10+
timeout: 1000, // 1 second timeout
1011
use: {
1112
baseURL: 'http://localhost:3000',
12-
trace: 'on-first-retry'
13+
trace: 'on-first-retry',
14+
actionTimeout: 1000, // 1 second for actions
15+
navigationTimeout: 5000 // 5 seconds for navigation
1316
},
1417

1518
projects: [
1619
{
1720
name: 'chromium',
1821
use: { ...devices['Desktop Chrome'] }
19-
},
20-
{
21-
name: 'firefox',
22-
use: { ...devices['Desktop Firefox'] }
23-
},
24-
{
25-
name: 'webkit',
26-
use: { ...devices['Desktop Safari'] }
2722
}
2823
],
2924

30-
webServer: {
31-
command: 'npm run build && npm run start',
32-
url: 'http://localhost:3000',
33-
reuseExistingServer: !process.env.CI
34-
}
25+
// webServer: {
26+
// command: 'pnpm run dev -- --port 3001',
27+
// url: 'http://localhost:3001',
28+
// reuseExistingServer: !process.env.CI,
29+
// timeout: 120 * 1000, // 2 minutes
30+
// env: {
31+
// // Test environment variables
32+
// OPENROUTER_API_KEY: 'test-api-key',
33+
// OPENROUTER_MODEL: 'test-model',
34+
// USE_MOCK_AI: 'true',
35+
// NEXT_PUBLIC_APP_URL: 'http://localhost:3001',
36+
// NODE_ENV: 'test'
37+
// }
38+
// }
3539
})

0 commit comments

Comments
 (0)