152 lines
4.5 KiB
TypeScript
152 lines
4.5 KiB
TypeScript
/**
|
|
* Note lifecycle hooks tests — background embed + auto-summarize.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock feature flags
|
|
const mockFlags: Record<string, boolean> = {};
|
|
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> = {}): 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();
|
|
});
|
|
});
|