bitsofchris_openaugi-obsidi.../tests/openai-service.test.ts

236 lines
8.1 KiB
TypeScript
Raw Permalink Normal View History

2026-02-25 13:03:28 +00:00
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { OpenAIService } from '../src/services/openai-service';
describe('OpenAIService', () => {
let service: OpenAIService;
beforeEach(() => {
service = new OpenAIService('test-api-key', 'gpt-5');
});
describe('constructor', () => {
it('stores API key and model', () => {
// Access private fields via any cast (testing internals)
expect((service as any).apiKey).toBe('test-api-key');
expect((service as any).model).toBe('gpt-5');
});
});
describe('extractCustomContext (via prompt building)', () => {
// We test this through the prompt generation methods since extractCustomContext is private
it('includes context: section in transcript prompt', () => {
const content = 'Some transcript\n\ncontext:\nFocus on tech topics\n\nMore content';
const prompt = (service as any).getPrompt(content);
expect(prompt).toContain('USER CONTEXT');
expect(prompt).toContain('Focus on tech topics');
});
it('includes fenced context: section in transcript prompt', () => {
const content = 'Some content\n\n```context:\nOnly extract action items\n```\n\nMore stuff';
const prompt = (service as any).getPrompt(content);
expect(prompt).toContain('USER CONTEXT');
expect(prompt).toContain('Only extract action items');
});
it('handles content without context section', () => {
const content = 'Just a regular transcript with no special context';
const prompt = (service as any).getPrompt(content);
expect(prompt).not.toContain('USER CONTEXT');
expect(prompt).toContain('Just a regular transcript');
});
});
describe('getPrompt', () => {
it('includes the transcript content', () => {
const prompt = (service as any).getPrompt('My voice note about productivity');
expect(prompt).toContain('Transcript:\nMy voice note about productivity');
});
it('includes atomic notes instructions', () => {
const prompt = (service as any).getPrompt('test');
expect(prompt).toContain('Atomic Notes');
expect(prompt).toContain('self-contained');
});
it('includes task extraction instructions', () => {
const prompt = (service as any).getPrompt('test');
expect(prompt).toContain('Tasks');
expect(prompt).toContain('actionable');
});
it('includes AUGI command handling', () => {
const prompt = (service as any).getPrompt('test');
expect(prompt).toContain('AUGI');
expect(prompt).toContain('auggie');
});
});
describe('getDistillPrompt', () => {
it('includes content to distill', () => {
const prompt = (service as any).getDistillPrompt('Note content here');
expect(prompt).toContain('Content to Distill:\nNote content here');
});
it('uses custom prompt when provided', () => {
const custom = 'You are a custom processor. Do something special.';
const prompt = (service as any).getDistillPrompt('content', custom);
expect(prompt).toContain(custom);
// Should NOT contain the default distill instructions
expect(prompt).not.toContain('expert knowledge curator');
});
it('uses default prompt when no custom prompt', () => {
const prompt = (service as any).getDistillPrompt('content');
expect(prompt).toContain('expert knowledge curator');
});
it('appends custom context from content', () => {
const content = 'Notes here\n\ncontext:\nFocus on architecture\n\nMore notes';
const prompt = (service as any).getDistillPrompt(content);
expect(prompt).toContain('USER CONTEXT');
expect(prompt).toContain('Focus on architecture');
});
});
describe('getPublishPrompt', () => {
it('includes content to transform', () => {
const prompt = (service as any).getPublishPrompt('Blog content');
expect(prompt).toContain('Content to Transform:\nBlog content');
});
it('uses default publish prompt', () => {
const prompt = (service as any).getPublishPrompt('content');
expect(prompt).toContain('publishable blog post');
expect(prompt).toContain('PRESERVE');
});
it('uses custom prompt when provided', () => {
const custom = 'Write a newsletter instead.';
const prompt = (service as any).getPublishPrompt('content', custom);
expect(prompt).toContain('Write a newsletter instead.');
expect(prompt).not.toContain('publishable blog post');
});
});
describe('parseTranscript', () => {
it('throws when API key is not set', async () => {
const noKeyService = new OpenAIService('', 'gpt-5');
await expect(noKeyService.parseTranscript('content')).rejects.toThrow('OpenAI API key not set');
});
it('calls OpenAI API with correct parameters', async () => {
const mockResponse = {
ok: true,
json: async () => ({
choices: [{
message: {
content: JSON.stringify({
summary: 'Test summary',
notes: [{ title: 'Note 1', content: 'Content 1' }],
tasks: ['- [ ] Task 1']
}),
refusal: null
}
}]
})
};
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);
const result = await service.parseTranscript('Test transcript');
expect(fetchSpy).toHaveBeenCalledOnce();
const [url, options] = fetchSpy.mock.calls[0];
expect(url).toBe('https://api.openai.com/v1/chat/completions');
expect((options as any).method).toBe('POST');
expect(JSON.parse((options as any).body).model).toBe('gpt-5');
expect(result.summary).toBe('Test summary');
expect(result.notes).toHaveLength(1);
expect(result.tasks).toHaveLength(1);
fetchSpy.mockRestore();
});
it('throws on API error response', async () => {
const mockResponse = {
ok: false,
status: 401,
statusText: 'Unauthorized',
json: async () => ({ error: { message: 'Invalid API key' } })
};
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);
await expect(service.parseTranscript('content')).rejects.toThrow('OpenAI API error');
fetchSpy.mockRestore();
});
});
describe('distillContent', () => {
it('throws when API key is not set', async () => {
const noKeyService = new OpenAIService('', 'gpt-5');
await expect(noKeyService.distillContent('content')).rejects.toThrow('OpenAI API key not set');
});
it('parses structured response correctly', async () => {
const mockResponse = {
ok: true,
json: async () => ({
choices: [{
message: {
content: JSON.stringify({
summary: 'Distilled summary about [[Topic A]]',
notes: [
{ title: 'Topic A', content: 'Deep dive into A' },
{ title: 'Topic B', content: 'Overview of B' },
],
tasks: []
}),
refusal: null
}
}]
})
};
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);
const result = await service.distillContent('aggregated content');
expect(result.summary).toContain('Topic A');
expect(result.notes).toHaveLength(2);
expect(result.sourceNotes).toEqual([]); // Initialized as empty
expect(result.tasks).toEqual([]);
fetchSpy.mockRestore();
});
});
describe('publishContent', () => {
it('returns plain text content', async () => {
const mockResponse = {
ok: true,
json: async () => ({
choices: [{
message: {
content: '# My Blog Post\n\nThis is the published content.',
refusal: null
}
}]
})
};
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);
const result = await service.publishContent('raw notes');
expect(result).toContain('# My Blog Post');
expect(result).toContain('published content');
fetchSpy.mockRestore();
});
});
});