import { describe, expect, test, beforeAll, afterAll } from 'bun:test';
import { Kysely } from 'kysely';
import { createTestDatabase } from './test-setup.js';
import { MemoryService } from '../src/services/memory-service.js';
import type { Database, RelationType } from '../src/types/database.js';
describe('Memory Relations Constraints', () => {
let db: Kysely<Database>;
let memoryService: MemoryService;
let memoryIds: string[] = [];
beforeAll(async () => {
db = createTestDatabase();
memoryService = new MemoryService(db);
// Create test memories
const memories = await db
.insertInto('memories')
.values([
{
user_context: 'test-relations',
content: JSON.stringify({ text: 'Memory A' }),
content_hash: 'hash-a',
type: 'fact',
source: 'test',
confidence: 0.9,
},
{
user_context: 'test-relations',
content: JSON.stringify({ text: 'Memory B' }),
content_hash: 'hash-b',
type: 'fact',
source: 'test',
confidence: 0.9,
},
])
.returning('id')
.execute();
memoryIds = memories.map(m => m.id);
});
afterAll(async () => {
// Clean up test data
await db
.deleteFrom('memory_relations')
.where((eb) => eb.or([
eb('from_memory_id', 'in', memoryIds),
eb('to_memory_id', 'in', memoryIds)
]))
.execute();
await db
.deleteFrom('memories')
.where('id', 'in', memoryIds)
.execute();
await db.destroy();
});
test('should enforce unique constraint on (from_memory_id, to_memory_id)', async () => {
// Create first relation
const relation1 = await memoryService.createRelation(
memoryIds[0],
memoryIds[1],
'references',
0.8
);
expect(relation1).toBeDefined();
expect(relation1.relation_type).toBe('references');
expect(relation1.strength).toBe(0.8);
// Try to create duplicate with different relation_type - should update
const relation2 = await memoryService.createRelation(
memoryIds[0],
memoryIds[1],
'supports',
0.9
);
expect(relation2).toBeDefined();
expect(relation2.id).toBe(relation1.id); // Same relation, updated
expect(relation2.relation_type).toBe('supports');
expect(relation2.strength).toBe(0.9);
});
test('should handle concurrent upserts without errors', async () => {
// Simulate concurrent requests to create/update the same relation
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(
memoryService.createRelation(
memoryIds[0],
memoryIds[1],
i % 2 === 0 ? 'extends' : 'contradicts',
0.5 + (i * 0.05)
)
);
}
// All should succeed without throwing errors
const results = await Promise.allSettled(promises);
const failures = results.filter(r => r.status === 'rejected');
expect(failures.length).toBe(0);
// Check final state - should have one relation with the last values
const finalRelation = await db
.selectFrom('memory_relations')
.selectAll()
.where('from_memory_id', '=', memoryIds[0])
.where('to_memory_id', '=', memoryIds[1])
.executeTakeFirst();
expect(finalRelation).toBeDefined();
});
test('should allow bidirectional relations as separate records', async () => {
// Create A -> B relation
const forward = await memoryService.createRelation(
memoryIds[0],
memoryIds[1],
'causes',
0.7
);
// Create B -> A relation (reverse)
const reverse = await memoryService.createRelation(
memoryIds[1],
memoryIds[0],
'caused_by',
0.7
);
expect(forward.id).not.toBe(reverse.id); // Different relations
expect(forward.from_memory_id).toBe(memoryIds[0]);
expect(forward.to_memory_id).toBe(memoryIds[1]);
expect(reverse.from_memory_id).toBe(memoryIds[1]);
expect(reverse.to_memory_id).toBe(memoryIds[0]);
});
test('should validate relation types', async () => {
// Since the CHECK constraint may not be enforced in all environments,
// we test that our service properly validates relation types
const validTypes: RelationType[] = [
'references', 'contradicts', 'supports', 'extends',
'causes', 'caused_by', 'precedes', 'follows',
'part_of', 'contains', 'relates_to'
];
// Test that all valid types work
for (const relType of validTypes) {
// Clean up first
await db
.deleteFrom('memory_relations')
.where('from_memory_id', '=', memoryIds[0])
.where('to_memory_id', '=', memoryIds[1])
.execute();
const relation = await memoryService.createRelation(
memoryIds[0],
memoryIds[1],
relType,
0.5
);
expect(relation.relation_type).toBe(relType);
}
// The database has CHECK constraints that should prevent invalid types
// The service should also validate types before sending to database
expect(validTypes.length).toBe(11);
});
test('should normalize legacy relation types in migration', async () => {
// This would be tested in actual migration run
// Here we verify the normalized types are valid
const validTypes = [
'references', 'contradicts', 'supports', 'extends',
'causes', 'caused_by', 'precedes', 'follows',
'part_of', 'contains', 'relates_to'
];
// All relations should have valid types
const relations = await db
.selectFrom('memory_relations')
.select('relation_type')
.distinct()
.execute();
relations.forEach(rel => {
expect(validTypes).toContain(rel.relation_type);
});
});
});