This commit is contained in:
Bastian Wagner
2026-06-24 12:56:42 +02:00
parent 712136a5e9
commit 17ac2953d6
5 changed files with 361 additions and 175 deletions

View File

@@ -9,7 +9,7 @@ describe('AssistantService', () => {
].join(' ');
const originalFetch = global.fetch;
const originalApiKey = process.env.MISTRAL_API_KEY;
const originalAgentId = process.env.MISTRAL_AGENT_ID;
const originalModel = process.env.MISTRAL_MODEL;
let chatLogsRepository: {
create: jest.Mock;
find: jest.Mock;
@@ -26,7 +26,7 @@ describe('AssistantService', () => {
beforeEach(() => {
process.env.MISTRAL_API_KEY = 'test-key';
process.env.MISTRAL_AGENT_ID = 'agent-listify';
process.env.MISTRAL_MODEL = 'mistral-large-test';
global.fetch = jest.fn();
chatLogsRepository = {
create: jest.fn((input) => input),
@@ -51,10 +51,10 @@ describe('AssistantService', () => {
afterEach(() => {
global.fetch = originalFetch;
process.env.MISTRAL_API_KEY = originalApiKey;
process.env.MISTRAL_AGENT_ID = originalAgentId;
process.env.MISTRAL_MODEL = originalModel;
});
it('forwards messages to the configured Mistral agent', async () => {
it('starts a Mistral conversation with the configured model', async () => {
const providerResponse = {
choices: [
{
@@ -78,7 +78,7 @@ describe('AssistantService', () => {
});
expect(global.fetch).toHaveBeenCalledWith(
'https://api.mistral.ai/v1/agents/completions',
'https://api.mistral.ai/v1/conversations',
expect.objectContaining({
method: 'POST',
headers: {
@@ -86,12 +86,12 @@ describe('AssistantService', () => {
'Content-Type': 'application/json',
},
body: JSON.stringify({
agent_id: 'agent-listify',
messages: [
model: 'mistral-large-test',
inputs: [
{ role: 'assistant', content: 'Hallo' },
{ role: 'user', content: 'Hallo' },
{ role: 'system', content: connectorSystemMessage },
],
instructions: connectorSystemMessage,
tools: [
{
type: 'connector',
@@ -99,9 +99,6 @@ describe('AssistantService', () => {
},
],
stream: false,
response_format: {
type: 'text',
},
}),
}),
);
@@ -117,11 +114,16 @@ describe('AssistantService', () => {
expect.objectContaining({
userId: 'user-1',
provider: 'mistral',
endpoint: 'https://api.mistral.ai/v1/agents/completions',
agentId: 'agent-listify',
endpoint: 'https://api.mistral.ai/v1/conversations',
agentId: null,
statusCode: 200,
requestPayload: expect.objectContaining({
agent_id: 'agent-listify',
model: 'mistral-large-test',
inputs: [
{ role: 'assistant', content: 'Hallo' },
{ role: 'user', content: 'Hallo' },
],
instructions: connectorSystemMessage,
tools: [{ type: 'connector', connector_id: 'listify' }],
}),
responsePayload: providerResponse,
@@ -131,6 +133,64 @@ describe('AssistantService', () => {
);
});
it('extracts assistant content and created list actions from conversation outputs', async () => {
const createdList = {
id: 'list-1',
ownerId: 'user-1',
accessRole: 'owner',
name: 'Einkauf',
kind: 'shopping',
items: [],
collaborators: [],
createdAt: '2026-06-12T00:00:00.000Z',
updatedAt: '2026-06-12T00:00:00.000Z',
};
const providerResponse = {
conversation_id: 'conv-1',
outputs: [
{
type: 'tool.execution',
tool_name: 'listify_create_list',
result: { list: createdList },
},
{
type: 'message.output',
role: 'assistant',
content: 'Ich habe die Liste Einkauf angelegt.',
},
],
};
mockMistralResponse(providerResponse);
const result = await service.chat('user-1', {
messages: [{ role: 'user', content: 'Erstelle eine Einkaufsliste' }],
});
expect(result).toEqual({
message: {
role: 'assistant',
content: 'Ich habe die Liste Einkauf angelegt.',
},
actions: [
{
type: 'list.created',
listId: 'list-1',
list: createdList,
},
],
});
expect(listRealtimeService.publishSnapshot).toHaveBeenCalledWith(
'user-1',
createdList,
);
expect(chatLogsRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
responsePayload: providerResponse,
assistantContent: 'Ich habe die Liste Einkauf angelegt.',
}),
);
});
it('answers open list read requests locally without calling Mistral', async () => {
listsService.listLists.mockResolvedValue([
{
@@ -262,15 +322,9 @@ describe('AssistantService', () => {
});
const payload = getMistralRequestPayload();
const contextMessage = payload.messages.at(-2);
const contextContent = contextMessage?.content ?? '';
const contextContent = payload.instructions;
expect(contextMessage).toEqual(
expect.objectContaining({
role: 'system',
content: expect.stringContaining('Aktueller Listify-Kontext:'),
}),
);
expect(contextContent).toContain('Aktueller Listify-Kontext:');
expect(contextContent).toContain(
'Der User befindet sich auf einer Listendetailseite.',
);
@@ -287,10 +341,7 @@ describe('AssistantService', () => {
expect(contextContent).not.toContain('ada@example.com');
expect(contextContent).not.toContain('grace@example.com');
expect(contextContent).not.toContain('checkedByUserId');
expect(payload.messages.at(-1)).toEqual({
role: 'system',
content: connectorSystemMessage,
});
expect(contextContent).toContain(connectorSystemMessage);
});
it('adds list items locally when the user writes onto the current list', async () => {
@@ -390,7 +441,7 @@ describe('AssistantService', () => {
statusCode: 502,
responsePayload: providerResponse,
assistantContent: null,
errorMessage: 'Mistral agent request failed.',
errorMessage: 'Mistral conversation request failed.',
}),
);
});
@@ -720,40 +771,20 @@ describe('AssistantService', () => {
);
});
it('fails clearly when the agent id is missing', async () => {
delete process.env.MISTRAL_AGENT_ID;
await expect(
service.chat('user-1', {
messages: [{ role: 'user', content: 'Hallo' }],
}),
).rejects.toThrow(ServiceUnavailableException);
expect(chatLogsRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'user-1',
agentId: null,
statusCode: null,
responsePayload: null,
assistantContent: null,
errorMessage: 'Mistral agent id is not configured.',
}),
);
});
it('lists the latest chat logs for the current user', async () => {
chatLogsRepository.find.mockResolvedValue([
{
id: 'log-1',
userId: 'user-1',
provider: 'mistral',
endpoint: 'https://api.mistral.ai/v1/agents/completions',
agentId: 'agent-listify',
endpoint: 'https://api.mistral.ai/v1/conversations',
agentId: null,
statusCode: 502,
durationMs: 123,
requestPayload: { messages: [] },
requestPayload: { inputs: [] },
responsePayload: { message: 'connector failed' },
assistantContent: null,
errorMessage: 'Mistral agent request failed.',
errorMessage: 'Mistral conversation request failed.',
createdAt: new Date('2026-06-24T08:00:00.000Z'),
},
]);
@@ -769,14 +800,14 @@ describe('AssistantService', () => {
{
id: 'log-1',
provider: 'mistral',
endpoint: 'https://api.mistral.ai/v1/agents/completions',
agentId: 'agent-listify',
endpoint: 'https://api.mistral.ai/v1/conversations',
agentId: null,
statusCode: 502,
durationMs: 123,
requestPayload: { messages: [] },
requestPayload: { inputs: [] },
responsePayload: { message: 'connector failed' },
assistantContent: null,
errorMessage: 'Mistral agent request failed.',
errorMessage: 'Mistral conversation request failed.',
createdAt: '2026-06-24T08:00:00.000Z',
},
]);
@@ -792,7 +823,8 @@ function mockMistralResponse(response: object, ok = true, status = 200): void {
}
function getMistralRequestPayload(): {
messages: Array<{ role: string; content: string }>;
inputs: Array<{ role: string; content: string }>;
instructions: string;
} {
const [, init] = jest.mocked(global.fetch).mock.calls.at(-1) ?? [];
const body = init?.body;
@@ -802,6 +834,7 @@ function getMistralRequestPayload(): {
}
return JSON.parse(body) as {
messages: Array<{ role: string; content: string }>;
inputs: Array<{ role: string; content: string }>;
instructions: string;
};
}