406 lines
16 KiB
TypeScript
406 lines
16 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
import { SessionStore } from '../src/services/sqlite/SessionStore.js';
|
|
|
|
/**
|
|
* Tests for Session ID Refactoring
|
|
*
|
|
* Validates the semantic renaming:
|
|
* - claudeSessionId → contentSessionId (user's observed Claude Code session)
|
|
* - sdkSessionId → memorySessionId (memory agent's session ID for resume)
|
|
*
|
|
* Also validates the memory session ID capture mechanism for resume functionality.
|
|
*/
|
|
describe('Session ID Refactor', () => {
|
|
let store: SessionStore;
|
|
|
|
beforeEach(() => {
|
|
store = new SessionStore(':memory:');
|
|
});
|
|
|
|
afterEach(() => {
|
|
store.close();
|
|
});
|
|
|
|
describe('Database Migration 17 - Column Renaming', () => {
|
|
it('should have content_session_id column in sdk_sessions table', () => {
|
|
const tableInfo = store.db.query('PRAGMA table_info(sdk_sessions)').all() as Array<{ name: string }>;
|
|
const columnNames = tableInfo.map(col => col.name);
|
|
|
|
expect(columnNames).toContain('content_session_id');
|
|
expect(columnNames).not.toContain('claude_session_id');
|
|
});
|
|
|
|
it('should have memory_session_id column in sdk_sessions table', () => {
|
|
const tableInfo = store.db.query('PRAGMA table_info(sdk_sessions)').all() as Array<{ name: string }>;
|
|
const columnNames = tableInfo.map(col => col.name);
|
|
|
|
expect(columnNames).toContain('memory_session_id');
|
|
expect(columnNames).not.toContain('sdk_session_id');
|
|
});
|
|
|
|
it('should have memory_session_id column in observations table', () => {
|
|
const tableInfo = store.db.query('PRAGMA table_info(observations)').all() as Array<{ name: string }>;
|
|
const columnNames = tableInfo.map(col => col.name);
|
|
|
|
expect(columnNames).toContain('memory_session_id');
|
|
expect(columnNames).not.toContain('sdk_session_id');
|
|
});
|
|
|
|
it('should have memory_session_id column in session_summaries table', () => {
|
|
const tableInfo = store.db.query('PRAGMA table_info(session_summaries)').all() as Array<{ name: string }>;
|
|
const columnNames = tableInfo.map(col => col.name);
|
|
|
|
expect(columnNames).toContain('memory_session_id');
|
|
expect(columnNames).not.toContain('sdk_session_id');
|
|
});
|
|
|
|
it('should have content_session_id column in user_prompts table', () => {
|
|
const tableInfo = store.db.query('PRAGMA table_info(user_prompts)').all() as Array<{ name: string }>;
|
|
const columnNames = tableInfo.map(col => col.name);
|
|
|
|
expect(columnNames).toContain('content_session_id');
|
|
expect(columnNames).not.toContain('claude_session_id');
|
|
});
|
|
|
|
it('should have content_session_id column in pending_messages table', () => {
|
|
const tableInfo = store.db.query('PRAGMA table_info(pending_messages)').all() as Array<{ name: string }>;
|
|
const columnNames = tableInfo.map(col => col.name);
|
|
|
|
expect(columnNames).toContain('content_session_id');
|
|
expect(columnNames).not.toContain('claude_session_id');
|
|
});
|
|
|
|
it('should record migration 17 in schema_versions', () => {
|
|
const result = store.db.prepare(
|
|
'SELECT version FROM schema_versions WHERE version = 17'
|
|
).get() as { version: number } | undefined;
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.version).toBe(17);
|
|
});
|
|
});
|
|
|
|
describe('createSDKSession - Session ID Initialization', () => {
|
|
it('should create session with content_session_id set to the provided session ID', () => {
|
|
const contentSessionId = 'user-claude-code-session-123';
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test prompt');
|
|
|
|
const session = store.db.prepare(
|
|
'SELECT content_session_id FROM sdk_sessions WHERE id = ?'
|
|
).get(sessionDbId) as { content_session_id: string };
|
|
|
|
expect(session.content_session_id).toBe(contentSessionId);
|
|
});
|
|
|
|
it('should create session with memory_session_id initially equal to content_session_id', () => {
|
|
const contentSessionId = 'user-session-456';
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test prompt');
|
|
|
|
const session = store.db.prepare(
|
|
'SELECT content_session_id, memory_session_id FROM sdk_sessions WHERE id = ?'
|
|
).get(sessionDbId) as { content_session_id: string; memory_session_id: string };
|
|
|
|
// Initially they're the same - memory_session_id gets updated when SDK responds
|
|
expect(session.memory_session_id).toBe(contentSessionId);
|
|
});
|
|
|
|
it('should be idempotent - return same ID for same content_session_id', () => {
|
|
const contentSessionId = 'idempotent-test-session';
|
|
|
|
const id1 = store.createSDKSession(contentSessionId, 'project-1', 'First prompt');
|
|
const id2 = store.createSDKSession(contentSessionId, 'project-2', 'Second prompt');
|
|
|
|
expect(id1).toBe(id2);
|
|
|
|
// Verify the original values are preserved (INSERT OR IGNORE)
|
|
const session = store.db.prepare(
|
|
'SELECT project, user_prompt FROM sdk_sessions WHERE id = ?'
|
|
).get(id1) as { project: string; user_prompt: string };
|
|
|
|
expect(session.project).toBe('project-1');
|
|
expect(session.user_prompt).toBe('First prompt');
|
|
});
|
|
});
|
|
|
|
describe('updateMemorySessionId - Memory Agent Session Capture', () => {
|
|
it('should update memory_session_id for existing session', () => {
|
|
const contentSessionId = 'content-session-789';
|
|
const memorySessionId = 'sdk-generated-memory-session-abc';
|
|
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
// Initially memory_session_id equals content_session_id
|
|
const beforeUpdate = store.db.prepare(
|
|
'SELECT memory_session_id FROM sdk_sessions WHERE id = ?'
|
|
).get(sessionDbId) as { memory_session_id: string };
|
|
expect(beforeUpdate.memory_session_id).toBe(contentSessionId);
|
|
|
|
// Update with SDK-captured memory session ID
|
|
store.updateMemorySessionId(sessionDbId, memorySessionId);
|
|
|
|
// Verify it was updated
|
|
const afterUpdate = store.db.prepare(
|
|
'SELECT memory_session_id FROM sdk_sessions WHERE id = ?'
|
|
).get(sessionDbId) as { memory_session_id: string };
|
|
expect(afterUpdate.memory_session_id).toBe(memorySessionId);
|
|
});
|
|
|
|
it('should allow updating memory_session_id multiple times', () => {
|
|
const contentSessionId = 'multi-update-session';
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
store.updateMemorySessionId(sessionDbId, 'first-memory-id');
|
|
store.updateMemorySessionId(sessionDbId, 'second-memory-id');
|
|
|
|
const session = store.db.prepare(
|
|
'SELECT memory_session_id FROM sdk_sessions WHERE id = ?'
|
|
).get(sessionDbId) as { memory_session_id: string };
|
|
|
|
expect(session.memory_session_id).toBe('second-memory-id');
|
|
});
|
|
});
|
|
|
|
describe('getSessionById - Session Retrieval', () => {
|
|
it('should return session with both content_session_id and memory_session_id', () => {
|
|
const contentSessionId = 'retrieve-test-session';
|
|
const memorySessionId = 'captured-memory-id';
|
|
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test prompt');
|
|
store.updateMemorySessionId(sessionDbId, memorySessionId);
|
|
|
|
const session = store.getSessionById(sessionDbId);
|
|
|
|
expect(session).not.toBeNull();
|
|
expect(session?.content_session_id).toBe(contentSessionId);
|
|
expect(session?.memory_session_id).toBe(memorySessionId);
|
|
});
|
|
|
|
it('should initialize memory_session_id to content_session_id before SDK capture', () => {
|
|
const contentSessionId = 'never-captured-session';
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
// createSDKSession sets memory_session_id = content_session_id initially
|
|
// The memory_session_id gets updated when SDK responds with its session ID
|
|
const session = store.getSessionById(sessionDbId);
|
|
expect(session?.memory_session_id).toBe(contentSessionId);
|
|
});
|
|
});
|
|
|
|
describe('storeObservation - Memory Session ID Reference', () => {
|
|
it('should store observation with memory_session_id as foreign key', () => {
|
|
const contentSessionId = 'obs-test-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
const obs = {
|
|
type: 'discovery',
|
|
title: 'Test Observation',
|
|
subtitle: null,
|
|
facts: ['Fact 1'],
|
|
narrative: 'Testing memory session ID reference',
|
|
concepts: ['testing'],
|
|
files_read: [],
|
|
files_modified: []
|
|
};
|
|
|
|
const result = store.storeObservation(contentSessionId, 'test-project', obs, 1);
|
|
|
|
// Verify the observation was stored with memory_session_id
|
|
const stored = store.db.prepare(
|
|
'SELECT memory_session_id FROM observations WHERE id = ?'
|
|
).get(result.id) as { memory_session_id: string };
|
|
|
|
expect(stored.memory_session_id).toBe(contentSessionId);
|
|
});
|
|
|
|
it('should be retrievable by getObservationsForSession using memory_session_id', () => {
|
|
const contentSessionId = 'obs-retrieval-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
const obs = {
|
|
type: 'feature',
|
|
title: 'New Feature',
|
|
subtitle: 'Sub',
|
|
facts: [],
|
|
narrative: null,
|
|
concepts: [],
|
|
files_read: ['file1.ts'],
|
|
files_modified: ['file2.ts']
|
|
};
|
|
|
|
store.storeObservation(contentSessionId, 'test-project', obs, 1);
|
|
|
|
const observations = store.getObservationsForSession(contentSessionId);
|
|
|
|
expect(observations.length).toBe(1);
|
|
expect(observations[0].title).toBe('New Feature');
|
|
});
|
|
});
|
|
|
|
describe('storeSummary - Memory Session ID Reference', () => {
|
|
it('should store summary with memory_session_id as foreign key', () => {
|
|
const contentSessionId = 'summary-test-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
const summary = {
|
|
request: 'Test request',
|
|
investigated: 'Investigated stuff',
|
|
learned: 'Learned things',
|
|
completed: 'Completed work',
|
|
next_steps: 'Next steps here',
|
|
notes: null
|
|
};
|
|
|
|
const result = store.storeSummary(contentSessionId, 'test-project', summary, 1);
|
|
|
|
// Verify the summary was stored with memory_session_id
|
|
const stored = store.db.prepare(
|
|
'SELECT memory_session_id FROM session_summaries WHERE id = ?'
|
|
).get(result.id) as { memory_session_id: string };
|
|
|
|
expect(stored.memory_session_id).toBe(contentSessionId);
|
|
});
|
|
|
|
it('should be retrievable by getSummaryForSession using memory_session_id', () => {
|
|
const contentSessionId = 'summary-retrieval-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
const summary = {
|
|
request: 'My request',
|
|
investigated: 'Investigation',
|
|
learned: 'Learnings',
|
|
completed: 'Completions',
|
|
next_steps: 'Next',
|
|
notes: 'Some notes'
|
|
};
|
|
|
|
store.storeSummary(contentSessionId, 'test-project', summary, 1);
|
|
|
|
const retrieved = store.getSummaryForSession(contentSessionId);
|
|
|
|
expect(retrieved).not.toBeNull();
|
|
expect(retrieved?.request).toBe('My request');
|
|
expect(retrieved?.notes).toBe('Some notes');
|
|
});
|
|
});
|
|
|
|
describe('saveUserPrompt - Content Session ID Reference', () => {
|
|
it('should store user prompt with content_session_id as foreign key', () => {
|
|
const contentSessionId = 'prompt-test-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Initial');
|
|
|
|
const promptId = store.saveUserPrompt(contentSessionId, 1, 'First user prompt');
|
|
|
|
// Verify the prompt was stored with content_session_id
|
|
const stored = store.db.prepare(
|
|
'SELECT content_session_id FROM user_prompts WHERE id = ?'
|
|
).get(promptId) as { content_session_id: string };
|
|
|
|
expect(stored.content_session_id).toBe(contentSessionId);
|
|
});
|
|
|
|
it('should be countable by getPromptNumberFromUserPrompts using content_session_id', () => {
|
|
const contentSessionId = 'prompt-count-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Initial');
|
|
|
|
expect(store.getPromptNumberFromUserPrompts(contentSessionId)).toBe(0);
|
|
|
|
store.saveUserPrompt(contentSessionId, 1, 'First');
|
|
expect(store.getPromptNumberFromUserPrompts(contentSessionId)).toBe(1);
|
|
|
|
store.saveUserPrompt(contentSessionId, 2, 'Second');
|
|
expect(store.getPromptNumberFromUserPrompts(contentSessionId)).toBe(2);
|
|
});
|
|
|
|
it('should be retrievable by getUserPrompt using content_session_id', () => {
|
|
const contentSessionId = 'prompt-retrieve-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Initial');
|
|
|
|
store.saveUserPrompt(contentSessionId, 1, 'Hello world');
|
|
|
|
const retrieved = store.getUserPrompt(contentSessionId, 1);
|
|
|
|
expect(retrieved).toBe('Hello world');
|
|
});
|
|
});
|
|
|
|
describe('getLatestUserPrompt - Joined Query with Both Session IDs', () => {
|
|
it('should return prompt with both content_session_id and memory_session_id', () => {
|
|
const contentSessionId = 'latest-prompt-session';
|
|
const memorySessionId = 'captured-memory-for-latest';
|
|
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'Initial');
|
|
store.updateMemorySessionId(sessionDbId, memorySessionId);
|
|
store.saveUserPrompt(contentSessionId, 1, 'Latest prompt text');
|
|
|
|
const latest = store.getLatestUserPrompt(contentSessionId);
|
|
|
|
expect(latest).toBeDefined();
|
|
expect(latest?.content_session_id).toBe(contentSessionId);
|
|
expect(latest?.memory_session_id).toBe(memorySessionId);
|
|
expect(latest?.prompt_text).toBe('Latest prompt text');
|
|
});
|
|
});
|
|
|
|
describe('getAllRecentUserPrompts - Joined Query with Project', () => {
|
|
it('should return prompts with content_session_id and project from session', () => {
|
|
const contentSessionId = 'all-prompts-session';
|
|
store.createSDKSession(contentSessionId, 'my-project', 'Initial');
|
|
store.saveUserPrompt(contentSessionId, 1, 'Prompt one');
|
|
store.saveUserPrompt(contentSessionId, 2, 'Prompt two');
|
|
|
|
const prompts = store.getAllRecentUserPrompts(10);
|
|
|
|
expect(prompts.length).toBe(2);
|
|
expect(prompts[0].content_session_id).toBe(contentSessionId);
|
|
expect(prompts[0].project).toBe('my-project');
|
|
});
|
|
});
|
|
|
|
describe('Resume Functionality - Memory Session ID Usage', () => {
|
|
it('should preserve memory_session_id across session re-initialization', () => {
|
|
const contentSessionId = 'resume-test-session';
|
|
const capturedMemoryId = 'sdk-memory-session-for-resume';
|
|
|
|
// Simulate first interaction: create session, then SDK responds with session ID
|
|
const sessionDbId = store.createSDKSession(contentSessionId, 'test-project', 'First prompt');
|
|
store.updateMemorySessionId(sessionDbId, capturedMemoryId);
|
|
|
|
// Simulate worker restart or new request: fetch session from database
|
|
const retrievedSession = store.getSessionById(sessionDbId);
|
|
|
|
// The memory_session_id should be available for resume parameter
|
|
expect(retrievedSession?.memory_session_id).toBe(capturedMemoryId);
|
|
});
|
|
|
|
it('should support multiple observations linked to same memory_session_id', () => {
|
|
const contentSessionId = 'multi-obs-session';
|
|
store.createSDKSession(contentSessionId, 'test-project', 'Test');
|
|
|
|
// Store multiple observations
|
|
for (let i = 1; i <= 5; i++) {
|
|
store.storeObservation(contentSessionId, 'test-project', {
|
|
type: 'discovery',
|
|
title: `Observation ${i}`,
|
|
subtitle: null,
|
|
facts: [],
|
|
narrative: null,
|
|
concepts: [],
|
|
files_read: [],
|
|
files_modified: []
|
|
}, i);
|
|
}
|
|
|
|
const observations = store.getObservationsForSession(contentSessionId);
|
|
expect(observations.length).toBe(5);
|
|
|
|
// All should have the same memory_session_id
|
|
const directQuery = store.db.prepare(
|
|
'SELECT DISTINCT memory_session_id FROM observations WHERE memory_session_id = ?'
|
|
).all(contentSessionId) as Array<{ memory_session_id: string }>;
|
|
|
|
expect(directQuery.length).toBe(1);
|
|
expect(directQuery[0].memory_session_id).toBe(contentSessionId);
|
|
});
|
|
});
|
|
});
|