/**
* ProjectMemoryService Tests
*
* Tests for the high-level memory service.
*
* @module @agentkits/memory/__tests__/index.test
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { tmpdir } from 'node:os';
import {
ProjectMemoryService,
createProjectMemory,
DEFAULT_NAMESPACES,
MemoryEntry,
createDefaultEntry,
} from '../index.js';
describe('ProjectMemoryService', () => {
let service: ProjectMemoryService;
let testDir: string;
beforeEach(async () => {
// Create temp directory for tests
testDir = path.join(tmpdir(), `memory-test-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
service = new ProjectMemoryService({
baseDir: testDir,
dbFilename: 'test.db',
cacheEnabled: false,
enableVectorIndex: false,
autoPersistInterval: 0,
});
await service.initialize();
});
afterEach(async () => {
await service.shutdown();
// Cleanup temp directory
fs.rmSync(testDir, { recursive: true, force: true });
});
describe('Initialization', () => {
it('should initialize successfully', async () => {
const newService = new ProjectMemoryService({
baseDir: path.join(testDir, 'init-test'),
dbFilename: 'init.db',
});
await newService.initialize();
const health = await newService.healthCheck();
expect(health.status).toBe('healthy');
await newService.shutdown();
});
it('should create directory if not exists', async () => {
const newDir = path.join(testDir, 'new-dir');
const newService = new ProjectMemoryService({
baseDir: newDir,
dbFilename: 'new.db',
});
await newService.initialize();
expect(fs.existsSync(newDir)).toBe(true);
await newService.shutdown();
});
it('should accept string as baseDir', async () => {
const dir = path.join(testDir, 'string-dir');
const newService = new ProjectMemoryService(dir);
await newService.initialize();
expect(fs.existsSync(dir)).toBe(true);
await newService.shutdown();
});
});
describe('Store and Retrieve', () => {
it('should store entry via storeEntry convenience method', async () => {
const entry = await service.storeEntry({
key: 'test-key',
content: 'Test content',
namespace: 'test',
tags: ['tag1'],
});
expect(typeof entry.id).toBe('string');
expect(entry.id.length).toBeGreaterThan(0);
expect(entry.key).toBe('test-key');
expect(entry.content).toBe('Test content');
});
it('should get entry by id', async () => {
const stored = await service.storeEntry({
key: 'get-test',
content: 'Content',
namespace: 'test',
});
const retrieved = await service.get(stored.id);
expect(retrieved).not.toBeNull();
expect(retrieved!.id).toBe(stored.id);
});
it('should get entry by namespace and key', async () => {
await service.storeEntry({
key: 'ns-key-test',
content: 'Content',
namespace: 'my-namespace',
});
const retrieved = await service.getByKey('my-namespace', 'ns-key-test');
expect(retrieved).not.toBeNull();
expect(retrieved!.key).toBe('ns-key-test');
expect(retrieved!.namespace).toBe('my-namespace');
});
it('should update entry', async () => {
const stored = await service.storeEntry({
key: 'update-test',
content: 'Original',
namespace: 'test',
});
const updated = await service.update(stored.id, { content: 'Updated' });
expect(updated!.content).toBe('Updated');
});
it('should delete entry', async () => {
const stored = await service.storeEntry({
key: 'delete-test',
content: 'To delete',
namespace: 'test',
});
expect(await service.count()).toBe(1);
const deleted = await service.delete(stored.id);
expect(deleted).toBe(true);
expect(await service.count()).toBe(0);
});
});
describe('Query Operations', () => {
beforeEach(async () => {
await service.storeEntry({ key: 'p1', content: 'Pattern 1', namespace: 'patterns', tags: ['auth'] });
await service.storeEntry({ key: 'p2', content: 'Pattern 2', namespace: 'patterns', tags: ['api'] });
await service.storeEntry({ key: 'd1', content: 'Decision 1', namespace: 'decisions', tags: ['db'] });
});
it('should query all entries', async () => {
const results = await service.query({ type: 'hybrid', limit: 10 });
expect(results.length).toBe(3);
});
it('should query by namespace', async () => {
const results = await service.query({ type: 'hybrid', namespace: 'patterns', limit: 10 });
expect(results.length).toBe(2);
});
it('should use getByNamespace convenience method', async () => {
const results = await service.getByNamespace('patterns');
expect(results.length).toBe(2);
});
});
describe('Get or Create', () => {
it('should create entry if not exists', async () => {
const entry = await service.getOrCreate('test-ns', 'new-key', () => ({
key: 'new-key',
content: 'New content',
namespace: 'test-ns',
}));
expect(entry.content).toBe('New content');
});
it('should return existing entry if exists', async () => {
await service.storeEntry({
key: 'existing-key',
content: 'Existing content',
namespace: 'test-ns',
});
const entry = await service.getOrCreate('test-ns', 'existing-key', () => ({
key: 'existing-key',
content: 'New content',
namespace: 'test-ns',
}));
expect(entry.content).toBe('Existing content');
});
});
describe('Session Management', () => {
it('should start session', async () => {
const session = await service.startSession();
expect(typeof session.id).toBe('string');
expect(session.id.length).toBeGreaterThan(0);
expect(session.status).toBe('active');
expect(typeof session.startedAt).toBe('number');
expect(session.startedAt).toBeGreaterThan(0);
});
it('should get current session', async () => {
expect(service.getCurrentSession()).toBeNull();
await service.startSession();
const current = service.getCurrentSession();
expect(current).not.toBeNull();
expect(current!.status).toBe('active');
});
it('should create checkpoint', async () => {
await service.startSession();
await service.checkpoint('Test checkpoint');
const session = service.getCurrentSession();
expect(session!.lastCheckpoint).toBe('Test checkpoint');
});
it('should throw error when checkpoint without session', async () => {
await expect(service.checkpoint('Test')).rejects.toThrow('No active session');
});
it('should end session', async () => {
await service.startSession();
const ended = await service.endSession('Session summary');
expect(ended).not.toBeNull();
expect(ended!.status).toBe('completed');
expect(ended!.summary).toBe('Session summary');
expect(typeof ended!.endedAt).toBe('number');
expect(ended!.endedAt).toBeGreaterThan(0);
});
it('should add session id to entries', async () => {
const session = await service.startSession();
const entry = await service.storeEntry({
key: 'session-entry',
content: 'Content',
namespace: 'test',
});
expect(entry.sessionId).toBe(session.id);
});
it('should get recent sessions', async () => {
await service.startSession();
await service.endSession('Session 1');
await service.startSession();
await service.endSession('Session 2');
const sessions = await service.getRecentSessions();
expect(sessions.length).toBeGreaterThanOrEqual(2);
});
});
describe('Namespace Operations', () => {
beforeEach(async () => {
await service.storeEntry({ key: 'ns1-1', content: 'C1', namespace: 'ns1' });
await service.storeEntry({ key: 'ns1-2', content: 'C2', namespace: 'ns1' });
await service.storeEntry({ key: 'ns2-1', content: 'C3', namespace: 'ns2' });
});
it('should list namespaces', async () => {
const namespaces = await service.listNamespaces();
expect(namespaces).toContain('ns1');
expect(namespaces).toContain('ns2');
});
it('should count by namespace', async () => {
const count = await service.count('ns1');
expect(count).toBe(2);
});
it('should clear namespace', async () => {
const cleared = await service.clearNamespace('ns1');
expect(cleared).toBe(2);
expect(await service.count('ns1')).toBe(0);
expect(await service.count('ns2')).toBe(1);
});
});
describe('Bulk Operations', () => {
it('should bulk insert entries', async () => {
const entries = [
createDefaultEntry({ key: 'b1', content: 'C1', namespace: 'bulk' }),
createDefaultEntry({ key: 'b2', content: 'C2', namespace: 'bulk' }),
];
await service.bulkInsert(entries);
expect(await service.count('bulk')).toBe(2);
});
it('should bulk delete entries', async () => {
const e1 = await service.storeEntry({ key: 'bd1', content: 'C1', namespace: 'bd' });
const e2 = await service.storeEntry({ key: 'bd2', content: 'C2', namespace: 'bd' });
const deleted = await service.bulkDelete([e1.id, e2.id]);
expect(deleted).toBe(2);
expect(await service.count('bd')).toBe(0);
});
});
describe('Statistics and Health', () => {
it('should get stats', async () => {
await service.storeEntry({ key: 's1', content: 'C1', namespace: 'ns1' });
await service.storeEntry({ key: 's2', content: 'C2', namespace: 'ns2' });
const stats = await service.getStats();
expect(stats.totalEntries).toBe(2);
expect(typeof stats.entriesByNamespace).toBe('object');
expect(stats.entriesByNamespace).not.toBeNull();
});
it('should health check', async () => {
const health = await service.healthCheck();
expect(health.status).toBe('healthy');
expect(typeof health.components).toBe('object');
expect(health.components).not.toBeNull();
});
});
describe('Events', () => {
it('should emit entry:stored event', async () => {
const listener = vi.fn();
service.on('entry:stored', listener);
await service.storeEntry({ key: 'event-test', content: 'Content', namespace: 'test' });
expect(listener).toHaveBeenCalled();
});
it('should emit session:started event', async () => {
const listener = vi.fn();
service.on('session:started', listener);
await service.startSession();
expect(listener).toHaveBeenCalled();
});
it('should emit session:ended event', async () => {
const listener = vi.fn();
service.on('session:ended', listener);
await service.startSession();
await service.endSession();
expect(listener).toHaveBeenCalled();
});
});
});
describe('createProjectMemory factory', () => {
let testDir: string;
beforeEach(() => {
testDir = path.join(tmpdir(), `factory-test-${Date.now()}`);
});
afterEach(() => {
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should create memory service with defaults', async () => {
const service = createProjectMemory(testDir);
await service.initialize();
expect(fs.existsSync(testDir)).toBe(true);
await service.shutdown();
});
it('should accept options', async () => {
const service = createProjectMemory(testDir, {
cacheEnabled: false,
verbose: true,
});
await service.initialize();
await service.shutdown();
});
});
describe('DEFAULT_NAMESPACES', () => {
it('should have all required namespaces', () => {
expect(DEFAULT_NAMESPACES.CONTEXT).toBe('context');
expect(DEFAULT_NAMESPACES.ACTIVE).toBe('active-context');
expect(DEFAULT_NAMESPACES.SESSION).toBe('session-state');
expect(DEFAULT_NAMESPACES.PROGRESS).toBe('progress');
expect(DEFAULT_NAMESPACES.PATTERNS).toBe('patterns');
expect(DEFAULT_NAMESPACES.DECISIONS).toBe('decisions');
expect(DEFAULT_NAMESPACES.ERRORS).toBe('errors');
});
});
describe('ProjectMemoryService with embeddings', () => {
let service: ProjectMemoryService;
let testDir: string;
const mockEmbeddingGenerator = vi.fn().mockImplementation(async (content: string) => {
// Simple mock embedding based on content length
const embedding = new Float32Array(8);
for (let i = 0; i < 8; i++) {
embedding[i] = (content.charCodeAt(i % content.length) / 255) - 0.5;
}
return embedding;
});
beforeEach(async () => {
testDir = path.join(tmpdir(), `embedding-test-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
service = new ProjectMemoryService({
baseDir: testDir,
dbFilename: 'test.db',
cacheEnabled: true,
enableVectorIndex: true,
dimensions: 8,
embeddingGenerator: mockEmbeddingGenerator,
});
await service.initialize();
});
afterEach(async () => {
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
vi.clearAllMocks();
});
it('should generate embeddings when storing entries', async () => {
await service.storeEntry({
key: 'embed-test',
content: 'Test content for embedding',
namespace: 'test',
});
expect(mockEmbeddingGenerator).toHaveBeenCalledWith('Test content for embedding');
});
it('should perform semantic search', async () => {
await service.storeEntry({
key: 'auth-pattern',
content: 'Use JWT for authentication',
namespace: 'patterns',
});
await service.storeEntry({
key: 'db-pattern',
content: 'Use PostgreSQL for database',
namespace: 'patterns',
});
const results = await service.semanticSearch('authentication', 5);
expect(results.length).toBeGreaterThan(0);
});
it('should throw error for semantic search without embedding generator', async () => {
const noEmbedService = new ProjectMemoryService({
baseDir: path.join(testDir, 'no-embed'),
dbFilename: 'test.db',
});
await noEmbedService.initialize();
await expect(noEmbedService.semanticSearch('test', 5)).rejects.toThrow(
'Embedding generator not configured'
);
await noEmbedService.shutdown();
});
it('should use sqlite-vec for vector search', async () => {
await service.storeEntry({
key: 'v1',
content: 'First vector content',
namespace: 'vectors',
});
await service.storeEntry({
key: 'v2',
content: 'Second vector content',
namespace: 'vectors',
});
const embedding = await mockEmbeddingGenerator('First');
const results = await service.search(embedding, { k: 2 });
expect(results.length).toBeLessThanOrEqual(2);
});
it('should update embedding when content changes', async () => {
const entry = await service.storeEntry({
key: 'update-embed',
content: 'Original content',
namespace: 'test',
});
const callsBefore = mockEmbeddingGenerator.mock.calls.length;
await service.update(entry.id, { content: 'Updated content' });
// Should have generated new embedding for updated content
expect(mockEmbeddingGenerator.mock.calls.length).toBeGreaterThan(callsBefore);
});
it('should handle embedding generator failure gracefully', async () => {
const failingGenerator = vi.fn().mockRejectedValue(new Error('Embedding failed'));
const failService = new ProjectMemoryService({
baseDir: path.join(testDir, 'fail-embed'),
dbFilename: 'test.db',
embeddingGenerator: failingGenerator,
verbose: true,
});
await failService.initialize();
// Should not throw, just log warning
const entry = await failService.storeEntry({
key: 'fail-test',
content: 'Content that will fail embedding',
namespace: 'test',
});
expect(typeof entry.id).toBe('string');
expect(entry.id.length).toBeGreaterThan(0);
await failService.shutdown();
});
it('should remove from vector index when deleting', async () => {
const entry = await service.storeEntry({
key: 'delete-vector',
content: 'Content to delete',
namespace: 'test',
});
// Verify entry exists in vector search
const embedding = await mockEmbeddingGenerator('Content to delete');
const beforeDelete = await service.search(embedding, { k: 5 });
const foundBefore = beforeDelete.some(r => r.entry.id === entry.id);
expect(foundBefore).toBe(true);
await service.delete(entry.id);
// Entry should no longer be found
const afterDelete = await service.search(embedding, { k: 5 });
const foundAfter = afterDelete.some(r => r.entry.id === entry.id);
expect(foundAfter).toBe(false);
});
it('should apply threshold in search results', async () => {
await service.storeEntry({
key: 'similar',
content: 'Very similar content',
namespace: 'test',
});
await service.storeEntry({
key: 'different',
content: 'Completely different words here',
namespace: 'test',
});
const embedding = await mockEmbeddingGenerator('Very similar');
const results = await service.search(embedding, { k: 10, threshold: 0.9 });
// High threshold should filter out dissimilar results
expect(results.length).toBeLessThanOrEqual(2);
});
it('should include vector search info in getStats', async () => {
await service.storeEntry({
key: 'stats-test',
content: 'Content for stats',
namespace: 'test',
});
const stats = await service.getStats();
// With sqlite-vec, vector stats are included in backend stats
expect(stats.totalEntries).toBeGreaterThanOrEqual(1);
});
it('should include cache stats in getStats', async () => {
await service.storeEntry({
key: 'cache-test',
content: 'Content for cache',
namespace: 'test',
});
// Trigger a cache hit
await service.get((await service.query({ type: 'hybrid', limit: 1 }))[0].id);
const stats = await service.getStats();
expect(typeof stats.cacheStats).toBe('object');
expect(stats.cacheStats).not.toBeNull();
});
});
describe('ProjectMemoryService with cache', () => {
let service: ProjectMemoryService;
let testDir: string;
beforeEach(async () => {
testDir = path.join(tmpdir(), `cache-test-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
service = new ProjectMemoryService({
baseDir: testDir,
dbFilename: 'test.db',
cacheEnabled: true,
cacheSize: 100,
cacheTtl: 60000,
});
await service.initialize();
});
afterEach(async () => {
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should cache entries on get', async () => {
const entry = await service.storeEntry({
key: 'cache-hit',
content: 'Cached content',
namespace: 'test',
});
// First get populates cache
const first = await service.get(entry.id);
expect(first).not.toBeNull();
// Second get should hit cache
const second = await service.get(entry.id);
expect(second).not.toBeNull();
expect(second!.id).toBe(first!.id);
});
it('should cache entries on getByKey', async () => {
await service.storeEntry({
key: 'cache-key',
content: 'Cached by key',
namespace: 'ns',
});
// First get populates cache
const first = await service.getByKey('ns', 'cache-key');
expect(first).not.toBeNull();
// Second get should hit cache
const second = await service.getByKey('ns', 'cache-key');
expect(second).not.toBeNull();
});
it('should update cache on update', async () => {
const entry = await service.storeEntry({
key: 'update-cache',
content: 'Original',
namespace: 'test',
});
await service.update(entry.id, { content: 'Updated' });
const retrieved = await service.get(entry.id);
expect(retrieved!.content).toBe('Updated');
});
it('should invalidate cache on delete', async () => {
const entry = await service.storeEntry({
key: 'delete-cache',
content: 'To delete',
namespace: 'test',
});
// Populate cache
await service.get(entry.id);
// Delete
await service.delete(entry.id);
// Should not find in cache or backend
const retrieved = await service.get(entry.id);
expect(retrieved).toBeNull();
});
it('should invalidate cache pattern on clearNamespace', async () => {
await service.storeEntry({ key: 'c1', content: 'C1', namespace: 'clear-ns' });
await service.storeEntry({ key: 'c2', content: 'C2', namespace: 'clear-ns' });
await service.storeEntry({ key: 'k1', content: 'K1', namespace: 'keep-ns' });
await service.clearNamespace('clear-ns');
expect(await service.count('clear-ns')).toBe(0);
expect(await service.count('keep-ns')).toBe(1);
});
});
describe('ProjectMemoryService error handling', () => {
it('should throw error when not initialized', async () => {
const service = new ProjectMemoryService({
baseDir: path.join(tmpdir(), 'not-init-test'),
});
await expect(service.get('some-id')).rejects.toThrow('not initialized');
await expect(service.query({ type: 'hybrid', limit: 10 })).rejects.toThrow('not initialized');
});
it('should handle shutdown with active session', async () => {
const testDir = path.join(tmpdir(), `session-shutdown-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
const service = new ProjectMemoryService({ baseDir: testDir });
await service.initialize();
await service.startSession();
// Shutdown should end the session automatically
await service.shutdown();
// Verify session was ended
expect(service.getCurrentSession()).toBeNull();
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should handle double initialization', async () => {
const testDir = path.join(tmpdir(), `double-init-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
const service = new ProjectMemoryService({ baseDir: testDir });
await service.initialize();
await service.initialize(); // Should not throw
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should handle double shutdown', async () => {
const testDir = path.join(tmpdir(), `double-shutdown-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
const service = new ProjectMemoryService({ baseDir: testDir });
await service.initialize();
await service.shutdown();
await service.shutdown(); // Should not throw
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should return null when ending non-existent session', async () => {
const testDir = path.join(tmpdir(), `no-session-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
const service = new ProjectMemoryService({ baseDir: testDir });
await service.initialize();
const result = await service.endSession('summary');
expect(result).toBeNull();
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should handle delete of non-existent entry', async () => {
const testDir = path.join(tmpdir(), `delete-none-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
const service = new ProjectMemoryService({ baseDir: testDir });
await service.initialize();
const result = await service.delete('non-existent-id');
expect(result).toBe(false);
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should handle getRecentSessions with invalid JSON', async () => {
const testDir = path.join(tmpdir(), `invalid-session-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
const service = new ProjectMemoryService({ baseDir: testDir });
await service.initialize();
// Store invalid session data
await service.storeEntry({
key: 'session:invalid',
content: 'not valid json {{{',
namespace: DEFAULT_NAMESPACES.SESSION,
tags: ['session'],
});
// Should filter out invalid entries
const sessions = await service.getRecentSessions();
expect(sessions.every(s => s.id !== undefined)).toBe(true);
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
});
});
describe('createEmbeddingMemory factory', () => {
let testDir: string;
beforeEach(() => {
testDir = path.join(tmpdir(), `embed-factory-${Date.now()}`);
});
afterEach(() => {
fs.rmSync(testDir, { recursive: true, force: true });
});
it('should create memory service with embedding support', async () => {
const { createEmbeddingMemory } = await import('../index.js');
const mockGenerator = vi.fn().mockResolvedValue(new Float32Array(128));
const service = createEmbeddingMemory(testDir, mockGenerator, 128);
await service.initialize();
// Store an entry to trigger embedding generation
await service.storeEntry({
key: 'embed-test',
content: 'Test content',
namespace: 'test',
});
expect(mockGenerator).toHaveBeenCalled();
await service.shutdown();
});
});
/**
* STRICT TESTS - Bug Catchers
*
* These tests verify actual behavior, not just that "something exists".
* They catch real bugs and ensure data integrity.
*/
describe('Strict Data Integrity Tests', () => {
let service: ProjectMemoryService;
let testDir: string;
beforeEach(async () => {
testDir = path.join(tmpdir(), `strict-test-${Date.now()}`);
fs.mkdirSync(testDir, { recursive: true });
service = new ProjectMemoryService({
baseDir: testDir,
dbFilename: 'test.db',
cacheEnabled: false,
});
await service.initialize();
});
afterEach(async () => {
await service.shutdown();
fs.rmSync(testDir, { recursive: true, force: true });
});
describe('Concurrent Operations', () => {
it('should not lose data on concurrent writes', async () => {
const count = 50;
const promises = Array.from({ length: count }, (_, i) =>
service.storeEntry({
key: `concurrent-${i}`,
content: `Data ${i}`,
namespace: 'test',
})
);
await Promise.all(promises);
const totalCount = await service.count();
expect(totalCount).toBe(count);
});
it('should handle concurrent reads and writes', async () => {
// Pre-populate
await service.storeEntry({
key: 'read-write-test',
content: 'Initial',
namespace: 'test',
});
const writePromises = Array.from({ length: 10 }, (_, i) =>
service.storeEntry({
key: `write-${i}`,
content: `Write ${i}`,
namespace: 'test',
})
);
const readPromises = Array.from({ length: 10 }, () =>
service.getByKey('test', 'read-write-test')
);
const results = await Promise.all([...writePromises, ...readPromises]);
// All reads should return the same entry
const reads = results.slice(10);
reads.forEach((entry) => {
expect(entry).not.toBeNull();
expect(entry!.key).toBe('read-write-test');
});
});
});
describe('Unicode and Special Characters', () => {
it('should preserve unicode content exactly', async () => {
const testCases = [
{ name: 'emoji', content: '🎉🚀💻🔥✨' },
{ name: 'chinese', content: '中文测试内容' },
{ name: 'japanese', content: '日本語のテスト' },
{ name: 'korean', content: '한국어 테스트' },
{ name: 'mixed', content: 'Hello 世界 🌍 مرحبا' },
{ name: 'special', content: 'Tab\tNewline\nCarriage\rReturn' },
{ name: 'quotes', content: 'Single\' Double" Backtick`' },
{ name: 'sql-injection-attempt', content: "'; DROP TABLE memories; --" },
];
for (const { name, content } of testCases) {
const entry = await service.storeEntry({
key: `unicode-${name}`,
content,
namespace: 'test',
});
const retrieved = await service.get(entry.id);
expect(retrieved!.content).toBe(content);
}
});
it('should handle null bytes gracefully', async () => {
const content = 'Before\0After';
const entry = await service.storeEntry({
key: 'null-byte',
content,
namespace: 'test',
});
const retrieved = await service.get(entry.id);
// SQLite may handle null bytes differently, so we check it doesn't crash
expect(retrieved).not.toBeNull();
});
});
describe('Boundary Conditions', () => {
it('should handle empty content', async () => {
const entry = await service.storeEntry({
key: 'empty-content',
content: '',
namespace: 'test',
});
const retrieved = await service.get(entry.id);
expect(retrieved!.content).toBe('');
});
it('should handle very long keys', async () => {
const longKey = 'k'.repeat(1000);
const entry = await service.storeEntry({
key: longKey,
content: 'Content',
namespace: 'test',
});
const retrieved = await service.getByKey('test', longKey);
expect(retrieved!.key).toBe(longKey);
});
it('should handle large content (100KB)', async () => {
const largeContent = 'x'.repeat(100_000);
const entry = await service.storeEntry({
key: 'large-content',
content: largeContent,
namespace: 'test',
});
const retrieved = await service.get(entry.id);
expect(retrieved!.content.length).toBe(100_000);
});
it('should handle many tags', async () => {
const tags = Array.from({ length: 100 }, (_, i) => `tag-${i}`);
const entry = await service.storeEntry({
key: 'many-tags',
content: 'Content',
namespace: 'test',
tags,
});
const retrieved = await service.get(entry.id);
expect(retrieved!.tags).toHaveLength(100);
expect(retrieved!.tags).toContain('tag-50');
});
});
describe('Data Consistency', () => {
it('should increment version on update', async () => {
const entry = await service.storeEntry({
key: 'version-test',
content: 'v1',
namespace: 'test',
});
expect(entry.version).toBe(1);
const updated1 = await service.update(entry.id, { content: 'v2' });
expect(updated1!.version).toBe(2);
const updated2 = await service.update(entry.id, { content: 'v3' });
expect(updated2!.version).toBe(3);
});
it('should update timestamp on modification', async () => {
const entry = await service.storeEntry({
key: 'timestamp-test',
content: 'Original',
namespace: 'test',
});
const originalUpdatedAt = entry.updatedAt;
// Wait a bit to ensure timestamp difference
await new Promise((r) => setTimeout(r, 10));
const updated = await service.update(entry.id, { content: 'Modified' });
expect(updated!.updatedAt).toBeGreaterThan(originalUpdatedAt);
});
it('should preserve createdAt on update', async () => {
const entry = await service.storeEntry({
key: 'created-at-test',
content: 'Original',
namespace: 'test',
});
const originalCreatedAt = entry.createdAt;
await service.update(entry.id, { content: 'Modified' });
const retrieved = await service.get(entry.id);
expect(retrieved!.createdAt).toBe(originalCreatedAt);
});
});
describe('Query Correctness', () => {
beforeEach(async () => {
// Setup test data
await service.storeEntry({ key: 'a1', content: 'Auth pattern', namespace: 'patterns', tags: ['auth', 'security'] });
await service.storeEntry({ key: 'a2', content: 'API error', namespace: 'errors', tags: ['api'] });
await service.storeEntry({ key: 'a3', content: 'Database decision', namespace: 'decisions', tags: ['db'] });
});
it('should return correct count after operations', async () => {
expect(await service.count()).toBe(3);
await service.storeEntry({ key: 'a4', content: 'New', namespace: 'test' });
expect(await service.count()).toBe(4);
const entries = await service.query({ type: 'hybrid', namespace: 'patterns', limit: 10 });
await service.delete(entries[0].id);
expect(await service.count()).toBe(3);
});
it('should correctly filter by namespace', async () => {
const patterns = await service.getByNamespace('patterns');
expect(patterns.length).toBe(1);
expect(patterns[0].key).toBe('a1');
const errors = await service.getByNamespace('errors');
expect(errors.length).toBe(1);
expect(errors[0].key).toBe('a2');
});
it('should return empty array for non-existent namespace', async () => {
const results = await service.getByNamespace('non-existent');
expect(results).toEqual([]);
});
});
describe('Delete Behavior', () => {
it('should return false when deleting non-existent entry', async () => {
const deleted = await service.delete('non-existent-id');
expect(deleted).toBe(false);
});
it('should actually remove entry on delete', async () => {
const entry = await service.storeEntry({
key: 'delete-me',
content: 'Temporary',
namespace: 'test',
});
await service.delete(entry.id);
const retrieved = await service.get(entry.id);
expect(retrieved).toBeNull();
const byKey = await service.getByKey('test', 'delete-me');
expect(byKey).toBeNull();
});
it('should clear namespace correctly', async () => {
await service.storeEntry({ key: 'k1', content: 'C1', namespace: 'clear-ns' });
await service.storeEntry({ key: 'k2', content: 'C2', namespace: 'clear-ns' });
await service.storeEntry({ key: 'k3', content: 'C3', namespace: 'keep-ns' });
const cleared = await service.clearNamespace('clear-ns');
expect(cleared).toBe(2);
const remaining = await service.getByNamespace('clear-ns');
expect(remaining).toHaveLength(0);
const kept = await service.getByNamespace('keep-ns');
expect(kept).toHaveLength(1);
});
});
});