learning_ai_notes/backend/src/lib/copilot-transform.test.ts

112 lines
3.7 KiB
TypeScript

/**
* Copilot transform tests — LLM-powered + fallback heuristics.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
const mockChatCompletion = vi.fn();
const mockIsConfigured = vi.fn(() => true);
vi.mock('./llm.js', () => ({
llm: () => ({
chatCompletion: mockChatCompletion,
isConfigured: mockIsConfigured,
}),
}));
vi.mock('./telemetry.js', () => ({
trackEvent: vi.fn(),
}));
import { runCopilotTransform, suggestTitleFromBody } from './copilot-transform.js';
describe('runCopilotTransform', () => {
beforeEach(() => {
vi.clearAllMocks();
mockIsConfigured.mockReturnValue(true);
mockChatCompletion.mockResolvedValue({
content: 'LLM result text',
model: 'gpt-4o-mini',
usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
});
});
it('returns LLM output for shorten action', async () => {
const result = await runCopilotTransform('shorten', 'Some long text here');
expect(result).toBe('LLM result text');
expect(mockChatCompletion).toHaveBeenCalledTimes(1);
});
it('falls back to heuristic when LLM is not configured', async () => {
mockIsConfigured.mockReturnValue(false);
const result = await runCopilotTransform('bulletize', 'Line one\nLine two');
expect(result).toContain('- Line one');
expect(result).toContain('- Line two');
});
it('falls back to heuristic when LLM returns empty', async () => {
mockChatCompletion.mockResolvedValue({
content: '',
model: 'gpt-4o-mini',
usage: { promptTokens: 5, completionTokens: 0, totalTokens: 5 },
});
const result = await runCopilotTransform('bulletize', 'Line one\nLine two');
expect(result).toContain('- Line one');
});
it('falls back on LLM error', async () => {
mockChatCompletion.mockRejectedValue(new Error('API down'));
const result = await runCopilotTransform('shorten', 'Some words here to shorten down significantly');
// Fallback shorten truncates
expect(typeof result).toBe('string');
expect(result.length).toBeGreaterThan(0);
});
it('explain fallback returns informative message', async () => {
mockIsConfigured.mockReturnValue(false);
const result = await runCopilotTransform('explain', 'quantum entanglement');
expect(result).toContain('not available');
});
it('fix-rewrite fallback returns original text', async () => {
mockIsConfigured.mockReturnValue(false);
const result = await runCopilotTransform('fix-rewrite', 'original text');
expect(result).toBe('original text');
});
});
describe('suggestTitleFromBody', () => {
beforeEach(() => {
vi.clearAllMocks();
mockIsConfigured.mockReturnValue(true);
mockChatCompletion.mockResolvedValue({
content: 'Suggested Title',
model: 'gpt-4o-mini',
usage: { promptTokens: 5, completionTokens: 3, totalTokens: 8 },
});
});
it('returns LLM-suggested title', async () => {
const title = await suggestTitleFromBody('<p>Some body content here.</p>');
expect(title).toBe('Suggested Title');
});
it('falls back to first sentence when LLM unavailable', async () => {
mockIsConfigured.mockReturnValue(false);
const title = await suggestTitleFromBody('First sentence here. Second sentence.');
expect(title).toBe('First sentence here');
});
it('strips HTML before processing', async () => {
mockIsConfigured.mockReturnValue(false);
const title = await suggestTitleFromBody('<h1>My Title</h1><p>Body text.</p>');
expect(title).toBe('My Title Body text');
});
it('returns "Untitled note" for empty body', async () => {
mockIsConfigured.mockReturnValue(false);
const title = await suggestTitleFromBody('');
expect(title).toBe('Untitled note');
});
});