112 lines
3.7 KiB
TypeScript
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');
|
|
});
|
|
});
|