Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
MessageStatus,
} from '../const';
import { GridCommands } from '../grid_commands';
import type { CommandResult } from '../types';
import type { AIMessage, CommandResult } from '../types';

jest.mock('../grid_commands');

Expand Down Expand Up @@ -274,87 +274,113 @@ describe('AIAssistantController', () => {

await expect(promise).rejects.toThrow('Default error message');
});
});

describe('isProcessing', () => {
it('should return false by default', () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

expect(controller.isProcessing()).toBe(false);
});

it('should return true after sendRequestToAI is called', () => {
it('should ignore second request while first request is still processing', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
text: 'First request',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);

expect(controller.isProcessing()).toBe(true);
controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Second request',
timestamp: '2026-04-16T10:00:01.000Z',
} as Message).catch(() => {});

const messages = await getStore(controller).load();

expect(messages).toHaveLength(1);
expect(mockAIIntegration.executeGridAssistant).toHaveBeenCalledTimes(1);
});

it('should return false after successful command completion', async () => {
it('should accept new request after previous request completes successfully', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const promise = controller.sendRequestToAI({
const firstPromise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
text: 'First request',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);

const actions = [{ name: 'sort', args: { column: 'Name' } }];
sendRequestCallbacks.onComplete?.({ actions });
await firstPromise;

await promise;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Second request',
timestamp: '2026-04-16T10:00:01.000Z',
} as Message);

const messages = await getStore(controller).load();

expect(controller.isProcessing()).toBe(false);
expect(messages).toHaveLength(2);
expect(mockAIIntegration.executeGridAssistant).toHaveBeenCalledTimes(2);
});

it('should return false after onError callback', async () => {
it('should accept new request after previous request fails with error', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const promise = controller.sendRequestToAI({
const firstPromise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
text: 'First request',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);
promise.catch(() => {});
firstPromise.catch(() => {});

sendRequestCallbacks.onError?.(new Error('Network error'));
await expect(firstPromise).rejects.toThrow('Network error');

await expect(promise).rejects.toThrow('Network error');
// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Second request',
timestamp: '2026-04-16T10:00:01.000Z',
} as Message);

const messages = await getStore(controller).load();

expect(controller.isProcessing()).toBe(false);
expect(messages).toHaveLength(2);
expect(mockAIIntegration.executeGridAssistant).toHaveBeenCalledTimes(2);
});

it('should return false after failed command processing', async () => {
it('should accept new request after previous request is aborted', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const promise = controller.sendRequestToAI({
const firstPromise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
text: 'First request',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);
promise.catch(() => {});
firstPromise.catch(() => {});

sendRequestCallbacks.onComplete?.({} as ExecuteGridAssistantCommandResult);
controller.abortRequest();
await expect(firstPromise).rejects.toThrow();

await expect(promise).rejects.toThrow('Default error message');
// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Second request',
timestamp: '2026-04-16T10:00:01.000Z',
} as Message);

const messages = await getStore(controller).load();

expect(controller.isProcessing()).toBe(false);
expect(messages).toHaveLength(2);
expect(mockAIIntegration.executeGridAssistant).toHaveBeenCalledTimes(2);
});
});

Expand Down Expand Up @@ -387,7 +413,7 @@ describe('AIAssistantController', () => {
await expect(promise).rejects.toThrow('Request stopped.');
});

it('should set isProcessing to false when request is aborted', async () => {
it('should call gridCommands.abort when request is aborted', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});
Expand All @@ -399,34 +425,169 @@ describe('AIAssistantController', () => {
} as Message);
promise.catch(() => {});

expect(controller.isProcessing()).toBe(true);
const gridCommandsInstance = MockedGridCommands.mock.results[0].value as { abort: jest.Mock };

controller.abortRequest();

await expect(promise).rejects.toThrow();

expect(controller.isProcessing()).toBe(false);
expect(gridCommandsInstance.abort).toHaveBeenCalledTimes(1);
});
});

it('should call gridCommands.abort when request is aborted', async () => {
describe('sendRequestToAI with AIMessage (regenerate)', () => {
it('should reset message status to pending when AIMessage is passed', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const promise = controller.sendRequestToAI({
author: { id: 'user', name: 'User' },
text: 'Generate values',
timestamp: '2026-04-16T10:00:00.000Z',
} as Message);
promise.catch(() => {});
const aiMessage: AIMessage = {
id: 'assistant-123',
author: AI_ASSISTANT_AUTHOR,
text: MessageStatus.Failure,
prompt: 'Generate values',
status: MessageStatus.Failure,
headerText: 'Failed to process request',
errorText: 'Network error',
};

const gridCommandsInstance = MockedGridCommands.mock.results[0].value as { abort: jest.Mock };
const store = getStore(controller);
await store.insert(aiMessage);

controller.abortRequest();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI(aiMessage);

await expect(promise).rejects.toThrow();
const messages = await store.load();

expect(gridCommandsInstance.abort).toHaveBeenCalledTimes(1);
expect(messages).toHaveLength(1);
expect(messages).toEqual([
expect.objectContaining({
id: 'assistant-123',
status: MessageStatus.Pending,
headerText: 'Request in progress',
text: MessageStatus.Pending,
}),
]);
});

it('should not create new message when AIMessage is passed', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const aiMessage: AIMessage = {
id: 'assistant-123',
author: AI_ASSISTANT_AUTHOR,
text: MessageStatus.Failure,
prompt: 'Generate values',
status: MessageStatus.Failure,
headerText: 'Failed to process request',
errorText: 'Network error',
};

const store = getStore(controller);
await store.insert(aiMessage);

// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI(aiMessage);

const messages = await store.load();

expect(messages).toHaveLength(1);
});

it('should send request with original prompt from AIMessage', () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const aiMessage: AIMessage = {
id: 'assistant-123',
author: AI_ASSISTANT_AUTHOR,
text: MessageStatus.Failure,
prompt: 'Sort by Name column',
status: MessageStatus.Failure,
headerText: 'Failed to process request',
errorText: 'Network error',
};

// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI(aiMessage);

expect(mockAIIntegration.executeGridAssistant).toHaveBeenCalledWith(
expect.objectContaining({
text: 'Sort by Name column',
}),
expect.any(Object),
);
});

it('should clear errorText and commands when regenerating', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const aiMessage: AIMessage = {
id: 'assistant-123',
author: AI_ASSISTANT_AUTHOR,
text: MessageStatus.Failure,
prompt: 'Generate values',
status: MessageStatus.Failure,
headerText: 'Failed to process request',
errorText: 'Network error',
commands: [{ status: 'failure', message: 'sort failed' }],
};

const store = getStore(controller);
await store.insert(aiMessage);

// eslint-disable-next-line @typescript-eslint/no-floating-promises
controller.sendRequestToAI(aiMessage);

const messages = await store.load();

expect(messages).toEqual([
expect.objectContaining({
errorText: undefined,
commands: undefined,
}),
]);
});

it('should complete regenerated message as success when command succeed', async () => {
const controller = createController({
'aiAssistant.aiIntegration': mockAIIntegration,
});

const aiMessage: AIMessage = {
id: 'assistant-123',
author: AI_ASSISTANT_AUTHOR,
text: MessageStatus.Failure,
prompt: 'Generate values',
status: MessageStatus.Failure,
headerText: 'Failed to process request',
errorText: 'Network error',
};

const store = getStore(controller);
await store.insert(aiMessage);

const promise = controller.sendRequestToAI(aiMessage);

const actions = [{ name: 'sort', args: { column: 'Name' } }];
sendRequestCallbacks.onComplete?.({ actions });

await promise;

const messages = await store.load();

expect(messages).toEqual([
expect.objectContaining({
id: 'assistant-123',
status: MessageStatus.Success,
commands: [{ status: 'success', message: 'sort' }],
}),
]);
});
});
});
Loading
Loading