Skip to content

Commit a5eac65

Browse files
authored
Add Postgres test coverage with pglite (#207)
## Summary - run the backend test suite in separate sqlite and postgres Vitest projects - back the postgres project with pglite while exercising the normal Postgres schema and migration paths - remove sqlite-only test assumptions so the same tests pass under both dialects ## Testing - bun run test
1 parent 01a9134 commit a5eac65

12 files changed

Lines changed: 252 additions & 76 deletions

File tree

bun.lock

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"zod": "^4.3.6"
3535
},
3636
"devDependencies": {
37+
"@electric-sql/pglite": "^0.4.4",
3738
"@types/bun": "latest",
3839
"@types/node": "^25.6.0",
3940
"@types/winston": "^2.4.4",

packages/backend/src/db/__tests__/encrypt-migration.test.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { beforeEach, afterEach, describe, expect, it } from 'vitest';
22
import { eq } from 'drizzle-orm';
3-
import { closeDatabase, getDatabase, getSchema, initializeDatabase } from '../client';
3+
import {
4+
closeDatabase,
5+
getCurrentDialect,
6+
getDatabase,
7+
getSchema,
8+
initializeDatabase,
9+
} from '../client';
410
import { runMigrations } from '../migrate';
511
import { runEncryptionMigration } from '../encrypt-migration';
612
import { decrypt, hashSecret, isEncrypted, resetEncryptionKeyCache } from '../../utils/encryption';
13+
import { toDbBoolean } from '../../utils/normalize';
714

815
const TEST_KEY = 'b'.repeat(64);
916

@@ -22,11 +29,15 @@ describe('encryption migration', () => {
2229

2330
beforeEach(async () => {
2431
await closeDatabase();
25-
process.env.DATABASE_URL = 'sqlite://:memory:';
32+
process.env.DATABASE_URL = process.env.PLEXUS_TEST_DB_URL ?? process.env.DATABASE_URL;
2633
initializeDatabase(process.env.DATABASE_URL);
2734
await runMigrations();
2835
db = getDatabase();
2936
schema = getSchema();
37+
await db.delete(schema.providers);
38+
await db.delete(schema.apiKeys);
39+
await db.delete(schema.oauthCredentials);
40+
await db.delete(schema.mcpServers);
3041
});
3142

3243
afterEach(async () => {
@@ -100,11 +111,11 @@ describe('encryption migration', () => {
100111
slug: 'test-provider',
101112
apiBaseUrl: '"https://api.example.com"',
102113
apiKey: 'provider-api-key-123',
103-
enabled: 1,
104-
disableCooldown: 0,
105-
estimateTokens: 0,
106-
useClaudeMasking: 0,
107-
quotaCheckerEnabled: 1,
114+
enabled: toDbBoolean(true, getCurrentDialect()),
115+
disableCooldown: toDbBoolean(false, getCurrentDialect()),
116+
estimateTokens: toDbBoolean(false, getCurrentDialect()),
117+
useClaudeMasking: toDbBoolean(false, getCurrentDialect()),
118+
quotaCheckerEnabled: toDbBoolean(true, getCurrentDialect()),
108119
quotaCheckerInterval: 30,
109120
createdAt: ts,
110121
updatedAt: ts,
@@ -130,11 +141,11 @@ describe('encryption migration', () => {
130141
headers,
131142
extraBody,
132143
quotaCheckerOptions: quotaOpts,
133-
enabled: 1,
134-
disableCooldown: 0,
135-
estimateTokens: 0,
136-
useClaudeMasking: 0,
137-
quotaCheckerEnabled: 1,
144+
enabled: toDbBoolean(true, getCurrentDialect()),
145+
disableCooldown: toDbBoolean(false, getCurrentDialect()),
146+
estimateTokens: toDbBoolean(false, getCurrentDialect()),
147+
useClaudeMasking: toDbBoolean(false, getCurrentDialect()),
148+
quotaCheckerEnabled: toDbBoolean(true, getCurrentDialect()),
138149
quotaCheckerInterval: 30,
139150
createdAt: ts,
140151
updatedAt: ts,
@@ -164,7 +175,7 @@ describe('encryption migration', () => {
164175
await db.insert(schema.mcpServers).values({
165176
name: 'test-mcp',
166177
upstreamUrl: 'https://mcp.example.com/mcp',
167-
enabled: 1,
178+
enabled: toDbBoolean(true, getCurrentDialect()),
168179
headers,
169180
createdAt: ts,
170181
updatedAt: ts,

packages/backend/src/db/client.ts

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import { Database } from 'bun:sqlite';
22
import { drizzle } from 'drizzle-orm/bun-sqlite';
33
import { drizzle as drizzlePg } from 'drizzle-orm/postgres-js';
44
import postgres from 'postgres';
5-
import { getDatabaseConfig } from '../config';
65
import { getCurrentLogLevel, logger } from '../utils/logger';
76
import path from 'node:path';
87
import fs from 'node:fs';
98

109
type SupportedDialect = 'sqlite' | 'postgres';
10+
type PostgresDriver = 'postgres-js' | 'pglite';
1111

12-
let dbInstance: ReturnType<typeof drizzle> | ReturnType<typeof drizzlePg> | null = null;
12+
type SqliteDb = ReturnType<typeof drizzle>;
13+
type PostgresJsDb = ReturnType<typeof drizzlePg>;
14+
type PgliteDb = any;
15+
16+
let dbInstance: SqliteDb | PostgresJsDb | PgliteDb | null = null;
1317
let sqlClient: postgres.Sql | null = null;
18+
let pgliteClient: any = null;
1419
let currentDialect: SupportedDialect | null = null;
1520
let currentSchema: any = null;
1621

@@ -45,6 +50,10 @@ function resolvePath(relPath: string): string {
4550
return path.join(projectRoot, relPath);
4651
}
4752

53+
function getPostgresDriver(): PostgresDriver {
54+
return process.env.PLEXUS_POSTGRES_DRIVER === 'pglite' ? 'pglite' : 'postgres-js';
55+
}
56+
4857
export function initializeDatabase(connectionString?: string) {
4958
if (dbInstance) {
5059
logger.silly('Database already initialized, skipping');
@@ -124,19 +133,7 @@ export function initializeDatabase(connectionString?: string) {
124133
logger: createDrizzleLogger(),
125134
});
126135
} else {
127-
sqlClient = postgres(connStr, {
128-
ssl: false,
129-
max: 10,
130-
idle_timeout: 20,
131-
connect_timeout: 10,
132-
onnotice: () => {},
133-
});
134-
135-
// Set statement timeout to prevent long-running queries from blocking
136-
sqlClient`SET statement_timeout = '30s'`.catch((err) => {
137-
logger.silly(`Failed to set statement_timeout: ${err}`);
138-
});
139-
136+
const postgresDriver = getPostgresDriver();
140137
const pgSchema = require('../../drizzle/schema/postgres/index');
141138
const {
142139
requestUsage,
@@ -157,26 +154,53 @@ export function initializeDatabase(connectionString?: string) {
157154
} = pgSchema;
158155

159156
currentSchema = pgSchema;
160-
dbInstance = drizzlePg(sqlClient, {
161-
schema: {
162-
requestUsage,
163-
providerCooldowns,
164-
debugLogs,
165-
inferenceErrors,
166-
providerPerformance,
167-
quotaState,
168-
providers: providersTable,
169-
providerModels,
170-
modelAliases,
171-
modelAliasTargets,
172-
apiKeys,
173-
userQuotaDefinitions,
174-
mcpServers,
175-
systemSettings,
176-
oauthCredentials,
177-
},
178-
logger: createDrizzleLogger(),
179-
});
157+
158+
const schema = {
159+
requestUsage,
160+
providerCooldowns,
161+
debugLogs,
162+
inferenceErrors,
163+
providerPerformance,
164+
quotaState,
165+
providers: providersTable,
166+
providerModels,
167+
modelAliases,
168+
modelAliasTargets,
169+
apiKeys,
170+
userQuotaDefinitions,
171+
mcpServers,
172+
systemSettings,
173+
oauthCredentials,
174+
};
175+
176+
if (postgresDriver === 'pglite') {
177+
const { PGlite } = require('@electric-sql/pglite');
178+
const { drizzle: drizzlePglite } = require('drizzle-orm/pglite');
179+
const dataDir = process.env.PLEXUS_PGLITE_DATA_DIR;
180+
pgliteClient = dataDir ? new PGlite(dataDir) : new PGlite();
181+
dbInstance = drizzlePglite(pgliteClient, {
182+
schema,
183+
logger: createDrizzleLogger(),
184+
});
185+
} else {
186+
sqlClient = postgres(connStr, {
187+
ssl: false,
188+
max: 10,
189+
idle_timeout: 20,
190+
connect_timeout: 10,
191+
onnotice: () => {},
192+
});
193+
194+
// Set statement timeout to prevent long-running queries from blocking
195+
sqlClient`SET statement_timeout = '30s'`.catch((err) => {
196+
logger.silly(`Failed to set statement_timeout: ${err}`);
197+
});
198+
199+
dbInstance = drizzlePg(sqlClient, {
200+
schema,
201+
logger: createDrizzleLogger(),
202+
});
203+
}
180204
}
181205

182206
return dbInstance;
@@ -186,7 +210,7 @@ export function getDatabase() {
186210
if (!dbInstance) {
187211
initializeDatabase();
188212
}
189-
return dbInstance as ReturnType<typeof drizzle>;
213+
return dbInstance as SqliteDb | PostgresJsDb | PgliteDb;
190214
}
191215

192216
export function getSchema() {
@@ -208,5 +232,9 @@ export async function closeDatabase() {
208232
await sqlClient.end();
209233
sqlClient = null;
210234
}
235+
if (pgliteClient) {
236+
await pgliteClient.close();
237+
pgliteClient = null;
238+
}
211239
dbInstance = null;
212240
}

packages/backend/src/routes/management/__tests__/usage-summary.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('Usage summary route', () => {
1212

1313
beforeEach(async () => {
1414
await closeDatabase();
15-
process.env.DATABASE_URL = 'sqlite://:memory:';
15+
process.env.DATABASE_URL = process.env.PLEXUS_TEST_DB_URL ?? process.env.DATABASE_URL;
1616
initializeDatabase(process.env.DATABASE_URL);
1717
await runMigrations();
1818

packages/backend/src/services/__tests__/usage-storage-performance.test.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { sql, eq, and } from 'drizzle-orm';
23
import { registerSpy } from '../../../test/test-utils';
34
import { UsageStorageService } from '../usage-storage';
45
import { closeDatabase, getDatabase, getSchema, initializeDatabase } from '../../db/client';
@@ -49,10 +50,15 @@ const createUsageRecord = (
4950
describe('UsageStorageService performance metrics', () => {
5051
beforeEach(async () => {
5152
await closeDatabase();
52-
process.env.DATABASE_URL = 'sqlite://:memory:';
53+
process.env.DATABASE_URL = process.env.PLEXUS_TEST_DB_URL ?? process.env.DATABASE_URL;
5354
delete process.env.PLEXUS_PROVIDER_PERFORMANCE_RETENTION_LIMIT;
5455
initializeDatabase(process.env.DATABASE_URL);
5556
await runMigrations();
57+
58+
const db = getDatabase() as any;
59+
const schema = getSchema() as any;
60+
await db.delete(schema.providerPerformance);
61+
await db.delete(schema.requestUsage);
5662
});
5763

5864
afterEach(async () => {
@@ -86,12 +92,16 @@ describe('UsageStorageService performance metrics', () => {
8692
);
8793
}
8894

89-
const rows = storage
95+
const schema = getSchema() as any;
96+
const rows = await storage
9097
.getDb()
91-
.$client.query(
92-
'SELECT provider, model, COUNT(*) as count FROM provider_performance GROUP BY provider, model'
93-
)
94-
.all() as Array<{ provider: string; model: string; count: number }>;
98+
.select({
99+
provider: schema.providerPerformance.provider,
100+
model: schema.providerPerformance.model,
101+
count: sql<number>`COUNT(*)`,
102+
})
103+
.from(schema.providerPerformance)
104+
.groupBy(schema.providerPerformance.provider, schema.providerPerformance.model);
95105

96106
const a = rows.find((r) => r.provider === 'provider-a' && r.model === 'model-1');
97107
const b = rows.find((r) => r.provider === 'provider-b' && r.model === 'model-2');
@@ -230,12 +240,22 @@ describe('UsageStorageService performance metrics', () => {
230240
);
231241
}
232242

233-
const rows = storage
243+
const schema = getSchema() as any;
244+
const rows = await storage
234245
.getDb()
235-
.$client.query(
236-
'SELECT provider, model, COUNT(*) as count FROM provider_performance WHERE provider = ? AND model = ? GROUP BY provider, model'
246+
.select({
247+
provider: schema.providerPerformance.provider,
248+
model: schema.providerPerformance.model,
249+
count: sql<number>`COUNT(*)`,
250+
})
251+
.from(schema.providerPerformance)
252+
.where(
253+
and(
254+
eq(schema.providerPerformance.provider, 'provider-c'),
255+
eq(schema.providerPerformance.model, 'model-3')
256+
)
237257
)
238-
.all('provider-c', 'model-3') as Array<{ provider: string; model: string; count: number }>;
258+
.groupBy(schema.providerPerformance.provider, schema.providerPerformance.model);
239259

240260
expect(rows[0]?.count).toBe(5);
241261
});

0 commit comments

Comments
 (0)