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 @@ -23,7 +23,7 @@ <h2 class="mat-heading-2 ai-panel-sidebar__title">
<div class="ai-message-chain">
<div *ngFor="let message of messagesChain" class="{{message.type}}-message">
<span style="white-space: pre-wrap" *ngIf="message.type == 'user'">{{message.text}}</span>
<markdown *ngIf="message.type == 'ai'" mermaid>{{message.text}}</markdown>
<markdown *ngIf="message.type == 'ai'" mermaid [data]="message.text"></markdown>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,35 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatIconTestingModule } from '@angular/material/icon/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Angulartics2Module } from 'angulartics2';
import { MarkdownService } from 'ngx-markdown';
import { of, throwError } from 'rxjs';
import { of } from 'rxjs';
import { AiService } from 'src/app/services/ai.service';
import { ConnectionsService } from 'src/app/services/connections.service';
import { TableStateService } from 'src/app/services/table-state.service';
import { TablesService } from 'src/app/services/tables.service';
import { DbTableAiPanelComponent } from './db-table-ai-panel.component';

async function* mockStream(...chunks: string[]): AsyncGenerator<string> {
for (const chunk of chunks) {
yield chunk;
}
}

describe('DbTableAiPanelComponent', () => {
let component: DbTableAiPanelComponent;
let fixture: ComponentFixture<DbTableAiPanelComponent>;
let tablesService: TablesService;
let tableStateService: TableStateService;

const mockMarkdownService = {
parse: vi.fn().mockReturnValue('parsed markdown'),
};

const mockConnectionsService = {
currentConnectionID: '12345678',
};

const mockTablesService = {
currentTableName: 'users',
createAIthread: vi.fn(),
requestAImessage: vi.fn(),
};

const mockAiService: Partial<AiService> = {
createThread: vi.fn(),
sendMessage: vi.fn(),
};

const mockTableStateService = {
Expand All @@ -41,16 +45,15 @@ describe('DbTableAiPanelComponent', () => {
imports: [Angulartics2Module.forRoot(), DbTableAiPanelComponent, BrowserAnimationsModule, MatIconTestingModule],
providers: [
provideHttpClient(),
{ provide: MarkdownService, useValue: mockMarkdownService },
{ provide: ConnectionsService, useValue: mockConnectionsService },
{ provide: TablesService, useValue: mockTablesService },
{ provide: AiService, useValue: mockAiService },
{ provide: TableStateService, useValue: mockTableStateService },
],
}).compileComponents();

fixture = TestBed.createComponent(DbTableAiPanelComponent);
component = fixture.componentInstance;
tablesService = TestBed.inject(TablesService);
tableStateService = TestBed.inject(TableStateService);
fixture.detectChanges();
});
Expand All @@ -77,7 +80,10 @@ describe('DbTableAiPanelComponent', () => {
});

it('should create thread on Enter key when no thread exists', () => {
mockTablesService.createAIthread.mockReturnValue(of({ threadId: 'thread-123', responseMessage: 'AI response' }));
(mockAiService.createThread as ReturnType<typeof vi.fn>).mockResolvedValue({
threadId: 'thread-123',
stream: mockStream('AI response'),
});

component.message = 'Test message';
component.threadID = null;
Expand All @@ -86,11 +92,16 @@ describe('DbTableAiPanelComponent', () => {
Object.defineProperty(event, 'preventDefault', { value: vi.fn() });
component.onKeydown(event);

expect(mockTablesService.createAIthread).toHaveBeenCalledWith('12345678', 'users', 'Test message');
expect(mockAiService.createThread).toHaveBeenCalledWith(
'12345678',
'users',
'Test message',
expect.any(AbortSignal),
);
});

it('should send message on Enter key when thread exists', () => {
mockTablesService.requestAImessage.mockReturnValue(of('AI response'));
(mockAiService.sendMessage as ReturnType<typeof vi.fn>).mockResolvedValue(mockStream('AI response'));

component.message = 'Follow up message';
component.threadID = 'existing-thread';
Expand All @@ -99,55 +110,65 @@ describe('DbTableAiPanelComponent', () => {
Object.defineProperty(event, 'preventDefault', { value: vi.fn() });
component.onKeydown(event);

expect(mockTablesService.requestAImessage).toHaveBeenCalledWith(
expect(mockAiService.sendMessage).toHaveBeenCalledWith(
'12345678',
'users',
'existing-thread',
'Follow up message',
expect.any(AbortSignal),
);
});

it('should add user message to chain when creating thread', () => {
mockTablesService.createAIthread.mockReturnValue(of({ threadId: 'thread-123', responseMessage: 'AI response' }));
it('should add user message to chain when creating thread', async () => {
(mockAiService.createThread as ReturnType<typeof vi.fn>).mockResolvedValue({
threadId: 'thread-123',
stream: mockStream('AI response'),
});

component.message = 'User question';
component.createThread();
await component.createThread();

expect(component.messagesChain[0]).toEqual({
type: 'user',
text: 'User question',
});
});

it('should add AI response to chain after thread creation', () => {
mockTablesService.createAIthread.mockReturnValue(of({ threadId: 'thread-123', responseMessage: 'AI response' }));
it('should add AI response to chain after thread creation', async () => {
(mockAiService.createThread as ReturnType<typeof vi.fn>).mockResolvedValue({
threadId: 'thread-123',
stream: mockStream('AI response'),
});

component.message = 'User question';
component.createThread();
await component.createThread();

expect(component.threadID).toBe('thread-123');
expect(component.messagesChain[1]).toEqual({
type: 'ai',
text: 'parsed markdown',
text: 'AI response',
});
});

it('should handle error when creating thread', () => {
mockTablesService.createAIthread.mockReturnValue(throwError(() => 'Error message'));
it('should handle error when creating thread', async () => {
(mockAiService.createThread as ReturnType<typeof vi.fn>).mockRejectedValue('Error message');

component.message = 'User question';
component.createThread();
await component.createThread();

expect(component.messagesChain[1]).toEqual({
type: 'ai-error',
text: 'Error message',
});
Comment on lines +153 to 162

Copilot AI Feb 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests model AiService.createThread() failures as a rejected promise, but the concrete implementation (ApiService.postStream) resolves null on request failure after showing a notification. As a result, the component’s catch path may never run in production unless the service contract is changed to throw. Align the test doubles with the real contract (resolve null) or update AiService/ApiService to reject on errors so the tests reflect production behavior.

Copilot uses AI. Check for mistakes.
});

it('should use suggested message when provided to createThread', () => {
mockTablesService.createAIthread.mockReturnValue(of({ threadId: 'thread-123', responseMessage: 'AI response' }));
it('should use suggested message when provided to createThread', async () => {
(mockAiService.createThread as ReturnType<typeof vi.fn>).mockResolvedValue({
threadId: 'thread-123',
stream: mockStream('AI response'),
});

component.createThread('Suggested question');
await component.createThread('Suggested question');

expect(component.messagesChain[0].text).toBe('Suggested question');
});
Expand All @@ -157,12 +178,12 @@ describe('DbTableAiPanelComponent', () => {
expect(mockTableStateService.handleViewAIpanel).toHaveBeenCalled();
});

it('should clear message after sending', () => {
mockTablesService.requestAImessage.mockReturnValue(of('AI response'));
it('should clear message after sending', async () => {
(mockAiService.sendMessage as ReturnType<typeof vi.fn>).mockResolvedValue(mockStream('AI response'));

component.message = 'Test message';
component.threadID = 'thread-123';
component.sendMessage();
await component.sendMessage();

expect(component.message).toBe('');
expect(component.charactrsNumber).toBe(0);
Expand Down
Loading
Loading