Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* PostgreSQL Persistence - Integration Tests
|
||||
*
|
||||
* Tests for database operations, transactions, and data integrity
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createMockPool, queryBuilderHelpers, type MockPool } from '../../mocks/postgres.mock';
|
||||
import { createAgent, createSession, createMemory, createTenant } from '../../factories';
|
||||
|
||||
describe('PostgreSQL Persistence', () => {
|
||||
let pool: MockPool;
|
||||
|
||||
beforeEach(async () => {
|
||||
pool = createMockPool();
|
||||
await pool.connect();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
describe('Connection Management', () => {
|
||||
it('should establish connection', async () => {
|
||||
expect(pool.isConnected()).toBe(true);
|
||||
});
|
||||
|
||||
it('should close connection', async () => {
|
||||
await pool.end();
|
||||
expect(pool.isConnected()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Agent Persistence', () => {
|
||||
it('should insert agent', async () => {
|
||||
const agent = createAgent({ name: 'Test Agent', type: 'coder' });
|
||||
|
||||
const result = await pool.query(
|
||||
'INSERT INTO agents (id, name, type, status, config) VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
||||
[agent.id, agent.name, agent.type, agent.status, JSON.stringify(agent.config)]
|
||||
);
|
||||
|
||||
expect(result.rowCount).toBe(1);
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /INSERT INTO agents/)).toBe(true);
|
||||
});
|
||||
|
||||
it('should select agent by ID', async () => {
|
||||
const agent = createAgent();
|
||||
|
||||
// Seed data
|
||||
pool.seedData('agents', [{ id: agent.id, name: agent.name, type: agent.type }]);
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM agents WHERE id = $1',
|
||||
[agent.id]
|
||||
);
|
||||
|
||||
expect(result.rows).toHaveLength(1);
|
||||
expect(result.rows[0].id).toBe(agent.id);
|
||||
});
|
||||
|
||||
it('should update agent', async () => {
|
||||
const agent = createAgent();
|
||||
pool.seedData('agents', [{ id: agent.id, name: agent.name, status: 'idle' }]);
|
||||
|
||||
const result = await pool.query(
|
||||
'UPDATE agents SET status = $1 WHERE id = $2',
|
||||
['busy', agent.id]
|
||||
);
|
||||
|
||||
expect(result.rowCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should delete agent', async () => {
|
||||
const agent = createAgent();
|
||||
pool.seedData('agents', [{ id: agent.id }]);
|
||||
|
||||
const result = await pool.query(
|
||||
'DELETE FROM agents WHERE id = $1',
|
||||
[agent.id]
|
||||
);
|
||||
|
||||
expect(result.rowCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session Persistence', () => {
|
||||
it('should insert session', async () => {
|
||||
const session = createSession();
|
||||
|
||||
const result = await pool.query(
|
||||
'INSERT INTO sessions (id, tenant_id, user_id, channel_id, status) VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
||||
[session.id, session.tenantId, session.userId, session.channelId, session.status]
|
||||
);
|
||||
|
||||
expect(result.rowCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should select sessions by tenant', async () => {
|
||||
const tenantId = 'tenant-001';
|
||||
pool.seedData('sessions', [
|
||||
{ id: 'session-1', tenantId, tenant_id: tenantId },
|
||||
{ id: 'session-2', tenantId, tenant_id: tenantId },
|
||||
{ id: 'session-3', tenantId: 'other-tenant', tenant_id: 'other-tenant' }
|
||||
]);
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM sessions WHERE tenant_id = $1',
|
||||
[tenantId]
|
||||
);
|
||||
|
||||
expect(result.rows).toHaveLength(2);
|
||||
result.rows.forEach(row => {
|
||||
expect(row.tenantId || row.tenant_id).toBe(tenantId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Memory Persistence', () => {
|
||||
it('should insert memory entry', async () => {
|
||||
const memory = createMemory({ key: 'test-key', value: { data: 'test' } });
|
||||
|
||||
const result = await pool.query(
|
||||
'INSERT INTO memories (id, tenant_id, key, value, type) VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
||||
[memory.id, memory.tenantId, memory.key, JSON.stringify(memory.value), memory.type]
|
||||
);
|
||||
|
||||
expect(result.rowCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should select memory by key', async () => {
|
||||
pool.seedData('memories', [
|
||||
{ id: 'mem-1', key: 'unique-key', tenantId: 'tenant-001' }
|
||||
]);
|
||||
|
||||
// Note: Mock implementation uses indexByKey
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM memories WHERE key = $1',
|
||||
['unique-key']
|
||||
);
|
||||
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /SELECT \* FROM memories/)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tenant Persistence', () => {
|
||||
it('should insert tenant', async () => {
|
||||
const tenant = createTenant();
|
||||
|
||||
const result = await pool.query(
|
||||
'INSERT INTO tenants (id, name, slack_team_id, status, plan) VALUES ($1, $2, $3, $4, $5) RETURNING *',
|
||||
[tenant.id, tenant.name, tenant.slackTeamId, tenant.status, tenant.plan]
|
||||
);
|
||||
|
||||
expect(result.rowCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should select tenant by slack team ID', async () => {
|
||||
pool.seedData('tenants', [
|
||||
{ id: 'tenant-1', slackTeamId: 'T12345678' }
|
||||
]);
|
||||
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM tenants WHERE id = $1',
|
||||
['tenant-1']
|
||||
);
|
||||
|
||||
expect(result.rows).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Transactions', () => {
|
||||
it('should execute transaction with commit', async () => {
|
||||
await pool.query('BEGIN');
|
||||
await pool.query('INSERT INTO agents (id, name) VALUES ($1, $2)', ['agent-1', 'Test']);
|
||||
await pool.query('INSERT INTO sessions (id, tenant_id) VALUES ($1, $2)', ['session-1', 'tenant-1']);
|
||||
await pool.query('COMMIT');
|
||||
|
||||
expect(queryBuilderHelpers.expectTransaction(pool)).toBe(true);
|
||||
});
|
||||
|
||||
it('should execute transaction with rollback', async () => {
|
||||
await pool.query('BEGIN');
|
||||
await pool.query('INSERT INTO agents (id, name) VALUES ($1, $2)', ['agent-1', 'Test']);
|
||||
await pool.query('ROLLBACK');
|
||||
|
||||
expect(queryBuilderHelpers.expectTransaction(pool)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query Logging', () => {
|
||||
it('should log all queries', async () => {
|
||||
await pool.query('SELECT 1');
|
||||
await pool.query('SELECT 2');
|
||||
await pool.query('SELECT 3');
|
||||
|
||||
const log = pool.getQueryLog();
|
||||
expect(log).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should log query values', async () => {
|
||||
await pool.query('INSERT INTO agents (id) VALUES ($1)', ['agent-1']);
|
||||
|
||||
const log = pool.getQueryLog();
|
||||
expect(log[0].values).toEqual(['agent-1']);
|
||||
});
|
||||
|
||||
it('should clear query log', async () => {
|
||||
await pool.query('SELECT 1');
|
||||
pool.clearQueryLog();
|
||||
|
||||
expect(pool.getQueryLog()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Query Helpers', () => {
|
||||
it('should match query patterns', async () => {
|
||||
await pool.query('SELECT * FROM agents WHERE type = $1', ['coder']);
|
||||
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /SELECT \* FROM agents/)).toBe(true);
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /SELECT \* FROM sessions/)).toBe(false);
|
||||
});
|
||||
|
||||
it('should count matching queries', async () => {
|
||||
await pool.query('SELECT * FROM agents');
|
||||
await pool.query('SELECT * FROM agents WHERE id = $1', ['1']);
|
||||
await pool.query('SELECT * FROM sessions');
|
||||
|
||||
const count = queryBuilderHelpers.expectQueryCount(pool, /SELECT \* FROM agents/);
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PostgreSQL Repository Patterns', () => {
|
||||
let pool: MockPool;
|
||||
|
||||
beforeEach(async () => {
|
||||
pool = createMockPool();
|
||||
await pool.connect();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
describe('Bulk Operations', () => {
|
||||
it('should handle bulk insert', async () => {
|
||||
const agents = Array.from({ length: 10 }, (_, i) =>
|
||||
createAgent({ id: `agent-${i}`, name: `Agent ${i}` })
|
||||
);
|
||||
|
||||
// Simulate bulk insert
|
||||
for (const agent of agents) {
|
||||
await pool.query(
|
||||
'INSERT INTO agents (id, name) VALUES ($1, $2)',
|
||||
[agent.id, agent.name]
|
||||
);
|
||||
}
|
||||
|
||||
expect(queryBuilderHelpers.expectQueryCount(pool, /INSERT INTO agents/)).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Upsert Operations', () => {
|
||||
it('should handle upsert pattern', async () => {
|
||||
pool.seedData('agents', [{ id: 'agent-1', name: 'Original' }]);
|
||||
|
||||
// Simulate upsert
|
||||
const result = await pool.query(
|
||||
`INSERT INTO agents (id, name) VALUES ($1, $2)
|
||||
ON CONFLICT (id) DO UPDATE SET name = $2
|
||||
RETURNING *`,
|
||||
['agent-1', 'Updated']
|
||||
);
|
||||
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /INSERT INTO agents/)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('should handle paginated queries', async () => {
|
||||
pool.seedData('agents', Array.from({ length: 25 }, (_, i) => ({
|
||||
id: `agent-${i}`,
|
||||
name: `Agent ${i}`
|
||||
})));
|
||||
|
||||
const page1 = await pool.query(
|
||||
'SELECT * FROM agents ORDER BY id LIMIT $1 OFFSET $2',
|
||||
[10, 0]
|
||||
);
|
||||
|
||||
const page2 = await pool.query(
|
||||
'SELECT * FROM agents ORDER BY id LIMIT $1 OFFSET $2',
|
||||
[10, 10]
|
||||
);
|
||||
|
||||
expect(queryBuilderHelpers.expectQueryCount(pool, /LIMIT/)).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Join Operations', () => {
|
||||
it('should log join queries', async () => {
|
||||
await pool.query(`
|
||||
SELECT s.*, a.name as agent_name
|
||||
FROM sessions s
|
||||
LEFT JOIN agents a ON a.id = ANY(s.active_agents)
|
||||
WHERE s.tenant_id = $1
|
||||
`, ['tenant-1']);
|
||||
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /JOIN/)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Aggregations', () => {
|
||||
it('should log aggregation queries', async () => {
|
||||
await pool.query(`
|
||||
SELECT tenant_id, COUNT(*) as session_count
|
||||
FROM sessions
|
||||
GROUP BY tenant_id
|
||||
HAVING COUNT(*) > $1
|
||||
`, [5]);
|
||||
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /GROUP BY/)).toBe(true);
|
||||
expect(queryBuilderHelpers.expectQuery(pool, /COUNT/)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PostgreSQL Error Handling', () => {
|
||||
let pool: MockPool;
|
||||
|
||||
beforeEach(async () => {
|
||||
pool = createMockPool();
|
||||
await pool.connect();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
it('should handle query errors gracefully', async () => {
|
||||
// In real implementation, this would test actual error scenarios
|
||||
const result = await pool.query('SELECT * FROM non_existent_table');
|
||||
|
||||
expect(result.rows).toEqual([]);
|
||||
});
|
||||
|
||||
it('should track failed transactions', async () => {
|
||||
await pool.query('BEGIN');
|
||||
await pool.query('INVALID SQL THAT WOULD FAIL');
|
||||
await pool.query('ROLLBACK');
|
||||
|
||||
expect(queryBuilderHelpers.expectTransaction(pool)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PostgreSQL Multi-tenancy', () => {
|
||||
let pool: MockPool;
|
||||
|
||||
beforeEach(async () => {
|
||||
pool = createMockPool();
|
||||
await pool.connect();
|
||||
|
||||
// Seed multi-tenant data
|
||||
pool.seedData('agents', [
|
||||
{ id: 'agent-1', tenantId: 'tenant-1', tenant_id: 'tenant-1', name: 'T1 Agent' },
|
||||
{ id: 'agent-2', tenantId: 'tenant-2', tenant_id: 'tenant-2', name: 'T2 Agent' },
|
||||
{ id: 'agent-3', tenantId: 'tenant-1', tenant_id: 'tenant-1', name: 'T1 Agent 2' }
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
it('should filter by tenant ID', async () => {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM agents WHERE tenant_id = $1',
|
||||
['tenant-1']
|
||||
);
|
||||
|
||||
expect(result.rows).toHaveLength(2);
|
||||
result.rows.forEach(row => {
|
||||
expect(row.tenantId || row.tenant_id).toBe('tenant-1');
|
||||
});
|
||||
});
|
||||
|
||||
it('should isolate tenant data', async () => {
|
||||
const tenant1Data = await pool.query(
|
||||
'SELECT * FROM agents WHERE tenant_id = $1',
|
||||
['tenant-1']
|
||||
);
|
||||
|
||||
const tenant2Data = await pool.query(
|
||||
'SELECT * FROM agents WHERE tenant_id = $1',
|
||||
['tenant-2']
|
||||
);
|
||||
|
||||
expect(tenant1Data.rows).toHaveLength(2);
|
||||
expect(tenant2Data.rows).toHaveLength(1);
|
||||
|
||||
// Verify no data leakage
|
||||
const tenant1Ids = tenant1Data.rows.map((r: any) => r.id);
|
||||
const tenant2Ids = tenant2Data.rows.map((r: any) => r.id);
|
||||
|
||||
expect(tenant1Ids).not.toContain('agent-2');
|
||||
expect(tenant2Ids).not.toContain('agent-1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user