@stratal/testing 0.0.27 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +570 -0
  2. package/README.md +137 -36
  3. package/dist/database/index.d.mts +2 -2
  4. package/dist/database/index.mjs +2 -2
  5. package/dist/database-Rojs69r3.mjs +453 -0
  6. package/dist/database-Rojs69r3.mjs.map +1 -0
  7. package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
  8. package/dist/feature-flags/index.mjs +1 -1
  9. package/dist/{feature-flags-BiLhfSGh.mjs → feature-flags-CZ1a4M2g.mjs} +2 -2
  10. package/dist/{feature-flags-BiLhfSGh.mjs.map → feature-flags-CZ1a4M2g.mjs.map} +1 -1
  11. package/dist/index-B8Mw1Dxk.d.mts +175 -0
  12. package/dist/index-B8Mw1Dxk.d.mts.map +1 -0
  13. package/dist/{index-CrHzUDKX.d.mts → index-D7FM6EKp.d.mts} +21 -3
  14. package/dist/index-D7FM6EKp.d.mts.map +1 -0
  15. package/dist/index-qgWNJRdC.d.mts.map +1 -1
  16. package/dist/index.d.mts +105 -46
  17. package/dist/index.d.mts.map +1 -1
  18. package/dist/index.mjs +218 -108
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/mocks/cloudflare-workers.d.mts +24 -0
  21. package/dist/mocks/cloudflare-workers.d.mts.map +1 -0
  22. package/dist/mocks/cloudflare-workers.mjs +28 -0
  23. package/dist/mocks/cloudflare-workers.mjs.map +1 -0
  24. package/dist/mocks/index.d.mts +2 -2
  25. package/dist/mocks/index.mjs +2 -2
  26. package/dist/mocks/noop-rate-limiter-store.d.mts +1 -3
  27. package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -1
  28. package/dist/mocks/zenstack-language.d.mts +26 -29
  29. package/dist/mocks/zenstack-language.d.mts.map +1 -1
  30. package/dist/mocks/zenstack-language.mjs +1 -1
  31. package/dist/mocks/zenstack-language.mjs.map +1 -1
  32. package/dist/storage/index.d.mts +1 -1
  33. package/dist/storage/index.mjs +1 -1
  34. package/dist/{storage-DhoxWqyF.mjs → storage-BiGamMA8.mjs} +73 -6
  35. package/dist/storage-BiGamMA8.mjs.map +1 -0
  36. package/dist/test-worker-exports-dCRARYEc.d.mts +144 -0
  37. package/dist/test-worker-exports-dCRARYEc.d.mts.map +1 -0
  38. package/dist/test-workers-cache-D02Pt_dE.mjs +183 -0
  39. package/dist/test-workers-cache-D02Pt_dE.mjs.map +1 -0
  40. package/dist/vitest-plugin/index.d.mts +14 -28
  41. package/dist/vitest-plugin/index.d.mts.map +1 -1
  42. package/dist/vitest-plugin/index.mjs +32 -20
  43. package/dist/vitest-plugin/index.mjs.map +1 -1
  44. package/package.json +30 -21
  45. package/dist/database-B02eYKhE.mjs +0 -334
  46. package/dist/database-B02eYKhE.mjs.map +0 -1
  47. package/dist/index-BIr5nLof.d.mts +0 -122
  48. package/dist/index-BIr5nLof.d.mts.map +0 -1
  49. package/dist/index-CrHzUDKX.d.mts.map +0 -1
  50. package/dist/storage-DhoxWqyF.mjs.map +0 -1
  51. package/dist/test-email-provider-B7cjj97-.d.mts +0 -21
  52. package/dist/test-email-provider-B7cjj97-.d.mts.map +0 -1
  53. package/dist/test-email-provider-Dr-nhE0x.mjs +0 -34
  54. package/dist/test-email-provider-Dr-nhE0x.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
- import { a as createDatabaseFromTemplate, c as deriveAdminConnectionString, d as dropDatabase, f as normalizeIsolation, i as buildConnectionString, l as deriveDbName, u as deriveTemplateName } from "./database-B02eYKhE.mjs";
2
- import { t as FakeStorageService } from "./storage-DhoxWqyF.mjs";
3
- import { t as __decorate } from "./decorate-B7nr7eBl.mjs";
1
+ import { i as BINDING_ENV_VAR, n as buildTruncateSql, p as leaseWorkerDatabase, t as buildTableDiscoverySql } from "./database-Rojs69r3.mjs";
2
+ import { t as FakeStorageService } from "./storage-BiGamMA8.mjs";
3
+ import { t as __decorate } from "./decorate-RQD1h28J.mjs";
4
4
  import { NoopRateLimiterStore } from "./mocks/noop-rate-limiter-store.mjs";
5
- import { t as TestEmailProvider } from "./test-email-provider-Dr-nhE0x.mjs";
6
- import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "./feature-flags-BiLhfSGh.mjs";
5
+ import { n as TestWorkerExports, r as TestEmailProvider, t as TestWorkersCache } from "./test-workers-cache-D02Pt_dE.mjs";
6
+ import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "./feature-flags-CZ1a4M2g.mjs";
7
7
  import { Application } from "stratal";
8
8
  import { LogLevel } from "stratal/logger";
9
+ import { markGatewayMode } from "stratal/response-cache";
9
10
  import { Module } from "stratal/module";
10
11
  import { EMAIL_TOKENS } from "stratal/email";
11
12
  import { RATE_LIMITER_TOKENS } from "stratal/rate-limiter";
@@ -117,6 +118,32 @@ var ProviderOverrideBuilder = class {
117
118
  }
118
119
  };
119
120
  //#endregion
121
+ //#region src/errors/test-error.ts
122
+ /**
123
+ * Base error class for all test framework errors.
124
+ * Extends from Error and allows easy identification via `instanceof`.
125
+ */
126
+ var TestError = class extends Error {
127
+ cause;
128
+ constructor(message, cause) {
129
+ super(message);
130
+ this.cause = cause;
131
+ this.name = "TestError";
132
+ Error.captureStackTrace(this, this.constructor);
133
+ }
134
+ };
135
+ //#endregion
136
+ //#region src/errors/setup-error.ts
137
+ /**
138
+ * Error thrown when test setup fails.
139
+ * Examples: schema creation failure, migration failure, application bootstrap failure.
140
+ */
141
+ var TestSetupError = class extends TestError {
142
+ constructor(message, cause) {
143
+ super(`Test setup failed: ${message}`, cause);
144
+ }
145
+ };
146
+ //#endregion
120
147
  //#region src/core/http/locale-helper.ts
121
148
  /**
122
149
  * Resolve the configured detection strategy from the testing module's DI container.
@@ -139,9 +166,7 @@ function applyLocaleToHeaders(headers, locale, strategy) {
139
166
  case "cookie":
140
167
  headers.set("Cookie", `locale=${locale}`);
141
168
  break;
142
- case "header":
143
- headers.set("Accept-Language", locale);
144
- break;
169
+ case "header": headers.set("Accept-Language", locale);
145
170
  }
146
171
  }
147
172
  /**
@@ -400,7 +425,8 @@ var TestResponse = class extends Macroable {
400
425
  * @param path - Dot-notation path (e.g., 'data.user.id')
401
426
  */
402
427
  async assertJsonPathExists(path) {
403
- expect(hasValueAtPath(await this.json(), path), `Expected JSON path "${path}" to exist`).toBe(true);
428
+ const exists = hasValueAtPath(await this.json(), path);
429
+ expect(exists, `Expected JSON path "${path}" to exist`).toBe(true);
404
430
  return this;
405
431
  }
406
432
  /**
@@ -409,7 +435,8 @@ var TestResponse = class extends Macroable {
409
435
  * @param path - Dot-notation path (e.g., 'data.user.deletedAt')
410
436
  */
411
437
  async assertJsonPathMissing(path) {
412
- expect(hasValueAtPath(await this.json(), path), `Expected JSON path "${path}" to not exist`).toBe(false);
438
+ const exists = hasValueAtPath(await this.json(), path);
439
+ expect(exists, `Expected JSON path "${path}" to not exist`).toBe(false);
413
440
  return this;
414
441
  }
415
442
  /**
@@ -750,19 +777,23 @@ var TestCommandResult = class {
750
777
  return this;
751
778
  }
752
779
  assertOutputContains(text) {
753
- expect(this.result.output.join("\n"), `Expected output to contain "${text}"`).toContain(text);
780
+ const joined = this.result.output.join("\n");
781
+ expect(joined, `Expected output to contain "${text}"`).toContain(text);
754
782
  return this;
755
783
  }
756
784
  assertOutputMissing(text) {
757
- expect(this.result.output.join("\n"), `Expected output NOT to contain "${text}"`).not.toContain(text);
785
+ const joined = this.result.output.join("\n");
786
+ expect(joined, `Expected output NOT to contain "${text}"`).not.toContain(text);
758
787
  return this;
759
788
  }
760
789
  assertErrorContains(text) {
761
- expect(this.result.errors.join("\n"), `Expected errors to contain "${text}"`).toContain(text);
790
+ const joined = this.result.errors.join("\n");
791
+ expect(joined, `Expected errors to contain "${text}"`).toContain(text);
762
792
  return this;
763
793
  }
764
794
  assertErrorMissing(text) {
765
- expect(this.result.errors.join("\n"), `Expected errors NOT to contain "${text}"`).not.toContain(text);
795
+ const joined = this.result.errors.join("\n");
796
+ expect(joined, `Expected errors NOT to contain "${text}"`).not.toContain(text);
766
797
  return this;
767
798
  }
768
799
  };
@@ -896,7 +927,8 @@ var TestSseConnection = class {
896
927
  * Assert that the next event matches the expected partial shape
897
928
  */
898
929
  async assertEvent(expected, timeout = 5e3) {
899
- expect(await this.waitForEvent(timeout)).toMatchObject(expected);
930
+ const event = await this.waitForEvent(timeout);
931
+ expect(event).toMatchObject(expected);
900
932
  }
901
933
  /**
902
934
  * Assert that the next event's data matches the expected string
@@ -910,7 +942,8 @@ var TestSseConnection = class {
910
942
  */
911
943
  async assertJsonEventData(expected, timeout = 5e3) {
912
944
  const event = await this.waitForEvent(timeout);
913
- expect(JSON.parse(event.data)).toEqual(expected);
945
+ const parsed = JSON.parse(event.data);
946
+ expect(parsed).toEqual(expected);
914
947
  }
915
948
  /**
916
949
  * Access the raw Response
@@ -1262,20 +1295,53 @@ var TestWsRequest = class {
1262
1295
  };
1263
1296
  //#endregion
1264
1297
  //#region src/core/testing-module.ts
1298
+ /**
1299
+ * TestingModule
1300
+ *
1301
+ * Provides access to the test application, container, HTTP client, and utilities.
1302
+ *
1303
+ * @example
1304
+ * ```typescript
1305
+ * const module = await Test.createTestingModule({
1306
+ * modules: [RegistrationModule],
1307
+ * }).compile()
1308
+ *
1309
+ * // Make HTTP requests
1310
+ * const response = await module.http
1311
+ * .post('/api/v1/register')
1312
+ * .withBody({ ... })
1313
+ * .send()
1314
+ *
1315
+ * // Access services
1316
+ * const service = module.get(REGISTRATION_TOKENS.RegistrationService)
1317
+ *
1318
+ * // Database utilities
1319
+ * await module.truncateDb()
1320
+ * await module.seed(UserSeeder)
1321
+ * await module.assertDatabaseHas('user', { email: 'test@example.com' })
1322
+ *
1323
+ * // Cleanup
1324
+ * await module.close()
1325
+ * ```
1326
+ */
1265
1327
  var TestingModule = class {
1266
1328
  app;
1267
1329
  env;
1268
1330
  ctx;
1269
- isolatedDatabase;
1270
1331
  testEmailProvider;
1332
+ pendingTasks;
1333
+ testWorkersCache;
1334
+ testWorkerExports;
1271
1335
  _http = null;
1272
1336
  _requestContainer;
1273
- constructor(app, env, ctx, isolatedDatabase = null, testEmailProvider = null) {
1337
+ constructor(app, env, ctx, testEmailProvider = null, pendingTasks = [], testWorkersCache = void 0, testWorkerExports = void 0) {
1274
1338
  this.app = app;
1275
1339
  this.env = env;
1276
1340
  this.ctx = ctx;
1277
- this.isolatedDatabase = isolatedDatabase;
1278
1341
  this.testEmailProvider = testEmailProvider;
1342
+ this.pendingTasks = pendingTasks;
1343
+ this.testWorkersCache = testWorkersCache;
1344
+ this.testWorkerExports = testWorkerExports;
1279
1345
  const mockContext = this.app.createMockRouterContext();
1280
1346
  this._requestContainer = this.app.container.createRequestScope(mockContext);
1281
1347
  }
@@ -1301,12 +1367,18 @@ var TestingModule = class {
1301
1367
  }
1302
1368
  /**
1303
1369
  * Get Inertia test client for making Inertia requests
1370
+ *
1371
+ * Sends no `X-Inertia-Version`. It used to send `'1'`, which meant any app configuring a real
1372
+ * asset version had every request from this client read as a stale client and answered with a
1373
+ * version-mismatch 409 — an entire Inertia suite failing on a value the tests never chose. The
1374
+ * check needs a version from BOTH sides, so omitting it here leaves the app under test in charge
1375
+ * rather than this header.
1376
+ *
1377
+ * A test that wants the mismatch path asks for it:
1378
+ * `module.inertia.withHeaders({ 'X-Inertia-Version': 'stale' })`.
1304
1379
  */
1305
1380
  get inertia() {
1306
- return this.http.withHeaders({
1307
- "X-Inertia": "true",
1308
- "X-Inertia-Version": "1"
1309
- });
1381
+ return this.http.withHeaders({ "X-Inertia": "true" });
1310
1382
  }
1311
1383
  /**
1312
1384
  * Get fake storage service for assertions
@@ -1322,6 +1394,36 @@ var TestingModule = class {
1322
1394
  return this.get(FEATURE_FLAG_SERVICE_TOKEN);
1323
1395
  }
1324
1396
  /**
1397
+ * The default `ctx.cache` stub — present unless this module was compiled
1398
+ * with `cache: false`. Assert `@PurgesCache` routes purged what they should
1399
+ * via `module.cache.purges`, in call order.
1400
+ *
1401
+ * @throws {TestSetupError} This module was compiled with `cache: false`,
1402
+ * so there is no stub to inspect.
1403
+ */
1404
+ get cache() {
1405
+ if (!this.testWorkersCache) throw new TestSetupError("module.cache is unavailable because this module was compiled with `cache: false`. Remove that option to use the default ctx.cache stub.");
1406
+ return this.testWorkersCache;
1407
+ }
1408
+ /**
1409
+ * The default `ctx.exports` stub — present unless this module was compiled
1410
+ * with `cache: false`.
1411
+ *
1412
+ * Assert which requests the response-cache gateway forwarded to the cached
1413
+ * entrypoint, and with which resolved partitions, via
1414
+ * `module.gateway.loopbacks` — in call order, mirroring
1415
+ * `module.cache.purges`. An empty `loopbacks` is the assertion for every
1416
+ * fail-closed case: an unresolved partition, a non-GET, or a `@Cacheable`
1417
+ * route with no `partitionBy` all run inline and never appear here.
1418
+ *
1419
+ * @throws {TestSetupError} This module was compiled with `cache: false`,
1420
+ * so there is no stub to inspect.
1421
+ */
1422
+ get gateway() {
1423
+ if (!this.testWorkerExports) throw new TestSetupError("module.gateway is unavailable because this module was compiled with `cache: false`. Remove that option to use the default ctx.exports stub.");
1424
+ return this.testWorkerExports;
1425
+ }
1426
+ /**
1325
1427
  * Create a WebSocket test request builder for the given path
1326
1428
  */
1327
1429
  ws(path) {
@@ -1355,7 +1457,20 @@ var TestingModule = class {
1355
1457
  * Execute an HTTP request through HonoApp
1356
1458
  */
1357
1459
  async fetch(request) {
1358
- return (await this.app.ensureHono()).fetch(request, this.env, this.ctx);
1460
+ const response = await (await this.app.ensureHono()).fetch(request, this.env, this.ctx);
1461
+ await this.flushDeferredWork();
1462
+ return response;
1463
+ }
1464
+ /**
1465
+ * Drain background work the most recent request(s) deferred via `ctx.waitUntil`, including any
1466
+ * follow-up tasks those tasks enqueue. Settles each (errors are surfaced by the work itself, not
1467
+ * here) so deferred side-effects don't outlive the request that triggered them.
1468
+ */
1469
+ async flushDeferredWork() {
1470
+ while (this.pendingTasks.length > 0) {
1471
+ const batch = this.pendingTasks.splice(0);
1472
+ await Promise.allSettled(batch);
1473
+ }
1359
1474
  }
1360
1475
  /**
1361
1476
  * Run callback in request scope (for DB operations, service access)
@@ -1369,18 +1484,17 @@ var TestingModule = class {
1369
1484
  return this._requestContainer.resolve(token);
1370
1485
  }
1371
1486
  /**
1372
- * Truncate all non-prisma tables in the database
1487
+ * Truncate mutable tables in the connection's schema(s), honoring a preserve-list.
1488
+ * Migration bookkeeping (`_prisma%`) is always preserved.
1373
1489
  */
1374
- async truncateDb(name) {
1490
+ async truncateDb(name, opts = {}) {
1375
1491
  const db = this.getDb(name);
1376
- const tables = await db.$queryRaw`
1377
- SELECT tablename::text as tablename FROM pg_tables
1378
- WHERE schemaname = current_schema()
1379
- AND tablename NOT LIKE '_prisma%'
1380
- `;
1381
- if (tables.length === 0) return;
1382
- const tableList = tables.map((t) => `"${t.tablename}"`).join(", ");
1383
- await db.$executeRawUnsafe(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`);
1492
+ const rows = await db.$queryRawUnsafe(buildTableDiscoverySql(opts.schemas ?? [], opts.preserve ?? []));
1493
+ const sql = buildTruncateSql(rows.map((t) => ({
1494
+ schema: t.schemaname,
1495
+ table: t.tablename
1496
+ })));
1497
+ if (sql) await db.$executeRawUnsafe(sql);
1384
1498
  }
1385
1499
  /**
1386
1500
  * Run seeders by class constructor in the request-scoped container
@@ -1396,13 +1510,15 @@ var TestingModule = class {
1396
1510
  * Assert that a record exists in the database
1397
1511
  */
1398
1512
  async assertDatabaseHas(table, data, name) {
1399
- expect(await this.getDb(name)[table].findFirst({ where: data }), `Expected ${table} with ${JSON.stringify(data)}`).not.toBeNull();
1513
+ const result = await this.getDb(name)[table].findFirst({ where: data });
1514
+ expect(result, `Expected ${table} with ${JSON.stringify(data)}`).not.toBeNull();
1400
1515
  }
1401
1516
  /**
1402
1517
  * Assert that a record does not exist in the database
1403
1518
  */
1404
1519
  async assertDatabaseMissing(table, data, name) {
1405
- expect(await this.getDb(name)[table].findFirst({ where: data }), `Expected ${table} NOT to have ${JSON.stringify(data)}`).toBeNull();
1520
+ const result = await this.getDb(name)[table].findFirst({ where: data });
1521
+ expect(result, `Expected ${table} NOT to have ${JSON.stringify(data)}`).toBeNull();
1406
1522
  }
1407
1523
  /**
1408
1524
  * Assert the number of records in a table
@@ -1415,21 +1531,21 @@ var TestingModule = class {
1415
1531
  * Cleanup - call in afterAll
1416
1532
  */
1417
1533
  async close() {
1534
+ await this.flushDeferredWork();
1418
1535
  await this._requestContainer.dispose();
1419
- try {
1420
- await this.app.shutdown();
1421
- } finally {
1422
- if (this.isolatedDatabase) try {
1423
- await dropDatabase(this.isolatedDatabase.adminConnectionString, this.isolatedDatabase.name);
1424
- } catch (error) {
1425
- console.warn(`[stratal-testing] Failed to drop isolated test database "${this.isolatedDatabase.name}"; it will be reclaimed by the next run's stale-database sweep.`, error);
1426
- }
1427
- }
1536
+ await this.app.shutdown();
1428
1537
  }
1429
1538
  };
1430
1539
  //#endregion
1431
1540
  //#region src/core/testing-module-builder.ts
1432
1541
  /**
1542
+ * This isolate's worker-database lease, per base connection string. The pool
1543
+ * runs each test file in its own isolate (`isolate: true`), so the first
1544
+ * `compile()` in a file leases and clones a database and later compiles in the
1545
+ * same file reuse it. The lease ends when the pool disposes the isolate.
1546
+ */
1547
+ const workerDatabases = /* @__PURE__ */ new Map();
1548
+ /**
1433
1549
  * Builder for creating test modules with provider overrides
1434
1550
  */
1435
1551
  var TestingModuleBuilder = class {
@@ -1481,28 +1597,40 @@ var TestingModuleBuilder = class {
1481
1597
  ...cf?.env,
1482
1598
  ...this.config.env
1483
1599
  };
1484
- const ctx = { waitUntil: cf ? cf.waitUntil : (p) => {
1485
- p.catch(() => {});
1486
- } };
1487
- const isolatedDb = await this.setupIsolatedDatabase(env);
1600
+ const pendingTasks = [];
1601
+ const testWorkersCache = this.config.cache === false ? void 0 : new TestWorkersCache();
1602
+ const testWorkerExports = this.config.cache === false ? void 0 : new TestWorkerExports(testWorkersCache);
1603
+ const ctx = {
1604
+ waitUntil: (promise) => {
1605
+ pendingTasks.push(Promise.resolve(promise).catch(() => void 0));
1606
+ cf?.waitUntil?.(promise);
1607
+ },
1608
+ cache: testWorkersCache,
1609
+ exports: testWorkerExports?.context
1610
+ };
1611
+ markGatewayMode(ctx);
1612
+ await this.attachWorkerDatabase(env);
1488
1613
  let app = null;
1489
1614
  try {
1490
1615
  const allImports = [...Test.getBaseModules(), ...this.config.imports ?? []];
1616
+ const rootModule = this.createTestRootModule({
1617
+ imports: allImports,
1618
+ providers: this.config.providers,
1619
+ controllers: this.config.controllers,
1620
+ consumers: this.config.consumers,
1621
+ jobs: this.config.jobs
1622
+ });
1491
1623
  app = new Application({
1492
- module: this.createTestRootModule({
1493
- imports: allImports,
1494
- providers: this.config.providers,
1495
- controllers: this.config.controllers,
1496
- consumers: this.config.consumers,
1497
- jobs: this.config.jobs
1498
- }),
1624
+ module: rootModule,
1499
1625
  logging: {
1500
1626
  level: this.config.logging?.level ?? LogLevel.ERROR,
1501
1627
  formatter: this.config.logging?.formatter ?? "pretty"
1502
1628
  },
1503
1629
  env,
1504
1630
  ctx,
1505
- exceptionHandler: this.config.exceptionHandler
1631
+ exceptionHandler: this.config.exceptionHandler,
1632
+ trailingSlash: this.config.trailingSlash,
1633
+ versioning: this.config.versioning
1506
1634
  });
1507
1635
  await app.initialize();
1508
1636
  app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService);
@@ -1520,49 +1648,56 @@ var TestingModuleBuilder = class {
1520
1648
  case "factory":
1521
1649
  app.container.registerFactory(override.token, override.implementation);
1522
1650
  break;
1523
- case "existing":
1524
- app.container.registerExisting(override.token, override.implementation);
1525
- break;
1651
+ case "existing": app.container.registerExisting(override.token, override.implementation);
1526
1652
  }
1527
1653
  await app.ensureHono();
1528
- return new TestingModule(app, env, ctx, isolatedDb, testEmailProvider);
1654
+ testWorkerExports?.setHandler(async (request, props) => {
1655
+ const hono = await app.ensureHono();
1656
+ const cachedCtx = {
1657
+ waitUntil: ctx.waitUntil.bind(ctx),
1658
+ cache: testWorkersCache,
1659
+ exports: ctx.exports,
1660
+ props
1661
+ };
1662
+ return hono.fetch(request, env, cachedCtx);
1663
+ });
1664
+ return new TestingModule(app, env, ctx, testEmailProvider, pendingTasks, testWorkersCache, testWorkerExports);
1529
1665
  } catch (error) {
1530
1666
  if (app) await app.shutdown().catch(() => {});
1531
- if (isolatedDb) await dropDatabase(isolatedDb.adminConnectionString, isolatedDb.name).catch(() => {});
1532
1667
  throw error;
1533
1668
  }
1534
1669
  }
1535
1670
  /**
1536
- * When `STRATAL_TEST_DB_ISOLATION=database`, create a fresh database cloned
1537
- * from the migrated template and rewrite the Hyperdrive binding's
1538
- * `connectionString` to target it. The binding name is configurable via
1539
- * `stratalTest({ database: { binding } })` (defaults to `DB`). Returns the
1540
- * created name + admin connection so `close()` can drop it, or `null` when
1541
- * isolation is off. Throws if isolation is on but the binding is missing — a
1542
- * misconfiguration, not a case to skip.
1671
+ * Point this app's Hyperdrive binding at the database this file leased,
1672
+ * leasing it on the file's first compile. Returns nothing to clean up — the
1673
+ * lease ends with the isolate. No-op when the consumer didn't opt into
1674
+ * `stratalTest({ database })`; throws if they did but no connection string is
1675
+ * configured (a misconfiguration that would otherwise silently skip isolation).
1543
1676
  */
1544
- async setupIsolatedDatabase(env) {
1677
+ async attachWorkerDatabase(env) {
1545
1678
  const bindings = env;
1546
- if (normalizeIsolation(bindings["STRATAL_TEST_DB_ISOLATION"]) !== "database") return null;
1547
- const bindingName = bindings["STRATAL_TEST_DB_BINDING"] ?? "DB";
1679
+ const bindingName = bindings[BINDING_ENV_VAR];
1680
+ if (!bindingName) return;
1548
1681
  const db = bindings[bindingName];
1549
1682
  const base = db?.connectionString;
1550
- if (!base) throw new Error(`Database isolation is 'database' but no \`${bindingName}\` Hyperdrive binding with a connectionString was found. Provide it via \`stratalTest({ miniflare: { hyperdrives: { ${bindingName} } } })\`, or set isolation to 'shared'.`);
1551
- const adminConnectionString = deriveAdminConnectionString(base);
1552
- const name = deriveDbName(base);
1553
- await createDatabaseFromTemplate(adminConnectionString, name, deriveTemplateName(base));
1683
+ if (!base) throw new Error(`[stratal-testing] stratalTest({ database }) is enabled but no \`${bindingName}\` Hyperdrive binding with a connectionString was found. Provide it via miniflare.hyperdrives (e.g. { ${bindingName}: DATABASE_URL }), or drop the \`database\` option.`);
1684
+ let lease = workerDatabases.get(base);
1685
+ if (!lease) {
1686
+ lease = leaseWorkerDatabase(base).catch((error) => {
1687
+ workerDatabases.delete(base);
1688
+ throw error;
1689
+ });
1690
+ workerDatabases.set(base, lease);
1691
+ }
1692
+ const { connectionString } = await lease;
1554
1693
  const isolated = Object.create(Object.getPrototypeOf(db), Object.getOwnPropertyDescriptors(db));
1555
1694
  Object.defineProperty(isolated, "connectionString", {
1556
- value: buildConnectionString(base, name),
1695
+ value: connectionString,
1557
1696
  enumerable: true,
1558
1697
  configurable: true,
1559
1698
  writable: true
1560
1699
  });
1561
1700
  bindings[bindingName] = isolated;
1562
- return {
1563
- name,
1564
- adminConnectionString
1565
- };
1566
1701
  }
1567
1702
  /**
1568
1703
  * Create a test root module with the given options
@@ -1689,7 +1824,8 @@ var MockFetch = class {
1689
1824
  * ```
1690
1825
  */
1691
1826
  mockJsonResponse(url, data, options = {}) {
1692
- const handler = http$1[(options.method ?? "GET").toLowerCase()](url, () => HttpResponse$1.json(data, {
1827
+ const method = (options.method ?? "GET").toLowerCase();
1828
+ const handler = http$1[method](url, () => HttpResponse$1.json(data, {
1693
1829
  status: options.status ?? 200,
1694
1830
  headers: options.headers
1695
1831
  }));
@@ -1739,32 +1875,6 @@ function createMockFetch(handlers) {
1739
1875
  return new MockFetch(handlers);
1740
1876
  }
1741
1877
  //#endregion
1742
- //#region src/errors/test-error.ts
1743
- /**
1744
- * Base error class for all test framework errors.
1745
- * Extends from Error and allows easy identification via `instanceof`.
1746
- */
1747
- var TestError = class extends Error {
1748
- cause;
1749
- constructor(message, cause) {
1750
- super(message);
1751
- this.cause = cause;
1752
- this.name = "TestError";
1753
- Error.captureStackTrace(this, this.constructor);
1754
- }
1755
- };
1756
- //#endregion
1757
- //#region src/errors/setup-error.ts
1758
- /**
1759
- * Error thrown when test setup fails.
1760
- * Examples: schema creation failure, migration failure, application bootstrap failure.
1761
- */
1762
- var TestSetupError = class extends TestError {
1763
- constructor(message, cause) {
1764
- super(`Test setup failed: ${message}`, cause);
1765
- }
1766
- };
1767
- //#endregion
1768
1878
  export { ActingAs, HttpResponse, MockFetch, ProviderOverrideBuilder, Test, TestCommandRequest, TestCommandResult, TestError, TestHttpClient, TestHttpRequest, TestResponse, TestSetupError, TestSseConnection, TestSseRequest, TestWsConnection, TestWsRequest, TestingModule, TestingModuleBuilder, createMockFetch, getValueAtPath, hasValueAtPath, http };
1769
1879
 
1770
1880
  //# sourceMappingURL=index.mjs.map