/** * 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('
Some body content here.
'); 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('Body text.
'); 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'); }); });