Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
205 lines
6.7 KiB
JavaScript
205 lines
6.7 KiB
JavaScript
/**
|
|
* ADR-024 Phase 2: conversation_summaries Table Schema Tests
|
|
*
|
|
* TDD: RED Phase - Tests for the new table that stores AI-generated
|
|
* summaries of conversation chunks for efficient context loading.
|
|
*
|
|
* Table: conversation_summaries
|
|
* Purpose: Cache AI summaries for old message chunks
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } 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';
|
|
|
|
/**
|
|
* Helper to get table columns
|
|
*/
|
|
async function getTableColumns(tableName) {
|
|
if (isPostgres()) {
|
|
const result = await dbAll(`
|
|
SELECT column_name, data_type, is_nullable
|
|
FROM information_schema.columns
|
|
WHERE table_name = $1
|
|
ORDER BY ordinal_position
|
|
`, [tableName]);
|
|
return result;
|
|
} else {
|
|
const result = await dbAll(`PRAGMA table_info(${tableName})`);
|
|
return result.map(r => ({
|
|
column_name: r.name,
|
|
data_type: r.type.toLowerCase(),
|
|
is_nullable: r.notnull === 0 ? 'YES' : 'NO'
|
|
}));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to check if index exists
|
|
*/
|
|
async function indexExists(tableName, indexName) {
|
|
if (isPostgres()) {
|
|
const result = await dbGet(`
|
|
SELECT indexname FROM pg_indexes
|
|
WHERE tablename = $1 AND indexname = $2
|
|
`, [tableName, indexName]);
|
|
return !!result;
|
|
} else {
|
|
const result = await dbAll(`PRAGMA index_list(${tableName})`);
|
|
return result.some(r => r.name === indexName);
|
|
}
|
|
}
|
|
|
|
describe('ADR-024 Phase 2: conversation_summaries Schema', () => {
|
|
|
|
beforeAll(async () => {
|
|
await setupTestDatabase();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await cleanupTestDatabase();
|
|
});
|
|
|
|
describe('Table Structure', () => {
|
|
|
|
it('should have conversation_summaries table with required columns', async () => {
|
|
const columns = await getTableColumns('conversation_summaries');
|
|
const columnNames = columns.map(c => c.column_name);
|
|
|
|
expect(columnNames).toContain('id');
|
|
expect(columnNames).toContain('conversation_id');
|
|
expect(columnNames).toContain('summary');
|
|
expect(columnNames).toContain('message_range_start');
|
|
expect(columnNames).toContain('message_range_end');
|
|
expect(columnNames).toContain('tokens_saved');
|
|
expect(columnNames).toContain('created_at');
|
|
});
|
|
|
|
it('should have index on conversation_id', async () => {
|
|
const hasIndex = await indexExists('conversation_summaries', 'idx_summaries_conversation');
|
|
expect(hasIndex).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('CRUD Operations', () => {
|
|
let testConversationId;
|
|
let testUserId;
|
|
|
|
beforeEach(async () => {
|
|
// Create test user
|
|
const ts = Date.now();
|
|
const userResult = await dbRun(
|
|
`INSERT INTO users (email, password_hash, name, encryption_key_encrypted, created_at)
|
|
VALUES ($1, 'hash', 'Test', 'enc', NOW())`,
|
|
[`summary-test-${ts}@test.com`]
|
|
);
|
|
testUserId = userResult.lastInsertRowid;
|
|
|
|
// Ticket #41154: Use unified 'chat' type (ADR-091)
|
|
const convResult = await dbRun(
|
|
`INSERT INTO conversations (type, created_by, created_at)
|
|
VALUES ('chat', $1, NOW())`,
|
|
[testUserId]
|
|
);
|
|
testConversationId = convResult.lastInsertRowid;
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Cleanup in order (summaries first due to FK)
|
|
if (testConversationId) {
|
|
await dbRun('DELETE FROM conversation_summaries 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]);
|
|
}
|
|
});
|
|
|
|
it('should insert and retrieve summary', async () => {
|
|
const insertResult = await dbRun(
|
|
`INSERT INTO conversation_summaries
|
|
(conversation_id, summary, message_range_start, message_range_end, tokens_saved, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, NOW())`,
|
|
[testConversationId, 'User discussed login bug', 1, 10, 500]
|
|
);
|
|
|
|
const summaryId = insertResult.lastInsertRowid;
|
|
expect(summaryId).toBeGreaterThan(0);
|
|
|
|
// Retrieve
|
|
const summary = await dbGet(
|
|
'SELECT * FROM conversation_summaries WHERE id = $1',
|
|
[summaryId]
|
|
);
|
|
|
|
expect(summary).toBeDefined();
|
|
expect(summary.conversation_id).toBe(testConversationId);
|
|
expect(summary.message_range_start).toBe(1);
|
|
expect(summary.message_range_end).toBe(10);
|
|
expect(summary.summary).toBe('User discussed login bug');
|
|
});
|
|
|
|
it.skip('should enforce unique constraint on (conversation_id, chunk_number) - chunk_number removed in PG schema', async () => {
|
|
});
|
|
|
|
it('should cascade delete summaries when conversation is deleted', async () => {
|
|
// Insert summary
|
|
await dbRun(
|
|
`INSERT INTO conversation_summaries
|
|
(conversation_id, summary, message_range_start, message_range_end, created_at)
|
|
VALUES ($1, 'Summary to delete', 1, 10, NOW())`,
|
|
[testConversationId]
|
|
);
|
|
|
|
// Verify it exists
|
|
const beforeDelete = await dbGet(
|
|
'SELECT * FROM conversation_summaries WHERE conversation_id = $1',
|
|
[testConversationId]
|
|
);
|
|
expect(beforeDelete).toBeDefined();
|
|
|
|
// Delete conversation
|
|
await dbRun(
|
|
'DELETE FROM conversations WHERE id = $1',
|
|
[testConversationId]
|
|
);
|
|
|
|
// Summary should be cascade deleted
|
|
const afterDelete = await dbGet(
|
|
'SELECT * FROM conversation_summaries WHERE conversation_id = $1',
|
|
[testConversationId]
|
|
);
|
|
expect(afterDelete).toBeFalsy();
|
|
|
|
// Mark as already deleted
|
|
testConversationId = null;
|
|
});
|
|
|
|
it('should support multiple summaries for same conversation', async () => {
|
|
// Insert 3 summaries with different ranges
|
|
for (let i = 1; i <= 3; i++) {
|
|
await dbRun(
|
|
`INSERT INTO conversation_summaries
|
|
(conversation_id, summary, message_range_start, message_range_end, created_at)
|
|
VALUES ($1, $2, $3, $4, NOW())`,
|
|
[testConversationId, `Summary for range ${i}`, (i - 1) * 10 + 1, i * 10]
|
|
);
|
|
}
|
|
|
|
// Retrieve all summaries
|
|
const summaries = await dbAll(
|
|
'SELECT * FROM conversation_summaries WHERE conversation_id = $1 ORDER BY message_range_start',
|
|
[testConversationId]
|
|
);
|
|
|
|
expect(summaries).toHaveLength(3);
|
|
expect(summaries[0].message_range_start).toBe(1);
|
|
expect(summaries[1].message_range_start).toBe(11);
|
|
expect(summaries[2].message_range_start).toBe(21);
|
|
});
|
|
});
|
|
});
|