import { expect, test, describe, beforeAll, afterAll } from 'vitest' import { post } from '@/tests/helpers/e2etest' import { startMockServer, stopMockServer } from '@/tests/mocks/start-mock-server' describe('AI Search Routes', () => { beforeAll(() => { startMockServer() }) afterAll(() => stopMockServer()) test('/api/ai-search/v1 should handle a successful response', async () => { const apiBody = { query: 'How do I create a Repository?', language: 'en', version: 'dotcom' } const response = await fetch('http://localhost:4000/api/ai-search/v1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(apiBody), }) expect(response.ok).toBe(true) expect(response.headers.get('content-type')).toBe('application/x-ndjson') expect(response.headers.get('transfer-encoding')).toBe('chunked') if (!response.body) { throw new Error('ReadableStream not supported in this environment.') } const decoder = new TextDecoder('utf-8') const reader = response.body.getReader() let done = false const chunks = [] while (!done) { const { value, done: readerDone } = await reader.read() done = readerDone if (value) { // Decode the Uint8Array chunk into a string const chunkStr = decoder.decode(value, { stream: true }) chunks.push(chunkStr) } } // Combine all chunks into a single string const fullResponse = chunks.join('') // Split the response into individual chunk lines const chunkLines = fullResponse.split('\n').filter((line) => line.trim() !== '') // Assertions: // 1. First chunk should be the SOURCES chunk expect(chunkLines.length).toBeGreaterThan(0) const firstChunkMatch = chunkLines[0].match(/^Chunk: (.+)$/) expect(firstChunkMatch).not.toBeNull() const sourcesChunk = JSON.parse(firstChunkMatch?.[1] || '') expect(sourcesChunk).toHaveProperty('chunkType', 'SOURCES') expect(sourcesChunk).toHaveProperty('sources') expect(Array.isArray(sourcesChunk.sources)).toBe(true) expect(sourcesChunk.sources.length).toBe(3) // 2. Subsequent chunks should be MESSAGE_CHUNKs for (let i = 1; i < chunkLines.length; i++) { const line = chunkLines[i] const messageChunk = JSON.parse(line) expect(messageChunk).toHaveProperty('chunkType', 'MESSAGE_CHUNK') expect(messageChunk).toHaveProperty('text') expect(typeof messageChunk.text).toBe('string') } // 3. Verify the complete message is expected const expectedMessage = 'Creating a repository on GitHub is something you should already know how to do :shrug:' const receivedMessage = chunkLines .slice(1) .map((line) => JSON.parse(line).text) .join('') expect(receivedMessage).toBe(expectedMessage) }) test('should handle validation errors: query missing', async () => { const body = { language: 'en', version: 'dotcom' } const response = await post('/api/ai-search/v1', { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) const responseBody = JSON.parse(response.body) expect(response.ok).toBe(false) expect(responseBody['errors']).toEqual([ { message: `Missing required key 'query' in request body` }, ]) }) test('should handle validation errors: version missing', async () => { const body = { query: 'example query' } const response = await post('/api/ai-search/v1', { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) const responseBody = JSON.parse(response.body) expect(response.ok).toBe(false) expect(responseBody['errors']).toEqual([ { message: `Missing required key 'version' in request body` }, ]) }) test('should handle multiple validation errors: query missing and version', async () => { const body = { language: 'fr', version: 'fpt' } const response = await post('/api/ai-search/v1', { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) const responseBody = JSON.parse(response.body) expect(response.ok).toBe(false) expect(responseBody['errors']).toEqual([ { message: `Missing required key 'query' in request body` }, ]) }) test('should handle streaming response correctly', async () => { // This test verifies the streaming response processing works const body = { query: 'test streaming query', version: 'dotcom' } const response = await fetch('http://localhost:4000/api/ai-search/v1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) expect(response.ok).toBe(true) expect(response.headers.get('content-type')).toBe('application/x-ndjson') // Verify we can read the stream without errors if (response.body) { const reader = response.body.getReader() const decoder = new TextDecoder() const chunks = [] try { while (true) { const { done, value } = await reader.read() if (done) break chunks.push(decoder.decode(value, { stream: true })) } expect(chunks.length).toBeGreaterThan(0) } finally { reader.releaseLock() } } }) test('should handle invalid version parameter', async () => { const body = { query: 'test query', version: 'invalid-version' } const response = await post('/api/ai-search/v1', { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) const responseBody = JSON.parse(response.body) expect(response.statusCode).toBe(400) expect(responseBody.errors).toBeDefined() expect(responseBody.errors[0].message).toContain("Invalid 'version' in request body") }) test('should handle non-string query parameter', async () => { const body = { query: 123, version: 'dotcom' } const response = await post('/api/ai-search/v1', { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' }, }) const responseBody = JSON.parse(response.body) expect(response.statusCode).toBe(400) expect(responseBody.errors).toBeDefined() expect(responseBody.errors[0].message).toBe("Invalid 'query' in request body. Must be a string") }) test('should handle malformed JSON in request body', async () => { const response = await post('/api/ai-search/v1', { body: '{ invalid json }', headers: { 'Content-Type': 'application/json' }, }) expect(response.statusCode).toBe(400) }) })