nixos/shared/linked-dotfiles/opencode/llmemory/test/integration.test.js
2025-10-29 18:46:16 -06:00

970 lines
28 KiB
JavaScript

import { describe, test, expect, beforeEach, afterEach } from 'vitest';
import Database from 'better-sqlite3';
import { initSchema, getSchemaVersion } from '../src/db/schema.js';
import { createMemoryDatabase } from '../src/db/connection.js';
import { storeMemory } from '../src/commands/store.js';
import { searchMemories } from '../src/commands/search.js';
import { listMemories } from '../src/commands/list.js';
import { pruneMemories } from '../src/commands/prune.js';
import { deleteMemories } from '../src/commands/delete.js';
describe('Database Layer', () => {
let db;
beforeEach(() => {
// Use in-memory database for speed
db = new Database(':memory:');
});
afterEach(() => {
if (db) {
db.close();
}
});
describe('Schema Initialization', () => {
test('creates memories table with correct schema', () => {
initSchema(db);
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='memories'"
).all();
expect(tables).toHaveLength(1);
expect(tables[0].name).toBe('memories');
// Check columns
const columns = db.prepare('PRAGMA table_info(memories)').all();
const columnNames = columns.map(c => c.name);
expect(columnNames).toContain('id');
expect(columnNames).toContain('content');
expect(columnNames).toContain('created_at');
expect(columnNames).toContain('entered_by');
expect(columnNames).toContain('expires_at');
});
test('creates tags table with correct schema', () => {
initSchema(db);
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='tags'"
).all();
expect(tables).toHaveLength(1);
const columns = db.prepare('PRAGMA table_info(tags)').all();
const columnNames = columns.map(c => c.name);
expect(columnNames).toContain('id');
expect(columnNames).toContain('name');
expect(columnNames).toContain('created_at');
});
test('creates memory_tags junction table', () => {
initSchema(db);
const tables = db.prepare(
"SELECT name FROM sqlite_master WHERE type='table' AND name='memory_tags'"
).all();
expect(tables).toHaveLength(1);
const columns = db.prepare('PRAGMA table_info(memory_tags)').all();
const columnNames = columns.map(c => c.name);
expect(columnNames).toContain('memory_id');
expect(columnNames).toContain('tag_id');
});
test('creates metadata table with schema_version', () => {
initSchema(db);
const version = db.prepare(
"SELECT value FROM metadata WHERE key = 'schema_version'"
).get();
expect(version).toBeDefined();
expect(version.value).toBe('1');
});
test('creates indexes on memories(created_at, expires_at)', () => {
initSchema(db);
const indexes = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='memories'"
).all();
const indexNames = indexes.map(i => i.name);
expect(indexNames).toContain('idx_memories_created');
expect(indexNames).toContain('idx_memories_expires');
});
test('creates indexes on tags(name) and memory_tags(tag_id)', () => {
initSchema(db);
const tagIndexes = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='tags'"
).all();
expect(tagIndexes.some(i => i.name === 'idx_tags_name')).toBe(true);
const junctionIndexes = db.prepare(
"SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='memory_tags'"
).all();
expect(junctionIndexes.some(i => i.name === 'idx_memory_tags_tag')).toBe(true);
});
test('enables WAL mode for better concurrency', () => {
initSchema(db);
const journalMode = db.pragma('journal_mode', { simple: true });
// In-memory databases return 'memory' instead of 'wal'
// This is expected behavior for :memory: databases
expect(['wal', 'memory']).toContain(journalMode);
});
});
describe('Connection Management', () => {
test('opens database connection', () => {
const testDb = createMemoryDatabase();
expect(testDb).toBeDefined();
// Should be able to query
const result = testDb.prepare('SELECT 1 as test').get();
expect(result.test).toBe(1);
testDb.close();
});
test('initializes schema on first run', () => {
const testDb = createMemoryDatabase();
// Check that tables exist
const tables = testDb.prepare(
"SELECT name FROM sqlite_master WHERE type='table'"
).all();
const tableNames = tables.map(t => t.name);
expect(tableNames).toContain('memories');
expect(tableNames).toContain('tags');
expect(tableNames).toContain('memory_tags');
expect(tableNames).toContain('metadata');
testDb.close();
});
test('skips schema creation if already initialized', () => {
const testDb = new Database(':memory:');
// Initialize twice
initSchema(testDb);
initSchema(testDb);
// Should still have correct schema version
const version = getSchemaVersion(testDb);
expect(version).toBe(1);
testDb.close();
});
test('sets pragmas (WAL, cache_size, synchronous)', () => {
const testDb = createMemoryDatabase();
const journalMode = testDb.pragma('journal_mode', { simple: true });
// In-memory databases return 'memory' instead of 'wal'
expect(['wal', 'memory']).toContain(journalMode);
const synchronous = testDb.pragma('synchronous', { simple: true });
expect(synchronous).toBe(1); // NORMAL
testDb.close();
});
test('closes connection properly', () => {
const testDb = createMemoryDatabase();
expect(() => testDb.close()).not.toThrow();
expect(testDb.open).toBe(false);
});
});
});
describe('Store Command', () => {
let db;
beforeEach(() => {
db = createMemoryDatabase();
});
afterEach(() => {
if (db) {
db.close();
}
});
test('stores memory with tags', () => {
const result = storeMemory(db, {
content: 'Docker uses bridge networks by default',
tags: 'docker,networking',
entered_by: 'test'
});
expect(result.id).toBeDefined();
expect(result.content).toBe('Docker uses bridge networks by default');
// Verify in database
const memory = db.prepare('SELECT * FROM memories WHERE id = ?').get(result.id);
expect(memory.content).toBe('Docker uses bridge networks by default');
expect(memory.entered_by).toBe('test');
// Verify tags
const tags = db.prepare(`
SELECT t.name FROM tags t
JOIN memory_tags mt ON t.id = mt.tag_id
WHERE mt.memory_id = ?
ORDER BY t.name
`).all(result.id);
expect(tags.map(t => t.name)).toEqual(['docker', 'networking']);
});
test('rejects content over 10KB', () => {
const longContent = 'x'.repeat(10001);
expect(() => {
storeMemory(db, { content: longContent });
}).toThrow('Content exceeds 10KB limit');
});
test('normalizes tags to lowercase', () => {
storeMemory(db, {
content: 'Test memory',
tags: 'Docker,NETWORKING,KuberNeteS'
});
const tags = db.prepare('SELECT name FROM tags ORDER BY name').all();
expect(tags.map(t => t.name)).toEqual(['docker', 'kubernetes', 'networking']);
});
test('handles missing tags gracefully', () => {
const result = storeMemory(db, {
content: 'Memory without tags'
});
expect(result.id).toBeDefined();
const tags = db.prepare(`
SELECT t.name FROM tags t
JOIN memory_tags mt ON t.id = mt.tag_id
WHERE mt.memory_id = ?
`).all(result.id);
expect(tags).toHaveLength(0);
});
test('handles expiration date parsing', () => {
const futureDate = new Date(Date.now() + 86400000); // Tomorrow
const result = storeMemory(db, {
content: 'Temporary memory',
expires_at: futureDate.toISOString()
});
const memory = db.prepare('SELECT expires_at FROM memories WHERE id = ?').get(result.id);
expect(memory.expires_at).toBeGreaterThan(Math.floor(Date.now() / 1000));
});
test('deduplicates tags across memories', () => {
storeMemory(db, { content: 'Memory 1', tags: 'docker,networking' });
storeMemory(db, { content: 'Memory 2', tags: 'docker,kubernetes' });
const tags = db.prepare('SELECT name FROM tags ORDER BY name').all();
expect(tags.map(t => t.name)).toEqual(['docker', 'kubernetes', 'networking']);
});
test('rejects empty content', () => {
expect(() => {
storeMemory(db, { content: '' });
}).toThrow(); // Just check that it throws, message might vary
});
test('rejects expiration in the past', () => {
const pastDate = new Date(Date.now() - 86400000); // Yesterday
expect(() => {
storeMemory(db, {
content: 'Test',
expires_at: pastDate.toISOString()
});
}).toThrow('Expiration date must be in the future');
});
});
describe('Search Command', () => {
let db;
beforeEach(() => {
db = createMemoryDatabase();
// Seed with test data
storeMemory(db, {
content: 'Docker uses bridge networks by default',
tags: 'docker,networking'
});
storeMemory(db, {
content: 'Kubernetes pods share network namespace',
tags: 'kubernetes,networking'
});
storeMemory(db, {
content: 'PostgreSQL requires explicit vacuum',
tags: 'postgresql,database'
});
});
afterEach(() => {
if (db) {
db.close();
}
});
test('finds memories by content (case-insensitive)', () => {
const results = searchMemories(db, 'docker');
expect(results).toHaveLength(1);
expect(results[0].content).toContain('Docker');
});
test('filters by tags (AND logic)', () => {
const results = searchMemories(db, '', { tags: ['networking'] });
expect(results).toHaveLength(2);
const contents = results.map(r => r.content);
expect(contents).toContain('Docker uses bridge networks by default');
expect(contents).toContain('Kubernetes pods share network namespace');
});
test('filters by tags (OR logic with anyTag)', () => {
const results = searchMemories(db, '', { tags: ['docker', 'postgresql'], anyTag: true });
expect(results).toHaveLength(2);
const contents = results.map(r => r.content);
expect(contents).toContain('Docker uses bridge networks by default');
expect(contents).toContain('PostgreSQL requires explicit vacuum');
});
test('filters by date range (after/before)', () => {
const now = Date.now();
// Add a memory from "yesterday"
db.prepare('UPDATE memories SET created_at = ? WHERE id = 1').run(
Math.floor((now - 86400000) / 1000)
);
// Search for memories after yesterday
const results = searchMemories(db, '', {
after: Math.floor((now - 43200000) / 1000) // 12 hours ago
});
expect(results.length).toBeGreaterThanOrEqual(2);
});
test('filters by entered_by (agent)', () => {
storeMemory(db, {
content: 'Memory from investigate agent',
entered_by: 'investigate-agent'
});
const results = searchMemories(db, '', { entered_by: 'investigate-agent' });
expect(results).toHaveLength(1);
expect(results[0].entered_by).toBe('investigate-agent');
});
test('excludes expired memories automatically', () => {
// Add expired memory (bypass CHECK constraint by inserting with created_at in past)
const pastTimestamp = Math.floor((Date.now() - 86400000) / 1000); // Yesterday
db.prepare('INSERT INTO memories (content, created_at, expires_at) VALUES (?, ?, ?)').run(
'Expired memory',
pastTimestamp - 86400, // created_at even earlier
pastTimestamp // expires_at in past but after created_at
);
const results = searchMemories(db, 'expired');
expect(results).toHaveLength(0);
});
test('respects limit option', () => {
// Add more memories
for (let i = 0; i < 10; i++) {
storeMemory(db, { content: `Memory ${i}`, tags: 'test' });
}
const results = searchMemories(db, '', { limit: 5 });
expect(results).toHaveLength(5);
});
test('orders by created_at DESC', () => {
const results = searchMemories(db, '');
// Results should be in descending order (newest first)
for (let i = 1; i < results.length; i++) {
expect(results[i - 1].created_at).toBeGreaterThanOrEqual(results[i].created_at);
}
});
test('returns memory with tags joined', () => {
const results = searchMemories(db, 'docker');
expect(results).toHaveLength(1);
expect(results[0].tags).toBeTruthy();
expect(results[0].tags).toContain('docker');
expect(results[0].tags).toContain('networking');
});
});
describe('Integration Tests', () => {
let db;
beforeEach(() => {
db = createMemoryDatabase();
});
afterEach(() => {
if (db) {
db.close();
}
});
describe('Full Workflow', () => {
test('store → search → retrieve workflow', () => {
// Store
const stored = storeMemory(db, {
content: 'Docker uses bridge networks',
tags: 'docker,networking'
});
expect(stored.id).toBeDefined();
// Search
const results = searchMemories(db, 'docker');
expect(results).toHaveLength(1);
expect(results[0].content).toBe('Docker uses bridge networks');
// List
const all = listMemories(db);
expect(all).toHaveLength(1);
expect(all[0].tags).toContain('docker');
});
test('store multiple → list → filter by tags', () => {
storeMemory(db, { content: 'Memory 1', tags: 'docker,networking' });
storeMemory(db, { content: 'Memory 2', tags: 'kubernetes,networking' });
storeMemory(db, { content: 'Memory 3', tags: 'postgresql,database' });
const all = listMemories(db);
expect(all).toHaveLength(3);
const networkingOnly = listMemories(db, { tags: ['networking'] });
expect(networkingOnly).toHaveLength(2);
});
test('store with expiration → prune → verify removed', () => {
// Store non-expired
storeMemory(db, { content: 'Active memory' });
// Store expired (manually set to past by updating both timestamps)
const expired = storeMemory(db, { content: 'Expired memory' });
const pastCreated = Math.floor((Date.now() - 172800000) / 1000); // 2 days ago
const pastExpired = Math.floor((Date.now() - 86400000) / 1000); // 1 day ago
db.prepare('UPDATE memories SET created_at = ?, expires_at = ? WHERE id = ?').run(
pastCreated,
pastExpired,
expired.id
);
// Verify both exist
const before = listMemories(db);
expect(before).toHaveLength(1); // Expired is filtered out
// Prune
const result = pruneMemories(db);
expect(result.count).toBe(1);
expect(result.deleted).toBe(true);
// Verify expired removed
const all = db.prepare('SELECT * FROM memories').all();
expect(all).toHaveLength(1);
expect(all[0].content).toBe('Active memory');
});
});
describe('Performance', () => {
test('searches 100 memories in <50ms (Phase 1 target)', () => {
// Insert 100 memories
for (let i = 0; i < 100; i++) {
storeMemory(db, {
content: `Memory ${i} about docker and networking`,
tags: i % 2 === 0 ? 'docker' : 'networking'
});
}
const start = Date.now();
const results = searchMemories(db, 'docker');
const duration = Date.now() - start;
expect(results.length).toBeGreaterThan(0);
expect(duration).toBeLessThan(50);
});
test('stores 100 memories in <1 second', () => {
const start = Date.now();
for (let i = 0; i < 100; i++) {
storeMemory(db, {
content: `Memory ${i}`,
tags: 'test'
});
}
const duration = Date.now() - start;
expect(duration).toBeLessThan(1000);
});
});
describe('Edge Cases', () => {
test('handles empty search query', () => {
storeMemory(db, { content: 'Test memory' });
const results = searchMemories(db, '');
expect(results).toHaveLength(1);
});
test('handles no results found', () => {
storeMemory(db, { content: 'Test memory' });
const results = searchMemories(db, 'nonexistent');
expect(results).toHaveLength(0);
});
test('handles special characters in content', () => {
const specialContent = 'Test with special chars: @#$%^&*()';
storeMemory(db, { content: specialContent });
const results = searchMemories(db, 'special chars');
expect(results).toHaveLength(1);
expect(results[0].content).toBe(specialContent);
});
test('handles unicode in content and tags', () => {
storeMemory(db, {
content: 'Unicode test: café, 日本語, emoji 🚀',
tags: 'café,日本語'
});
const results = searchMemories(db, 'café');
expect(results).toHaveLength(1);
});
test('handles very long tag lists', () => {
const manyTags = Array.from({ length: 20 }, (_, i) => `tag${i}`).join(',');
const stored = storeMemory(db, {
content: 'Memory with many tags',
tags: manyTags
});
const results = searchMemories(db, '', { tags: ['tag5'] });
expect(results).toHaveLength(1);
});
});
});
describe('Delete Command', () => {
let db;
beforeEach(() => {
db = createMemoryDatabase();
// Seed with test data
storeMemory(db, {
content: 'Test memory 1',
tags: 'test,demo',
entered_by: 'test-agent'
});
storeMemory(db, {
content: 'Test memory 2',
tags: 'test,sample',
entered_by: 'test-agent'
});
storeMemory(db, {
content: 'Production memory',
tags: 'prod,important',
entered_by: 'prod-agent'
});
storeMemory(db, {
content: 'Docker networking notes',
tags: 'docker,networking',
entered_by: 'manual'
});
});
afterEach(() => {
if (db) {
db.close();
}
});
describe('Delete by IDs', () => {
test('deletes memories by single ID', () => {
const result = deleteMemories(db, { ids: [1] });
expect(result.count).toBe(1);
expect(result.deleted).toBe(true);
const remaining = db.prepare('SELECT * FROM memories').all();
expect(remaining).toHaveLength(3);
expect(remaining.find(m => m.id === 1)).toBeUndefined();
});
test('deletes memories by comma-separated IDs', () => {
const result = deleteMemories(db, { ids: [1, 2] });
expect(result.count).toBe(2);
expect(result.deleted).toBe(true);
const remaining = db.prepare('SELECT * FROM memories').all();
expect(remaining).toHaveLength(2);
});
test('handles non-existent IDs gracefully', () => {
const result = deleteMemories(db, { ids: [999, 1000] });
expect(result.count).toBe(0);
expect(result.deleted).toBe(true);
});
test('handles mix of valid and invalid IDs', () => {
const result = deleteMemories(db, { ids: [1, 999, 2] });
expect(result.count).toBe(2);
});
});
describe('Delete by Tags', () => {
test('deletes memories by single tag', () => {
const result = deleteMemories(db, { tags: ['test'] });
expect(result.count).toBe(2);
expect(result.deleted).toBe(true);
const remaining = db.prepare('SELECT * FROM memories').all();
expect(remaining).toHaveLength(2);
expect(remaining.find(m => m.content.includes('Test'))).toBeUndefined();
});
test('deletes memories by multiple tags (AND logic)', () => {
const result = deleteMemories(db, { tags: ['test', 'demo'] });
expect(result.count).toBe(1);
expect(result.deleted).toBe(true);
const memory = db.prepare('SELECT * FROM memories WHERE id = 1').get();
expect(memory).toBeUndefined();
});
test('deletes memories by tags with OR logic (anyTag)', () => {
const result = deleteMemories(db, {
tags: ['demo', 'docker'],
anyTag: true
});
expect(result.count).toBe(2); // Memory 1 (demo) and Memory 4 (docker)
expect(result.deleted).toBe(true);
});
test('returns zero count when no tags match', () => {
const result = deleteMemories(db, { tags: ['nonexistent'] });
expect(result.count).toBe(0);
});
});
describe('Delete by Content (LIKE query)', () => {
test('deletes memories matching LIKE query', () => {
const result = deleteMemories(db, { query: 'Test' });
expect(result.count).toBe(2);
expect(result.deleted).toBe(true);
});
test('case-insensitive LIKE matching', () => {
const result = deleteMemories(db, { query: 'DOCKER' });
expect(result.count).toBe(1);
expect(result.deleted).toBe(true);
});
test('handles partial matches', () => {
const result = deleteMemories(db, { query: 'memory' });
expect(result.count).toBe(3); // Matches "Test memory 1", "Test memory 2", "Production memory"
});
});
describe('Delete by Date Range', () => {
test('deletes memories before date', () => {
const now = Date.now();
// Update memory 1 to be from yesterday
db.prepare('UPDATE memories SET created_at = ? WHERE id = 1').run(
Math.floor((now - 86400000) / 1000)
);
const result = deleteMemories(db, {
before: Math.floor(now / 1000)
});
expect(result.count).toBeGreaterThanOrEqual(1);
});
test('deletes memories after date', () => {
const yesterday = Math.floor((Date.now() - 86400000) / 1000);
const result = deleteMemories(db, {
after: yesterday
});
expect(result.count).toBeGreaterThanOrEqual(3);
});
test('deletes memories in date range (after + before)', () => {
const now = Date.now();
const yesterday = Math.floor((now - 86400000) / 1000);
const tomorrow = Math.floor((now + 86400000) / 1000);
// Set specific timestamps
db.prepare('UPDATE memories SET created_at = ? WHERE id = 1').run(yesterday - 86400);
db.prepare('UPDATE memories SET created_at = ? WHERE id = 2').run(yesterday);
db.prepare('UPDATE memories SET created_at = ? WHERE id = 3').run(Math.floor(now / 1000));
const result = deleteMemories(db, {
after: yesterday - 3600, // After memory 1
before: Math.floor(now / 1000) - 3600 // Before memory 3
});
expect(result.count).toBe(1); // Only memory 2
});
});
describe('Delete by Agent', () => {
test('deletes memories by entered_by agent', () => {
const result = deleteMemories(db, { entered_by: 'test-agent' });
expect(result.count).toBe(2);
expect(result.deleted).toBe(true);
const remaining = db.prepare('SELECT * FROM memories').all();
expect(remaining.every(m => m.entered_by !== 'test-agent')).toBe(true);
});
test('combination: agent + tags', () => {
const result = deleteMemories(db, {
entered_by: 'test-agent',
tags: ['demo']
});
expect(result.count).toBe(1); // Only memory 1
});
});
describe('Expired Memory Handling', () => {
test('excludes expired memories by default', () => {
// Create expired memory
const pastCreated = Math.floor((Date.now() - 172800000) / 1000);
const pastExpired = Math.floor((Date.now() - 86400000) / 1000);
db.prepare('INSERT INTO memories (content, created_at, expires_at, entered_by) VALUES (?, ?, ?, ?)').run(
'Expired test memory',
pastCreated,
pastExpired,
'test-agent'
);
const result = deleteMemories(db, { entered_by: 'test-agent' });
expect(result.count).toBe(2); // Only non-expired test-agent memories
});
test('includes expired with includeExpired flag', () => {
// Create expired memory
const pastCreated = Math.floor((Date.now() - 172800000) / 1000);
const pastExpired = Math.floor((Date.now() - 86400000) / 1000);
db.prepare('INSERT INTO memories (content, created_at, expires_at, entered_by) VALUES (?, ?, ?, ?)').run(
'Expired test memory',
pastCreated,
pastExpired,
'test-agent'
);
const result = deleteMemories(db, {
entered_by: 'test-agent',
includeExpired: true
});
expect(result.count).toBe(3); // All test-agent memories including expired
});
test('deletes only expired with expiredOnly flag', () => {
// Create expired memory
const pastCreated = Math.floor((Date.now() - 172800000) / 1000);
const pastExpired = Math.floor((Date.now() - 86400000) / 1000);
db.prepare('INSERT INTO memories (content, created_at, expires_at, entered_by) VALUES (?, ?, ?, ?)').run(
'Expired memory',
pastCreated,
pastExpired,
'test-agent'
);
const result = deleteMemories(db, { expiredOnly: true });
expect(result.count).toBe(1);
// Verify non-expired still exist
const remaining = db.prepare('SELECT * FROM memories').all();
expect(remaining).toHaveLength(4);
});
});
describe('Dry Run Mode', () => {
test('dry-run returns memories without deleting', () => {
const result = deleteMemories(db, {
tags: ['test'],
dryRun: true
});
expect(result.count).toBe(2);
expect(result.deleted).toBe(false);
expect(result.memories).toHaveLength(2);
// Verify nothing was deleted
const all = db.prepare('SELECT * FROM memories').all();
expect(all).toHaveLength(4);
});
test('dry-run includes memory details', () => {
const result = deleteMemories(db, {
ids: [1],
dryRun: true
});
expect(result.memories[0]).toHaveProperty('id');
expect(result.memories[0]).toHaveProperty('content');
expect(result.memories[0]).toHaveProperty('created_at');
});
});
describe('Safety Features', () => {
test('requires at least one filter criterion', () => {
expect(() => {
deleteMemories(db, {});
}).toThrow('At least one filter criterion is required');
});
test('handles empty result set gracefully', () => {
const result = deleteMemories(db, { tags: ['nonexistent'] });
expect(result.count).toBe(0);
expect(result.deleted).toBe(true);
});
});
describe('Combination Filters', () => {
test('combines tags + query', () => {
const result = deleteMemories(db, {
tags: ['test'],
query: 'memory 1'
});
expect(result.count).toBe(1); // Only "Test memory 1"
});
test('combines agent + date range', () => {
const now = Date.now();
const yesterday = Math.floor((now - 86400000) / 1000);
db.prepare('UPDATE memories SET created_at = ? WHERE id = 1').run(yesterday);
const result = deleteMemories(db, {
entered_by: 'test-agent',
after: yesterday - 3600
});
expect(result.count).toBeGreaterThanOrEqual(1);
});
test('combines all filter types', () => {
const result = deleteMemories(db, {
tags: ['test'],
query: 'memory',
entered_by: 'test-agent',
dryRun: true
});
expect(result.count).toBe(2);
expect(result.deleted).toBe(false);
});
});
});