@@ -3,7 +3,7 @@ import { CompactionHandler } from "./compactionHandler";
33import type { HistoryService } from "./historyService" ;
44import type { EventEmitter } from "events" ;
55import { createMuxMessage , type MuxMessage } from "@/common/types/message" ;
6- import type { StreamEndEvent , StreamAbortEvent } from "@/common/types/stream" ;
6+ import type { StreamEndEvent } from "@/common/types/stream" ;
77import { Ok , Err , type Result } from "@/common/types/result" ;
88import type { LanguageModelV2Usage } from "@ai-sdk/provider" ;
99
@@ -98,21 +98,6 @@ const createStreamEndEvent = (
9898 } ,
9999} ) ;
100100
101- const createStreamAbortEvent = (
102- abandonPartial = false ,
103- metadata ?: Record < string , unknown >
104- ) : StreamAbortEvent => ( {
105- type : "stream-abort" ,
106- workspaceId : "test-workspace" ,
107- messageId : "msg-id" ,
108- abandonPartial,
109- metadata : {
110- usage : { inputTokens : 100 , outputTokens : 25 , totalTokens : undefined } ,
111- duration : 800 ,
112- ...metadata ,
113- } ,
114- } ) ;
115-
116101// DRY helper to set up successful compaction scenario
117102const setupSuccessfulCompaction = (
118103 mockHistoryService : ReturnType < typeof createMockHistoryService > ,
@@ -145,227 +130,6 @@ describe("CompactionHandler", () => {
145130 } ) ;
146131 } ) ;
147132
148- describe ( "handleAbort() - Ctrl+C (cancel) Flow" , ( ) => {
149- it ( "should return false when no compaction request found in history" , async ( ) => {
150- const normalUserMsg = createMuxMessage ( "msg1" , "user" , "Hello" , {
151- historySequence : 0 ,
152- muxMetadata : { type : "normal" } ,
153- } ) ;
154- mockHistoryService . mockGetHistory ( Ok ( [ normalUserMsg ] ) ) ;
155-
156- const event = createStreamAbortEvent ( false ) ;
157- const result = await handler . handleAbort ( event ) ;
158-
159- expect ( result ) . toBe ( false ) ;
160- expect ( emittedEvents ) . toHaveLength ( 0 ) ;
161- } ) ;
162-
163- it ( "should return false when abandonPartial=true (Ctrl+C cancel)" , async ( ) => {
164- const compactionReq = createCompactionRequest ( ) ;
165- const assistantMsg = createAssistantMessage ( "Partial summary..." ) ;
166- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
167-
168- const event = createStreamAbortEvent ( true ) ;
169- const result = await handler . handleAbort ( event ) ;
170-
171- expect ( result ) . toBe ( false ) ;
172- expect ( mockHistoryService . clearHistory . mock . calls ) . toHaveLength ( 0 ) ;
173- expect ( emittedEvents ) . toHaveLength ( 0 ) ;
174- } ) ;
175-
176- it ( "should not perform compaction when cancelled" , async ( ) => {
177- const compactionReq = createCompactionRequest ( ) ;
178- const assistantMsg = createAssistantMessage ( "Partial summary" ) ;
179- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
180-
181- const event = createStreamAbortEvent ( true ) ;
182- await handler . handleAbort ( event ) ;
183-
184- expect ( mockHistoryService . clearHistory . mock . calls ) . toHaveLength ( 0 ) ;
185- expect ( mockHistoryService . appendToHistory . mock . calls ) . toHaveLength ( 0 ) ;
186- } ) ;
187-
188- it ( "should not emit events when cancelled" , async ( ) => {
189- const compactionReq = createCompactionRequest ( ) ;
190- const assistantMsg = createAssistantMessage ( "Partial" ) ;
191- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
192-
193- const event = createStreamAbortEvent ( true ) ;
194- await handler . handleAbort ( event ) ;
195-
196- expect ( emittedEvents ) . toHaveLength ( 0 ) ;
197- } ) ;
198- } ) ;
199-
200- describe ( "handleAbort() - Ctrl+A (accept early) Flow" , ( ) => {
201- it ( "should return false when last message is not assistant role" , async ( ) => {
202- const compactionReq = createCompactionRequest ( ) ;
203- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq ] ) ) ;
204-
205- const event = createStreamAbortEvent ( false ) ;
206- const result = await handler . handleAbort ( event ) ;
207-
208- expect ( result ) . toBe ( false ) ;
209- } ) ;
210-
211- it ( "should return true when successful" , async ( ) => {
212- const compactionReq = createCompactionRequest ( ) ;
213- const assistantMsg = createAssistantMessage ( "Partial summary" ) ;
214- setupSuccessfulCompaction ( mockHistoryService , [ compactionReq , assistantMsg ] ) ;
215-
216- const event = createStreamAbortEvent ( false ) ;
217- const result = await handler . handleAbort ( event ) ;
218-
219- expect ( result ) . toBe ( true ) ;
220- } ) ;
221-
222- it ( "should read partial summary from last assistant message in history" , async ( ) => {
223- const compactionReq = createCompactionRequest ( ) ;
224- const assistantMsg = createAssistantMessage ( "Here is a partial summary" ) ;
225- setupSuccessfulCompaction ( mockHistoryService , [ compactionReq , assistantMsg ] ) ;
226-
227- const event = createStreamAbortEvent ( false ) ;
228- await handler . handleAbort ( event ) ;
229-
230- expect ( mockHistoryService . appendToHistory . mock . calls ) . toHaveLength ( 1 ) ;
231- const appendedMsg = mockHistoryService . appendToHistory . mock . calls [ 0 ] [ 1 ] as MuxMessage ;
232- expect ( ( appendedMsg . parts [ 0 ] as { type : "text" ; text : string } ) . text ) . toContain (
233- "Here is a partial summary"
234- ) ;
235- } ) ;
236-
237- it ( "should append [truncated] sentinel to partial summary" , async ( ) => {
238- const compactionReq = createCompactionRequest ( ) ;
239- const assistantMsg = createAssistantMessage ( "Partial text" ) ;
240- setupSuccessfulCompaction ( mockHistoryService , [ compactionReq , assistantMsg ] ) ;
241-
242- const event = createStreamAbortEvent ( false ) ;
243- await handler . handleAbort ( event ) ;
244-
245- const appendedMsg = mockHistoryService . appendToHistory . mock . calls [ 0 ] [ 1 ] as MuxMessage ;
246- expect ( ( appendedMsg . parts [ 0 ] as { type : "text" ; text : string } ) . text ) . toContain (
247- "[truncated]"
248- ) ;
249- } ) ;
250-
251- it ( "should call clearHistory() and appendToHistory() with summary message" , async ( ) => {
252- const compactionReq = createCompactionRequest ( ) ;
253- const assistantMsg = createAssistantMessage ( "Summary" ) ;
254- setupSuccessfulCompaction ( mockHistoryService , [ compactionReq , assistantMsg ] ) ;
255-
256- const event = createStreamAbortEvent ( false ) ;
257- await handler . handleAbort ( event ) ;
258-
259- expect ( mockHistoryService . clearHistory . mock . calls ) . toHaveLength ( 1 ) ;
260- expect ( mockHistoryService . clearHistory . mock . calls [ 0 ] [ 0 ] ) . toBe ( workspaceId ) ;
261- expect ( mockHistoryService . appendToHistory . mock . calls ) . toHaveLength ( 1 ) ;
262- expect ( mockHistoryService . appendToHistory . mock . calls [ 0 ] [ 0 ] ) . toBe ( workspaceId ) ;
263- const appendedMsg = mockHistoryService . appendToHistory . mock . calls [ 0 ] [ 1 ] as MuxMessage ;
264- expect ( appendedMsg . role ) . toBe ( "assistant" ) ;
265- expect ( ( appendedMsg . parts [ 0 ] as { type : "text" ; text : string } ) . text ) . toContain (
266- "[truncated]"
267- ) ;
268- } ) ;
269-
270- it ( "should emit delete event with cleared sequence numbers" , async ( ) => {
271- const compactionReq = createCompactionRequest ( ) ;
272- const assistantMsg = createAssistantMessage ( "Summary" ) ;
273- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
274- mockHistoryService . mockClearHistory ( Ok ( [ 0 , 1 , 2 ] ) ) ;
275- mockHistoryService . mockAppendToHistory ( Ok ( undefined ) ) ;
276-
277- const event = createStreamAbortEvent ( false ) ;
278- await handler . handleAbort ( event ) ;
279-
280- const deleteEvent = emittedEvents . find (
281- ( _e ) => ( _e . data . message as { type ?: string } ) ?. type === "delete"
282- ) ;
283- expect ( deleteEvent ) . toBeDefined ( ) ;
284- expect ( deleteEvent ?. data ) . toEqual ( {
285- workspaceId,
286- message : {
287- type : "delete" ,
288- historySequences : [ 0 , 1 , 2 ] ,
289- } ,
290- } ) ;
291- } ) ;
292-
293- it ( "should emit summary message as assistant message" , async ( ) => {
294- const compactionReq = createCompactionRequest ( ) ;
295- const assistantMsg = createAssistantMessage ( "Summary text" ) ;
296- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
297- mockHistoryService . mockClearHistory ( Ok ( [ 0 , 1 ] ) ) ;
298- mockHistoryService . mockAppendToHistory ( Ok ( undefined ) ) ;
299-
300- const event = createStreamAbortEvent ( false ) ;
301- await handler . handleAbort ( event ) ;
302-
303- const summaryEvent = emittedEvents . find ( ( _e ) => {
304- const msg = _e . data . message as MuxMessage | undefined ;
305- return msg ?. role === "assistant" && msg ?. parts !== undefined ;
306- } ) ;
307- expect ( summaryEvent ) . toBeDefined ( ) ;
308- expect ( summaryEvent ?. data . workspaceId ) . toBe ( workspaceId ) ;
309- const summaryMsg = summaryEvent ?. data . message as MuxMessage ;
310- expect ( ( summaryMsg . parts [ 0 ] as { type : "text" ; text : string } ) . text ) . toContain ( "[truncated]" ) ;
311- } ) ;
312-
313- it ( "should emit original stream-abort event to frontend" , async ( ) => {
314- const compactionReq = createCompactionRequest ( ) ;
315- const assistantMsg = createAssistantMessage ( "Summary" ) ;
316- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
317- mockHistoryService . mockClearHistory ( Ok ( [ 0 , 1 ] ) ) ;
318- mockHistoryService . mockAppendToHistory ( Ok ( undefined ) ) ;
319-
320- const event = createStreamAbortEvent ( false , { duration : 999 } ) ;
321- await handler . handleAbort ( event ) ;
322-
323- const abortEvent = emittedEvents . find ( ( _e ) => _e . data . message === event ) ;
324- expect ( abortEvent ) . toBeDefined ( ) ;
325- expect ( abortEvent ?. event ) . toBe ( "chat-event" ) ;
326- expect ( abortEvent ?. data . workspaceId ) . toBe ( workspaceId ) ;
327- const abortMsg = abortEvent ?. data . message as StreamAbortEvent ;
328- expect ( abortMsg . metadata ?. duration ) . toBe ( 999 ) ;
329- } ) ;
330-
331- it ( "should preserve metadata (model, usage, duration, systemMessageTokens)" , async ( ) => {
332- const compactionReq = createCompactionRequest ( ) ;
333- const usage = { inputTokens : 100 , outputTokens : 25 , totalTokens : 125 } ;
334- const assistantMsg = createAssistantMessage ( "Summary" , {
335- usage,
336- duration : 800 ,
337- model : "claude-3-opus-20240229" ,
338- } ) ;
339- assistantMsg . metadata ! . systemMessageTokens = 50 ;
340- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
341- mockHistoryService . mockClearHistory ( Ok ( [ 0 , 1 ] ) ) ;
342- mockHistoryService . mockAppendToHistory ( Ok ( undefined ) ) ;
343-
344- const event = createStreamAbortEvent ( false , { usage, duration : 800 } ) ;
345- await handler . handleAbort ( event ) ;
346-
347- const appendedMsg = mockHistoryService . appendToHistory . mock . calls [ 0 ] [ 1 ] as MuxMessage ;
348- expect ( appendedMsg . metadata ?. model ) . toBe ( "claude-3-opus-20240229" ) ;
349- expect ( appendedMsg . metadata ?. usage ) . toEqual ( usage ) ;
350- expect ( appendedMsg . metadata ?. duration ) . toBe ( 800 ) ;
351- expect ( appendedMsg . metadata ?. systemMessageTokens ) . toBe ( 50 ) ;
352- } ) ;
353-
354- it ( "should handle empty partial text gracefully (just [truncated])" , async ( ) => {
355- const compactionReq = createCompactionRequest ( ) ;
356- const assistantMsg = createAssistantMessage ( "" ) ;
357- mockHistoryService . mockGetHistory ( Ok ( [ compactionReq , assistantMsg ] ) ) ;
358- mockHistoryService . mockClearHistory ( Ok ( [ 0 , 1 ] ) ) ;
359- mockHistoryService . mockAppendToHistory ( Ok ( undefined ) ) ;
360-
361- const event = createStreamAbortEvent ( false ) ;
362- await handler . handleAbort ( event ) ;
363-
364- const appendedMsg = mockHistoryService . appendToHistory . mock . calls [ 0 ] [ 1 ] as MuxMessage ;
365- expect ( ( appendedMsg . parts [ 0 ] as { type : "text" ; text : string } ) . text ) . toBe ( "\n\n[truncated]" ) ;
366- } ) ;
367- } ) ;
368-
369133 describe ( "handleCompletion() - Normal Compaction Flow" , ( ) => {
370134 it ( "should return false when no compaction request found" , async ( ) => {
371135 const normalMsg = createMuxMessage ( "msg1" , "user" , "Hello" , {
0 commit comments