UNPKG

@reldens/storage

Version:
886 lines (714 loc) 28.9 kB
# Test Suite Architecture - @reldens/storage ## Critical Patterns and Why They Exist ### Why Dynamic Model Creation Instead of Generated Models? **Problem:** Generated models in `.test-entities/` use `const { ObjectionJsRawModel } = require('@reldens/storage')` which loads the package index.js, which requires PrismaDataServer, which requires `@prisma/client` at top-level, which doesn't exist yet. **Solution:** Create dynamic models in-memory without requiring the package: ```javascript // ❌ WRONG - Would require @reldens/storage → loads Prisma → fails const { TestCategoriesModel } = require('.test-entities/...'); // ✅ CORRECT - Create models dynamically without package dependency const { Model } = require('objection'); class DynamicModel extends Model { static get tableName(){ return tableName; } } ``` ### Prisma Client Loading Pattern **Problem:** Cannot use `require('@prisma/client')` at top-level because the client doesn't exist until generated by tests. **Solution:** Generate client first, then load from specific path using PrismaClientLoader: ```javascript // 1. Generate Prisma client (subprocess) await generatePrismaSchema(config); await generatePrismaClient(); // 2. Load client from generated path (NOT '@prisma/client') const { PrismaClientLoader } = require('../../lib/prisma/prisma-client-loader'); let prismaClient = PrismaClientLoader.load(process.cwd(), null, config); // 3. Pass loaded client to DataServer serverConfig.prismaClient = prismaClient; let dataServer = new PrismaDataServer(serverConfig); ``` **This pattern is used in:** - `reldens-cms` installer (`bin/reldens-cms.js:87-108`) - `reldens` main application - Test suite setup **Key Points:** - Always load Prisma client from explicit path, never default `@prisma/client` - Generate client via subprocess before requiring PrismaDataServer - Use `PrismaClientLoader.load()` to handle path resolution and connection config - Pass client instance to DataServer via `prismaClient` prop --- ## Critical Test Execution Flow ### **THE GOLDEN RULE: Connect Once, Test Many** **CRITICAL:** Database connections, table creation, and entity generation happen **ONCE per driver**, NOT per test. ``` Per Driver Suite: 1. Connect to database ← ONCE in before() hook 2. Create tables via SQL ← ONCE in before() hook 3. Generate entities ← ONCE in before() hook 4. Run all tests ← Many times 5. Cleanup and disconnect ← ONCE in after() hook Per Test: 1. Delete data from tables ← ONLY this in beforeEach() hook 2. Run test ← Test execution ``` --- ## Integration Test Lifecycle Hooks ### ✅ CORRECT Pattern (Current Implementation) ```javascript describe('Driver: '+driverName, () => { let dataServer; let categoriesRepo; let productsRepo; let reviewsRepo; let schemaPath = FileHandler.joinPaths(__dirname, '..', 'fixtures', 'sql', 'test-schema.sql'); // RUNS ONCE - Setup everything before(async function(){ this.timeout(30000); let rawEntities = {}; // Step 1: Connect to database dataServer = await TestHelpers.setupDriver(driverName, rawEntities); if(!dataServer){ throw new Error('Failed to setup driver: '+driverName); } // Step 2: Create tables via raw SQL let schemaSql = FileHandler.readFile(schemaPath); await TestHelpers.executeRawSQL(dataServer, schemaSql); // Step 3: Generate entities (introspects database, creates models) let entitiesGenerated = await TestHelpers.generateTestEntities(dataServer, driverName); if(!entitiesGenerated){ throw new Error('Failed to generate test entities for '+driverName); } // Step 4: Get repository references categoriesRepo = dataServer.getEntity('testCategories'); productsRepo = dataServer.getEntity('testProducts'); reviewsRepo = dataServer.getEntity('testReviews'); }); // RUNS BEFORE EACH TEST - Only delete data beforeEach(async () => { await TestHelpers.cleanDatabase(dataServer); }); // RUNS ONCE - Final cleanup after(async () => { if(dataServer){ await TestHelpers.dropTestTables(dataServer); await TestHelpers.teardownDriver(dataServer); } }); describe('CREATE Operations', () => { it('should create single record', async () => { // Test code here }); }); }); ``` ### ❌ WRONG Pattern (What Was Causing Issues) ```javascript describe('Driver: '+driverName, () => { // ❌ WRONG - This reconnects to database before EVERY test beforeEach(async () => { dataServer = await TestHelpers.setupDriver(driverName, rawEntities); let schemaSql = FileHandler.readFile(schemaPath); await TestHelpers.executeRawSQL(dataServer, schemaSql); await TestHelpers.generateTestEntities(dataServer, driverName); categoriesRepo = dataServer.getEntity('testCategories'); }); // ❌ WRONG - This disconnects after EVERY test afterEach(async () => { await TestHelpers.teardownDriver(dataServer); }); it('should create single record', async () => { // This single test just did: // 1. Connect to database // 2. Drop all tables // 3. Create all tables // 4. Introspect database schema // 5. Generate entity models // 6. (For Prisma: Generate schema file + Generate client + Reconnect) // 7. Run the actual test // 8. Disconnect // Result: Tests take FOREVER!!! }); }); ``` **Why this is catastrophically wrong:** - Creates/drops tables hundreds of times - Reconnects to database hundreds of times - For Prisma: Generates schema + client + reconnects hundreds of times - Makes tests 100x slower - Creates terrible indentation in test output - Pollutes logs with connection messages --- ## DataServer Architecture ### Flow: Connection → Tables → Entity Generation → Repositories All three drivers follow this pattern: ``` 1. new DataServer({config, rawEntities}) ↓ 2. await dataServer.connect() - Establishes database connection - Sets this.initialized = Date.now() ↓ 3. await executeRawSQL(dataServer, schemaSql) - Creates tables in database - Tables must exist before next step! ↓ 4. await dataServer.generateEntities() - Introspects database schema - Creates driver instances (repositories) - Registers entities in EntityManager ↓ 5. dataServer.getEntity('entityName') - Returns repository with all 31 BaseDriver methods ``` ### ObjectionJS DataServer **File:** `lib/objection-js/objection-js-data-server.js` ```javascript // Step 1: Connect (lines 26-48) async connect() { // Creates Knex instance this.knex = Knex({ client: this.client, connection: this.config }); // Binds Knex to Objection Model globally Model.knex(this.knex); this.initialized = Date.now(); } // Step 2: Generate entities (lines 59-76) generateEntities() { for(let i of Object.keys(this.rawEntities)){ let rawEntity = this.rawEntities[i]; // Creates ObjectionJsDriver for each entity this.entities[i] = new ObjectionJsDriver({ rawModel: rawEntity, id: i, name: i, config: this.config, knex: this.knex }); } this.entityManager.setEntities(this.entities); } // Step 3: Fetch database schema (lines 89-95) async fetchEntitiesFromDatabase() { return await MySQLTablesProvider.fetchTables(this); } ``` **Key Points:** - ✅ No pre-generation required - ✅ Models can be plain classes extending Objection Model - ✅ Relation mappings defined in static relationMappings getter - ✅ Fast and reliable ### MikroORM DataServer **File:** `lib/mikro-orm/mikro-orm-data-server.js` ```javascript // Step 1: Connect (lines 31-55) async connect() { this.orm = await MikroORM.init({ driver: this.clientsMapped[(this.client || 'mongodb')], dbName: this.config.database, clientUrl: this.connectString, entities: Object.values(this.rawEntities), allowGlobalContext: true }); this.initialized = Date.now(); } // Step 2: Generate entities (lines 66-90) generateEntities() { for(let i of Object.keys(this.rawEntities)){ let rawEntity = this.rawEntities[i]; // Creates MikroOrmDriver for each entity this.entities[i] = new MikroOrmDriver({ rawModel: rawEntity, id: i, name: i, config: this.orm.driver.connection.options, orm: this.orm }); } this.entityManager.setEntities(this.entities); } // Step 3: Fetch database schema (lines 111-137) async fetchEntitiesFromDatabase() { if('mysql' === this.client){ return await MySQLTablesProvider.fetchTables(this); } if('mongodb' === this.client){ // MongoDB specific introspection let collections = await this.orm.em.getDriver() .getConnection().db().listCollections().toArray(); // ... returns collection metadata } } ``` **Key Points:** - ✅ No pre-generation required - ✅ Supports MySQL and MongoDB - ✅ Entity metadata via decorators - ✅ Dynamic entity discovery ### Prisma DataServer **File:** `lib/prisma/prisma-data-server.js` ```javascript // Step 1: Connect (lines 25-50) async connect() { // Checks if PrismaClient exists if(!this.prisma && this.defaultClientIsGenerated()){ this.prisma = new PrismaClient({ datasources: {db: {url: this.connectString}} }); } if(!this.prisma){ Logger.error('Prisma client could not be initialized.'); return false; } await this.prisma.$connect(); this.initialized = Date.now(); } // Step 2: Generate entities (lines 66-99) generateEntities() { this.collectAllRelationMappings(); for(let i of Object.keys(this.rawEntities)){ let rawEntity = this.rawEntities[i]; let tableName = rawEntity.tableName; // REQUIRES: Prisma model must exist in PrismaClient let prismaModel = this.prisma[tableName]; if(!prismaModel){ Logger.critical('No matching Prisma model found for "'+tableName+'".'); continue; } // Creates PrismaDriver for each entity this.entities[i] = new PrismaDriver({ rawModel: rawEntity, id: i, name: i, config: this.config, prisma: this.prisma, model: prismaModel, allRelationMappings: this.allRelationMappings }); } this.entityManager.setEntities(this.entities); } // Step 3: Fetch database schema (lines 240-252) async fetchEntitiesFromDatabase() { return await MySQLTablesProvider.fetchTables(this); } ``` **Key Points:** - ❌ REQUIRES pre-generation: PrismaClient must exist BEFORE connect() - ❌ Schema file must exist: `prisma/schema.prisma` - ❌ Client must be generated: `npx prisma generate` - ⚠️ Requires subprocess execution in tests --- ## Test Helpers Entity Generation ### Critical: Tables Must Exist First! **File:** `tests/utils/test-helpers.js` ```javascript static async generateTestEntities(dataServer, driverName) { // Prisma only: generate schema + client if not already present if('prisma' === driverName){ let config = this.getTestDbConfig(); config.client = 'mysql'; let schemaPath = FileHandler.joinPaths(process.cwd(), 'prisma', 'schema.prisma'); if(!FileHandler.exists(schemaPath)){ if(!await this.generatePrismaSchema(config)){ throw new Error('Failed to generate Prisma schema'); } if(!await this.generatePrismaClient()){ throw new Error('Failed to generate Prisma client'); } await dataServer.disconnect(); delete require.cache[require.resolve(FileHandler.joinPaths(process.cwd(), 'prisma', 'client'))]; await dataServer.connect(); } } // Run EntitiesGenerator — introspects DB, writes entity + model files to generated-entities/ await this.runEntitiesGenerator(dataServer, driverName); this.fixGeneratedRequirePaths(); this.compareGeneratedWithExpected(driverName, 'entities'); this.compareGeneratedWithExpected(driverName, 'models/'+driverName); if('objection-js' === driverName){ this.compareGeneratedWithExpected(driverName, 'entities-config.js'); this.compareGeneratedWithExpected(driverName, 'entities-translations.js'); } // Load generated registered-models file and populate entity manager await this.loadGeneratedEntities(dataServer, driverName); return true; } ``` ### Entity Generation Flow `runEntitiesGenerator()` creates an `EntitiesGenerator` instance pointed at the connected `dataServer`, calls `generator.generate()`, which internally calls `dataServer.fetchEntitiesFromDatabase()` to read real table metadata from `information_schema`. This is why tables must exist before calling `generateTestEntities()` — without tables, introspection returns empty and generation fails. After generation, `loadGeneratedEntities()` loads the `generated-entities/models/[driver]/registered-models-[driver].js` file, sets `dataServer.rawEntities`, and calls `dataServer.generateEntities()` to register all driver instances in the entity manager. **Why tables must exist first:** - `fetchEntitiesFromDatabase()` queries MySQL `information_schema` - Returns actual table structures with columns, types, foreign keys - Without tables, this returns empty/null - Entity generation fails without table metadata --- ## Test Database Operations ### cleanDatabase() - DELETE Data Only **File:** `tests/utils/test-helpers.js` (lines 183-198) ```javascript static async cleanDatabase(dataServer) { try { // Disable foreign key checks await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;'); // DELETE data (fast, keeps tables intact) await dataServer.rawQuery('DELETE FROM test_reviews;'); await dataServer.rawQuery('DELETE FROM test_products;'); await dataServer.rawQuery('DELETE FROM test_categories;'); // Re-enable foreign key checks await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); return true; } catch(error) { try { await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); } catch(e) {} return false; } } ``` **Used in:** `beforeEach()` hook - runs before every test **Purpose:** Clean slate for each test without recreating tables **Speed:** Very fast (milliseconds) ### dropTestTables() - DROP Tables **File:** `tests/utils/test-helpers.js` (lines 200-215) ```javascript static async dropTestTables(dataServer) { try { await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;'); // DROP tables completely await dataServer.rawQuery('DROP TABLE IF EXISTS test_reviews;'); await dataServer.rawQuery('DROP TABLE IF EXISTS test_products;'); await dataServer.rawQuery('DROP TABLE IF EXISTS test_categories;'); await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); return true; } catch(error) { try { await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); } catch(e) {} return false; } } ``` **Used in:** `after()` hook - runs once after all tests **Purpose:** Complete cleanup, remove tables from database **Speed:** Fast but slower than DELETE ### executeRawSQL() - Create Tables **File:** `tests/utils/test-helpers.js` (lines 140-159) ```javascript static async executeRawSQL(dataServer, sql) { try { await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;'); // Split SQL into statements let statements = sql.split(';').filter(stmt => stmt.trim().length > 0); // Execute each statement for(let i = 0; i < statements.length; i++){ let statement = statements[i].trim(); if(!statement){ continue; } await dataServer.rawQuery(statement+';'); } await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); return true; } catch(error) { try { await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); } catch(e) {} throw error; } } ``` **Used in:** `before()` hook - runs once before all tests **Purpose:** Create all tables from SQL schema file **Speed:** Slow (can take seconds), only runs once --- ## Prisma Special Handling ### Why Prisma is Different **All other ORMs:** - Models are code (classes/objects) - Can be defined at runtime - Database can change independently **Prisma:** - Models are code GENERATED from schema file - Schema defines data model (not code) - PrismaClient is generated code (not dynamic) - Changes require regeneration ### Prisma Test Flow ``` 1. Connect to database ↓ 2. Create tables via SQL ↓ 3. Introspect database schema ↓ 4. Generate schema.prisma file ← SUBPROCESS (npx reldens-storage-prisma) ↓ 5. Generate PrismaClient ← SUBPROCESS (npx prisma generate) ↓ 6. Disconnect old client ↓ 7. Clear require cache ← delete require.cache[...] ↓ 8. Reconnect with new client ↓ 9. Generate entities (now PrismaClient has models) ↓ 10. Get repositories ``` ### Prisma Generation Commands **Generate Schema from Database:** ```bash npx reldens-storage-prisma \ --host=localhost \ --port=3306 \ --user=test_user \ --password=test_password \ --database=reldens_storage_test \ --client=mysql ``` **Generate Prisma Client:** ```bash npx prisma generate --schema=prisma/schema.prisma ``` **In Tests (lines 306-327):** ```javascript static async generatePrismaSchema(config){ let cmd = 'npx reldens-storage-prisma' +' --host='+config.host +' --port='+config.port +' --user='+config.user +' --password='+config.password +' --database='+config.database +' --client='+config.client; try { let { stdout, stderr } = await execAsync(cmd); return true; } catch(error) { Logger.critical('Failed to generate Prisma schema: '+error.message); return false; } } static async generatePrismaClient(){ let cmd = 'npx prisma generate --schema='+schemaPath; try { let { stdout, stderr } = await execAsync(cmd); return true; } catch(error) { Logger.critical('Failed to generate Prisma client: '+error.message); return false; } } ``` --- ## Test Execution Order ### Per Driver Test Suite **1. Initialization — runs once per driver (`DriverRegistry.initialize()`):** - `setupDriver()` — connect to database - `executeRawSQL()` — create tables from SQL schema - `generateTestEntities()` — introspect DB, run EntitiesGenerator, load entities - `dataServer.getEntity()` — obtain repository references **2. Test execution — per group method in each test class:** Each test class (DriversTest, NestedFiltersTest, RelationsTest, RawQueriesTest) has group methods. Each group method begins with `await TestHelpers.cleanDatabase(this.dataServer)` to DELETE all rows, then runs its tests via `runner.test()`. Example: ```javascript async testCreateOperations() { this.runner.group('CREATE Operations'); await TestHelpers.cleanDatabase(this.dataServer); // insert fixture data, then call runner.test() for each assertion } ``` **3. Teardown — runs once per driver (`DriverRegistry.cleanup()`):** - `dropTestTables()` — DROP all test tables - `teardownDriver()` — disconnect from database ### All Drivers Execution **Driver: objection-js** - Initialize: connect, create tables, generate entities - Run all tests (cleanDatabase at start of each group) - Teardown: drop tables, disconnect **Driver: mikro-orm** - Initialize: connect, create tables, generate entities - Run all tests (cleanDatabase at start of each group) - Teardown: drop tables, disconnect **Driver: prisma** - Initialize: connect, create tables, generate Prisma schema (subprocess), generate Prisma client (subprocess), reconnect, generate entities - Run all tests (cleanDatabase at start of each group) - Teardown: drop tables, disconnect **Total connections:** 3 (one per driver) **Total table creations:** 3 (one per driver) **Total entity generations:** 3 (one per driver) --- ## Common Issues and Solutions ### Issue 1: Tests Taking Forever **Symptoms:** - Each test takes 1000+ milliseconds - Connection logs appear between tests - Test output has bad indentation **Cause:** - Using `beforeEach()` for setup instead of `before()` - Reconnecting to database before every test - Recreating tables before every test - Regenerating entities before every test **Solution:** - Move connection/tables/entities to `before()` hook - Use `beforeEach()` only for data cleanup (DELETE) - Use `after()` for final teardown (DROP tables, disconnect) ### Issue 2: Tests "Cancelled" Instead of Failed **Symptoms:** - Test output shows: `ℹ tests 147, ℹ suites 0, ℹ pass 0, ℹ fail 0, ℹ cancelled 147` - No error messages shown **Cause:** - `before()` hook fails silently - Helper methods return `false` instead of throwing errors - Node.js test runner marks all tests as "cancelled" when `before()` fails **Solution:** - Make all helper methods throw errors instead of returning false - Add explicit error checking: `if(!result){ throw new Error(...) }` - Use try/catch in `before()` hook to log errors before re-throwing ### Issue 3: Tables Not Found During Entity Generation **Symptoms:** - `fetchEntitiesFromDatabase()` returns empty/null - `EntitiesGenerator.generate()` fails or produces no entities - Entity generation fails with "No entities generated" **Cause:** - Calling `generateTestEntities()` BEFORE creating tables - Tables must exist before introspection **Solution:** - Ensure execution order: connect → create tables → generate entities - Never call `generateTestEntities()` before `executeRawSQL()` ### Issue 4: Prisma Client Not Found **Symptoms:** - `connect()` fails with "Prisma client could not be initialized" - Error: "Cannot find module '@prisma/client'" **Cause:** - PrismaClient not generated - Schema file missing - Generated client cleared but not regenerated **Solution:** - Run `npx reldens-storage-prisma` to generate schema - Run `npx prisma generate` to generate client - Ensure `generateTestEntities()` handles full Prisma flow - Clear cache and reconnect after generation ### Issue 5: Foreign Key Constraint Violations **Symptoms:** - DELETE fails with "Cannot delete or update a parent row" - DROP TABLE fails with foreign key references **Cause:** - MySQL enforces foreign key constraints - Can't delete parent record with child records - Can't drop table referenced by other tables **Solution:** - Always wrap DELETE/DROP operations: ```javascript await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=0;'); // ... DELETE or DROP operations await dataServer.rawQuery('SET FOREIGN_KEY_CHECKS=1;'); ``` - Delete in correct order (children before parents) - Drop in correct order (children before parents) --- ## Summary: The Complete Flow **For each driver (objection-js, mikro-orm, prisma):** **Initialization — once per driver:** 1. Connect to database - ObjectionJS: create Knex instance - MikroORM: initialize MikroORM - Prisma: create PrismaClient (if already generated) 2. Create tables from SQL file - SET FOREIGN_KEY_CHECKS=0 - DROP TABLE IF EXISTS (cleanup from previous run) - CREATE TABLE (with all constraints) - SET FOREIGN_KEY_CHECKS=1 3. Generate entities from database - Run EntitiesGenerator (introspects DB, writes files to generated-entities/) - Prisma only: generate schema.prisma (subprocess), generate PrismaClient (subprocess), reconnect - Load registered-models file, call dataServer.generateEntities() **Per test group — cleanDatabase() at start of each group method:** - SET FOREIGN_KEY_CHECKS=0 - DELETE FROM test_reviews - DELETE FROM test_products - DELETE FROM test_categories - SET FOREIGN_KEY_CHECKS=1 **Per test:** create test data, perform operations, assert results. **Teardown — once per driver:** 1. Drop all tables - SET FOREIGN_KEY_CHECKS=0 - DROP TABLE IF EXISTS test_reviews - DROP TABLE IF EXISTS test_products - DROP TABLE IF EXISTS test_categories - SET FOREIGN_KEY_CHECKS=1 2. Disconnect from database - ObjectionJS: `await knex.destroy()` - MikroORM: `await orm.close()` - Prisma: `await prisma.$disconnect()` --- ## Files Modified to Fix Issues ### Integration Test Files (All Fixed) **test-drivers.js:** - ✅ Changed `beforeEach``before` for setup - ✅ Added `beforeEach` for data cleanup only - ✅ Changed `afterEach``after` for teardown - ✅ Added error checking for entity generation **test-nested-filters.js:** - ✅ Changed `beforeEach``before` for setup - ✅ Added `beforeEach` for data cleanup only - ✅ Changed `afterEach``after` for teardown - ✅ Added error checking for entity generation **test-relations.js:** - ✅ Changed `beforeEach``before` for setup - ✅ Added `beforeEach` for data cleanup only - ✅ Changed `afterEach``after` for teardown - ✅ Added error checking for entity generation **test-raw-queries.js:** - ✅ Added — covers rawQuery with single and multiple SQL statements ### Test Helpers (All Fixed) **test-helpers.js:** - ✅ Removed ALL verbose logging (process.stderr.write statements) - ✅ Changed `cleanDatabase()` to use DELETE instead of DROP - ✅ Created `dropTestTables()` for final cleanup - ✅ Changed `generateTestEntities()` to throw errors instead of returning false - ✅ Replaced `createModelsFromDatabase()` with `runEntitiesGenerator()` + `loadGeneratedEntities()` - ✅ Removed logging from `setupDriver()` - ✅ Removed logging from `executeRawSQL()` **test-runner.js:** - ✅ Added Logger import: `const { Logger } = require('@reldens/utils');` - ✅ Replaced `process.stderr.write()` with Logger methods (lines 24, 30, 41, 45, 46) - ✅ Uses `Logger.info()` for test progress (suite, group, passed tests) - ✅ Uses `Logger.error()` for test failures and error messages - ✅ Follows Rule #29 from ai-coding-rules.md (no direct console/stderr output) --- ## Performance Comparison ### Before Fixes (Wrong Pattern) Each test reconnected, recreated tables, and regenerated entities independently: - Connect: ~500ms - Create tables: ~1000ms - Generate entities: ~500ms - Run test: ~10ms - Teardown: ~100ms - **Total per test: ~2110ms** - Total for 100 tests, 3 drivers: ~10+ minutes ### After Fixes (Correct Pattern) Setup and teardown happen once per driver. Only data cleanup runs between tests: - Setup once: connect + create tables + generate entities = ~2 seconds - Each test: DELETE data (~5ms) + run test (~10ms) = ~15ms - Teardown once: drop tables + disconnect = ~200ms - **Total for 100 tests, 1 driver: ~3.7 seconds** - Total for 3 drivers: ~10-15 seconds (includes Prisma subprocess generation) **Speed improvement: ~60-90x faster** --- ## Conclusion **The test suite architecture is now correct:** - ✅ Connects once per driver - ✅ Creates tables once per driver - ✅ Generates entities once per driver - ✅ Cleans data between tests (fast DELETE) - ✅ Disconnects once per driver - ✅ Handles Prisma subprocess correctly - ✅ Fast execution (seconds instead of minutes) - ✅ Clean output (no connection spam) - ✅ Proper error handling (throws instead of silent fails) **DO NOT CHANGE THIS PATTERN** - It is the correct and only way to structure integration tests for this package.