godcrm/backend/services/__tests__/chatChunkingService.v2.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

338 lines
14 KiB
JavaScript

/**
* ADR-024 Phase 2: chatChunkingService v2 Tests
*
* TDD: RED Phase - Tests for refactored service that uses DB tables
* instead of JSON blob for summaries.
*
* Key changes:
* - Uses `messages` table instead of JSON array
* - Uses `conversation_summaries` table for cached summaries
* - Provides efficient context building for AI
*/
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, vi } from 'vitest';
// Set env vars BEFORE imports
process.env.SKIP_DEV_USER = 'true';
import { dbRun, dbGet, dbAll, isPostgres } from '../../database/connection.js';
import { setupTestDatabase, cleanupTestDatabase } from '../../tests/helpers/test-db.js';
// Import service under test
import {
CHUNK_SIZE,
KEEP_RECENT_MESSAGES,
shouldCreateSummary,
createSummary,
buildAIContext,
getMessageCount,
getSummarizedMessageCount
} from '../chatChunkingService.js';
describe('ADR-024 Phase 2: chatChunkingService v2', () => {
let testUserId;
let testConversationId;
beforeAll(async () => {
await setupTestDatabase();
});
afterAll(async () => {
await cleanupTestDatabase();
});
beforeEach(async () => {
const ts = Date.now();
// Create test user
const userResult = await dbRun(
`INSERT INTO users (email, password_hash, name, encryption_key_encrypted, created_at) VALUES ($1, 'hash', 'Test', 'enc', NOW())`,
[`chunking-test-${ts}@test.com`]
);
testUserId = userResult.lastInsertRowid;
// Create test conversation
const convResult = await dbRun(
`INSERT INTO conversations (type, created_by, created_at) VALUES ('ai_chat', $1, NOW())`,
[testUserId]
);
testConversationId = convResult.lastInsertRowid;
});
afterEach(async () => {
// Cleanup
if (testConversationId) {
await dbRun('DELETE FROM conversation_summaries WHERE conversation_id = $1', [testConversationId]);
await dbRun('DELETE FROM messages WHERE conversation_id = $1', [testConversationId]);
await dbRun('DELETE FROM conversations WHERE id = $1', [testConversationId]);
}
if (testUserId) {
await dbRun('DELETE FROM users WHERE id = $1', [testUserId]);
}
});
/**
* Helper to insert test messages
*/
async function insertMessages(conversationId, senderId, count) {
const messageIds = [];
for (let i = 0; i < count; i++) {
const result = await dbRun(
`INSERT INTO messages (conversation_id, sender_id, role, content, created_at)
VALUES ($1, $2, 'user', $3, NOW())`,
[conversationId, senderId, `Test message ${i + 1}`]
);
messageIds.push(result.lastInsertRowid);
}
return messageIds;
}
describe('getMessageCount()', () => {
it('should return 0 for empty conversation', async () => {
const count = await getMessageCount(testConversationId);
expect(count).toBe(0);
});
it('should return correct count for conversation with messages', async () => {
await insertMessages(testConversationId, testUserId, 15);
const count = await getMessageCount(testConversationId);
expect(count).toBe(15);
});
});
describe('getSummarizedMessageCount()', () => {
it('should return 0 when no summaries exist', async () => {
const count = await getSummarizedMessageCount(testConversationId);
expect(count).toBe(0);
});
it('should return sum of messages_count from all summaries', async () => {
// Insert 2 summaries with 10 messages each
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 1, 1, 10, 10, 'Summary 1', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 1, 1, 10, 10, 'Summary 1', datetime('now'))`,
[testConversationId]
);
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 2, 11, 20, 10, 'Summary 2', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 2, 11, 20, 10, 'Summary 2', datetime('now'))`,
[testConversationId]
);
const count = await getSummarizedMessageCount(testConversationId);
expect(count).toBe(20);
});
});
describe('shouldCreateSummary()', () => {
it('returns false when less than CHUNK_SIZE + KEEP_RECENT messages', async () => {
// Less than 10 + 5 = 15 messages
await insertMessages(testConversationId, testUserId, 14);
const should = await shouldCreateSummary(testConversationId);
expect(should).toBe(false);
});
it('returns true when CHUNK_SIZE + KEEP_RECENT unsummarized messages exist', async () => {
// Exactly 15 messages = CHUNK_SIZE(10) + KEEP_RECENT(5)
await insertMessages(testConversationId, testUserId, 15);
const should = await shouldCreateSummary(testConversationId);
expect(should).toBe(true);
});
it('returns false when all eligible messages are summarized', async () => {
// 25 messages, but 20 are summarized
await insertMessages(testConversationId, testUserId, 25);
// Add summary for first 20 messages
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 1, 1, 10, 10, 'Summary 1', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 1, 1, 10, 10, 'Summary 1', datetime('now'))`,
[testConversationId]
);
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 2, 11, 20, 10, 'Summary 2', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 2, 11, 20, 10, 'Summary 2', datetime('now'))`,
[testConversationId]
);
// 25 total - 20 summarized = 5 unsummarized (= KEEP_RECENT, so no new summary needed)
const should = await shouldCreateSummary(testConversationId);
expect(should).toBe(false);
});
it('returns true when new chunk of unsummarized messages exists', async () => {
// 35 messages, first 20 summarized
await insertMessages(testConversationId, testUserId, 35);
// Add summary for first 20 messages
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 1, 1, 10, 10, 'Summary 1', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 1, 1, 10, 10, 'Summary 1', datetime('now'))`,
[testConversationId]
);
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 2, 11, 20, 10, 'Summary 2', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 2, 11, 20, 10, 'Summary 2', datetime('now'))`,
[testConversationId]
);
// 35 total - 20 summarized - 5 keep_recent = 10 unsummarized >= CHUNK_SIZE
const should = await shouldCreateSummary(testConversationId);
expect(should).toBe(true);
});
});
describe('createSummary()', () => {
it('creates summary for oldest unsummarized chunk and saves to DB', async () => {
// Create 20 messages
const messageIds = await insertMessages(testConversationId, testUserId, 20);
// Mock the AI summary generation (we'll inject this later)
const mockSummaryText = 'User discussed testing the chunking service.';
// Create summary (should summarize first 10 messages)
const summary = await createSummary(testConversationId, () => Promise.resolve(mockSummaryText));
expect(summary).toBeDefined();
expect(summary.chunk_number).toBe(1);
expect(summary.messages_count).toBe(CHUNK_SIZE);
expect(summary.summary).toBe(mockSummaryText);
// Verify it was saved to DB
const saved = await dbGet(
isPostgres()
? 'SELECT * FROM conversation_summaries WHERE conversation_id = $1 AND chunk_number = 1'
: 'SELECT * FROM conversation_summaries WHERE conversation_id = ? AND chunk_number = 1',
[testConversationId]
);
expect(saved).toBeDefined();
expect(saved.summary).toBe(mockSummaryText);
});
it('creates next chunk summary when previous chunks are summarized', async () => {
// Create 30 messages
await insertMessages(testConversationId, testUserId, 30);
// Add summary for first chunk
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 1, 1, 10, 10, 'First chunk summary', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 1, 1, 10, 10, 'First chunk summary', datetime('now'))`,
[testConversationId]
);
// Create summary for second chunk
const summary = await createSummary(testConversationId, () => Promise.resolve('Second chunk summary'));
expect(summary.chunk_number).toBe(2);
expect(summary.summary).toBe('Second chunk summary');
});
it('returns null when no summarization needed', async () => {
// Only 10 messages (less than CHUNK_SIZE + KEEP_RECENT)
await insertMessages(testConversationId, testUserId, 10);
const summary = await createSummary(testConversationId, () => Promise.resolve('Should not be called'));
expect(summary).toBeNull();
});
});
describe('buildAIContext()', () => {
it('returns empty context for empty conversation', async () => {
const context = await buildAIContext(testConversationId);
expect(context.summaries).toEqual([]);
expect(context.recentMessages).toEqual([]);
expect(context.totalMessages).toBe(0);
});
it('returns only recent messages when no summaries exist', async () => {
// Create 8 messages (less than would trigger summary)
await insertMessages(testConversationId, testUserId, 8);
const context = await buildAIContext(testConversationId);
expect(context.summaries).toEqual([]);
expect(context.recentMessages).toHaveLength(8);
expect(context.totalMessages).toBe(8);
});
it('returns summaries + recent messages for long conversations', async () => {
// Create 25 messages
await insertMessages(testConversationId, testUserId, 25);
// Add summaries for first 20
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 1, 1, 10, 10, 'Chunk 1 summary', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 1, 1, 10, 10, 'Chunk 1 summary', datetime('now'))`,
[testConversationId]
);
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 2, 11, 20, 10, 'Chunk 2 summary', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 2, 11, 20, 10, 'Chunk 2 summary', datetime('now'))`,
[testConversationId]
);
const context = await buildAIContext(testConversationId);
// Should have 2 summaries
expect(context.summaries).toHaveLength(2);
expect(context.summaries[0]).toBe('Chunk 1 summary');
expect(context.summaries[1]).toBe('Chunk 2 summary');
// Should have recent messages (KEEP_RECENT + some buffer)
expect(context.recentMessages.length).toBeGreaterThanOrEqual(KEEP_RECENT_MESSAGES);
// Total messages count
expect(context.totalMessages).toBe(25);
expect(context.summarizedMessages).toBe(20);
});
it('returns system context string for AI prompt', async () => {
// Create messages and summaries
await insertMessages(testConversationId, testUserId, 15);
await dbRun(
isPostgres()
? `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES ($1, 1, 1, 10, 10, 'User asked about login bug', NOW())`
: `INSERT INTO conversation_summaries (conversation_id, chunk_number, messages_start_id, messages_end_id, messages_count, summary, created_at)
VALUES (?, 1, 1, 10, 10, 'User asked about login bug', datetime('now'))`,
[testConversationId]
);
const context = await buildAIContext(testConversationId);
expect(context.systemContext).toContain('Previous conversation summary');
expect(context.systemContext).toContain('User asked about login bug');
});
});
});