/**
* Observations module tests
* Tests modular observation functions with in-memory database
*
* Sources:
* - API patterns from src/core/sqlite/observations/store.ts
* - API patterns from src/core/sqlite/observations/get.ts
* - API patterns from src/core/sqlite/observations/recent.ts
* - Type definitions from src/core/sqlite/observations/types.ts
*/
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import { ClaudeRecallDatabase } from '../../src/core/sqlite/Database.js';
import {
storeObservation,
getObservationById,
getRecentObservations,
} from '../../src/core/sqlite/Observations.js';
import {
createSDKSession,
updateMemorySessionId,
} from '../../src/core/sqlite/Sessions.js';
import type { ObservationInput } from '../../src/core/sqlite/observations/types.js';
import type { Database } from 'bun:sqlite';
describe('Observations Module', () => {
let db: Database;
beforeEach(() => {
db = new ClaudeRecallDatabase(':memory:').db;
});
afterEach(() => {
db.close();
});
// Helper to create a valid observation input
function createObservationInput(overrides: Partial<ObservationInput> = {}): ObservationInput {
return {
type: 'discovery',
title: 'Test Observation',
subtitle: 'Test Subtitle',
facts: ['fact1', 'fact2'],
narrative: 'Test narrative content',
concepts: ['concept1', 'concept2'],
files_read: ['/path/to/file1.ts'],
files_modified: ['/path/to/file2.ts'],
...overrides,
};
}
// Helper to create a session and return memory_session_id for FK constraints
function createSessionWithMemoryId(contentSessionId: string, memorySessionId: string, project: string = 'test-project'): string {
const sessionId = createSDKSession(db, contentSessionId, project, 'initial prompt');
updateMemorySessionId(db, sessionId, memorySessionId);
return memorySessionId;
}
describe('storeObservation', () => {
it('should store observation and return id and createdAtEpoch', () => {
const memorySessionId = createSessionWithMemoryId('content-123', 'mem-session-123');
const project = 'test-project';
const observation = createObservationInput();
const result = storeObservation(db, memorySessionId, project, observation);
expect(typeof result.id).toBe('number');
expect(result.id).toBeGreaterThan(0);
expect(typeof result.createdAtEpoch).toBe('number');
expect(result.createdAtEpoch).toBeGreaterThan(0);
});
it('should store all observation fields correctly', () => {
const memorySessionId = createSessionWithMemoryId('content-456', 'mem-session-456');
const project = 'test-project';
const observation = createObservationInput({
type: 'bugfix',
title: 'Fixed critical bug',
subtitle: 'Memory leak',
facts: ['leak found', 'patched'],
narrative: 'Fixed memory leak in parser',
concepts: ['memory', 'gc'],
files_read: ['/src/parser.ts'],
files_modified: ['/src/parser.ts', '/tests/parser.test.ts'],
});
const result = storeObservation(db, memorySessionId, project, observation, 1, 100);
const stored = getObservationById(db, result.id);
expect(stored).not.toBeNull();
expect(stored?.type).toBe('bugfix');
expect(stored?.title).toBe('Fixed critical bug');
expect(stored?.memory_session_id).toBe(memorySessionId);
expect(stored?.project).toBe(project);
});
it('should respect overrideTimestampEpoch', () => {
const memorySessionId = createSessionWithMemoryId('content-789', 'mem-session-789');
const project = 'test-project';
const observation = createObservationInput();
const pastTimestamp = 1600000000000; // Sep 13, 2020
const result = storeObservation(
db,
memorySessionId,
project,
observation,
1,
0,
pastTimestamp
);
expect(result.createdAtEpoch).toBe(pastTimestamp);
const stored = getObservationById(db, result.id);
expect(stored?.created_at_epoch).toBe(pastTimestamp);
// Verify ISO string matches epoch
expect(new Date(stored!.created_at).getTime()).toBe(pastTimestamp);
});
it('should use current time when overrideTimestampEpoch not provided', () => {
const memorySessionId = createSessionWithMemoryId('content-now', 'session-now');
const before = Date.now();
const result = storeObservation(
db,
memorySessionId,
'project',
createObservationInput()
);
const after = Date.now();
expect(result.createdAtEpoch).toBeGreaterThanOrEqual(before);
expect(result.createdAtEpoch).toBeLessThanOrEqual(after);
});
it('should handle null subtitle and narrative', () => {
const memorySessionId = createSessionWithMemoryId('content-null', 'session-null');
const observation = createObservationInput({
subtitle: null,
narrative: null,
});
const result = storeObservation(db, memorySessionId, 'project', observation);
const stored = getObservationById(db, result.id);
expect(stored).not.toBeNull();
expect(stored?.id).toBe(result.id);
});
});
describe('getObservationById', () => {
it('should retrieve observation by ID', () => {
const memorySessionId = createSessionWithMemoryId('content-get', 'session-get');
const observation = createObservationInput({ title: 'Unique Title' });
const result = storeObservation(db, memorySessionId, 'project', observation);
const retrieved = getObservationById(db, result.id);
expect(retrieved).not.toBeNull();
expect(retrieved?.id).toBe(result.id);
expect(retrieved?.title).toBe('Unique Title');
});
it('should return null for non-existent observation', () => {
const retrieved = getObservationById(db, 99999);
expect(retrieved).toBeNull();
});
});
describe('getRecentObservations', () => {
it('should return observations ordered by date DESC', () => {
const project = 'test-project';
// Create sessions and store observations with different timestamps (oldest first)
const mem1 = createSessionWithMemoryId('content-1', 'session1', project);
const mem2 = createSessionWithMemoryId('content-2', 'session2', project);
const mem3 = createSessionWithMemoryId('content-3', 'session3', project);
storeObservation(db, mem1, project, createObservationInput(), 1, 0, 1000000000000);
storeObservation(db, mem2, project, createObservationInput(), 2, 0, 2000000000000);
storeObservation(db, mem3, project, createObservationInput(), 3, 0, 3000000000000);
const recent = getRecentObservations(db, project, 10);
expect(recent.length).toBe(3);
// Most recent first (DESC order)
expect(recent[0].prompt_number).toBe(3);
expect(recent[1].prompt_number).toBe(2);
expect(recent[2].prompt_number).toBe(1);
});
it('should respect limit parameter', () => {
const project = 'test-project';
const mem1 = createSessionWithMemoryId('content-lim1', 'session-lim1', project);
const mem2 = createSessionWithMemoryId('content-lim2', 'session-lim2', project);
const mem3 = createSessionWithMemoryId('content-lim3', 'session-lim3', project);
storeObservation(db, mem1, project, createObservationInput(), 1, 0, 1000000000000);
storeObservation(db, mem2, project, createObservationInput(), 2, 0, 2000000000000);
storeObservation(db, mem3, project, createObservationInput(), 3, 0, 3000000000000);
const recent = getRecentObservations(db, project, 2);
expect(recent.length).toBe(2);
});
it('should filter by project', () => {
const memA1 = createSessionWithMemoryId('content-a1', 'session-a1', 'project-a');
const memB1 = createSessionWithMemoryId('content-b1', 'session-b1', 'project-b');
const memA2 = createSessionWithMemoryId('content-a2', 'session-a2', 'project-a');
storeObservation(db, memA1, 'project-a', createObservationInput());
storeObservation(db, memB1, 'project-b', createObservationInput());
storeObservation(db, memA2, 'project-a', createObservationInput());
const recentA = getRecentObservations(db, 'project-a', 10);
const recentB = getRecentObservations(db, 'project-b', 10);
expect(recentA.length).toBe(2);
expect(recentB.length).toBe(1);
});
it('should return empty array for project with no observations', () => {
const recent = getRecentObservations(db, 'nonexistent-project', 10);
expect(recent).toEqual([]);
});
});
});