970 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			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);
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |