/** * Note lifecycle hooks tests — background embed + auto-summarize. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock feature flags const mockFlags: Record = {}; vi.mock('./feature-flags.js', () => ({ isFeatureEnabled: (flag: string) => mockFlags[flag] ?? false, })); // Mock embeddings const mockEmbedText = vi.fn(); vi.mock('./embeddings.js', () => ({ embedText: (...args: unknown[]) => mockEmbedText(...args), stripHtmlForEmbedding: (html: string) => html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(), })); // Mock LLM const mockChatCompletion = vi.fn(); vi.mock('./llm.js', () => ({ llm: () => ({ chatCompletion: mockChatCompletion, }), })); // Mock datastore const mockFindById = vi.fn(); const mockUpsert = vi.fn(); vi.mock('./datastore.js', () => ({ getCollection: () => ({ findById: mockFindById, upsert: mockUpsert, }), })); // Mock artifact/action repos (lazy imports in note-hooks) vi.mock('../modules/note-artifacts/repository.js', () => ({ createNoteArtifact: vi.fn().mockResolvedValue({ id: 'artifact-1' }), })); vi.mock('../modules/note-agent-actions/repository.js', () => ({ createNoteAgentAction: vi.fn().mockResolvedValue({ id: 'action-1' }), })); import { runPostSaveHooks } from './note-hooks.js'; import type { NoteDoc } from '../modules/notes/types.js'; const mockLog = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), } as unknown as import('fastify').FastifyBaseLogger; function makeNote(overrides: Partial = {}): NoteDoc { const base: NoteDoc = { id: 'note-1', productId: 'notelett', userId: 'user-1', workspaceId: 'ws-1', title: 'Test Note', body: 'Some body text for testing purposes.', status: 'active', tags: [], links: [], createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z', createdBy: 'user-1', updatedBy: 'user-1', }; return Object.assign(base, overrides); } describe('runPostSaveHooks', () => { beforeEach(() => { vi.clearAllMocks(); Object.keys(mockFlags).forEach((k) => delete mockFlags[k]); }); it('does nothing when all flags are disabled', () => { runPostSaveHooks(makeNote(), mockLog); // No errors, no embedding calls expect(mockEmbedText).not.toHaveBeenCalled(); expect(mockChatCompletion).not.toHaveBeenCalled(); }); it('computes embedding when auto_embed flag is enabled', async () => { mockFlags['notelett_auto_embed_enabled'] = true; mockEmbedText.mockResolvedValue([0.1, 0.2, 0.3]); mockFindById.mockResolvedValue(makeNote()); mockUpsert.mockResolvedValue(makeNote()); runPostSaveHooks(makeNote(), mockLog); // Wait for fire-and-forget promise await new Promise((r) => setTimeout(r, 50)); expect(mockEmbedText).toHaveBeenCalled(); }); it('skips embedding for very short text', async () => { mockFlags['notelett_auto_embed_enabled'] = true; const note = makeNote({ body: 'Hi' }); // < 20 chars runPostSaveHooks(note, mockLog); await new Promise((r) => setTimeout(r, 50)); expect(mockEmbedText).not.toHaveBeenCalled(); }); it('skips auto-summarize for short notes', async () => { mockFlags['notelett_auto_summarize_enabled'] = true; const note = makeNote({ body: 'Short note.' }); // < 300 words runPostSaveHooks(note, mockLog); await new Promise((r) => setTimeout(r, 50)); expect(mockChatCompletion).not.toHaveBeenCalled(); }); it('skips auto-summarize if summary already exists', async () => { mockFlags['notelett_auto_summarize_enabled'] = true; const longBody = Array.from({ length: 400 }, (_, i) => `word${i}`).join(' '); const note = makeNote({ body: longBody, summaryArtifactId: 'existing-summary' }); runPostSaveHooks(note, mockLog); await new Promise((r) => setTimeout(r, 50)); expect(mockChatCompletion).not.toHaveBeenCalled(); }); it('generates auto-summary for long notes', async () => { mockFlags['notelett_auto_summarize_enabled'] = true; const longBody = Array.from({ length: 400 }, (_, i) => `word${i}`).join(' '); const note = makeNote({ body: longBody }); mockChatCompletion.mockResolvedValue({ content: 'Auto generated summary.', model: 'gpt-4o-mini', usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 }, }); mockFindById.mockResolvedValue(note); mockUpsert.mockResolvedValue(note); runPostSaveHooks(note, mockLog); await new Promise((r) => setTimeout(r, 100)); expect(mockChatCompletion).toHaveBeenCalled(); }); });