chat
This commit is contained in:
220
listify-api/src/assistant/assistant.service.spec.ts
Normal file
220
listify-api/src/assistant/assistant.service.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { ServiceUnavailableException } from '@nestjs/common';
|
||||
import { UserList } from '../list-templates/list-template.types';
|
||||
import { ListsService } from '../lists/lists.service';
|
||||
import { AssistantService } from './assistant.service';
|
||||
|
||||
describe('AssistantService', () => {
|
||||
const originalFetch = global.fetch;
|
||||
const originalApiKey = process.env.MISTRAL_API_KEY;
|
||||
let listsService: Pick<ListsService, 'listLists' | 'createList' | 'addItem'>;
|
||||
let service: AssistantService;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.MISTRAL_API_KEY = 'test-key';
|
||||
global.fetch = jest.fn();
|
||||
listsService = {
|
||||
listLists: jest.fn(),
|
||||
createList: jest.fn(),
|
||||
addItem: jest.fn(),
|
||||
};
|
||||
service = new AssistantService(listsService as ListsService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.fetch = originalFetch;
|
||||
process.env.MISTRAL_API_KEY = originalApiKey;
|
||||
delete process.env.MISTRAL_MODEL;
|
||||
});
|
||||
|
||||
it('returns a plain assistant response without tool calls', async () => {
|
||||
mockMistralResponse({
|
||||
choices: [{ message: { content: 'Klar, ich helfe dir.' } }],
|
||||
});
|
||||
|
||||
const result = await service.chat('user-1', {
|
||||
messages: [{ role: 'user', content: 'Hallo' }],
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
message: { role: 'assistant', content: 'Klar, ich helfe dir.' },
|
||||
actions: [],
|
||||
});
|
||||
expect(global.fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('executes create_list tool calls and returns the final assistant response', async () => {
|
||||
const createdList = list({ id: 'list-1', name: 'Sommerurlaub' });
|
||||
const completedList = list({
|
||||
id: 'list-1',
|
||||
name: 'Sommerurlaub',
|
||||
items: ['Pass'],
|
||||
});
|
||||
jest.mocked(listsService.createList).mockResolvedValue(createdList);
|
||||
jest.mocked(listsService.addItem).mockResolvedValue(completedList);
|
||||
mockMistralResponse(
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: '',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call-1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'create_list',
|
||||
arguments: JSON.stringify({
|
||||
name: 'Sommerurlaub',
|
||||
kind: 'packing',
|
||||
items: [{ title: 'Pass' }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: 'Ich habe die Packliste Sommerurlaub erstellt.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const result = await service.chat('user-1', {
|
||||
messages: [{ role: 'user', content: 'Erstelle eine Packliste' }],
|
||||
});
|
||||
|
||||
expect(listsService.createList).toHaveBeenCalledWith('user-1', {
|
||||
name: 'Sommerurlaub',
|
||||
description: undefined,
|
||||
kind: 'packing',
|
||||
});
|
||||
expect(listsService.addItem).toHaveBeenCalledWith('user-1', 'list-1', {
|
||||
title: 'Pass',
|
||||
notes: undefined,
|
||||
quantity: undefined,
|
||||
required: undefined,
|
||||
});
|
||||
expect(result.actions).toEqual([
|
||||
{
|
||||
type: 'list.created',
|
||||
listId: 'list-1',
|
||||
list: completedList,
|
||||
},
|
||||
]);
|
||||
expect(result.message.content).toBe(
|
||||
'Ich habe die Packliste Sommerurlaub erstellt.',
|
||||
);
|
||||
});
|
||||
|
||||
it('executes add_list_item tool calls', async () => {
|
||||
const updatedList = list({
|
||||
id: 'list-1',
|
||||
name: 'Einkauf',
|
||||
items: ['Milch'],
|
||||
});
|
||||
jest.mocked(listsService.addItem).mockResolvedValue(updatedList);
|
||||
mockMistralResponse(
|
||||
{
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: '',
|
||||
tool_calls: [
|
||||
{
|
||||
id: 'call-1',
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'add_list_item',
|
||||
arguments: JSON.stringify({
|
||||
listId: 'list-1',
|
||||
title: 'Milch',
|
||||
quantity: 2,
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
choices: [{ message: { content: 'Milch wurde hinzugefuegt.' } }],
|
||||
},
|
||||
);
|
||||
|
||||
const result = await service.chat('user-1', {
|
||||
messages: [{ role: 'user', content: 'Fuege Milch hinzu' }],
|
||||
});
|
||||
|
||||
expect(listsService.addItem).toHaveBeenCalledWith('user-1', 'list-1', {
|
||||
title: 'Milch',
|
||||
notes: undefined,
|
||||
quantity: 2,
|
||||
required: undefined,
|
||||
});
|
||||
expect(result.actions[0]).toEqual({
|
||||
type: 'list.item_added',
|
||||
listId: 'list-1',
|
||||
itemTitle: 'Milch',
|
||||
list: updatedList,
|
||||
});
|
||||
});
|
||||
|
||||
it('fails clearly when the api key is missing', async () => {
|
||||
delete process.env.MISTRAL_API_KEY;
|
||||
|
||||
await expect(
|
||||
service.chat('user-1', {
|
||||
messages: [{ role: 'user', content: 'Hallo' }],
|
||||
}),
|
||||
).rejects.toThrow(ServiceUnavailableException);
|
||||
});
|
||||
});
|
||||
|
||||
function mockMistralResponse(...responses: object[]): void {
|
||||
jest.mocked(global.fetch).mockImplementation(async () => {
|
||||
const response = responses.shift() ?? responses[responses.length - 1];
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => response,
|
||||
} as Response;
|
||||
});
|
||||
}
|
||||
|
||||
function list(options: {
|
||||
id: string;
|
||||
name: string;
|
||||
items?: string[];
|
||||
}): UserList {
|
||||
return {
|
||||
id: options.id,
|
||||
ownerId: 'user-1',
|
||||
accessRole: 'owner',
|
||||
name: options.name,
|
||||
kind: 'custom',
|
||||
items: (options.items ?? []).map((title, position) => ({
|
||||
id: `item-${position}`,
|
||||
title,
|
||||
required: true,
|
||||
checked: false,
|
||||
position,
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
})),
|
||||
collaborators: [],
|
||||
createdAt: now(),
|
||||
updatedAt: now(),
|
||||
};
|
||||
}
|
||||
|
||||
function now(): string {
|
||||
return new Date(0).toISOString();
|
||||
}
|
||||
Reference in New Issue
Block a user