mirror of
https://github.com/thedotmack/claude-mem
synced 2026-04-25 17:15:04 +02:00
Deletes files and symbols with zero importers or self-declared deprecation.
No behavior change — structural cleanup only.
Removed:
- hook-response.ts, utils/bun-path.ts (zero importers)
- cli/handlers/user-message.ts (not wired in hooks.json)
- services/Context.ts + context-generator.ts (deprecated stubs)
- sqlite/migrations.ts (645 lines, pre-SDK schema, unused)
- DatabaseManager singleton + getDatabase + initializeDatabase
- 6 sqlite re-export shells (Observations/Sessions/Summaries/Prompts/Timeline/Import)
- worker/search/{strategies,filters}/ dirs (dead via unused SearchOrchestrator)
- SearchOrchestrator, TimelineBuilder, ResultFormatter
- TimelineService.formatTimeline (137 lines, unused)
- ProcessManager.cleanupOrphanedProcesses + createSignalHandler
- Duplicate php: key in smart-file-read/parser.ts
Rewired:
- SearchRoutes dynamic imports (services/Context → services/context)
- CorpusBuilder: SearchOrchestrator → SearchManager.search({format:'json'})
- build-hooks.js entry: context-generator.ts → services/context/index.ts
- scripts/ imports for moved transcript-parser.ts
Moved:
- utils/transcript-parser.ts → scripts/transcript-parser.ts (only callers)
Skipped (plan was wrong):
- consecutiveRestarts: live backoff math at SessionRoutes.ts:348
- AgentFormatter stubs: wired to HeaderRenderer with passing tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
/**
|
|
* 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/index.js';
|
|
import { getSummaryForSession } from '../../src/services/sqlite/index.js';
|
|
import {
|
|
createSDKSession,
|
|
updateMemorySessionId,
|
|
} from '../../src/services/sqlite/index.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();
|
|
});
|
|
});
|
|
});
|