@stratal/testing 0.0.0-canary-ccb3f17 → 0.0.0-canary-e5681b8

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 (46) hide show
  1. package/CHANGELOG.md +603 -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-DeMI4Igi.mjs → database-Rojs69r3.mjs} +112 -60
  6. package/dist/database-Rojs69r3.mjs.map +1 -0
  7. package/dist/{decorate-ZDdbRmsv.mjs → decorate-RQD1h28J.mjs} +1 -1
  8. package/dist/feature-flags/index.mjs +1 -1
  9. package/dist/{feature-flags-BMYurC1B.mjs → feature-flags-CZ1a4M2g.mjs} +2 -2
  10. package/dist/{feature-flags-BMYurC1B.mjs.map → feature-flags-CZ1a4M2g.mjs.map} +1 -1
  11. package/dist/{index-DUScdrKC.d.mts → index-B8Mw1Dxk.d.mts} +46 -33
  12. package/dist/index-B8Mw1Dxk.d.mts.map +1 -0
  13. package/dist/{index-Dm6e2N61.d.mts → index-D7FM6EKp.d.mts} +21 -2
  14. package/dist/index-D7FM6EKp.d.mts.map +1 -0
  15. package/dist/index.d.mts +55 -28
  16. package/dist/index.d.mts.map +1 -1
  17. package/dist/index.mjs +67 -61
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/mocks/cloudflare-workers.d.mts +6 -7
  20. package/dist/mocks/cloudflare-workers.d.mts.map +1 -1
  21. package/dist/mocks/index.d.mts +1 -1
  22. package/dist/mocks/index.mjs +1 -1
  23. package/dist/mocks/noop-rate-limiter-store.d.mts +1 -2
  24. package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -1
  25. package/dist/mocks/zenstack-language.d.mts +21 -21
  26. package/dist/mocks/zenstack-language.d.mts.map +1 -1
  27. package/dist/mocks/zenstack-language.mjs +1 -1
  28. package/dist/mocks/zenstack-language.mjs.map +1 -1
  29. package/dist/storage/index.d.mts +1 -1
  30. package/dist/storage/index.mjs +1 -1
  31. package/dist/{storage-q8RFahgU.mjs → storage-BiGamMA8.mjs} +70 -3
  32. package/dist/storage-BiGamMA8.mjs.map +1 -0
  33. package/dist/{test-worker-exports-DMct0Egv.d.mts → test-worker-exports-dCRARYEc.d.mts} +8 -7
  34. package/dist/test-worker-exports-dCRARYEc.d.mts.map +1 -0
  35. package/dist/{test-workers-cache-eiGMw9Yj.mjs → test-workers-cache-D02Pt_dE.mjs} +8 -7
  36. package/dist/{test-workers-cache-eiGMw9Yj.mjs.map → test-workers-cache-D02Pt_dE.mjs.map} +1 -1
  37. package/dist/vitest-plugin/index.d.mts +9 -9
  38. package/dist/vitest-plugin/index.d.mts.map +1 -1
  39. package/dist/vitest-plugin/index.mjs +22 -15
  40. package/dist/vitest-plugin/index.mjs.map +1 -1
  41. package/package.json +22 -19
  42. package/dist/database-DeMI4Igi.mjs.map +0 -1
  43. package/dist/index-DUScdrKC.d.mts.map +0 -1
  44. package/dist/index-Dm6e2N61.d.mts.map +0 -1
  45. package/dist/storage-q8RFahgU.mjs.map +0 -1
  46. package/dist/test-worker-exports-DMct0Egv.d.mts.map +0 -1
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@stratal/testing",
3
- "version": "0.0.0-canary-ccb3f17",
3
+ "version": "0.0.0-canary-e5681b8",
4
4
  "description": "Testing utilities and mocks for Stratal framework applications",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "license": "MIT",
8
- "author": "Stratal Contributors",
9
- "homepage": "https://github.com/strataljs/stratal#readme",
8
+ "author": {
9
+ "name": "Temitayo Fadojutimi",
10
+ "url": "https://x.com/adesege_"
11
+ },
12
+ "homepage": "https://stratal.dev",
10
13
  "repository": {
11
14
  "type": "git",
12
15
  "url": "git+https://github.com/strataljs/stratal.git",
@@ -80,16 +83,16 @@
80
83
  "lint:fix": "npx oxlint --fix ."
81
84
  },
82
85
  "dependencies": {
83
- "@cloudflare/vitest-pool-workers": "^0.18.5",
86
+ "@cloudflare/vitest-plugin": "^1.1.13",
84
87
  "@golevelup/ts-vitest": "^4.0.0",
85
88
  "msw": "^2.15.0"
86
89
  },
87
90
  "peerDependencies": {
88
91
  "@better-auth/core": ">=1.6",
89
- "@stratal/framework": "0.0.0-canary-ccb3f17",
92
+ "@stratal/framework": "0.0.0-canary-e5681b8",
90
93
  "better-auth": ">=1.4",
91
94
  "pg": "^8.0.0",
92
- "stratal": "0.0.0-canary-ccb3f17",
95
+ "stratal": "0.0.0-canary-e5681b8",
93
96
  "vitest": "^4.1.0"
94
97
  },
95
98
  "peerDependenciesMeta": {
@@ -107,18 +110,18 @@
107
110
  }
108
111
  },
109
112
  "devDependencies": {
110
- "@better-auth/core": "^1.6.23",
111
- "@cloudflare/workers-types": "5.20260716.1",
112
- "@stratal/framework": "workspace:*",
113
- "@types/node": "^26.1.1",
114
- "@types/pg": "^8.20.0",
115
- "@vitest/runner": "~4.1.10",
116
- "@vitest/snapshot": "~4.1.10",
117
- "better-auth": "^1.6.23",
118
- "pg": "^8.22.0",
119
- "stratal": "workspace:*",
120
- "tsdown": "^0.22.9",
113
+ "@better-auth/core": "^1.7.5",
114
+ "@cloudflare/workers-types": "5.20260919.1",
115
+ "@stratal/framework": "0.0.0-canary-e5681b8",
116
+ "@types/node": "^26.6.2",
117
+ "@types/pg": "^8.23.1",
118
+ "@vitest/runner": "~4.1.11",
119
+ "@vitest/snapshot": "~4.1.11",
120
+ "better-auth": "^1.7.5",
121
+ "pg": "^8.23.0",
122
+ "stratal": "0.0.0-canary-e5681b8",
123
+ "tsdown": "^0.23.0",
121
124
  "typescript": "^7.0.2",
122
- "vitest": "~4.1.10"
125
+ "vitest": "~4.1.11"
123
126
  }
124
- }
127
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"database-DeMI4Igi.mjs","names":["quoteIdent","importPg"],"sources":["../src/database/test-database.ts","../src/database/reset.ts"],"sourcesContent":["/**\n * Test database isolation helpers.\n *\n * Implements database-per-worker isolation for parallel e2e runs against\n * Postgres. A migrated **template** database is built once in global setup;\n * each worker clones it once into a per-worker database that is reused (and\n * reset between tests, not dropped per file). See `@stratal/testing/database`.\n *\n * `pg` is imported dynamically so this module loads without it — consumers\n * that don't use a database never pay the dependency.\n */\n\nimport { createHash } from 'node:crypto'\nimport { readFileSync, readdirSync, statSync } from 'node:fs'\nimport { basename, dirname, join, resolve } from 'node:path'\n\nimport type pg from 'pg'\n\n/** Env var carrying the name of the Hyperdrive binding to isolate. */\nexport const BINDING_ENV_VAR = 'STRATAL_TEST_DB_BINDING'\n\n/** Default Hyperdrive binding name when none is configured. */\nexport const DEFAULT_DB_BINDING = 'DB'\n\n/**\n * Postgres' identifier length limit. Names longer than this are silently\n * truncated by the server, which would cause distinct logical names to collide\n * on the same physical database. We assert against it everywhere a name is\n * derived.\n */\nconst MAX_IDENTIFIER_LENGTH = 63\n\n/** Quote a Postgres identifier for safe interpolation into DDL. */\nfunction quoteIdent(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`\n}\n\n/** Quote a Postgres string literal for safe interpolation into SQL. */\nfunction quoteLiteral(value: string): string {\n return `'${value.replace(/'/g, \"''\")}'`\n}\n\n/**\n * Dynamically import `pg` (an optional peer), surfacing an actionable error\n * instead of a raw module-not-found when database isolation is requested\n * without the dependency installed.\n */\nasync function importPg(): Promise<typeof pg> {\n try {\n const { default: pg } = await import('pg')\n return pg\n } catch (error) {\n throw new Error(\n \"[stratal-testing] `pg` is required for database isolation but is not installed. \" +\n 'Install it: `npm install --save-dev pg` (or `yarn add -D pg`).',\n { cause: error },\n )\n }\n}\n\n/**\n * Assert a derived identifier fits within Postgres' {@link MAX_IDENTIFIER_LENGTH}\n * limit. Throws a clear, actionable error instead of letting the server\n * silently truncate and collide names.\n */\nfunction assertIdentifierLength(name: string, kind: string): void {\n if (name.length > MAX_IDENTIFIER_LENGTH) {\n throw new Error(\n `[stratal-testing] Derived ${kind} \"${name}\" is ${name.length} characters, ` +\n `exceeding Postgres' ${MAX_IDENTIFIER_LENGTH}-character identifier limit. ` +\n 'Use a shorter base database name so the test-isolation suffix fits.',\n )\n }\n}\n\n/** Parse the database name out of a Postgres connection URL. */\nfunction databaseNameOf(connectionString: string): string {\n const name = new URL(connectionString).pathname.replace(/^\\//, '')\n return name || 'postgres'\n}\n\n/**\n * Derive an admin connection string pointing at the `postgres` maintenance\n * database. `CREATE`/`DROP DATABASE` cannot run on a connection bound to the\n * target database, so administration always goes through `postgres`.\n */\nexport function deriveAdminConnectionString(connectionString: string): string {\n const url = new URL(connectionString)\n url.pathname = '/postgres'\n return url.toString()\n}\n\n/** Build a connection string identical to `base` but pointing at `dbName`. */\nexport function buildConnectionString(base: string, dbName: string): string {\n const url = new URL(base)\n url.pathname = `/${dbName}`\n return url.toString()\n}\n\n/** The shared prefix for per-file databases, used as the leak-sweep key. */\nexport function databasePrefix(base: string): string {\n const baseName = databaseNameOf(base).replace(/[^a-z0-9_]/gi, '_')\n return `${baseName}_f_`\n}\n\n/** Name of the migrated template database cloned per worker. */\nexport function deriveTemplateName(base: string): string {\n const name = `${databaseNameOf(base).replace(/[^a-z0-9_]/gi, '_')}_template`\n assertIdentifierLength(name, 'template database name')\n return name\n}\n\n/**\n * Name of the database owned by a single test FILE, keyed by a per-file `token`.\n * Each test file clones the template into its OWN database — no two files ever\n * share one — because `@cloudflare/vitest-pool-workers` isolates per file and can\n * run a worker's files concurrently; per-file databases make cross-file\n * contamination impossible by construction. Asserted against Postgres' 63-char\n * identifier limit (keep the base name short so `_f_<token>` fits).\n */\nexport function deriveFileDbName(base: string, token: string): string {\n const baseName = databaseNameOf(base).replace(/[^a-z0-9_]/gi, '_')\n const name = `${baseName}_f_${token}`\n assertIdentifierLength(name, 'per-file database name')\n return name\n}\n\nasync function withAdminClient<T>(adminConn: string, fn: (query: (sql: string) => Promise<unknown>) => Promise<T>): Promise<T> {\n const pg = await importPg()\n const client = new pg.Client({ connectionString: adminConn })\n await client.connect()\n try {\n return await fn((sql) => client.query(sql))\n } finally {\n await client.end()\n }\n}\n\n/** True when the error is Postgres' \"duplicate database\" (concurrent create). */\nfunction isDuplicateDatabase(error: unknown): boolean {\n return (error as { code?: string })?.code === '42P04'\n}\n\n/**\n * True for SQLSTATE 55006 (\"source database is being accessed by other users\") —\n * what `CREATE DATABASE ... TEMPLATE t` raises while another session is using the\n * template (e.g. a concurrent clone of the same template).\n */\nfunction isTemplateInUse(error: unknown): boolean {\n return (error as { code?: string })?.code === '55006'\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))\n\n/** True when a database with `dbName` already exists. */\nasync function databaseExists(\n query: (sql: string) => Promise<unknown>,\n dbName: string,\n): Promise<boolean> {\n const { rows } = (await query(\n `SELECT 1 AS one FROM pg_database WHERE datname = ${quoteLiteral(dbName)}`,\n )) as { rows: unknown[] }\n return rows.length > 0\n}\n\n/**\n * Ensure worker database `dbName` exists, cloned from `template`. Idempotent and\n * lock-frugal: it first checks `pg_database`, so the common \"already exists\" path\n * (every file after a slot's first) never issues `CREATE DATABASE ... TEMPLATE`\n * and never takes the clone lock.\n *\n * The clone is serialized across all workers and processes by one Postgres\n * advisory lock keyed on the template. Postgres permits only one\n * `CREATE DATABASE ... TEMPLATE t` at a time — a concurrent one fails with\n * SQLSTATE 55006. On a fresh run every worker's first compile races to clone the\n * same template, so without this they'd all fire `CREATE ... TEMPLATE` at once\n * and all but one would fail their first file. The lock funnels them one-at-a-\n * time, each blocking *in Postgres* (not busy-waiting) until its turn. A create\n * that still hits a transient 55006 is retried with backoff; one that loses the\n * race for the same name (SQLSTATE 42P04) is treated as success.\n */\nexport async function ensureWorkerDatabase(\n adminConn: string,\n dbName: string,\n template: string,\n): Promise<void> {\n await withAdminClient(adminConn, async (query) => {\n if (await databaseExists(query, dbName)) return\n\n const lockKey = quoteLiteral(`stratal:worker-db-clone:${template}`)\n await query(`SELECT pg_advisory_lock(hashtext(${lockKey}))`)\n try {\n // A sibling worker may have cloned it while we waited for the lock.\n if (await databaseExists(query, dbName)) return\n const create = `CREATE DATABASE ${quoteIdent(dbName)} TEMPLATE ${quoteIdent(template)}`\n for (let attempt = 1; ; attempt++) {\n try {\n await query(create)\n return\n } catch (error) {\n if (isDuplicateDatabase(error)) return\n if (isTemplateInUse(error) && attempt < 5) {\n await sleep(250 * attempt)\n continue\n }\n throw error\n }\n }\n } finally {\n await query(`SELECT pg_advisory_unlock(hashtext(${lockKey}))`)\n }\n })\n}\n\n/**\n * Drop leaked per-file databases matching the prefix while leaving a concurrent\n * process's **live** databases intact.\n *\n * Multiple setups can run at once (CI sharding, several e2e projects). A blanket\n * \"drop everything matching the prefix\" sweep would delete a sibling process's\n * in-flight per-file databases. So we only drop databases that currently have\n * **no active backend connections** — i.e. true leaks from a crashed prior run.\n * A live per-file database always has the test worker's pool connected, so it is\n * skipped. The `WITH (FORCE)` covers the narrow race where a connection appears\n * between the check and the drop.\n */\nasync function sweepStaleDatabases(adminConn: string, prefix: string): Promise<void> {\n // Escape both the SQL-string quote and the LIKE metacharacters (`\\`, `%`, `_`)\n // so a prefix containing `_` (a single-char wildcard) can't over-match and drop\n // an unrelated database. `\\` is the explicit ESCAPE character below.\n const likePrefix = prefix\n .replace(/'/g, \"''\")\n .replace(/[\\\\%_]/g, (c) => `\\\\${c}`)\n await withAdminClient(adminConn, async (query) => {\n const { rows } = (await query(\n `SELECT d.datname FROM pg_database d ` +\n `WHERE d.datname LIKE '${likePrefix}%' ESCAPE '\\\\' ` +\n `AND NOT EXISTS (SELECT 1 FROM pg_stat_activity a WHERE a.datname = d.datname)`,\n )) as { rows: { datname: string }[] }\n for (const { datname } of rows) {\n await query(`DROP DATABASE IF EXISTS ${quoteIdent(datname)} WITH (FORCE)`)\n }\n })\n}\n\n/**\n * Run `fn` while holding a session-level Postgres advisory lock keyed to\n * `lockKey`, serializing template setup across concurrent processes (CI\n * sharding, multiple e2e projects) so they don't drop/recreate the template out\n * from under each other. The lock is released in `finally`.\n */\nasync function withAdvisoryLock<T>(adminConn: string, lockKey: string, fn: () => Promise<T>): Promise<T> {\n return withAdminClient(adminConn, async (query) => {\n await query(`SELECT pg_advisory_lock(hashtext(${quoteLiteral(lockKey)}))`)\n try {\n return await fn()\n } finally {\n await query(`SELECT pg_advisory_unlock(hashtext(${quoteLiteral(lockKey)}))`)\n }\n })\n}\n\n/** Schema source file extensions hashed into the template fingerprint. */\nconst SCHEMA_FILE_RE = /\\.(zmodel|prisma|sql)$/\n\n/** Matches ZModel `import \"...\"` / `import '...'` statements. */\nconst ZMODEL_IMPORT_RE = /^\\s*import\\s+['\"]([^'\"]+)['\"]/gm\n\n/**\n * Follow a ZModel file's `import` graph, collecting the root plus every\n * transitively imported `.zmodel` file — ZenStack supports multi-file schemas,\n * so editing an imported file must invalidate the fingerprint. Import paths\n * resolve relative to the importing file; the `.zmodel` extension is optional.\n * A missing import target is skipped (migration surfaces the real error).\n */\nfunction collectZmodelImports(file: string, seen: Set<string>): void {\n if (seen.has(file)) return\n let content: string\n try {\n content = readFileSync(file, 'utf8')\n } catch {\n return // missing import target — not part of the fingerprint\n }\n seen.add(file)\n for (const [, importPath] of content.matchAll(ZMODEL_IMPORT_RE)) {\n const target = resolve(dirname(file), importPath)\n collectZmodelImports(target.endsWith('.zmodel') ? target : `${target}.zmodel`, seen)\n }\n}\n\n/**\n * Expand a schema path into the concrete files to hash. A directory contributes\n * every schema file in its tree; a single `.zmodel` file contributes its whole\n * `import` graph; any other file contributes itself.\n */\nfunction collectSchemaFiles(path: string): string[] {\n if (statSync(path).isDirectory()) {\n const out: string[] = []\n for (const entry of readdirSync(path, { withFileTypes: true })) {\n const full = join(path, entry.name)\n if (entry.isDirectory()) out.push(...collectSchemaFiles(full))\n else if (SCHEMA_FILE_RE.test(entry.name)) out.push(full)\n }\n return out\n }\n if (path.endsWith('.zmodel')) {\n const seen = new Set<string>()\n collectZmodelImports(path, seen)\n return [...seen]\n }\n return [path]\n}\n\n/**\n * A content-derived fingerprint of the schema source(s) plus the migrate and\n * prepare routines. The template is reused across runs while this is\n * unchanged; any edit to a schema file — or to how migration/preparation run\n * — changes it and forces a rebuild. Uses file basenames (not absolute paths)\n * so it is stable across checkouts.\n *\n * @internal exported for fingerprint unit tests\n */\nexport function computeSchemaFingerprint(\n schema: string | string[],\n migrate: TestDatabaseGlobalSetupOptions['migrate'],\n prepare?: TestDatabaseGlobalSetupOptions['prepare'],\n): string {\n const roots = Array.isArray(schema) ? schema : [schema]\n const files = roots.flatMap(collectSchemaFiles).sort()\n if (files.length === 0) {\n throw new Error(\n `[stratal-testing] No schema files found for fingerprinting under: ${roots.join(', ')}`,\n )\n }\n const hash = createHash('sha256')\n for (const file of files) {\n hash.update(basename(file))\n hash.update('\\0')\n hash.update(readFileSync(file))\n hash.update('\\0')\n }\n hash.update(migrate.toString())\n hash.update('\\0')\n hash.update(prepare ? prepare.toString() : '')\n return hash.digest('hex')\n}\n\n/**\n * Read the template database's stored schema fingerprint (kept as the database\n * COMMENT). Returns `null` when the template does not exist or carries no\n * fingerprint. Database comments are NOT copied by `CREATE DATABASE ...\n * TEMPLATE`, so per-file clones never inherit it.\n */\nasync function readTemplateFingerprint(\n query: (sql: string) => Promise<unknown>,\n template: string,\n): Promise<string | null> {\n const { rows } = (await query(\n `SELECT shobj_description(oid, 'pg_database') AS fingerprint ` +\n `FROM pg_database WHERE datname = ${quoteLiteral(template)}`,\n )) as { rows: { fingerprint: string | null }[] }\n return rows.length === 0 ? null : rows[0].fingerprint\n}\n\n/** Options for {@link createTestDatabaseGlobalSetup}. */\nexport interface TestDatabaseGlobalSetupOptions {\n /** Run migrations against the template connection string. */\n migrate: (connectionString: string) => void | Promise<void>\n /**\n * Schema source(s) — file or directory path(s). Contents + `migrate` + `prepare`\n * are hashed into the template fingerprint; the template is reused across runs\n * while unchanged. For a ZenStack multi-file schema pass the root `.zmodel`.\n */\n schema: string | string[]\n /**\n * One-time preparation run against the template **after** migrations (before\n * the fingerprint is stamped). Bake expensive baseline state here — seed data,\n * a default tenant schema, reference rows — so every worker database inherits\n * it via the clone instead of rebuilding it per test.\n *\n * The fingerprint hashes this hook's **source text only** — if `prepare` reads\n * external data files (seed JSON/SQL) at runtime, changing only those files\n * will NOT invalidate the template. Bump the hook's source (e.g. a version\n * comment) or drop the template manually when seed data changes.\n */\n prepare?: (connectionString: string) => void | Promise<void>\n /** Base/admin connection string. Defaults to `process.env.DATABASE_URL`. */\n connectionString?: string\n /** Template database name. Defaults to `<baseDbName>_template`. */\n templateName?: string\n}\n\n/**\n * Build a Vitest `globalSetup` default export that prepares the test database.\n *\n * Under a Postgres advisory lock (so concurrent setups across CI shards /\n * multiple e2e projects don't clobber each other), sweeps leaked per-worker\n * databases, then ensures a migrated (and optionally prepared) template exists\n * — ready to be cloned per worker.\n *\n * **Template reuse.** The template is fingerprinted from the `schema` source(s)\n * + the `migrate` routine + the `prepare` routine, and the fingerprint is\n * stored as the template's database COMMENT. On each run, a matching\n * fingerprint means nothing has changed and the existing template is reused\n * as-is — `migrate`/`prepare` run **only** on the first run after an edit (or on\n * a fresh database). The fingerprint is stamped only after a successful\n * migrate + prepare, so a match always implies a complete template; there is no\n * force/skip flag — reuse is purely fingerprint-driven.\n *\n * **Concurrency model.** The reuse check + rebuild runs under\n * `pg_advisory_lock(hashtext(<template>))`, so only one process rebuilds the\n * template at a time. The stale-database sweep only drops per-worker databases\n * with **no active connections**, leaving a sibling process's live databases\n * intact. Teardown deliberately does **not** sweep or drop the template: a\n * concurrent process may still be using both, and the next run's setup sweep is\n * the backstop for any leak.\n *\n * @example\n * ```ts\n * // test/global-setup.ts\n * import { createTestDatabaseGlobalSetup } from '@stratal/testing/database'\n *\n * export default createTestDatabaseGlobalSetup({\n * schema: schemaPath, // file or directory — reused-when-unchanged fingerprint\n * migrate: (conn) => execFileSync(zenstackBin, ['db', 'push', '--force-reset', `--schema=${schemaPath}`, '--accept-data-loss'],\n * { env: { ...process.env, DATABASE_URL: conn }, stdio: 'inherit' }),\n * })\n * ```\n */\nexport function createTestDatabaseGlobalSetup(\n opts: TestDatabaseGlobalSetupOptions,\n): () => Promise<void> {\n return async () => {\n const base = opts.connectionString ?? process.env.DATABASE_URL\n if (!base) {\n throw new Error(\n '[stratal-testing] No connection string for test database setup. Set process.env.DATABASE_URL or pass `connectionString`.',\n )\n }\n\n const adminConn = deriveAdminConnectionString(base)\n const template = opts.templateName ?? deriveTemplateName(base)\n const prefix = databasePrefix(base)\n const fingerprint = computeSchemaFingerprint(opts.schema, opts.migrate, opts.prepare)\n\n // Serialize template rebuild across concurrent setups so they don't drop or\n // migrate the template out from under each other.\n await withAdvisoryLock(adminConn, template, async () => {\n await sweepStaleDatabases(adminConn, prefix)\n\n // Reuse the existing template when its stored fingerprint matches — the\n // schema is unchanged, so the migrated template is ready to clone. Only a\n // fingerprint mismatch (or a missing template) pays the migration cost.\n const current = await withAdminClient(adminConn, (query) =>\n readTemplateFingerprint(query, template),\n )\n if (current === fingerprint) return\n\n await withAdminClient(adminConn, async (query) => {\n await query(`DROP DATABASE IF EXISTS ${quoteIdent(template)} WITH (FORCE)`)\n await query(`CREATE DATABASE ${quoteIdent(template)}`)\n })\n const templateConn = buildConnectionString(base, template)\n await opts.migrate(templateConn)\n if (opts.prepare) await opts.prepare(templateConn)\n // Stamp the fingerprint only after a successful migrate + prepare, so a\n // matching fingerprint always implies a complete, ready template. Database\n // COMMENTs are not copied by CREATE DATABASE ... TEMPLATE, so clones stay\n // clean.\n await withAdminClient(adminConn, (query) =>\n query(`COMMENT ON DATABASE ${quoteIdent(template)} IS ${quoteLiteral(fingerprint)}`),\n )\n })\n\n // No teardown hook: anything destructive here (sweeping per-worker databases\n // or dropping the template) could clobber a concurrent process that is still\n // running. The next run's setup sweep (connection-guarded) reclaims leaks.\n }\n}\n","import type pg from 'pg'\n\n/** Options controlling which tables a per-test reset truncates. */\nexport interface ResetOptions {\n /** Extra schemas to include beyond `current_schema()` (e.g. a tenant schema). */\n schemas?: string[]\n /**\n * Bare table names or SQL LIKE patterns (matched against `tablename` across\n * all target schemas) that must survive the reset — reference/seed data baked\n * into the template. Not schema-qualified: a `schema.table` entry matches a\n * table literally named `schema.table`, not `table` in `schema`. Migration\n * bookkeeping (`_prisma%`) is always preserved.\n */\n preserve?: string[]\n}\n\nfunction quoteIdent(ident: string): string {\n return `\"${ident.replace(/\"/g, '\"\"')}\"`\n}\n\n/**\n * Build a single `TRUNCATE ... RESTART IDENTITY CASCADE` (empty when no tables).\n * Takes `{ schema, table }` pairs and quotes each part independently, so a\n * dotted identifier (a legal quoted name, e.g. `\"my.table\"`) is preserved\n * verbatim rather than mis-split into extra qualifier segments.\n */\nexport function buildTruncateSql(tables: { schema: string; table: string }[]): string {\n if (tables.length === 0) return ''\n const quoted = tables.map((t) => `${quoteIdent(t.schema)}.${quoteIdent(t.table)}`)\n return `TRUNCATE ${quoted.join(', ')} RESTART IDENTITY CASCADE`\n}\n\n/**\n * Sentinel meaning \"the active search-path schema\" (`current_schema()`). A\n * Symbol so it can never collide with a real schema name — a schema literally\n * named `current` is passed through as the string literal `'current'`.\n */\nconst CURRENT_SCHEMA = Symbol('current_schema')\n\n/**\n * Build the SQL that discovers truncatable tables: every base table in the\n * given schemas ({@link CURRENT_SCHEMA} → `current_schema()`), excluding\n * migration bookkeeping (`_prisma%`) and any caller-supplied preserve patterns.\n * Returns a SELECT of `schemaname, tablename`. Escaping lives here so both reset\n * paths share one implementation.\n */\nexport function buildTableDiscoverySql(schemas: string[], preserve: string[]): string {\n const schemaFilter = [CURRENT_SCHEMA, ...schemas]\n .map((s) => (typeof s === 'symbol' ? 'current_schema()' : `'${s.replace(/'/g, \"''\")}'`))\n .join(', ')\n const notLike = ['_prisma%', ...preserve]\n .map((p) => `tablename NOT LIKE '${p.replace(/'/g, \"''\")}'`)\n .join(' AND ')\n return `SELECT schemaname::text AS schemaname, tablename::text AS tablename FROM pg_tables WHERE schemaname IN (${schemaFilter}) AND ${notLike}`\n}\n\nasync function importPg(): Promise<typeof pg> {\n const { default: mod } = await import('pg')\n return mod\n}\n\n/**\n * Reset a worker database between tests: discover every non-preserved table in\n * the target schemas and truncate them in one statement. Fast because the\n * schema/DDL is baked into the worker DB — reset only clears mutable rows and\n * resets identities. Opens and closes a short-lived direct `pg` connection.\n */\nexport async function resetWorkerDatabase(connectionString: string, opts: ResetOptions = {}): Promise<void> {\n const pgMod = await importPg()\n const client = new pgMod.Client({ connectionString })\n await client.connect()\n try {\n const sql = buildTableDiscoverySql(opts.schemas ?? [], opts.preserve ?? [])\n const { rows } = await client.query<{ schemaname: string; tablename: string }>(sql)\n const tables = rows.map((r) => ({ schema: r.schemaname, table: r.tablename }))\n const truncateSql = buildTruncateSql(tables)\n if (truncateSql) await client.query(truncateSql)\n } finally {\n await client.end()\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-DUScdrKC.d.mts","names":[],"sources":["../src/database/test-database.ts","../src/database/reset.ts"],"mappings":";;;;;;;;;;;;;cAmBa;;cAGA;;;;;;iBAgEG,4BAA4B;;iBAO5B,sBAAsB,cAAc;;iBAOpC,eAAe;;iBAMf,mBAAmB;;;;;;;;;iBAcnB,iBAAiB,cAAc;;;;;;;;;;;;;;;;;iBA6DzB,qBACpB,mBACA,gBACA,mBACC;;UAoLc;;EAEf,UAAU,oCAAoC;;;;;;EAM9C;;;;;;;;;;;;EAYA,WAAW,oCAAoC;;EAE/C;;EAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCc,8BACd,MAAM,uCACC;;;;UC5aQ;;EAEf;;;;;;;;EAQA;;;;;;;;iBAac,iBAAiB;EAAU;EAAgB;;;;;;;;;iBAoB3C,uBAAuB,mBAAmB;;;;;;;iBAqBpC,oBAAoB,0BAA0B,OAAM,eAAoB"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-Dm6e2N61.d.mts","names":[],"sources":["../src/storage/fake-storage.service.ts"],"mappings":";;;;;UAkBiB;EACf,SAAS;EACT;EACA;EACA,WAAW;EACX,YAAY;;;;;;;;;;;;;;;;;;;cAqBD,2BAA2B;qBAKjB,gBAAgB;qBAEhB,SAAS;UANtB;EAER,YAEqB,gBAAgB,uBAEhB,SAAS;;;;EAQxB,OACJ,MAAM,gCACN,sBACA,SAAS,eACT,gBACC,QAAQ;;;;EAyBX,SAAS,eAAe,QAAQ;;;;EAyBhC,OAAO,eAAe;;;;EAQtB,OAAO,eAAe;;;;EAOtB,wBACE,cACA,qBACC,QAAQ;;;;EAOX,sBACE,cACA,qBACC,QAAQ;;;;EAOX,sBACE,cACA,qBACC,QAAQ;;;;EAOL,cACJ,MAAM,gCACN,cACA,SAAS,KAAK;IAA2B;KACzC,gBACC,QAAQ;;;;;;;EAoBX,aAAa;;;;;;;EAab,cAAc;;;;;;EAYd;;;;;;;EAaA,YAAY;;;;EAUZ,kBAAkB,YAAY;;;;EAO9B;;;;EAOA,QAAQ,eAAe;;;;EAOvB;UAQQ;UAeM"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage-q8RFahgU.mjs","names":[],"sources":["../src/storage/fake-storage.service.ts"],"sourcesContent":["import { Transient, inject } from 'stratal/di'\nimport {\n FileNotFoundError,\n STORAGE_TOKENS,\n type StorageManagerService,\n StorageService,\n type StreamingBlobPayloadInputTypes,\n type DownloadResult,\n type PresignedUrlResult,\n type StorageConfig,\n type UploadOptions,\n type UploadResult,\n} from 'stratal/storage'\nimport { expect } from 'vitest'\n\n/**\n * Stored file representation in memory\n */\nexport interface StoredFile {\n content: Uint8Array\n mimeType: string\n size: number\n metadata?: Record<string, string>\n uploadedAt: Date\n}\n\n/**\n * FakeStorageService\n *\n * In-memory storage implementation for testing.\n * Registered by default in TestingModuleBuilder.\n *\n * Similar to Laravel's Storage::fake() - stores files in memory\n * and provides assertion helpers for testing.\n *\n * @example\n * ```typescript\n * // Access via TestingModule\n * module.storage.assertExists('path/to/file.pdf')\n * module.storage.assertMissing('deleted/file.pdf')\n * module.storage.clear() // Reset between tests\n * ```\n */\n@Transient(STORAGE_TOKENS.StorageService)\nexport class FakeStorageService extends StorageService {\n private files = new Map<string, StoredFile>()\n\n constructor(\n @inject(STORAGE_TOKENS.StorageManager)\n protected readonly storageManager: StorageManagerService,\n @inject(STORAGE_TOKENS.Options)\n protected readonly options: StorageConfig\n ) {\n super(storageManager, options)\n }\n\n /**\n * Upload content to fake storage\n */\n async upload(\n body: StreamingBlobPayloadInputTypes,\n relativePath: string,\n options: UploadOptions,\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const diskName = this.resolveDisk(disk)\n\n this.files.set(relativePath, {\n content,\n mimeType: options.mimeType ?? 'application/octet-stream',\n size: options.size,\n metadata: options.metadata,\n uploadedAt: new Date(),\n })\n\n return {\n path: relativePath,\n disk: diskName,\n fullPath: relativePath,\n size: options.size,\n mimeType: options.mimeType ?? 'application/octet-stream',\n uploadedAt: new Date(),\n }\n }\n\n /**\n * Download a file from fake storage\n */\n download(path: string): Promise<DownloadResult> {\n const file = this.files.get(path)\n\n if (!file) {\n return Promise.reject(new FileNotFoundError(path))\n }\n\n return Promise.resolve({\n toStream: () => new ReadableStream({\n start(controller) {\n controller.enqueue(file.content)\n controller.close()\n },\n }),\n toString: () => Promise.resolve(new TextDecoder().decode(file.content)),\n toArrayBuffer: () => Promise.resolve(file.content),\n contentType: file.mimeType,\n size: file.size,\n metadata: file.metadata,\n })\n }\n\n /**\n * Delete a file from fake storage\n */\n delete(path: string): Promise<void> {\n this.files.delete(path)\n return Promise.resolve()\n }\n\n /**\n * Check if a file exists in fake storage\n */\n exists(path: string): Promise<boolean> {\n return Promise.resolve(this.files.has(path))\n }\n\n /**\n * Generate a fake presigned download URL\n */\n getPresignedDownloadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'GET', expiresIn))\n }\n\n /**\n * Generate a fake presigned upload URL\n */\n getPresignedUploadUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'PUT', expiresIn))\n }\n\n /**\n * Generate a fake presigned delete URL\n */\n getPresignedDeleteUrl(\n path: string,\n expiresIn?: number\n ): Promise<PresignedUrlResult> {\n return Promise.resolve(this.createPresignedUrl(path, 'DELETE', expiresIn))\n }\n\n /**\n * Chunked upload (same as regular upload for fake)\n */\n async chunkedUpload(\n body: StreamingBlobPayloadInputTypes,\n path: string,\n options: Omit<UploadOptions, 'size'> & { size?: number },\n disk?: string\n ): Promise<UploadResult> {\n const content = await this.bodyToUint8Array(body)\n const size = options.size ?? content.length\n\n // Reuse the already-materialized bytes rather than `body`: a `ReadableStream`\n // is single-use, so reading it here and again inside `upload` would throw\n // \"ReadableStream is disturbed\". `content` is a Uint8Array and safe to re-read.\n return this.upload(content, path, { ...options, size }, disk)\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Test Assertion Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Assert that a file exists at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file does not exist\n */\n assertExists(path: string): void {\n expect(\n this.files.has(path),\n `Expected file to exist at: ${path}\\nStored files: ${this.getStoredPaths().join(', ') || '(none)'}`\n ).toBe(true)\n }\n\n /**\n * Assert that a file does NOT exist at the given path\n *\n * @param path - Path to check\n * @throws AssertionError if file exists\n */\n assertMissing(path: string): void {\n expect(\n this.files.has(path),\n `Expected file NOT to exist at: ${path}`\n ).toBe(false)\n }\n\n /**\n * Assert storage is empty\n *\n * @throws AssertionError if any files exist\n */\n assertEmpty(): void {\n expect(\n this.files.size,\n `Expected storage to be empty but found ${this.files.size} files: ${this.getStoredPaths().join(', ')}`\n ).toBe(0)\n }\n\n /**\n * Assert storage has exactly N files\n *\n * @param count - Expected number of files\n * @throws AssertionError if count doesn't match\n */\n assertCount(count: number): void {\n expect(\n this.files.size,\n `Expected ${count} files in storage but found ${this.files.size}`\n ).toBe(count)\n }\n\n /**\n * Get all stored files (for inspection)\n */\n getStoredFiles(): Map<string, StoredFile> {\n return new Map(this.files)\n }\n\n /**\n * Get all stored file paths\n */\n getStoredPaths(): string[] {\n return Array.from(this.files.keys())\n }\n\n /**\n * Get a specific file by path\n */\n getFile(path: string): StoredFile | undefined {\n return this.files.get(path)\n }\n\n /**\n * Clear all stored files (call in beforeEach for test isolation)\n */\n clear(): void {\n this.files.clear()\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Private Helpers\n // ─────────────────────────────────────────────────────────────────────────\n\n private createPresignedUrl(\n path: string,\n method: 'GET' | 'PUT' | 'DELETE' | 'HEAD',\n expiresIn = 300\n ): PresignedUrlResult {\n const expiresAt = new Date(Date.now() + expiresIn * 1000)\n\n return {\n url: `https://fake-storage.test/${path}?method=${method}&expires=${expiresAt.toISOString()}`,\n expiresIn,\n expiresAt,\n method,\n }\n }\n\n private async bodyToUint8Array(body: StreamingBlobPayloadInputTypes | null | undefined): Promise<Uint8Array> {\n if (!body) {\n return new Uint8Array(0)\n }\n\n if (body instanceof Uint8Array) {\n return body\n }\n\n if (body instanceof ArrayBuffer) {\n return new Uint8Array(body)\n }\n\n if (typeof body === 'string') {\n return new TextEncoder().encode(body)\n }\n\n if (body instanceof Blob) {\n const buffer = await body.arrayBuffer()\n return new Uint8Array(buffer)\n }\n\n if (body instanceof ReadableStream) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n // FormData or URLSearchParams - convert via Response\n if (body instanceof FormData || body instanceof URLSearchParams) {\n return new Uint8Array(await new Response(body).arrayBuffer())\n }\n\n return new Uint8Array(0)\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-worker-exports-DMct0Egv.d.mts","names":[],"sources":["../src/mocks/test-email-provider.ts","../src/mocks/test-workers-cache.ts","../src/mocks/test-worker-exports.ts"],"mappings":";;;;;;;;;;;;cAgBa,6BAA6B;;WAEhC,MAAM;EAEf,KAAK,SAAS,uBAAuB,QAAQ;EAKvC,UAAU,UAAU,yBAAyB,QAAQ;;;;;;;;;;;;;;;;;;;;;;;cCJ/C,4BAA4B;;WAE9B,QAAQ;EAEjB,MAAM,MAAM,YAAY;IAAU;;;;;;UCrBnB;;EAEf;EACA;EACA;;EAEA,OAAO;;;KAIG,mBACV,SAAS,SACT,OAAO,4BACJ,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAsCA;mBAOkB;;WALpB,WAAW;UAEZ;mBACS;EAEjB,YAA6B,OAAO;;;;;;;;;;EAWpC,WAAW,SAAS;;;;;;;;;;;;;;;;;;MAqBhB,WAAW;;;;;;;UAmBP;UAkBM;UAiBA"}