@@ -17,7 +17,7 @@ import {
1717 MessageStatus ,
1818} from '../const' ;
1919import { GridCommands } from '../grid_commands' ;
20- import type { CommandResult } from '../types' ;
20+ import type { AIMessage , CommandResult } from '../types' ;
2121
2222jest . mock ( '../grid_commands' ) ;
2323
@@ -274,87 +274,114 @@ describe('AIAssistantController', () => {
274274
275275 await expect ( promise ) . rejects . toThrow ( 'Default error message' ) ;
276276 } ) ;
277- } ) ;
278277
279- describe ( 'isProcessing' , ( ) => {
280- it ( 'should return false by default' , ( ) => {
281- const controller = createController ( {
282- 'aiAssistant.aiIntegration' : mockAIIntegration ,
283- } ) ;
284-
285- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
286- } ) ;
287-
288- it ( 'should return true after sendRequestToAI is called' , ( ) => {
278+ it ( 'should ignore second request while first request is still processing' , async ( ) => {
289279 const controller = createController ( {
290280 'aiAssistant.aiIntegration' : mockAIIntegration ,
291281 } ) ;
292282
293283 // eslint-disable-next-line @typescript-eslint/no-floating-promises
294284 controller . sendRequestToAI ( {
295285 author : { id : 'user' , name : 'User' } ,
296- text : 'Generate values ' ,
286+ text : 'First request ' ,
297287 timestamp : '2026-04-16T10:00:00.000Z' ,
298288 } as Message ) ;
299289
300- expect ( controller . isProcessing ( ) ) . toBe ( true ) ;
290+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
291+ controller . sendRequestToAI ( {
292+ author : { id : 'user' , name : 'User' } ,
293+ text : 'Second request' ,
294+ timestamp : '2026-04-16T10:00:01.000Z' ,
295+ } as Message ) ;
296+
297+ const messages = await getStore ( controller ) . load ( ) ;
298+
299+ expect ( messages ) . toHaveLength ( 1 ) ;
300+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 1 ) ;
301301 } ) ;
302302
303- it ( 'should return false after successful command completion ' , async ( ) => {
303+ it ( 'should accept new request after previous request completes successfully ' , async ( ) => {
304304 const controller = createController ( {
305305 'aiAssistant.aiIntegration' : mockAIIntegration ,
306306 } ) ;
307307
308- const promise = controller . sendRequestToAI ( {
308+ const firstPromise = controller . sendRequestToAI ( {
309309 author : { id : 'user' , name : 'User' } ,
310- text : 'Generate values ' ,
310+ text : 'First request ' ,
311311 timestamp : '2026-04-16T10:00:00.000Z' ,
312312 } as Message ) ;
313313
314314 const actions = [ { name : 'sort' , args : { column : 'Name' } } ] ;
315315 sendRequestCallbacks . onComplete ?.( { actions } ) ;
316+ await firstPromise ;
316317
317- await promise ;
318+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
319+ controller . sendRequestToAI ( {
320+ author : { id : 'user' , name : 'User' } ,
321+ text : 'Second request' ,
322+ timestamp : '2026-04-16T10:00:01.000Z' ,
323+ } as Message ) ;
324+
325+ const messages = await getStore ( controller ) . load ( ) ;
318326
319- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
327+ expect ( messages ) . toHaveLength ( 2 ) ;
328+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 2 ) ;
320329 } ) ;
321330
322- it ( 'should return false after onError callback ' , async ( ) => {
331+ it ( 'should accept new request after previous request fails with error ' , async ( ) => {
323332 const controller = createController ( {
324333 'aiAssistant.aiIntegration' : mockAIIntegration ,
325334 } ) ;
326335
327- const promise = controller . sendRequestToAI ( {
336+ const firstPromise = controller . sendRequestToAI ( {
328337 author : { id : 'user' , name : 'User' } ,
329- text : 'Generate values ' ,
338+ text : 'First request ' ,
330339 timestamp : '2026-04-16T10:00:00.000Z' ,
331340 } as Message ) ;
332- promise . catch ( ( ) => { } ) ;
341+ firstPromise . catch ( ( ) => { } ) ;
333342
334343 sendRequestCallbacks . onError ?.( new Error ( 'Network error' ) ) ;
344+ await expect ( firstPromise ) . rejects . toThrow ( 'Network error' ) ;
335345
336- await expect ( promise ) . rejects . toThrow ( 'Network error' ) ;
346+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
347+ controller . sendRequestToAI ( {
348+ author : { id : 'user' , name : 'User' } ,
349+ text : 'Second request' ,
350+ timestamp : '2026-04-16T10:00:01.000Z' ,
351+ } as Message ) ;
352+
353+ const messages = await getStore ( controller ) . load ( ) ;
337354
338- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
355+ expect ( messages ) . toHaveLength ( 2 ) ;
356+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 2 ) ;
339357 } ) ;
340358
341- it ( 'should return false after failed command processing ' , async ( ) => {
359+ it ( 'should accept new request after previous request is aborted ' , async ( ) => {
342360 const controller = createController ( {
343361 'aiAssistant.aiIntegration' : mockAIIntegration ,
344362 } ) ;
345363
346- const promise = controller . sendRequestToAI ( {
364+ const firstPromise = controller . sendRequestToAI ( {
347365 author : { id : 'user' , name : 'User' } ,
348- text : 'Generate values ' ,
366+ text : 'First request ' ,
349367 timestamp : '2026-04-16T10:00:00.000Z' ,
350368 } as Message ) ;
351- promise . catch ( ( ) => { } ) ;
369+ firstPromise . catch ( ( ) => { } ) ;
352370
353- sendRequestCallbacks . onComplete ?.( { } as ExecuteGridAssistantCommandResult ) ;
371+ controller . abortRequest ( ) ;
372+ await expect ( firstPromise ) . rejects . toThrow ( ) ;
354373
355- await expect ( promise ) . rejects . toThrow ( 'Default error message' ) ;
374+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
375+ controller . sendRequestToAI ( {
376+ author : { id : 'user' , name : 'User' } ,
377+ text : 'Second request' ,
378+ timestamp : '2026-04-16T10:00:01.000Z' ,
379+ } as Message ) ;
380+
381+ const messages = await getStore ( controller ) . load ( ) ;
356382
357- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
383+ expect ( messages ) . toHaveLength ( 2 ) ;
384+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 2 ) ;
358385 } ) ;
359386 } ) ;
360387
@@ -387,7 +414,7 @@ describe('AIAssistantController', () => {
387414 await expect ( promise ) . rejects . toThrow ( 'Request stopped.' ) ;
388415 } ) ;
389416
390- it ( 'should set isProcessing to false when request is aborted' , async ( ) => {
417+ it ( 'should call gridCommands.abort when request is aborted' , async ( ) => {
391418 const controller = createController ( {
392419 'aiAssistant.aiIntegration' : mockAIIntegration ,
393420 } ) ;
@@ -399,34 +426,169 @@ describe('AIAssistantController', () => {
399426 } as Message ) ;
400427 promise . catch ( ( ) => { } ) ;
401428
402- expect ( controller . isProcessing ( ) ) . toBe ( true ) ;
429+ const gridCommandsInstance = MockedGridCommands . mock . results [ 0 ] . value as { abort : jest . Mock } ;
403430
404431 controller . abortRequest ( ) ;
405432
406433 await expect ( promise ) . rejects . toThrow ( ) ;
407434
408- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
435+ expect ( gridCommandsInstance . abort ) . toHaveBeenCalledTimes ( 1 ) ;
409436 } ) ;
437+ } ) ;
410438
411- it ( 'should call gridCommands.abort when request is aborted' , async ( ) => {
439+ describe ( 'sendRequestToAI with AIMessage (regenerate)' , ( ) => {
440+ it ( 'should reset message status to pending when AIMessage is passed' , async ( ) => {
412441 const controller = createController ( {
413442 'aiAssistant.aiIntegration' : mockAIIntegration ,
414443 } ) ;
415444
416- const promise = controller . sendRequestToAI ( {
417- author : { id : 'user' , name : 'User' } ,
418- text : 'Generate values' ,
419- timestamp : '2026-04-16T10:00:00.000Z' ,
420- } as Message ) ;
421- promise . catch ( ( ) => { } ) ;
445+ const aiMessage : AIMessage = {
446+ id : 'assistant-123' ,
447+ author : AI_ASSISTANT_AUTHOR ,
448+ text : MessageStatus . Failure ,
449+ prompt : 'Generate values' ,
450+ status : MessageStatus . Failure ,
451+ headerText : 'Failed to process request' ,
452+ errorText : 'Network error' ,
453+ } ;
422454
423- const gridCommandsInstance = MockedGridCommands . mock . results [ 0 ] . value as { abort : jest . Mock } ;
455+ const store = getStore ( controller ) ;
456+ await store . insert ( aiMessage ) ;
424457
425- controller . abortRequest ( ) ;
458+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
459+ controller . sendRequestToAI ( aiMessage ) ;
426460
427- await expect ( promise ) . rejects . toThrow ( ) ;
461+ const messages = await store . load ( ) ;
428462
429- expect ( gridCommandsInstance . abort ) . toHaveBeenCalledTimes ( 1 ) ;
463+ expect ( messages ) . toHaveLength ( 1 ) ;
464+ expect ( messages ) . toEqual ( [
465+ expect . objectContaining ( {
466+ id : 'assistant-123' ,
467+ status : MessageStatus . Pending ,
468+ headerText : 'Request in progress' ,
469+ text : MessageStatus . Pending ,
470+ } ) ,
471+ ] ) ;
472+ } ) ;
473+
474+ it ( 'should not create new message when AIMessage is passed' , async ( ) => {
475+ const controller = createController ( {
476+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
477+ } ) ;
478+
479+ const aiMessage : AIMessage = {
480+ id : 'assistant-123' ,
481+ author : AI_ASSISTANT_AUTHOR ,
482+ text : MessageStatus . Failure ,
483+ prompt : 'Generate values' ,
484+ status : MessageStatus . Failure ,
485+ headerText : 'Failed to process request' ,
486+ errorText : 'Network error' ,
487+ } ;
488+
489+ const store = getStore ( controller ) ;
490+ await store . insert ( aiMessage ) ;
491+
492+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
493+ controller . sendRequestToAI ( aiMessage ) ;
494+
495+ const messages = await store . load ( ) ;
496+
497+ expect ( messages ) . toHaveLength ( 1 ) ;
498+ } ) ;
499+
500+ it ( 'should send request with original prompt from AIMessage' , ( ) => {
501+ const controller = createController ( {
502+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
503+ } ) ;
504+
505+ const aiMessage : AIMessage = {
506+ id : 'assistant-123' ,
507+ author : AI_ASSISTANT_AUTHOR ,
508+ text : MessageStatus . Failure ,
509+ prompt : 'Sort by Name column' ,
510+ status : MessageStatus . Failure ,
511+ headerText : 'Failed to process request' ,
512+ errorText : 'Network error' ,
513+ } ;
514+
515+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
516+ controller . sendRequestToAI ( aiMessage ) ;
517+
518+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledWith (
519+ expect . objectContaining ( {
520+ text : 'Sort by Name column' ,
521+ } ) ,
522+ expect . any ( Object ) ,
523+ ) ;
524+ } ) ;
525+
526+ it ( 'should clear errorText and commands when regenerating' , async ( ) => {
527+ const controller = createController ( {
528+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
529+ } ) ;
530+
531+ const aiMessage : AIMessage = {
532+ id : 'assistant-123' ,
533+ author : AI_ASSISTANT_AUTHOR ,
534+ text : MessageStatus . Failure ,
535+ prompt : 'Generate values' ,
536+ status : MessageStatus . Failure ,
537+ headerText : 'Failed to process request' ,
538+ errorText : 'Network error' ,
539+ commands : [ { status : 'failure' , message : 'sort failed' } ] ,
540+ } ;
541+
542+ const store = getStore ( controller ) ;
543+ await store . insert ( aiMessage ) ;
544+
545+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
546+ controller . sendRequestToAI ( aiMessage ) ;
547+
548+ const messages = await store . load ( ) ;
549+
550+ expect ( messages ) . toEqual ( [
551+ expect . objectContaining ( {
552+ errorText : undefined ,
553+ commands : undefined ,
554+ } ) ,
555+ ] ) ;
556+ } ) ;
557+
558+ it ( 'should complete regenerated message as success when command succeed' , async ( ) => {
559+ const controller = createController ( {
560+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
561+ } ) ;
562+
563+ const aiMessage : AIMessage = {
564+ id : 'assistant-123' ,
565+ author : AI_ASSISTANT_AUTHOR ,
566+ text : MessageStatus . Failure ,
567+ prompt : 'Generate values' ,
568+ status : MessageStatus . Failure ,
569+ headerText : 'Failed to process request' ,
570+ errorText : 'Network error' ,
571+ } ;
572+
573+ const store = getStore ( controller ) ;
574+ await store . insert ( aiMessage ) ;
575+
576+ const promise = controller . sendRequestToAI ( aiMessage ) ;
577+
578+ const actions = [ { name : 'sort' , args : { column : 'Name' } } ] ;
579+ sendRequestCallbacks . onComplete ?.( { actions } ) ;
580+
581+ await promise ;
582+
583+ const messages = await store . load ( ) ;
584+
585+ expect ( messages ) . toEqual ( [
586+ expect . objectContaining ( {
587+ id : 'assistant-123' ,
588+ status : MessageStatus . Success ,
589+ commands : [ { status : 'success' , message : 'sort' } ] ,
590+ } ) ,
591+ ] ) ;
430592 } ) ;
431593 } ) ;
432594} ) ;
0 commit comments