@@ -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,113 @@ 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+ controller . sendRequestToAI ( {
291+ author : { id : 'user' , name : 'User' } ,
292+ text : 'Second request' ,
293+ timestamp : '2026-04-16T10:00:01.000Z' ,
294+ } as Message ) . catch ( ( ) => { } ) ;
295+
296+ const messages = await getStore ( controller ) . load ( ) ;
297+
298+ expect ( messages ) . toHaveLength ( 1 ) ;
299+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 1 ) ;
301300 } ) ;
302301
303- it ( 'should return false after successful command completion ' , async ( ) => {
302+ it ( 'should accept new request after previous request completes successfully ' , async ( ) => {
304303 const controller = createController ( {
305304 'aiAssistant.aiIntegration' : mockAIIntegration ,
306305 } ) ;
307306
308- const promise = controller . sendRequestToAI ( {
307+ const firstPromise = controller . sendRequestToAI ( {
309308 author : { id : 'user' , name : 'User' } ,
310- text : 'Generate values ' ,
309+ text : 'First request ' ,
311310 timestamp : '2026-04-16T10:00:00.000Z' ,
312311 } as Message ) ;
313312
314313 const actions = [ { name : 'sort' , args : { column : 'Name' } } ] ;
315314 sendRequestCallbacks . onComplete ?.( { actions } ) ;
315+ await firstPromise ;
316316
317- await promise ;
317+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
318+ controller . sendRequestToAI ( {
319+ author : { id : 'user' , name : 'User' } ,
320+ text : 'Second request' ,
321+ timestamp : '2026-04-16T10:00:01.000Z' ,
322+ } as Message ) ;
323+
324+ const messages = await getStore ( controller ) . load ( ) ;
318325
319- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
326+ expect ( messages ) . toHaveLength ( 2 ) ;
327+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 2 ) ;
320328 } ) ;
321329
322- it ( 'should return false after onError callback ' , async ( ) => {
330+ it ( 'should accept new request after previous request fails with error ' , async ( ) => {
323331 const controller = createController ( {
324332 'aiAssistant.aiIntegration' : mockAIIntegration ,
325333 } ) ;
326334
327- const promise = controller . sendRequestToAI ( {
335+ const firstPromise = controller . sendRequestToAI ( {
328336 author : { id : 'user' , name : 'User' } ,
329- text : 'Generate values ' ,
337+ text : 'First request ' ,
330338 timestamp : '2026-04-16T10:00:00.000Z' ,
331339 } as Message ) ;
332- promise . catch ( ( ) => { } ) ;
340+ firstPromise . catch ( ( ) => { } ) ;
333341
334342 sendRequestCallbacks . onError ?.( new Error ( 'Network error' ) ) ;
343+ await expect ( firstPromise ) . rejects . toThrow ( 'Network error' ) ;
335344
336- await expect ( promise ) . rejects . toThrow ( 'Network error' ) ;
345+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
346+ controller . sendRequestToAI ( {
347+ author : { id : 'user' , name : 'User' } ,
348+ text : 'Second request' ,
349+ timestamp : '2026-04-16T10:00:01.000Z' ,
350+ } as Message ) ;
351+
352+ const messages = await getStore ( controller ) . load ( ) ;
337353
338- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
354+ expect ( messages ) . toHaveLength ( 2 ) ;
355+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 2 ) ;
339356 } ) ;
340357
341- it ( 'should return false after failed command processing ' , async ( ) => {
358+ it ( 'should accept new request after previous request is aborted ' , async ( ) => {
342359 const controller = createController ( {
343360 'aiAssistant.aiIntegration' : mockAIIntegration ,
344361 } ) ;
345362
346- const promise = controller . sendRequestToAI ( {
363+ const firstPromise = controller . sendRequestToAI ( {
347364 author : { id : 'user' , name : 'User' } ,
348- text : 'Generate values ' ,
365+ text : 'First request ' ,
349366 timestamp : '2026-04-16T10:00:00.000Z' ,
350367 } as Message ) ;
351- promise . catch ( ( ) => { } ) ;
368+ firstPromise . catch ( ( ) => { } ) ;
352369
353- sendRequestCallbacks . onComplete ?.( { } as ExecuteGridAssistantCommandResult ) ;
370+ controller . abortRequest ( ) ;
371+ await expect ( firstPromise ) . rejects . toThrow ( ) ;
354372
355- await expect ( promise ) . rejects . toThrow ( 'Default error message' ) ;
373+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
374+ controller . sendRequestToAI ( {
375+ author : { id : 'user' , name : 'User' } ,
376+ text : 'Second request' ,
377+ timestamp : '2026-04-16T10:00:01.000Z' ,
378+ } as Message ) ;
379+
380+ const messages = await getStore ( controller ) . load ( ) ;
356381
357- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
382+ expect ( messages ) . toHaveLength ( 2 ) ;
383+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledTimes ( 2 ) ;
358384 } ) ;
359385 } ) ;
360386
@@ -387,7 +413,7 @@ describe('AIAssistantController', () => {
387413 await expect ( promise ) . rejects . toThrow ( 'Request stopped.' ) ;
388414 } ) ;
389415
390- it ( 'should set isProcessing to false when request is aborted' , async ( ) => {
416+ it ( 'should call gridCommands.abort when request is aborted' , async ( ) => {
391417 const controller = createController ( {
392418 'aiAssistant.aiIntegration' : mockAIIntegration ,
393419 } ) ;
@@ -399,34 +425,169 @@ describe('AIAssistantController', () => {
399425 } as Message ) ;
400426 promise . catch ( ( ) => { } ) ;
401427
402- expect ( controller . isProcessing ( ) ) . toBe ( true ) ;
428+ const gridCommandsInstance = MockedGridCommands . mock . results [ 0 ] . value as { abort : jest . Mock } ;
403429
404430 controller . abortRequest ( ) ;
405431
406432 await expect ( promise ) . rejects . toThrow ( ) ;
407433
408- expect ( controller . isProcessing ( ) ) . toBe ( false ) ;
434+ expect ( gridCommandsInstance . abort ) . toHaveBeenCalledTimes ( 1 ) ;
409435 } ) ;
436+ } ) ;
410437
411- it ( 'should call gridCommands.abort when request is aborted' , async ( ) => {
438+ describe ( 'sendRequestToAI with AIMessage (regenerate)' , ( ) => {
439+ it ( 'should reset message status to pending when AIMessage is passed' , async ( ) => {
412440 const controller = createController ( {
413441 'aiAssistant.aiIntegration' : mockAIIntegration ,
414442 } ) ;
415443
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 ( ( ) => { } ) ;
444+ const aiMessage : AIMessage = {
445+ id : 'assistant-123' ,
446+ author : AI_ASSISTANT_AUTHOR ,
447+ text : MessageStatus . Failure ,
448+ prompt : 'Generate values' ,
449+ status : MessageStatus . Failure ,
450+ headerText : 'Failed to process request' ,
451+ errorText : 'Network error' ,
452+ } ;
422453
423- const gridCommandsInstance = MockedGridCommands . mock . results [ 0 ] . value as { abort : jest . Mock } ;
454+ const store = getStore ( controller ) ;
455+ await store . insert ( aiMessage ) ;
424456
425- controller . abortRequest ( ) ;
457+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
458+ controller . sendRequestToAI ( aiMessage ) ;
426459
427- await expect ( promise ) . rejects . toThrow ( ) ;
460+ const messages = await store . load ( ) ;
428461
429- expect ( gridCommandsInstance . abort ) . toHaveBeenCalledTimes ( 1 ) ;
462+ expect ( messages ) . toHaveLength ( 1 ) ;
463+ expect ( messages ) . toEqual ( [
464+ expect . objectContaining ( {
465+ id : 'assistant-123' ,
466+ status : MessageStatus . Pending ,
467+ headerText : 'Request in progress' ,
468+ text : MessageStatus . Pending ,
469+ } ) ,
470+ ] ) ;
471+ } ) ;
472+
473+ it ( 'should not create new message when AIMessage is passed' , async ( ) => {
474+ const controller = createController ( {
475+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
476+ } ) ;
477+
478+ const aiMessage : AIMessage = {
479+ id : 'assistant-123' ,
480+ author : AI_ASSISTANT_AUTHOR ,
481+ text : MessageStatus . Failure ,
482+ prompt : 'Generate values' ,
483+ status : MessageStatus . Failure ,
484+ headerText : 'Failed to process request' ,
485+ errorText : 'Network error' ,
486+ } ;
487+
488+ const store = getStore ( controller ) ;
489+ await store . insert ( aiMessage ) ;
490+
491+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
492+ controller . sendRequestToAI ( aiMessage ) ;
493+
494+ const messages = await store . load ( ) ;
495+
496+ expect ( messages ) . toHaveLength ( 1 ) ;
497+ } ) ;
498+
499+ it ( 'should send request with original prompt from AIMessage' , ( ) => {
500+ const controller = createController ( {
501+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
502+ } ) ;
503+
504+ const aiMessage : AIMessage = {
505+ id : 'assistant-123' ,
506+ author : AI_ASSISTANT_AUTHOR ,
507+ text : MessageStatus . Failure ,
508+ prompt : 'Sort by Name column' ,
509+ status : MessageStatus . Failure ,
510+ headerText : 'Failed to process request' ,
511+ errorText : 'Network error' ,
512+ } ;
513+
514+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
515+ controller . sendRequestToAI ( aiMessage ) ;
516+
517+ expect ( mockAIIntegration . executeGridAssistant ) . toHaveBeenCalledWith (
518+ expect . objectContaining ( {
519+ text : 'Sort by Name column' ,
520+ } ) ,
521+ expect . any ( Object ) ,
522+ ) ;
523+ } ) ;
524+
525+ it ( 'should clear errorText and commands when regenerating' , async ( ) => {
526+ const controller = createController ( {
527+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
528+ } ) ;
529+
530+ const aiMessage : AIMessage = {
531+ id : 'assistant-123' ,
532+ author : AI_ASSISTANT_AUTHOR ,
533+ text : MessageStatus . Failure ,
534+ prompt : 'Generate values' ,
535+ status : MessageStatus . Failure ,
536+ headerText : 'Failed to process request' ,
537+ errorText : 'Network error' ,
538+ commands : [ { status : 'failure' , message : 'sort failed' } ] ,
539+ } ;
540+
541+ const store = getStore ( controller ) ;
542+ await store . insert ( aiMessage ) ;
543+
544+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
545+ controller . sendRequestToAI ( aiMessage ) ;
546+
547+ const messages = await store . load ( ) ;
548+
549+ expect ( messages ) . toEqual ( [
550+ expect . objectContaining ( {
551+ errorText : undefined ,
552+ commands : undefined ,
553+ } ) ,
554+ ] ) ;
555+ } ) ;
556+
557+ it ( 'should complete regenerated message as success when command succeed' , async ( ) => {
558+ const controller = createController ( {
559+ 'aiAssistant.aiIntegration' : mockAIIntegration ,
560+ } ) ;
561+
562+ const aiMessage : AIMessage = {
563+ id : 'assistant-123' ,
564+ author : AI_ASSISTANT_AUTHOR ,
565+ text : MessageStatus . Failure ,
566+ prompt : 'Generate values' ,
567+ status : MessageStatus . Failure ,
568+ headerText : 'Failed to process request' ,
569+ errorText : 'Network error' ,
570+ } ;
571+
572+ const store = getStore ( controller ) ;
573+ await store . insert ( aiMessage ) ;
574+
575+ const promise = controller . sendRequestToAI ( aiMessage ) ;
576+
577+ const actions = [ { name : 'sort' , args : { column : 'Name' } } ] ;
578+ sendRequestCallbacks . onComplete ?.( { actions } ) ;
579+
580+ await promise ;
581+
582+ const messages = await store . load ( ) ;
583+
584+ expect ( messages ) . toEqual ( [
585+ expect . objectContaining ( {
586+ id : 'assistant-123' ,
587+ status : MessageStatus . Success ,
588+ commands : [ { status : 'success' , message : 'sort' } ] ,
589+ } ) ,
590+ ] ) ;
430591 } ) ;
431592 } ) ;
432593} ) ;
0 commit comments