mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
import type * as AiImport from 'ai';
|
|
import type { LanguageModel } from 'ai';
|
|
|
|
import { generateTitleFromMessage } from '../runtime/title-generation';
|
|
|
|
type GenerateTextCall = {
|
|
messages: Array<{ role: string; content: string }>;
|
|
};
|
|
|
|
const mockGenerateText = jest.fn<Promise<{ text: string }>, [GenerateTextCall]>();
|
|
|
|
jest.mock('ai', () => {
|
|
const actual = jest.requireActual<typeof AiImport>('ai');
|
|
return {
|
|
...actual,
|
|
generateText: async (call: GenerateTextCall): Promise<{ text: string }> =>
|
|
await mockGenerateText(call),
|
|
};
|
|
});
|
|
|
|
const fakeModel = {} as LanguageModel;
|
|
|
|
describe('generateTitleFromMessage', () => {
|
|
beforeEach(() => {
|
|
mockGenerateText.mockReset();
|
|
});
|
|
|
|
it('returns null for empty input without calling the LLM', async () => {
|
|
const result = await generateTitleFromMessage(fakeModel, ' ');
|
|
expect(result).toBeNull();
|
|
expect(mockGenerateText).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns the message itself for trivial greetings without calling the LLM', async () => {
|
|
const result = await generateTitleFromMessage(fakeModel, 'hey');
|
|
expect(result).toBe('hey');
|
|
expect(mockGenerateText).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('skips the LLM for short multi-word messages', async () => {
|
|
const result = await generateTitleFromMessage(fakeModel, 'hi there');
|
|
expect(result).toBe('hi there');
|
|
expect(mockGenerateText).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('strips markdown heading prefixes from the LLM response', async () => {
|
|
mockGenerateText.mockResolvedValue({ text: '# Daily Berlin rain alert' });
|
|
const result = await generateTitleFromMessage(
|
|
fakeModel,
|
|
'Build a daily Berlin rain alert workflow',
|
|
);
|
|
expect(result).toBe('Daily Berlin rain alert');
|
|
});
|
|
|
|
it('strips inline emphasis markers from the LLM response', async () => {
|
|
mockGenerateText.mockResolvedValue({ text: 'Your **Berlin** rain alert' });
|
|
const result = await generateTitleFromMessage(
|
|
fakeModel,
|
|
'Build a daily Berlin rain alert workflow',
|
|
);
|
|
expect(result).toBe('Your Berlin rain alert');
|
|
});
|
|
|
|
it('strips <think> reasoning blocks from the LLM response', async () => {
|
|
mockGenerateText.mockResolvedValue({
|
|
text: '<think>Let me think about this</think>Deploy release pipeline',
|
|
});
|
|
const result = await generateTitleFromMessage(
|
|
fakeModel,
|
|
'Help me set up an automated deploy pipeline',
|
|
);
|
|
expect(result).toBe('Deploy release pipeline');
|
|
});
|
|
|
|
it('strips surrounding quotes from the LLM response', async () => {
|
|
mockGenerateText.mockResolvedValue({ text: '"Build Gmail to Slack workflow"' });
|
|
const result = await generateTitleFromMessage(
|
|
fakeModel,
|
|
'Build a workflow that forwards Gmail to Slack',
|
|
);
|
|
expect(result).toBe('Build Gmail to Slack workflow');
|
|
});
|
|
|
|
it('truncates titles longer than 80 characters at a word boundary', async () => {
|
|
mockGenerateText.mockResolvedValue({
|
|
text: 'Create a data table for users, then build a workflow that syncs them to our CRM every hour',
|
|
});
|
|
const result = await generateTitleFromMessage(
|
|
fakeModel,
|
|
'Create a data table for users and sync them to our CRM every hour with error alerting',
|
|
);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.length).toBeLessThanOrEqual(81);
|
|
expect(result!.endsWith('\u2026')).toBe(true);
|
|
});
|
|
|
|
it('returns null when the LLM returns empty text', async () => {
|
|
mockGenerateText.mockResolvedValue({ text: ' ' });
|
|
const result = await generateTitleFromMessage(
|
|
fakeModel,
|
|
'Build a daily Berlin rain alert workflow',
|
|
);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('passes the default instructions to the LLM', async () => {
|
|
mockGenerateText.mockResolvedValue({ text: 'Berlin rain alert' });
|
|
await generateTitleFromMessage(fakeModel, 'Build a daily Berlin rain alert workflow');
|
|
const call = mockGenerateText.mock.calls[0][0];
|
|
expect(call.messages[0].role).toBe('system');
|
|
expect(call.messages[0].content).toContain('markdown');
|
|
expect(call.messages[0].content).toContain('sentence case');
|
|
});
|
|
|
|
it('accepts custom instructions', async () => {
|
|
mockGenerateText.mockResolvedValue({ text: 'Custom title' });
|
|
await generateTitleFromMessage(fakeModel, 'Build a daily Berlin rain alert workflow', {
|
|
instructions: 'Custom system prompt',
|
|
});
|
|
const call = mockGenerateText.mock.calls[0][0];
|
|
expect(call.messages[0].content).toBe('Custom system prompt');
|
|
});
|
|
});
|