wip: [01-stabilize] paused at task 1/1 - OCR Hallucination Immune logic via Semantic delta window and fret-isolation
This commit is contained in:
199
.agent/services/claude-mem/tests/sqlite/data-integrity.test.ts
Normal file
199
.agent/services/claude-mem/tests/sqlite/data-integrity.test.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Data integrity tests for TRIAGE-03
|
||||
* Tests: content-hash deduplication, project name collision, empty project guard, stuck isProcessing
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ClaudeMemDatabase } from '../../src/services/sqlite/Database.js';
|
||||
import {
|
||||
storeObservation,
|
||||
computeObservationContentHash,
|
||||
findDuplicateObservation,
|
||||
} from '../../src/services/sqlite/observations/store.js';
|
||||
import {
|
||||
createSDKSession,
|
||||
updateMemorySessionId,
|
||||
} from '../../src/services/sqlite/Sessions.js';
|
||||
import { storeObservations } from '../../src/services/sqlite/transactions.js';
|
||||
import { PendingMessageStore } from '../../src/services/sqlite/PendingMessageStore.js';
|
||||
import type { ObservationInput } from '../../src/services/sqlite/observations/types.js';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionWithMemoryId(db: Database, contentSessionId: string, memorySessionId: string, project: string = 'test-project'): string {
|
||||
const sessionId = createSDKSession(db, contentSessionId, project, 'initial prompt');
|
||||
updateMemorySessionId(db, sessionId, memorySessionId);
|
||||
return memorySessionId;
|
||||
}
|
||||
|
||||
describe('TRIAGE-03: Data Integrity', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new ClaudeMemDatabase(':memory:').db;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
describe('Content-hash deduplication', () => {
|
||||
it('computeObservationContentHash produces consistent hashes', () => {
|
||||
const hash1 = computeObservationContentHash('session-1', 'Title A', 'Narrative A');
|
||||
const hash2 = computeObservationContentHash('session-1', 'Title A', 'Narrative A');
|
||||
expect(hash1).toBe(hash2);
|
||||
expect(hash1.length).toBe(16);
|
||||
});
|
||||
|
||||
it('computeObservationContentHash produces different hashes for different content', () => {
|
||||
const hash1 = computeObservationContentHash('session-1', 'Title A', 'Narrative A');
|
||||
const hash2 = computeObservationContentHash('session-1', 'Title B', 'Narrative B');
|
||||
expect(hash1).not.toBe(hash2);
|
||||
});
|
||||
|
||||
it('computeObservationContentHash handles nulls', () => {
|
||||
const hash = computeObservationContentHash('session-1', null, null);
|
||||
expect(hash.length).toBe(16);
|
||||
});
|
||||
|
||||
it('storeObservation deduplicates identical observations within 30s window', () => {
|
||||
const memId = createSessionWithMemoryId(db, 'content-dedup-1', 'mem-dedup-1');
|
||||
const obs = createObservationInput({ title: 'Same Title', narrative: 'Same Narrative' });
|
||||
|
||||
const now = Date.now();
|
||||
const result1 = storeObservation(db, memId, 'test-project', obs, 1, 0, now);
|
||||
const result2 = storeObservation(db, memId, 'test-project', obs, 1, 0, now + 1000);
|
||||
|
||||
// Second call should return the same id as the first (deduped)
|
||||
expect(result2.id).toBe(result1.id);
|
||||
});
|
||||
|
||||
it('storeObservation allows same content after dedup window expires', () => {
|
||||
const memId = createSessionWithMemoryId(db, 'content-dedup-2', 'mem-dedup-2');
|
||||
const obs = createObservationInput({ title: 'Same Title', narrative: 'Same Narrative' });
|
||||
|
||||
const now = Date.now();
|
||||
const result1 = storeObservation(db, memId, 'test-project', obs, 1, 0, now);
|
||||
// 31 seconds later — outside the 30s window
|
||||
const result2 = storeObservation(db, memId, 'test-project', obs, 1, 0, now + 31_000);
|
||||
|
||||
expect(result2.id).not.toBe(result1.id);
|
||||
});
|
||||
|
||||
it('storeObservation allows different content at same time', () => {
|
||||
const memId = createSessionWithMemoryId(db, 'content-dedup-3', 'mem-dedup-3');
|
||||
const obs1 = createObservationInput({ title: 'Title A', narrative: 'Narrative A' });
|
||||
const obs2 = createObservationInput({ title: 'Title B', narrative: 'Narrative B' });
|
||||
|
||||
const now = Date.now();
|
||||
const result1 = storeObservation(db, memId, 'test-project', obs1, 1, 0, now);
|
||||
const result2 = storeObservation(db, memId, 'test-project', obs2, 1, 0, now);
|
||||
|
||||
expect(result2.id).not.toBe(result1.id);
|
||||
});
|
||||
|
||||
it('content_hash column is populated on new observations', () => {
|
||||
const memId = createSessionWithMemoryId(db, 'content-hash-col', 'mem-hash-col');
|
||||
const obs = createObservationInput();
|
||||
|
||||
storeObservation(db, memId, 'test-project', obs);
|
||||
|
||||
const row = db.prepare('SELECT content_hash FROM observations LIMIT 1').get() as { content_hash: string };
|
||||
expect(row.content_hash).toBeTruthy();
|
||||
expect(row.content_hash.length).toBe(16);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transaction-level deduplication', () => {
|
||||
it('storeObservations deduplicates within a batch', () => {
|
||||
const memId = createSessionWithMemoryId(db, 'content-tx-1', 'mem-tx-1');
|
||||
const obs = createObservationInput({ title: 'Duplicate', narrative: 'Same content' });
|
||||
|
||||
const result = storeObservations(db, memId, 'test-project', [obs, obs, obs], null);
|
||||
|
||||
// First is inserted, second and third are deduped to the first
|
||||
expect(result.observationIds.length).toBe(3);
|
||||
expect(result.observationIds[1]).toBe(result.observationIds[0]);
|
||||
expect(result.observationIds[2]).toBe(result.observationIds[0]);
|
||||
|
||||
// Only 1 row in the database
|
||||
const count = db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
|
||||
expect(count.count).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty project string guard', () => {
|
||||
it('storeObservation replaces empty project with cwd-derived name', () => {
|
||||
const memId = createSessionWithMemoryId(db, 'content-empty-proj', 'mem-empty-proj');
|
||||
const obs = createObservationInput();
|
||||
|
||||
const result = storeObservation(db, memId, '', obs);
|
||||
const row = db.prepare('SELECT project FROM observations WHERE id = ?').get(result.id) as { project: string };
|
||||
|
||||
// Should not be empty — will be derived from cwd
|
||||
expect(row.project).toBeTruthy();
|
||||
expect(row.project.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stuck isProcessing flag', () => {
|
||||
it('hasAnyPendingWork resets stuck processing messages older than 5 minutes', () => {
|
||||
// Create a pending_messages table entry that's stuck in 'processing'
|
||||
const sessionId = createSDKSession(db, 'content-stuck', 'stuck-project', 'test');
|
||||
|
||||
// Insert a processing message stuck for 6 minutes
|
||||
const sixMinutesAgo = Date.now() - (6 * 60 * 1000);
|
||||
db.prepare(`
|
||||
INSERT INTO pending_messages (session_db_id, content_session_id, message_type, status, retry_count, created_at_epoch, started_processing_at_epoch)
|
||||
VALUES (?, 'content-stuck', 'observation', 'processing', 0, ?, ?)
|
||||
`).run(sessionId, sixMinutesAgo, sixMinutesAgo);
|
||||
|
||||
const pendingStore = new PendingMessageStore(db);
|
||||
|
||||
// hasAnyPendingWork should reset the stuck message and still return true (it's now pending again)
|
||||
const hasPending = pendingStore.hasAnyPendingWork();
|
||||
expect(hasPending).toBe(true);
|
||||
|
||||
// Verify the message was reset to 'pending'
|
||||
const msg = db.prepare('SELECT status FROM pending_messages WHERE content_session_id = ?').get('content-stuck') as { status: string };
|
||||
expect(msg.status).toBe('pending');
|
||||
});
|
||||
|
||||
it('hasAnyPendingWork does NOT reset recently-started processing messages', () => {
|
||||
const sessionId = createSDKSession(db, 'content-recent', 'recent-project', 'test');
|
||||
|
||||
// Insert a processing message started 1 minute ago (well within 5-minute threshold)
|
||||
const oneMinuteAgo = Date.now() - (1 * 60 * 1000);
|
||||
db.prepare(`
|
||||
INSERT INTO pending_messages (session_db_id, content_session_id, message_type, status, retry_count, created_at_epoch, started_processing_at_epoch)
|
||||
VALUES (?, 'content-recent', 'observation', 'processing', 0, ?, ?)
|
||||
`).run(sessionId, oneMinuteAgo, oneMinuteAgo);
|
||||
|
||||
const pendingStore = new PendingMessageStore(db);
|
||||
const hasPending = pendingStore.hasAnyPendingWork();
|
||||
expect(hasPending).toBe(true);
|
||||
|
||||
// Verify the message is still 'processing' (not reset)
|
||||
const msg = db.prepare('SELECT status FROM pending_messages WHERE content_session_id = ?').get('content-recent') as { status: string };
|
||||
expect(msg.status).toBe('processing');
|
||||
});
|
||||
|
||||
it('hasAnyPendingWork returns false when no pending or processing messages exist', () => {
|
||||
const pendingStore = new PendingMessageStore(db);
|
||||
expect(pendingStore.hasAnyPendingWork()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
231
.agent/services/claude-mem/tests/sqlite/observations.test.ts
Normal file
231
.agent/services/claude-mem/tests/sqlite/observations.test.ts
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Observations module tests
|
||||
* Tests modular observation functions with in-memory database
|
||||
*
|
||||
* Sources:
|
||||
* - API patterns from src/services/sqlite/observations/store.ts
|
||||
* - API patterns from src/services/sqlite/observations/get.ts
|
||||
* - API patterns from src/services/sqlite/observations/recent.ts
|
||||
* - Type definitions from src/services/sqlite/observations/types.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ClaudeMemDatabase } from '../../src/services/sqlite/Database.js';
|
||||
import {
|
||||
storeObservation,
|
||||
getObservationById,
|
||||
getRecentObservations,
|
||||
} from '../../src/services/sqlite/Observations.js';
|
||||
import {
|
||||
createSDKSession,
|
||||
updateMemorySessionId,
|
||||
} from '../../src/services/sqlite/Sessions.js';
|
||||
import type { ObservationInput } from '../../src/services/sqlite/observations/types.js';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
describe('Observations Module', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new ClaudeMemDatabase(':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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
129
.agent/services/claude-mem/tests/sqlite/prompts.test.ts
Normal file
129
.agent/services/claude-mem/tests/sqlite/prompts.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Prompts module tests
|
||||
* Tests modular prompt functions with in-memory database
|
||||
*
|
||||
* Sources:
|
||||
* - API patterns from src/services/sqlite/prompts/store.ts
|
||||
* - API patterns from src/services/sqlite/prompts/get.ts
|
||||
* - Test pattern from tests/session_store.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ClaudeMemDatabase } from '../../src/services/sqlite/Database.js';
|
||||
import {
|
||||
saveUserPrompt,
|
||||
getPromptNumberFromUserPrompts,
|
||||
} from '../../src/services/sqlite/Prompts.js';
|
||||
import { createSDKSession } from '../../src/services/sqlite/Sessions.js';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
describe('Prompts Module', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new ClaudeMemDatabase(':memory:').db;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
// Helper to create a session (for FK constraint on user_prompts.content_session_id)
|
||||
function createSession(contentSessionId: string, project: string = 'test-project'): string {
|
||||
createSDKSession(db, contentSessionId, project, 'initial prompt');
|
||||
return contentSessionId;
|
||||
}
|
||||
|
||||
describe('saveUserPrompt', () => {
|
||||
it('should store prompt and return numeric ID', () => {
|
||||
const contentSessionId = createSession('content-session-prompt-1');
|
||||
const promptNumber = 1;
|
||||
const promptText = 'First user prompt';
|
||||
|
||||
const id = saveUserPrompt(db, contentSessionId, promptNumber, promptText);
|
||||
|
||||
expect(typeof id).toBe('number');
|
||||
expect(id).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should store multiple prompts with incrementing IDs', () => {
|
||||
const contentSessionId = createSession('content-session-prompt-2');
|
||||
|
||||
const id1 = saveUserPrompt(db, contentSessionId, 1, 'First prompt');
|
||||
const id2 = saveUserPrompt(db, contentSessionId, 2, 'Second prompt');
|
||||
const id3 = saveUserPrompt(db, contentSessionId, 3, 'Third prompt');
|
||||
|
||||
expect(id1).toBeGreaterThan(0);
|
||||
expect(id2).toBeGreaterThan(id1);
|
||||
expect(id3).toBeGreaterThan(id2);
|
||||
});
|
||||
|
||||
it('should allow prompts from different sessions', () => {
|
||||
const sessionA = createSession('session-a');
|
||||
const sessionB = createSession('session-b');
|
||||
|
||||
const id1 = saveUserPrompt(db, sessionA, 1, 'Prompt A1');
|
||||
const id2 = saveUserPrompt(db, sessionB, 1, 'Prompt B1');
|
||||
|
||||
expect(id1).not.toBe(id2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPromptNumberFromUserPrompts', () => {
|
||||
it('should return 0 when no prompts exist', () => {
|
||||
const count = getPromptNumberFromUserPrompts(db, 'nonexistent-session');
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('should return count of prompts for session', () => {
|
||||
const contentSessionId = createSession('count-test-session');
|
||||
|
||||
expect(getPromptNumberFromUserPrompts(db, contentSessionId)).toBe(0);
|
||||
|
||||
saveUserPrompt(db, contentSessionId, 1, 'First prompt');
|
||||
expect(getPromptNumberFromUserPrompts(db, contentSessionId)).toBe(1);
|
||||
|
||||
saveUserPrompt(db, contentSessionId, 2, 'Second prompt');
|
||||
expect(getPromptNumberFromUserPrompts(db, contentSessionId)).toBe(2);
|
||||
|
||||
saveUserPrompt(db, contentSessionId, 3, 'Third prompt');
|
||||
expect(getPromptNumberFromUserPrompts(db, contentSessionId)).toBe(3);
|
||||
});
|
||||
|
||||
it('should maintain session isolation', () => {
|
||||
const sessionA = createSession('isolation-session-a');
|
||||
const sessionB = createSession('isolation-session-b');
|
||||
|
||||
// Add prompts to session A
|
||||
saveUserPrompt(db, sessionA, 1, 'A1');
|
||||
saveUserPrompt(db, sessionA, 2, 'A2');
|
||||
|
||||
// Add prompts to session B
|
||||
saveUserPrompt(db, sessionB, 1, 'B1');
|
||||
|
||||
// Session A should have 2 prompts
|
||||
expect(getPromptNumberFromUserPrompts(db, sessionA)).toBe(2);
|
||||
|
||||
// Session B should have 1 prompt
|
||||
expect(getPromptNumberFromUserPrompts(db, sessionB)).toBe(1);
|
||||
|
||||
// Adding to session B shouldn't affect session A
|
||||
saveUserPrompt(db, sessionB, 2, 'B2');
|
||||
saveUserPrompt(db, sessionB, 3, 'B3');
|
||||
|
||||
expect(getPromptNumberFromUserPrompts(db, sessionA)).toBe(2);
|
||||
expect(getPromptNumberFromUserPrompts(db, sessionB)).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle edge case of many prompts', () => {
|
||||
const contentSessionId = createSession('many-prompts-session');
|
||||
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
saveUserPrompt(db, contentSessionId, i, `Prompt ${i}`);
|
||||
}
|
||||
|
||||
expect(getPromptNumberFromUserPrompts(db, contentSessionId)).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
166
.agent/services/claude-mem/tests/sqlite/sessions.test.ts
Normal file
166
.agent/services/claude-mem/tests/sqlite/sessions.test.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Session module tests
|
||||
* Tests modular session functions with in-memory database
|
||||
*
|
||||
* Sources:
|
||||
* - API patterns from src/services/sqlite/sessions/create.ts
|
||||
* - API patterns from src/services/sqlite/sessions/get.ts
|
||||
* - Test pattern from tests/session_store.test.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ClaudeMemDatabase } from '../../src/services/sqlite/Database.js';
|
||||
import {
|
||||
createSDKSession,
|
||||
getSessionById,
|
||||
updateMemorySessionId,
|
||||
} from '../../src/services/sqlite/Sessions.js';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
describe('Sessions Module', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new ClaudeMemDatabase(':memory:').db;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
describe('createSDKSession', () => {
|
||||
it('should create a new session and return numeric ID', () => {
|
||||
const contentSessionId = 'content-session-123';
|
||||
const project = 'test-project';
|
||||
const userPrompt = 'Initial user prompt';
|
||||
|
||||
const sessionId = createSDKSession(db, contentSessionId, project, userPrompt);
|
||||
|
||||
expect(typeof sessionId).toBe('number');
|
||||
expect(sessionId).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should be idempotent - return same ID for same content_session_id', () => {
|
||||
const contentSessionId = 'content-session-456';
|
||||
const project = 'test-project';
|
||||
const userPrompt = 'Initial user prompt';
|
||||
|
||||
const sessionId1 = createSDKSession(db, contentSessionId, project, userPrompt);
|
||||
const sessionId2 = createSDKSession(db, contentSessionId, project, 'Different prompt');
|
||||
|
||||
expect(sessionId1).toBe(sessionId2);
|
||||
});
|
||||
|
||||
it('should create different sessions for different content_session_ids', () => {
|
||||
const sessionId1 = createSDKSession(db, 'session-a', 'project', 'prompt');
|
||||
const sessionId2 = createSDKSession(db, 'session-b', 'project', 'prompt');
|
||||
|
||||
expect(sessionId1).not.toBe(sessionId2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSessionById', () => {
|
||||
it('should retrieve session by ID', () => {
|
||||
const contentSessionId = 'content-session-get';
|
||||
const project = 'test-project';
|
||||
const userPrompt = 'Test prompt';
|
||||
|
||||
const sessionId = createSDKSession(db, contentSessionId, project, userPrompt);
|
||||
const session = getSessionById(db, sessionId);
|
||||
|
||||
expect(session).not.toBeNull();
|
||||
expect(session?.id).toBe(sessionId);
|
||||
expect(session?.content_session_id).toBe(contentSessionId);
|
||||
expect(session?.project).toBe(project);
|
||||
expect(session?.user_prompt).toBe(userPrompt);
|
||||
// memory_session_id should be null initially (set via updateMemorySessionId)
|
||||
expect(session?.memory_session_id).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for non-existent session', () => {
|
||||
const session = getSessionById(db, 99999);
|
||||
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom_title', () => {
|
||||
it('should store custom_title when provided at creation', () => {
|
||||
const sessionId = createSDKSession(db, 'session-title-1', 'project', 'prompt', 'My Agent');
|
||||
const session = getSessionById(db, sessionId);
|
||||
|
||||
expect(session?.custom_title).toBe('My Agent');
|
||||
});
|
||||
|
||||
it('should default custom_title to null when not provided', () => {
|
||||
const sessionId = createSDKSession(db, 'session-title-2', 'project', 'prompt');
|
||||
const session = getSessionById(db, sessionId);
|
||||
|
||||
expect(session?.custom_title).toBeNull();
|
||||
});
|
||||
|
||||
it('should backfill custom_title on idempotent call if not already set', () => {
|
||||
const sessionId = createSDKSession(db, 'session-title-3', 'project', 'prompt');
|
||||
let session = getSessionById(db, sessionId);
|
||||
expect(session?.custom_title).toBeNull();
|
||||
|
||||
// Second call with custom_title should backfill
|
||||
createSDKSession(db, 'session-title-3', 'project', 'prompt', 'Backfilled Title');
|
||||
session = getSessionById(db, sessionId);
|
||||
expect(session?.custom_title).toBe('Backfilled Title');
|
||||
});
|
||||
|
||||
it('should not overwrite existing custom_title on idempotent call', () => {
|
||||
const sessionId = createSDKSession(db, 'session-title-4', 'project', 'prompt', 'Original');
|
||||
let session = getSessionById(db, sessionId);
|
||||
expect(session?.custom_title).toBe('Original');
|
||||
|
||||
// Second call should NOT overwrite
|
||||
createSDKSession(db, 'session-title-4', 'project', 'prompt', 'Attempted Override');
|
||||
session = getSessionById(db, sessionId);
|
||||
expect(session?.custom_title).toBe('Original');
|
||||
});
|
||||
|
||||
it('should handle empty string custom_title as no title', () => {
|
||||
const sessionId = createSDKSession(db, 'session-title-5', 'project', 'prompt', '');
|
||||
const session = getSessionById(db, sessionId);
|
||||
|
||||
// Empty string becomes null via the || null conversion
|
||||
expect(session?.custom_title).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMemorySessionId', () => {
|
||||
it('should update memory_session_id for existing session', () => {
|
||||
const contentSessionId = 'content-session-update';
|
||||
const project = 'test-project';
|
||||
const userPrompt = 'Test prompt';
|
||||
const memorySessionId = 'memory-session-abc123';
|
||||
|
||||
const sessionId = createSDKSession(db, contentSessionId, project, userPrompt);
|
||||
|
||||
// Verify memory_session_id is null initially
|
||||
let session = getSessionById(db, sessionId);
|
||||
expect(session?.memory_session_id).toBeNull();
|
||||
|
||||
// Update memory session ID
|
||||
updateMemorySessionId(db, sessionId, memorySessionId);
|
||||
|
||||
// Verify update
|
||||
session = getSessionById(db, sessionId);
|
||||
expect(session?.memory_session_id).toBe(memorySessionId);
|
||||
});
|
||||
|
||||
it('should allow updating to different memory_session_id', () => {
|
||||
const sessionId = createSDKSession(db, 'session-x', 'project', 'prompt');
|
||||
|
||||
updateMemorySessionId(db, sessionId, 'memory-1');
|
||||
let session = getSessionById(db, sessionId);
|
||||
expect(session?.memory_session_id).toBe('memory-1');
|
||||
|
||||
updateMemorySessionId(db, sessionId, 'memory-2');
|
||||
session = getSessionById(db, sessionId);
|
||||
expect(session?.memory_session_id).toBe('memory-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
214
.agent/services/claude-mem/tests/sqlite/summaries.test.ts
Normal file
214
.agent/services/claude-mem/tests/sqlite/summaries.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Summaries module tests
|
||||
* Tests modular summary functions with in-memory database
|
||||
*
|
||||
* Sources:
|
||||
* - API patterns from src/services/sqlite/summaries/store.ts
|
||||
* - API patterns from src/services/sqlite/summaries/get.ts
|
||||
* - Type definitions from src/services/sqlite/summaries/types.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ClaudeMemDatabase } from '../../src/services/sqlite/Database.js';
|
||||
import {
|
||||
storeSummary,
|
||||
getSummaryForSession,
|
||||
} from '../../src/services/sqlite/Summaries.js';
|
||||
import {
|
||||
createSDKSession,
|
||||
updateMemorySessionId,
|
||||
} from '../../src/services/sqlite/Sessions.js';
|
||||
import type { SummaryInput } from '../../src/services/sqlite/summaries/types.js';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
describe('Summaries Module', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new ClaudeMemDatabase(':memory:').db;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
// Helper to create a valid summary input
|
||||
function createSummaryInput(overrides: Partial<SummaryInput> = {}): SummaryInput {
|
||||
return {
|
||||
request: 'User requested feature X',
|
||||
investigated: 'Explored the codebase',
|
||||
learned: 'Discovered pattern Y',
|
||||
completed: 'Implemented feature X',
|
||||
next_steps: 'Add tests and documentation',
|
||||
notes: 'Consider edge case Z',
|
||||
...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('storeSummary', () => {
|
||||
it('should store summary and return id and createdAtEpoch', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-sum-123', 'mem-session-sum-123');
|
||||
const project = 'test-project';
|
||||
const summary = createSummaryInput();
|
||||
|
||||
const result = storeSummary(db, memorySessionId, project, summary);
|
||||
|
||||
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 summary fields correctly', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-sum-456', 'mem-session-sum-456');
|
||||
const project = 'test-project';
|
||||
const summary = createSummaryInput({
|
||||
request: 'Refactor the database layer',
|
||||
investigated: 'Analyzed current schema',
|
||||
learned: 'Found N+1 query issues',
|
||||
completed: 'Optimized queries',
|
||||
next_steps: 'Monitor performance',
|
||||
notes: 'May need caching',
|
||||
});
|
||||
|
||||
const result = storeSummary(db, memorySessionId, project, summary, 1, 500);
|
||||
|
||||
const stored = getSummaryForSession(db, memorySessionId);
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored?.request).toBe('Refactor the database layer');
|
||||
expect(stored?.investigated).toBe('Analyzed current schema');
|
||||
expect(stored?.learned).toBe('Found N+1 query issues');
|
||||
expect(stored?.completed).toBe('Optimized queries');
|
||||
expect(stored?.next_steps).toBe('Monitor performance');
|
||||
expect(stored?.notes).toBe('May need caching');
|
||||
expect(stored?.prompt_number).toBe(1);
|
||||
});
|
||||
|
||||
it('should respect overrideTimestampEpoch', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-sum-789', 'mem-session-sum-789');
|
||||
const project = 'test-project';
|
||||
const summary = createSummaryInput();
|
||||
const pastTimestamp = 1650000000000; // Apr 15, 2022
|
||||
|
||||
const result = storeSummary(
|
||||
db,
|
||||
memorySessionId,
|
||||
project,
|
||||
summary,
|
||||
1,
|
||||
0,
|
||||
pastTimestamp
|
||||
);
|
||||
|
||||
expect(result.createdAtEpoch).toBe(pastTimestamp);
|
||||
|
||||
const stored = getSummaryForSession(db, memorySessionId);
|
||||
expect(stored?.created_at_epoch).toBe(pastTimestamp);
|
||||
});
|
||||
|
||||
it('should use current time when overrideTimestampEpoch not provided', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-sum-now', 'session-sum-now');
|
||||
const before = Date.now();
|
||||
const result = storeSummary(
|
||||
db,
|
||||
memorySessionId,
|
||||
'project',
|
||||
createSummaryInput()
|
||||
);
|
||||
const after = Date.now();
|
||||
|
||||
expect(result.createdAtEpoch).toBeGreaterThanOrEqual(before);
|
||||
expect(result.createdAtEpoch).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
it('should handle null notes', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-sum-null', 'session-sum-null');
|
||||
const summary = createSummaryInput({ notes: null });
|
||||
|
||||
const result = storeSummary(db, memorySessionId, 'project', summary);
|
||||
const stored = getSummaryForSession(db, memorySessionId);
|
||||
|
||||
expect(stored).not.toBeNull();
|
||||
expect(stored?.notes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSummaryForSession', () => {
|
||||
it('should retrieve summary by memory_session_id', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-unique', 'unique-mem-session');
|
||||
const summary = createSummaryInput({ request: 'Unique request' });
|
||||
|
||||
storeSummary(db, memorySessionId, 'project', summary);
|
||||
|
||||
const retrieved = getSummaryForSession(db, memorySessionId);
|
||||
|
||||
expect(retrieved).not.toBeNull();
|
||||
expect(retrieved?.request).toBe('Unique request');
|
||||
});
|
||||
|
||||
it('should return null for session with no summary', () => {
|
||||
const retrieved = getSummaryForSession(db, 'nonexistent-session');
|
||||
|
||||
expect(retrieved).toBeNull();
|
||||
});
|
||||
|
||||
it('should return most recent summary when multiple exist', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-multi', 'multi-summary-session');
|
||||
|
||||
// Store older summary
|
||||
storeSummary(
|
||||
db,
|
||||
memorySessionId,
|
||||
'project',
|
||||
createSummaryInput({ request: 'First request' }),
|
||||
1,
|
||||
0,
|
||||
1000000000000
|
||||
);
|
||||
|
||||
// Store newer summary
|
||||
storeSummary(
|
||||
db,
|
||||
memorySessionId,
|
||||
'project',
|
||||
createSummaryInput({ request: 'Second request' }),
|
||||
2,
|
||||
0,
|
||||
2000000000000
|
||||
);
|
||||
|
||||
const retrieved = getSummaryForSession(db, memorySessionId);
|
||||
|
||||
expect(retrieved).not.toBeNull();
|
||||
expect(retrieved?.request).toBe('Second request');
|
||||
expect(retrieved?.prompt_number).toBe(2);
|
||||
});
|
||||
|
||||
it('should return summary with all expected fields', () => {
|
||||
const memorySessionId = createSessionWithMemoryId('content-fields', 'fields-check-session');
|
||||
const summary = createSummaryInput();
|
||||
|
||||
storeSummary(db, memorySessionId, 'project', summary, 1, 100, 1500000000000);
|
||||
|
||||
const retrieved = getSummaryForSession(db, memorySessionId);
|
||||
|
||||
expect(retrieved).not.toBeNull();
|
||||
expect(retrieved).toHaveProperty('request');
|
||||
expect(retrieved).toHaveProperty('investigated');
|
||||
expect(retrieved).toHaveProperty('learned');
|
||||
expect(retrieved).toHaveProperty('completed');
|
||||
expect(retrieved).toHaveProperty('next_steps');
|
||||
expect(retrieved).toHaveProperty('notes');
|
||||
expect(retrieved).toHaveProperty('prompt_number');
|
||||
expect(retrieved).toHaveProperty('created_at');
|
||||
expect(retrieved).toHaveProperty('created_at_epoch');
|
||||
});
|
||||
});
|
||||
});
|
||||
309
.agent/services/claude-mem/tests/sqlite/transactions.test.ts
Normal file
309
.agent/services/claude-mem/tests/sqlite/transactions.test.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* Transactions module tests
|
||||
* Tests atomic transaction functions with in-memory database
|
||||
*
|
||||
* Sources:
|
||||
* - API patterns from src/services/sqlite/transactions.ts
|
||||
* - Type definitions from src/services/sqlite/transactions.ts
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { ClaudeMemDatabase } from '../../src/services/sqlite/Database.js';
|
||||
import {
|
||||
storeObservations,
|
||||
storeObservationsAndMarkComplete,
|
||||
} from '../../src/services/sqlite/transactions.js';
|
||||
import { getObservationById } from '../../src/services/sqlite/Observations.js';
|
||||
import { getSummaryForSession } from '../../src/services/sqlite/Summaries.js';
|
||||
import {
|
||||
createSDKSession,
|
||||
updateMemorySessionId,
|
||||
} from '../../src/services/sqlite/Sessions.js';
|
||||
import type { ObservationInput } from '../../src/services/sqlite/observations/types.js';
|
||||
import type { SummaryInput } from '../../src/services/sqlite/summaries/types.js';
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
describe('Transactions Module', () => {
|
||||
let db: Database;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new ClaudeMemDatabase(':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 valid summary input
|
||||
function createSummaryInput(overrides: Partial<SummaryInput> = {}): SummaryInput {
|
||||
return {
|
||||
request: 'User requested feature X',
|
||||
investigated: 'Explored the codebase',
|
||||
learned: 'Discovered pattern Y',
|
||||
completed: 'Implemented feature X',
|
||||
next_steps: 'Add tests and documentation',
|
||||
notes: 'Consider edge case Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to create a session and return memory_session_id for FK constraints
|
||||
function createSessionWithMemoryId(contentSessionId: string, memorySessionId: string, project: string = 'test-project'): { memorySessionId: string; sessionDbId: number } {
|
||||
const sessionDbId = createSDKSession(db, contentSessionId, project, 'initial prompt');
|
||||
updateMemorySessionId(db, sessionDbId, memorySessionId);
|
||||
return { memorySessionId, sessionDbId };
|
||||
}
|
||||
|
||||
describe('storeObservations', () => {
|
||||
it('should store multiple observations atomically and return result', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-atomic-123', 'atomic-session-123');
|
||||
const project = 'test-project';
|
||||
const observations = [
|
||||
createObservationInput({ title: 'Obs 1' }),
|
||||
createObservationInput({ title: 'Obs 2' }),
|
||||
createObservationInput({ title: 'Obs 3' }),
|
||||
];
|
||||
|
||||
const result = storeObservations(db, memorySessionId, project, observations, null);
|
||||
|
||||
expect(result.observationIds).toHaveLength(3);
|
||||
expect(result.observationIds.every((id) => typeof id === 'number')).toBe(true);
|
||||
expect(result.summaryId).toBeNull();
|
||||
expect(typeof result.createdAtEpoch).toBe('number');
|
||||
});
|
||||
|
||||
it('should store all observations with same timestamp', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-ts', 'timestamp-session');
|
||||
const project = 'test-project';
|
||||
const observations = [
|
||||
createObservationInput({ title: 'Obs A' }),
|
||||
createObservationInput({ title: 'Obs B' }),
|
||||
];
|
||||
const fixedTimestamp = 1600000000000;
|
||||
|
||||
const result = storeObservations(
|
||||
db,
|
||||
memorySessionId,
|
||||
project,
|
||||
observations,
|
||||
null,
|
||||
1,
|
||||
0,
|
||||
fixedTimestamp
|
||||
);
|
||||
|
||||
expect(result.createdAtEpoch).toBe(fixedTimestamp);
|
||||
|
||||
// Verify each observation has the same timestamp
|
||||
for (const id of result.observationIds) {
|
||||
const obs = getObservationById(db, id);
|
||||
expect(obs?.created_at_epoch).toBe(fixedTimestamp);
|
||||
}
|
||||
});
|
||||
|
||||
it('should store observations with summary', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-with-sum', 'with-summary-session');
|
||||
const project = 'test-project';
|
||||
const observations = [createObservationInput({ title: 'Main Obs' })];
|
||||
const summary = createSummaryInput({ request: 'Test request' });
|
||||
|
||||
const result = storeObservations(db, memorySessionId, project, observations, summary);
|
||||
|
||||
expect(result.observationIds).toHaveLength(1);
|
||||
expect(result.summaryId).not.toBeNull();
|
||||
expect(typeof result.summaryId).toBe('number');
|
||||
|
||||
// Verify summary was stored
|
||||
const storedSummary = getSummaryForSession(db, memorySessionId);
|
||||
expect(storedSummary).not.toBeNull();
|
||||
expect(storedSummary?.request).toBe('Test request');
|
||||
});
|
||||
|
||||
it('should handle empty observations array', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-empty', 'empty-obs-session');
|
||||
const project = 'test-project';
|
||||
const observations: ObservationInput[] = [];
|
||||
|
||||
const result = storeObservations(db, memorySessionId, project, observations, null);
|
||||
|
||||
expect(result.observationIds).toHaveLength(0);
|
||||
expect(result.summaryId).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle summary-only (no observations)', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-sum-only', 'summary-only-session');
|
||||
const project = 'test-project';
|
||||
const summary = createSummaryInput({ request: 'Summary-only request' });
|
||||
|
||||
const result = storeObservations(db, memorySessionId, project, [], summary);
|
||||
|
||||
expect(result.observationIds).toHaveLength(0);
|
||||
expect(result.summaryId).not.toBeNull();
|
||||
|
||||
const storedSummary = getSummaryForSession(db, memorySessionId);
|
||||
expect(storedSummary?.request).toBe('Summary-only request');
|
||||
});
|
||||
|
||||
it('should return correct createdAtEpoch', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-epoch', 'session-epoch');
|
||||
const before = Date.now();
|
||||
const result = storeObservations(
|
||||
db,
|
||||
memorySessionId,
|
||||
'project',
|
||||
[createObservationInput()],
|
||||
null
|
||||
);
|
||||
const after = Date.now();
|
||||
|
||||
expect(result.createdAtEpoch).toBeGreaterThanOrEqual(before);
|
||||
expect(result.createdAtEpoch).toBeLessThanOrEqual(after);
|
||||
});
|
||||
|
||||
it('should apply promptNumber to all observations', () => {
|
||||
const { memorySessionId } = createSessionWithMemoryId('content-pn', 'prompt-num-session');
|
||||
const project = 'test-project';
|
||||
const observations = [
|
||||
createObservationInput({ title: 'Obs 1' }),
|
||||
createObservationInput({ title: 'Obs 2' }),
|
||||
];
|
||||
const promptNumber = 5;
|
||||
|
||||
const result = storeObservations(
|
||||
db,
|
||||
memorySessionId,
|
||||
project,
|
||||
observations,
|
||||
null,
|
||||
promptNumber
|
||||
);
|
||||
|
||||
for (const id of result.observationIds) {
|
||||
const obs = getObservationById(db, id);
|
||||
expect(obs?.prompt_number).toBe(promptNumber);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeObservationsAndMarkComplete', () => {
|
||||
// Note: This function also marks a pending message as processed.
|
||||
// For testing, we need a pending_messages row to exist first.
|
||||
|
||||
it('should store observations, summary, and mark message complete', () => {
|
||||
const { memorySessionId, sessionDbId } = createSessionWithMemoryId('content-complete', 'complete-session');
|
||||
const project = 'test-project';
|
||||
const observations = [createObservationInput({ title: 'Complete Obs' })];
|
||||
const summary = createSummaryInput({ request: 'Complete request' });
|
||||
|
||||
// First, insert a pending message to mark as complete
|
||||
const insertStmt = db.prepare(`
|
||||
INSERT INTO pending_messages
|
||||
(session_db_id, content_session_id, message_type, created_at_epoch, status)
|
||||
VALUES (?, ?, 'observation', ?, 'processing')
|
||||
`);
|
||||
const msgResult = insertStmt.run(sessionDbId, 'content-complete', Date.now());
|
||||
const messageId = Number(msgResult.lastInsertRowid);
|
||||
|
||||
const result = storeObservationsAndMarkComplete(
|
||||
db,
|
||||
memorySessionId,
|
||||
project,
|
||||
observations,
|
||||
summary,
|
||||
messageId
|
||||
);
|
||||
|
||||
expect(result.observationIds).toHaveLength(1);
|
||||
expect(result.summaryId).not.toBeNull();
|
||||
|
||||
// Verify message was marked as processed
|
||||
const msgStmt = db.prepare('SELECT status FROM pending_messages WHERE id = ?');
|
||||
const msg = msgStmt.get(messageId) as { status: string } | undefined;
|
||||
expect(msg?.status).toBe('processed');
|
||||
});
|
||||
|
||||
it('should maintain atomicity - all operations share same timestamp', () => {
|
||||
const { memorySessionId, sessionDbId } = createSessionWithMemoryId('content-atomic-ts', 'atomic-timestamp-session');
|
||||
const project = 'test-project';
|
||||
const observations = [
|
||||
createObservationInput({ title: 'Obs 1' }),
|
||||
createObservationInput({ title: 'Obs 2' }),
|
||||
];
|
||||
const summary = createSummaryInput();
|
||||
const fixedTimestamp = 1700000000000;
|
||||
|
||||
// Create pending message
|
||||
db.prepare(`
|
||||
INSERT INTO pending_messages
|
||||
(session_db_id, content_session_id, message_type, created_at_epoch, status)
|
||||
VALUES (?, ?, 'observation', ?, 'processing')
|
||||
`).run(sessionDbId, 'content-atomic-ts', Date.now());
|
||||
const messageId = db.prepare('SELECT last_insert_rowid() as id').get() as { id: number };
|
||||
|
||||
const result = storeObservationsAndMarkComplete(
|
||||
db,
|
||||
memorySessionId,
|
||||
project,
|
||||
observations,
|
||||
summary,
|
||||
messageId.id,
|
||||
1,
|
||||
0,
|
||||
fixedTimestamp
|
||||
);
|
||||
|
||||
expect(result.createdAtEpoch).toBe(fixedTimestamp);
|
||||
|
||||
// All observations should have same timestamp
|
||||
for (const id of result.observationIds) {
|
||||
const obs = getObservationById(db, id);
|
||||
expect(obs?.created_at_epoch).toBe(fixedTimestamp);
|
||||
}
|
||||
|
||||
// Summary should have same timestamp
|
||||
const storedSummary = getSummaryForSession(db, memorySessionId);
|
||||
expect(storedSummary?.created_at_epoch).toBe(fixedTimestamp);
|
||||
});
|
||||
|
||||
it('should handle null summary', () => {
|
||||
const { memorySessionId, sessionDbId } = createSessionWithMemoryId('content-no-sum', 'no-summary-session');
|
||||
const project = 'test-project';
|
||||
const observations = [createObservationInput({ title: 'Only Obs' })];
|
||||
|
||||
// Create pending message
|
||||
db.prepare(`
|
||||
INSERT INTO pending_messages
|
||||
(session_db_id, content_session_id, message_type, created_at_epoch, status)
|
||||
VALUES (?, ?, 'observation', ?, 'processing')
|
||||
`).run(sessionDbId, 'content-no-sum', Date.now());
|
||||
const messageId = db.prepare('SELECT last_insert_rowid() as id').get() as { id: number };
|
||||
|
||||
const result = storeObservationsAndMarkComplete(
|
||||
db,
|
||||
memorySessionId,
|
||||
project,
|
||||
observations,
|
||||
null,
|
||||
messageId.id
|
||||
);
|
||||
|
||||
expect(result.observationIds).toHaveLength(1);
|
||||
expect(result.summaryId).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user