22 * @vitest -environment node
33 */
44
5- import { NextRequest } from " next/server" ;
6- import { beforeEach , describe , expect , it , vi } from " vitest" ;
5+ import { NextRequest } from ' next/server'
6+ import { beforeEach , describe , expect , it , vi } from ' vitest'
77import {
88 MothershipStreamV1CompletionStatus ,
99 MothershipStreamV1EventType ,
10- } from " @/lib/copilot/generated/mothership-stream-v1" ;
10+ } from ' @/lib/copilot/generated/mothership-stream-v1'
1111
1212const {
1313 getLatestRunForStream,
@@ -21,13 +21,13 @@ const {
2121 readFilePreviewSessions : vi . fn ( ) ,
2222 checkForReplayGap : vi . fn ( ) ,
2323 authenticateCopilotRequestSessionOnly : vi . fn ( ) ,
24- } ) ) ;
24+ } ) )
2525
26- vi . mock ( " @/lib/copilot/async-runs/repository" , ( ) => ( {
26+ vi . mock ( ' @/lib/copilot/async-runs/repository' , ( ) => ( {
2727 getLatestRunForStream,
28- } ) ) ;
28+ } ) )
2929
30- vi . mock ( " @/lib/copilot/request/session" , ( ) => ( {
30+ vi . mock ( ' @/lib/copilot/request/session' , ( ) => ( {
3131 readEvents,
3232 readFilePreviewSessions,
3333 checkForReplayGap,
@@ -37,180 +37,172 @@ vi.mock("@/lib/copilot/request/session", () => ({
3737 cursor : event . cursor ,
3838 } ,
3939 seq : event . seq ,
40- trace : { requestId : event . requestId ?? "" } ,
40+ trace : { requestId : event . requestId ?? '' } ,
4141 type : event . type ,
4242 payload : event . payload ,
4343 } ) ,
4444 encodeSSEEnvelope : ( event : Record < string , unknown > ) =>
4545 new TextEncoder ( ) . encode ( `data: ${ JSON . stringify ( event ) } \n\n` ) ,
4646 SSE_RESPONSE_HEADERS : {
47- " Content-Type" : " text/event-stream" ,
47+ ' Content-Type' : ' text/event-stream' ,
4848 } ,
49- } ) ) ;
49+ } ) )
5050
51- vi . mock ( " @/lib/copilot/request/http" , ( ) => ( {
51+ vi . mock ( ' @/lib/copilot/request/http' , ( ) => ( {
5252 authenticateCopilotRequestSessionOnly,
53- } ) ) ;
53+ } ) )
5454
55- import { GET } from " ./route" ;
55+ import { GET } from ' ./route'
5656
5757async function readAllChunks ( response : Response ) : Promise < string [ ] > {
58- const reader = response . body ?. getReader ( ) ;
59- expect ( reader ) . toBeTruthy ( ) ;
58+ const reader = response . body ?. getReader ( )
59+ expect ( reader ) . toBeTruthy ( )
6060
61- const chunks : string [ ] = [ ] ;
61+ const chunks : string [ ] = [ ]
6262 while ( true ) {
63- const { done, value } = await reader ! . read ( ) ;
63+ const { done, value } = await reader ! . read ( )
6464 if ( done ) {
65- break ;
65+ break
6666 }
67- chunks . push ( new TextDecoder ( ) . decode ( value ) ) ;
67+ chunks . push ( new TextDecoder ( ) . decode ( value ) )
6868 }
69- return chunks ;
69+ return chunks
7070}
7171
72- describe ( " copilot chat stream replay route" , ( ) => {
72+ describe ( ' copilot chat stream replay route' , ( ) => {
7373 beforeEach ( ( ) => {
74- vi . clearAllMocks ( ) ;
74+ vi . clearAllMocks ( )
7575 authenticateCopilotRequestSessionOnly . mockResolvedValue ( {
76- userId : " user-1" ,
76+ userId : ' user-1' ,
7777 isAuthenticated : true ,
78- } ) ;
79- readEvents . mockResolvedValue ( [ ] ) ;
80- readFilePreviewSessions . mockResolvedValue ( [ ] ) ;
81- checkForReplayGap . mockResolvedValue ( null ) ;
82- } ) ;
78+ } )
79+ readEvents . mockResolvedValue ( [ ] )
80+ readFilePreviewSessions . mockResolvedValue ( [ ] )
81+ checkForReplayGap . mockResolvedValue ( null )
82+ } )
8383
84- it ( " returns preview sessions in batch mode" , async ( ) => {
84+ it ( ' returns preview sessions in batch mode' , async ( ) => {
8585 getLatestRunForStream . mockResolvedValue ( {
86- status : " active" ,
87- executionId : " exec-1" ,
88- id : " run-1" ,
89- } ) ;
86+ status : ' active' ,
87+ executionId : ' exec-1' ,
88+ id : ' run-1' ,
89+ } )
9090 readFilePreviewSessions . mockResolvedValue ( [
9191 {
9292 schemaVersion : 1 ,
93- id : " preview-1" ,
94- streamId : " stream-1" ,
95- toolCallId : " preview-1" ,
96- status : " streaming" ,
97- fileName : " draft.md" ,
98- previewText : " hello" ,
93+ id : ' preview-1' ,
94+ streamId : ' stream-1' ,
95+ toolCallId : ' preview-1' ,
96+ status : ' streaming' ,
97+ fileName : ' draft.md' ,
98+ previewText : ' hello' ,
9999 previewVersion : 2 ,
100- updatedAt : " 2026-04-10T00:00:00.000Z" ,
100+ updatedAt : ' 2026-04-10T00:00:00.000Z' ,
101101 } ,
102- ] ) ;
102+ ] )
103103
104104 const response = await GET (
105105 new NextRequest (
106- " http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0&batch=true" ,
107- ) ,
108- ) ;
106+ ' http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0&batch=true'
107+ )
108+ )
109109
110- expect ( response . status ) . toBe ( 200 ) ;
110+ expect ( response . status ) . toBe ( 200 )
111111 await expect ( response . json ( ) ) . resolves . toMatchObject ( {
112112 success : true ,
113113 previewSessions : [
114114 expect . objectContaining ( {
115- id : " preview-1" ,
116- previewText : " hello" ,
115+ id : ' preview-1' ,
116+ previewText : ' hello' ,
117117 previewVersion : 2 ,
118118 } ) ,
119119 ] ,
120- status : " active" ,
121- } ) ;
122- } ) ;
120+ status : ' active' ,
121+ } )
122+ } )
123123
124- it ( " stops replay polling when run becomes cancelled" , async ( ) => {
124+ it ( ' stops replay polling when run becomes cancelled' , async ( ) => {
125125 getLatestRunForStream
126126 . mockResolvedValueOnce ( {
127- status : " active" ,
128- executionId : " exec-1" ,
129- id : " run-1" ,
127+ status : ' active' ,
128+ executionId : ' exec-1' ,
129+ id : ' run-1' ,
130130 } )
131131 . mockResolvedValueOnce ( {
132- status : " cancelled" ,
133- executionId : " exec-1" ,
134- id : " run-1" ,
135- } ) ;
132+ status : ' cancelled' ,
133+ executionId : ' exec-1' ,
134+ id : ' run-1' ,
135+ } )
136136
137137 const response = await GET (
138- new NextRequest (
139- "http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0" ,
140- ) ,
141- ) ;
138+ new NextRequest ( 'http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0' )
139+ )
142140
143- const chunks = await readAllChunks ( response ) ;
144- expect ( chunks . join ( "" ) ) . toContain (
141+ const chunks = await readAllChunks ( response )
142+ expect ( chunks . join ( '' ) ) . toContain (
145143 JSON . stringify ( {
146144 status : MothershipStreamV1CompletionStatus . cancelled ,
147- reason : " terminal_status" ,
148- } ) ,
149- ) ;
150- expect ( getLatestRunForStream ) . toHaveBeenCalledTimes ( 2 ) ;
151- } ) ;
145+ reason : ' terminal_status' ,
146+ } )
147+ )
148+ expect ( getLatestRunForStream ) . toHaveBeenCalledTimes ( 2 )
149+ } )
152150
153- it ( " emits structured terminal replay error when run metadata disappears" , async ( ) => {
151+ it ( ' emits structured terminal replay error when run metadata disappears' , async ( ) => {
154152 getLatestRunForStream
155153 . mockResolvedValueOnce ( {
156- status : " active" ,
157- executionId : " exec-1" ,
158- id : " run-1" ,
154+ status : ' active' ,
155+ executionId : ' exec-1' ,
156+ id : ' run-1' ,
159157 } )
160- . mockResolvedValueOnce ( null ) ;
158+ . mockResolvedValueOnce ( null )
161159
162160 const response = await GET (
163- new NextRequest (
164- "http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0" ,
165- ) ,
166- ) ;
167-
168- const chunks = await readAllChunks ( response ) ;
169- const body = chunks . join ( "" ) ;
170- expect ( body ) . toContain ( `"type":"${ MothershipStreamV1EventType . error } "` ) ;
171- expect ( body ) . toContain ( '"code":"resume_run_unavailable"' ) ;
172- expect ( body ) . toContain ( `"type":"${ MothershipStreamV1EventType . complete } "` ) ;
173- } ) ;
174-
175- it ( "uses the latest live request id for synthetic terminal replay events" , async ( ) => {
161+ new NextRequest ( 'http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0' )
162+ )
163+
164+ const chunks = await readAllChunks ( response )
165+ const body = chunks . join ( '' )
166+ expect ( body ) . toContain ( `"type":"${ MothershipStreamV1EventType . error } "` )
167+ expect ( body ) . toContain ( '"code":"resume_run_unavailable"' )
168+ expect ( body ) . toContain ( `"type":"${ MothershipStreamV1EventType . complete } "` )
169+ } )
170+
171+ it ( 'uses the latest live request id for synthetic terminal replay events' , async ( ) => {
176172 getLatestRunForStream
177173 . mockResolvedValueOnce ( {
178- status : " active" ,
179- executionId : " exec-1" ,
180- id : " run-1" ,
174+ status : ' active' ,
175+ executionId : ' exec-1' ,
176+ id : ' run-1' ,
181177 } )
182178 . mockResolvedValueOnce ( {
183- status : " cancelled" ,
184- executionId : " exec-1" ,
185- id : " run-1" ,
186- } ) ;
179+ status : ' cancelled' ,
180+ executionId : ' exec-1' ,
181+ id : ' run-1' ,
182+ } )
187183 readEvents
188184 . mockResolvedValueOnce ( [
189185 {
190- stream : { streamId : " stream-1" , cursor : "1" } ,
186+ stream : { streamId : ' stream-1' , cursor : '1' } ,
191187 seq : 1 ,
192- trace : { requestId : " req-live-123" } ,
188+ trace : { requestId : ' req-live-123' } ,
193189 type : MothershipStreamV1EventType . text ,
194190 payload : {
195- channel : " assistant" ,
196- text : " hello" ,
191+ channel : ' assistant' ,
192+ text : ' hello' ,
197193 } ,
198194 } ,
199195 ] )
200- . mockResolvedValueOnce ( [ ] ) ;
196+ . mockResolvedValueOnce ( [ ] )
201197
202198 const response = await GET (
203- new NextRequest (
204- "http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0" ,
205- ) ,
206- ) ;
207-
208- const chunks = await readAllChunks ( response ) ;
209- const terminalChunk = chunks [ chunks . length - 1 ] ?? "" ;
210- expect ( terminalChunk ) . toContain (
211- `"type":"${ MothershipStreamV1EventType . complete } "` ,
212- ) ;
213- expect ( terminalChunk ) . toContain ( '"requestId":"req-live-123"' ) ;
214- expect ( terminalChunk ) . toContain ( '"status":"cancelled"' ) ;
215- } ) ;
216- } ) ;
199+ new NextRequest ( 'http://localhost:3000/api/copilot/chat/stream?streamId=stream-1&after=0' )
200+ )
201+
202+ const chunks = await readAllChunks ( response )
203+ const terminalChunk = chunks [ chunks . length - 1 ] ?? ''
204+ expect ( terminalChunk ) . toContain ( `"type":"${ MothershipStreamV1EventType . complete } "` )
205+ expect ( terminalChunk ) . toContain ( '"requestId":"req-live-123"' )
206+ expect ( terminalChunk ) . toContain ( '"status":"cancelled"' )
207+ } )
208+ } )
0 commit comments