@@ -382,18 +382,106 @@ describe('AI Routes', () => {
382382 expect ( paths ) . toContain ( 'DELETE /api/v1/ai/conversations/:id' ) ;
383383 } ) ;
384384
385- it ( 'POST /api/v1/ai/chat should return chat result' , async ( ) => {
385+ it ( 'POST /api/v1/ai/chat should return JSON result when stream=false' , async ( ) => {
386+ const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
387+ const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
388+
389+ const response = await chatRoute . handler ( {
390+ body : { messages : [ { role : 'user' , content : 'Hi' } ] , stream : false } ,
391+ } ) ;
392+
393+ expect ( response . status ) . toBe ( 200 ) ;
394+ expect ( ( response . body as any ) . content ) . toBe ( '[memory] Hi' ) ;
395+ } ) ;
396+
397+ it ( 'POST /api/v1/ai/chat should default to Vercel Data Stream mode' , async ( ) => {
386398 const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
387399 const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
388400
389401 const response = await chatRoute . handler ( {
390402 body : { messages : [ { role : 'user' , content : 'Hi' } ] } ,
391403 } ) ;
392404
405+ expect ( response . status ) . toBe ( 200 ) ;
406+ expect ( response . stream ) . toBe ( true ) ;
407+ expect ( response . vercelDataStream ) . toBe ( true ) ;
408+ expect ( response . events ) . toBeDefined ( ) ;
409+
410+ // Consume the Vercel Data Stream events
411+ const events : unknown [ ] = [ ] ;
412+ for await ( const event of response . events ! ) {
413+ events . push ( event ) ;
414+ }
415+ expect ( events . length ) . toBeGreaterThan ( 0 ) ;
416+ } ) ;
417+
418+ it ( 'POST /api/v1/ai/chat should prepend systemPrompt as system message' , async ( ) => {
419+ const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
420+ const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
421+
422+ const response = await chatRoute . handler ( {
423+ body : {
424+ messages : [ { role : 'user' , content : 'Hello' } ] ,
425+ system : 'You are a helpful assistant' ,
426+ stream : false ,
427+ } ,
428+ } ) ;
429+
430+ expect ( response . status ) . toBe ( 200 ) ;
431+ // MemoryLLMAdapter echoes the last user message
432+ expect ( ( response . body as any ) . content ) . toBe ( '[memory] Hello' ) ;
433+ } ) ;
434+
435+ it ( 'POST /api/v1/ai/chat should accept deprecated systemPrompt field' , async ( ) => {
436+ const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
437+ const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
438+
439+ const response = await chatRoute . handler ( {
440+ body : {
441+ messages : [ { role : 'user' , content : 'Hi' } ] ,
442+ systemPrompt : 'Be concise' ,
443+ stream : false ,
444+ } ,
445+ } ) ;
446+
393447 expect ( response . status ) . toBe ( 200 ) ;
394448 expect ( ( response . body as any ) . content ) . toBe ( '[memory] Hi' ) ;
395449 } ) ;
396450
451+ it ( 'POST /api/v1/ai/chat should accept flat Vercel-style fields (model, temperature)' , async ( ) => {
452+ const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
453+ const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
454+
455+ const response = await chatRoute . handler ( {
456+ body : {
457+ messages : [ { role : 'user' , content : 'Hi' } ] ,
458+ model : 'gpt-4o' ,
459+ temperature : 0.5 ,
460+ stream : false ,
461+ } ,
462+ } ) ;
463+
464+ expect ( response . status ) . toBe ( 200 ) ;
465+ // MemoryLLMAdapter uses the model from options when provided
466+ expect ( ( response . body as any ) . model ) . toBe ( 'gpt-4o' ) ;
467+ } ) ;
468+
469+ it ( 'POST /api/v1/ai/chat should accept array content (Vercel multi-part)' , async ( ) => {
470+ const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
471+ const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
472+
473+ const response = await chatRoute . handler ( {
474+ body : {
475+ messages : [ { role : 'user' , content : [ { type : 'text' , text : 'Hi' } ] } ] ,
476+ stream : false ,
477+ } ,
478+ } ) ;
479+
480+ // MemoryLLMAdapter falls back to "(complex content)" for non-string
481+ expect ( response . status ) . toBe ( 200 ) ;
482+ expect ( ( response . body as any ) . content ) . toBe ( '[memory] (complex content)' ) ;
483+ } ) ;
484+
397485 it ( 'POST /api/v1/ai/chat should return 400 without messages' , async ( ) => {
398486 const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
399487 const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
@@ -531,16 +619,30 @@ describe('AI Routes', () => {
531619 expect ( ( response . body as any ) . error ) . toContain ( 'message.role' ) ;
532620 } ) ;
533621
534- it ( 'POST /api/v1/ai/chat should return 400 for messages with non-string content' , async ( ) => {
622+ it ( 'POST /api/v1/ai/chat should return 400 for messages with non-string/non-array content' , async ( ) => {
535623 const routes = buildAIRoutes ( service , service . conversationService , silentLogger ) ;
536624 const chatRoute = routes . find ( r => r . path === '/api/v1/ai/chat' ) ! ;
537625
626+ // Numeric content should be rejected
538627 const response = await chatRoute . handler ( {
539628 body : { messages : [ { role : 'user' , content : 123 } ] } ,
540629 } ) ;
541-
542630 expect ( response . status ) . toBe ( 400 ) ;
543631 expect ( ( response . body as any ) . error ) . toContain ( 'content' ) ;
632+
633+ // Object content (not an array) should be rejected
634+ const response2 = await chatRoute . handler ( {
635+ body : { messages : [ { role : 'user' , content : { nested : true } } ] } ,
636+ } ) ;
637+ expect ( response2 . status ) . toBe ( 400 ) ;
638+ expect ( ( response2 . body as any ) . error ) . toContain ( 'content' ) ;
639+
640+ // Boolean content should be rejected
641+ const response3 = await chatRoute . handler ( {
642+ body : { messages : [ { role : 'user' , content : true } ] } ,
643+ } ) ;
644+ expect ( response3 . status ) . toBe ( 400 ) ;
645+ expect ( ( response3 . body as any ) . error ) . toContain ( 'content' ) ;
544646 } ) ;
545647
546648 it ( 'POST /api/v1/ai/conversations/:id/messages should return 400 for invalid role' , async ( ) => {
@@ -620,6 +722,7 @@ describe('AI Routes', () => {
620722 { role : 'assistant' , content : '' } ,
621723 { role : 'tool' , content : '{"temp": 22}' , toolCallId : 'call_1' } ,
622724 ] ,
725+ stream : false ,
623726 } ,
624727 } ) ;
625728
0 commit comments