@danceroutine/tango-testing 0.1.0 → 1.0.0

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 (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/aDBClient-W6eXsK3X.js +21 -0
  4. package/dist/aDBClient-W6eXsK3X.js.map +1 -0
  5. package/dist/assertions/index.js +1 -1
  6. package/dist/{assertions-CN6KxXhH.js → assertions-CCFZ53Y-.js} +1 -1
  7. package/dist/assertions-CCFZ53Y-.js.map +1 -0
  8. package/dist/express/anExpressRequest.d.ts +24 -0
  9. package/dist/express/anExpressResponse.d.ts +9 -0
  10. package/dist/express/index.d.ts +3 -0
  11. package/dist/express/index.js +3 -0
  12. package/dist/express-Czpfz_Ay.js +68 -0
  13. package/dist/express-Czpfz_Ay.js.map +1 -0
  14. package/dist/factories/ModelDataFactory.d.ts +16 -1
  15. package/dist/factories/index.js +1 -1
  16. package/dist/{factories-CCAZ6E-g.js → factories-Cl_CAzbj.js} +19 -4
  17. package/dist/factories-Cl_CAzbj.js.map +1 -0
  18. package/dist/index.d.ts +5 -3
  19. package/dist/index.js +8 -11
  20. package/dist/integration/HarnessStrategyRegistry.d.ts +15 -0
  21. package/dist/integration/TestHarness.d.ts +23 -2
  22. package/dist/integration/anIntegrationHarness.d.ts +5 -0
  23. package/dist/integration/config.d.ts +4 -0
  24. package/dist/integration/conformance/index.d.ts +1 -0
  25. package/dist/integration/conformance/runDialectConformanceSuite.d.ts +11 -0
  26. package/dist/integration/domain/Dialect.d.ts +5 -4
  27. package/dist/integration/domain/ResetMode.d.ts +6 -5
  28. package/dist/integration/index.d.ts +8 -1
  29. package/dist/integration/index.js +3 -2
  30. package/dist/integration/migrations/ApplyAndVerifyMigrations.d.ts +3 -0
  31. package/dist/integration/migrations/AssertMigrationPlan.d.ts +3 -0
  32. package/dist/integration/migrations/IntrospectSchema.d.ts +3 -0
  33. package/dist/integration/orm/createQuerySetFixture.d.ts +10 -0
  34. package/dist/integration/orm/expectQueryResult.d.ts +4 -0
  35. package/dist/integration/orm/index.d.ts +6 -0
  36. package/dist/integration/orm/seedTable.d.ts +5 -0
  37. package/dist/integration/runtime/aTangoConfig.d.ts +8 -0
  38. package/dist/integration/runtime/index.d.ts +6 -0
  39. package/dist/integration/runtime/setupTestTangoRuntime.d.ts +6 -0
  40. package/dist/integration/smoke/AppProcessHarness.d.ts +83 -0
  41. package/dist/integration/smoke/index.d.ts +4 -0
  42. package/dist/integration/strategies/PostgresHarnessStrategy.d.ts +9 -0
  43. package/dist/integration/strategies/SqliteHarnessStrategy.d.ts +9 -0
  44. package/dist/integration-BrJw6NzG.js +747 -0
  45. package/dist/integration-BrJw6NzG.js.map +1 -0
  46. package/dist/mocks/DBClient.d.ts +1 -9
  47. package/dist/mocks/MockQuerySetResult.d.ts +5 -12
  48. package/dist/mocks/aDBClient.d.ts +21 -0
  49. package/dist/mocks/aManager.d.ts +17 -0
  50. package/dist/mocks/aQueryExecutor.d.ts +14 -0
  51. package/dist/mocks/aQueryResult.d.ts +5 -0
  52. package/dist/mocks/aQuerySet.d.ts +8 -0
  53. package/dist/mocks/aRequestContext.d.ts +22 -0
  54. package/dist/mocks/index.d.ts +9 -4
  55. package/dist/mocks/index.js +4 -6
  56. package/dist/mocks-BkwkXQQt.js +136 -0
  57. package/dist/mocks-BkwkXQQt.js.map +1 -0
  58. package/dist/vitest/index.js +3 -2
  59. package/dist/vitest/registerVitestTango.d.ts +3 -3
  60. package/dist/{vitest-PxMJue7R.js → vitest-37qN8D93.js} +4 -4
  61. package/dist/vitest-37qN8D93.js.map +1 -0
  62. package/package.json +81 -68
  63. package/dist/assertions/assertions.js +0 -8
  64. package/dist/assertions-CN6KxXhH.js.map +0 -1
  65. package/dist/factories/ModelDataFactory.js +0 -33
  66. package/dist/factories-CCAZ6E-g.js.map +0 -1
  67. package/dist/index.js.map +0 -1
  68. package/dist/integration/orm.d.ts +0 -9
  69. package/dist/integration/orm.js +0 -39
  70. package/dist/integration/strategies/PostgresHarnessStrategy.js +0 -95
  71. package/dist/integration-CDdpboYz.js +0 -378
  72. package/dist/integration-CDdpboYz.js.map +0 -1
  73. package/dist/mocks/DBClient.js +0 -1
  74. package/dist/mocks/MockQuerySetResult.js +0 -1
  75. package/dist/mocks/RepositoryLike.d.ts +0 -12
  76. package/dist/mocks/RepositoryLike.js +0 -1
  77. package/dist/mocks/aMockDBClient.d.ts +0 -2
  78. package/dist/mocks/aMockDBClient.js +0 -13
  79. package/dist/mocks/aMockQuerySet.d.ts +0 -2
  80. package/dist/mocks/aMockQuerySet.js +0 -15
  81. package/dist/mocks/aMockRepository.d.ts +0 -2
  82. package/dist/mocks/aMockRepository.js +0 -20
  83. package/dist/mocks/types.d.ts +0 -33
  84. package/dist/mocks-qo-1vCez.js +0 -72
  85. package/dist/mocks-qo-1vCez.js.map +0 -1
  86. package/dist/version.d.ts +0 -1
  87. package/dist/vitest/registerVitestTango.js +0 -90
  88. package/dist/vitest-PxMJue7R.js.map +0 -1
@@ -0,0 +1,747 @@
1
+ import { __export } from "./chunk-BkvOhyD0.js";
2
+ import { aDBClient } from "./aDBClient-W6eXsK3X.js";
3
+ import { vi } from "vitest";
4
+ import { QuerySet, initializeTangoRuntime, resetTangoRuntime } from "@danceroutine/tango-orm";
5
+ import { MigrationRunner, MigrationRunner as MigrationRunner$1, createDefaultIntrospectorStrategy } from "@danceroutine/tango-migrations";
6
+ import { defineConfig, loadConfig } from "@danceroutine/tango-config";
7
+ import { spawn } from "node:child_process";
8
+ import { quoteSqlIdentifier, quoteSqlIdentifier as quoteSqlIdentifier$1, quoteSqlIdentifier as quoteSqlIdentifier$2, validateSqlIdentifier, validateSqlIdentifier as validateSqlIdentifier$1, validateSqlIdentifier as validateSqlIdentifier$2 } from "@danceroutine/tango-core";
9
+ import { PostgresAdapter, SqliteAdapter } from "@danceroutine/tango-orm/connection";
10
+ import { rm } from "node:fs/promises";
11
+
12
+ //#region src/integration/domain/Dialect.ts
13
+ const Dialect = {
14
+ Sqlite: "sqlite",
15
+ Postgres: "postgres"
16
+ };
17
+
18
+ //#endregion
19
+ //#region src/integration/domain/ResetMode.ts
20
+ const ResetMode = {
21
+ Transaction: "transaction",
22
+ Truncate: "truncate",
23
+ DropSchema: "drop-schema"
24
+ };
25
+
26
+ //#endregion
27
+ //#region src/integration/domain/index.ts
28
+ var domain_exports = {};
29
+ __export(domain_exports, {
30
+ Dialect: () => Dialect,
31
+ ResetMode: () => ResetMode
32
+ });
33
+
34
+ //#endregion
35
+ //#region src/integration/migrations/AssertMigrationPlan.ts
36
+ async function assertMigrationPlan(harness, options) {
37
+ const runner = harness.migrationRunner(options.migrationsDir);
38
+ const plan = await runner.plan();
39
+ for (const snippet of options.expectSqlContains ?? []) if (!plan.includes(snippet)) throw new Error(`Expected migration plan to contain: ${snippet}`);
40
+ return plan;
41
+ }
42
+
43
+ //#endregion
44
+ //#region src/integration/migrations/ApplyAndVerifyMigrations.ts
45
+ async function applyAndVerifyMigrations(harness, options) {
46
+ const runner = harness.migrationRunner(options.migrationsDir);
47
+ await runner.apply(options.toId);
48
+ const statuses = await runner.status();
49
+ for (const id of options.expectedAppliedIds ?? []) {
50
+ const row = statuses.find((status) => status.id === id);
51
+ if (!row || !row.applied) throw new Error(`Expected migration ${id} to be applied`);
52
+ }
53
+ return { statuses };
54
+ }
55
+
56
+ //#endregion
57
+ //#region src/integration/migrations/IntrospectSchema.ts
58
+ const introspectorStrategy = createDefaultIntrospectorStrategy();
59
+ async function introspectSchema(harness) {
60
+ if (harness.dialect !== Dialect.Postgres && harness.dialect !== Dialect.Sqlite) throw new Error(`No introspector registered for dialect: ${String(harness.dialect)}`);
61
+ const dialect = harness.dialect === Dialect.Postgres ? "postgres" : "sqlite";
62
+ return introspectorStrategy.introspect(dialect, harness.dbClient);
63
+ }
64
+
65
+ //#endregion
66
+ //#region src/integration/migrations/index.ts
67
+ var migrations_exports = {};
68
+ __export(migrations_exports, {
69
+ applyAndVerifyMigrations: () => applyAndVerifyMigrations,
70
+ assertMigrationPlan: () => assertMigrationPlan,
71
+ introspectSchema: () => introspectSchema
72
+ });
73
+
74
+ //#endregion
75
+ //#region src/integration/conformance/runDialectConformanceSuite.ts
76
+ async function runDialectConformanceSuite(strategy, options = {}) {
77
+ const harness = await strategy.create(options.createOptions);
78
+ if (harness.dialect !== strategy.dialect) throw new Error(`Conformance failed: harness dialect '${String(harness.dialect)}' does not match strategy dialect '${String(strategy.dialect)}'`);
79
+ if (harness.capabilities !== strategy.capabilities) throw new Error("Conformance failed: harness capabilities must be strategy capabilities reference");
80
+ let resetBeforeSetupThrew = false;
81
+ try {
82
+ await harness.reset();
83
+ } catch {
84
+ resetBeforeSetupThrew = true;
85
+ }
86
+ if (!resetBeforeSetupThrew) throw new Error("Conformance failed: reset() must throw before setup()");
87
+ await harness.setup();
88
+ await harness.reset();
89
+ harness.migrationRunner(options.migrationsDir ?? "/tmp/migrations");
90
+ await harness.teardown();
91
+ let dbClientAfterTeardownThrew = false;
92
+ try {
93
+ harness.dbClient;
94
+ } catch {
95
+ dbClientAfterTeardownThrew = true;
96
+ }
97
+ if (!dbClientAfterTeardownThrew) throw new Error("Conformance failed: dbClient getter must throw after teardown()");
98
+ }
99
+
100
+ //#endregion
101
+ //#region src/integration/conformance/index.ts
102
+ var conformance_exports = {};
103
+ __export(conformance_exports, { runDialectConformanceSuite: () => runDialectConformanceSuite });
104
+
105
+ //#endregion
106
+ //#region src/integration/runtime/aTangoConfig.ts
107
+ function aTangoConfig(options = {}) {
108
+ const adapter = options.adapter ?? "sqlite";
109
+ return defineConfig({
110
+ current: "test",
111
+ environments: {
112
+ development: {
113
+ name: "development",
114
+ db: adapter === "sqlite" ? {
115
+ adapter: "sqlite",
116
+ filename: ":memory:",
117
+ maxConnections: 1
118
+ } : {
119
+ adapter: "postgres",
120
+ url: "postgres://postgres:postgres@localhost:5432/tango",
121
+ maxConnections: 1
122
+ },
123
+ migrations: {
124
+ dir: "migrations",
125
+ online: adapter === "postgres"
126
+ }
127
+ },
128
+ test: {
129
+ name: "test",
130
+ db: adapter === "sqlite" ? {
131
+ adapter: "sqlite",
132
+ filename: ":memory:",
133
+ maxConnections: 1
134
+ } : {
135
+ adapter: "postgres",
136
+ url: "postgres://postgres:postgres@localhost:5432/tango_test",
137
+ maxConnections: 1
138
+ },
139
+ migrations: {
140
+ dir: "migrations",
141
+ online: adapter === "postgres"
142
+ }
143
+ },
144
+ production: {
145
+ name: "production",
146
+ db: adapter === "sqlite" ? {
147
+ adapter: "sqlite",
148
+ filename: ":memory:",
149
+ maxConnections: 1
150
+ } : {
151
+ adapter: "postgres",
152
+ url: "postgres://postgres:postgres@localhost:5432/tango",
153
+ maxConnections: 1
154
+ },
155
+ migrations: {
156
+ dir: "migrations",
157
+ online: adapter === "postgres"
158
+ }
159
+ }
160
+ }
161
+ });
162
+ }
163
+
164
+ //#endregion
165
+ //#region src/integration/runtime/setupTestTangoRuntime.ts
166
+ async function setupTestTangoRuntime(options = {}) {
167
+ await resetTangoRuntime();
168
+ return initializeTangoRuntime(() => aTangoConfig(options));
169
+ }
170
+
171
+ //#endregion
172
+ //#region src/integration/runtime/index.ts
173
+ var runtime_exports = {};
174
+ __export(runtime_exports, {
175
+ aTangoConfig: () => aTangoConfig,
176
+ setupTestTangoRuntime: () => setupTestTangoRuntime
177
+ });
178
+
179
+ //#endregion
180
+ //#region src/integration/smoke/AppProcessHarness.ts
181
+ const DEFAULT_READY_TIMEOUT_MS = 3e4;
182
+ const DEFAULT_READY_INTERVAL_MS = 250;
183
+ const DEFAULT_STOP_TIMEOUT_MS = 1e4;
184
+ const MAX_LOG_BUFFER_CHARS = 2e4;
185
+ const defaultDeps = {
186
+ spawnProcess: (command, args, options) => spawn(command, args, options),
187
+ fetchImpl: fetch,
188
+ sleep: (ms) => new Promise((resolve) => {
189
+ setTimeout(resolve, ms);
190
+ })
191
+ };
192
+ var AppProcessHarness = class AppProcessHarness {
193
+ static BRAND = "tango.testing.app_process_harness";
194
+ __tangoBrand = AppProcessHarness.BRAND;
195
+ child;
196
+ baseUrl;
197
+ readyUrl;
198
+ readyTimeoutMs;
199
+ readyIntervalMs;
200
+ stopTimeoutMs;
201
+ deps;
202
+ stopped = false;
203
+ stdoutBuffer = "";
204
+ stderrBuffer = "";
205
+ constructor(options, deps) {
206
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
207
+ this.readyUrl = `${this.baseUrl}${normalizePath(options.readyPath ?? "/health")}`;
208
+ this.readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
209
+ this.readyIntervalMs = options.readyIntervalMs ?? DEFAULT_READY_INTERVAL_MS;
210
+ this.stopTimeoutMs = options.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS;
211
+ this.deps = deps;
212
+ this.child = this.deps.spawnProcess(options.command, options.args ?? [], {
213
+ cwd: options.cwd,
214
+ env: {
215
+ ...process.env,
216
+ ...options.env
217
+ },
218
+ stdio: "pipe"
219
+ });
220
+ this.child.stdout?.on("data", (chunk) => {
221
+ this.stdoutBuffer = appendBuffer(this.stdoutBuffer, String(chunk));
222
+ });
223
+ this.child.stderr?.on("data", (chunk) => {
224
+ this.stderrBuffer = appendBuffer(this.stderrBuffer, String(chunk));
225
+ });
226
+ }
227
+ /**
228
+ * Narrow an unknown value to the smoke-test harness that owns a child process.
229
+ */
230
+ static isAppProcessHarness(value) {
231
+ return typeof value === "object" && value !== null && value.__tangoBrand === AppProcessHarness.BRAND;
232
+ }
233
+ /**
234
+ * Spawn the target process and wait until its readiness endpoint responds successfully.
235
+ */
236
+ static async start(options, deps = {}) {
237
+ const mergedDeps = {
238
+ ...defaultDeps,
239
+ ...deps
240
+ };
241
+ const harness = new AppProcessHarness(options, mergedDeps);
242
+ await harness.waitForReady();
243
+ return harness;
244
+ }
245
+ /**
246
+ * Return the buffered stdout log for recent process output.
247
+ */
248
+ getStdoutLog() {
249
+ return this.stdoutBuffer;
250
+ }
251
+ /**
252
+ * Return the buffered stderr log for recent process output.
253
+ */
254
+ getStderrLog() {
255
+ return this.stderrBuffer;
256
+ }
257
+ /**
258
+ * Return stdout and stderr in a single formatted string for debugging failures.
259
+ */
260
+ getCombinedLog() {
261
+ const stdout = this.stdoutBuffer.trim();
262
+ const stderr = this.stderrBuffer.trim();
263
+ if (!stdout && !stderr) return "";
264
+ return [`[stdout]\n${stdout}`, `[stderr]\n${stderr}`].join("\n\n").trim();
265
+ }
266
+ /**
267
+ * Issue an HTTP request against the managed application process.
268
+ */
269
+ async request(path, init) {
270
+ const target = path.startsWith("http") ? path : `${this.baseUrl}${normalizePath(path)}`;
271
+ return this.deps.fetchImpl(target, init);
272
+ }
273
+ /**
274
+ * Assert an HTTP response status and include process logs when it mismatches.
275
+ */
276
+ async assertResponseStatus(response, expectedStatus, label) {
277
+ if (response.status === expectedStatus) return;
278
+ let bodyText;
279
+ try {
280
+ bodyText = await response.text();
281
+ } catch (error) {
282
+ bodyText = `failed to read response body: ${String(error)}`;
283
+ }
284
+ throw new Error([
285
+ `${label}. expected ${String(expectedStatus)} got ${String(response.status)}`,
286
+ `response body: ${bodyText}`,
287
+ `process logs:\n${this.getCombinedLog()}`
288
+ ].join("\n"));
289
+ }
290
+ /**
291
+ * Stop the managed process, escalating from SIGTERM to SIGKILL when necessary.
292
+ */
293
+ async stop() {
294
+ if (this.stopped) return;
295
+ this.stopped = true;
296
+ if (this.child.exitCode !== null || this.child.killed) return;
297
+ this.child.kill("SIGTERM");
298
+ const exited = await this.waitForExit(this.stopTimeoutMs);
299
+ if (!exited && !this.child.killed) {
300
+ this.child.kill("SIGKILL");
301
+ await this.waitForExit(this.stopTimeoutMs);
302
+ }
303
+ }
304
+ async waitForReady() {
305
+ const deadline = Date.now() + this.readyTimeoutMs;
306
+ while (Date.now() < deadline) {
307
+ if (this.child.exitCode !== null) throw new Error(`Process exited before ready check succeeded (exitCode=${String(this.child.exitCode)}).\n${this.getCombinedLog()}`);
308
+ try {
309
+ const response = await this.deps.fetchImpl(this.readyUrl);
310
+ if (response.ok) return;
311
+ } catch {}
312
+ await this.deps.sleep(this.readyIntervalMs);
313
+ }
314
+ await this.stop();
315
+ throw new Error(`Timed out waiting for readiness at ${this.readyUrl}.\n${this.getCombinedLog()}`);
316
+ }
317
+ async waitForExit(timeoutMs) {
318
+ if (this.child.exitCode !== null) return true;
319
+ return await new Promise((resolve) => {
320
+ const timer = setTimeout(() => {
321
+ this.child.off("exit", onExit);
322
+ resolve(false);
323
+ }, timeoutMs);
324
+ const onExit = () => {
325
+ clearTimeout(timer);
326
+ resolve(true);
327
+ };
328
+ this.child.once("exit", onExit);
329
+ });
330
+ }
331
+ };
332
+ function appendBuffer(current, chunk) {
333
+ const next = current + chunk;
334
+ if (next.length <= MAX_LOG_BUFFER_CHARS) return next;
335
+ return next.slice(next.length - MAX_LOG_BUFFER_CHARS);
336
+ }
337
+ function normalizePath(path) {
338
+ if (!path) return "/";
339
+ return path.startsWith("/") ? path : `/${path}`;
340
+ }
341
+
342
+ //#endregion
343
+ //#region src/integration/smoke/index.ts
344
+ var smoke_exports = {};
345
+ __export(smoke_exports, { AppProcessHarness: () => AppProcessHarness });
346
+
347
+ //#endregion
348
+ //#region src/integration/anIntegrationHarness.ts
349
+ const defaultCapabilities = {
350
+ transactionalDDL: true,
351
+ supportsSchemas: false,
352
+ supportsConcurrentIndex: false,
353
+ supportsDeferredFkValidation: false,
354
+ supportsJsonb: false
355
+ };
356
+ function anIntegrationHarness(overrides = {}) {
357
+ return {
358
+ dialect: Dialect.Sqlite,
359
+ capabilities: defaultCapabilities,
360
+ resetMode: ResetMode.DropSchema,
361
+ dbClient: aDBClient(),
362
+ setup: vi.fn(async () => {}),
363
+ reset: vi.fn(async () => {}),
364
+ teardown: vi.fn(async () => {}),
365
+ migrationRunner: vi.fn(() => ({})),
366
+ ...overrides
367
+ };
368
+ }
369
+
370
+ //#endregion
371
+ //#region src/integration/HarnessStrategyRegistry.ts
372
+ var HarnessStrategyRegistry = class HarnessStrategyRegistry {
373
+ static BRAND = "tango.testing.harness_strategy_registry";
374
+ __tangoBrand = HarnessStrategyRegistry.BRAND;
375
+ strategies = new Map();
376
+ /**
377
+ * Narrow an unknown value to `HarnessStrategyRegistry`.
378
+ */
379
+ static isHarnessStrategyRegistry(value) {
380
+ return typeof value === "object" && value !== null && value.__tangoBrand === HarnessStrategyRegistry.BRAND;
381
+ }
382
+ /**
383
+ * Register or replace a dialect strategy.
384
+ */
385
+ register(strategy) {
386
+ this.strategies.set(String(strategy.dialect), strategy);
387
+ return this;
388
+ }
389
+ /**
390
+ * Resolve a strategy for a dialect, or throw if none is registered.
391
+ */
392
+ get(dialect) {
393
+ const strategy = this.strategies.get(String(dialect));
394
+ if (!strategy) throw new Error(`No harness strategy registered for dialect: ${String(dialect)}`);
395
+ return strategy;
396
+ }
397
+ /**
398
+ * List all registered strategies.
399
+ */
400
+ list() {
401
+ return [...this.strategies.values()];
402
+ }
403
+ };
404
+
405
+ //#endregion
406
+ //#region src/integration/config.ts
407
+ function readNumber(value) {
408
+ if (!value) return undefined;
409
+ const parsed = Number(value);
410
+ return Number.isFinite(parsed) ? parsed : undefined;
411
+ }
412
+ function resolveAdapterConfig(dialect, opts) {
413
+ const fromOptions = opts.config ?? {};
414
+ if (opts.tangoConfigLoader) {
415
+ const loaded = loadConfig(opts.tangoConfigLoader);
416
+ const current = loaded.current.db;
417
+ const merged = {
418
+ url: fromOptions.url ?? current.url,
419
+ host: fromOptions.host ?? current.host,
420
+ port: fromOptions.port ?? current.port,
421
+ database: fromOptions.database ?? current.database,
422
+ user: fromOptions.user ?? current.user,
423
+ password: fromOptions.password ?? current.password,
424
+ filename: fromOptions.filename ?? current.filename,
425
+ maxConnections: fromOptions.maxConnections ?? current.maxConnections
426
+ };
427
+ if (dialect === Dialect.Sqlite) merged.filename = opts.sqliteFile ?? merged.filename ?? ":memory:";
428
+ return merged;
429
+ }
430
+ if (dialect === Dialect.Postgres) return {
431
+ url: fromOptions.url ?? process.env.TANGO_DATABASE_URL ?? process.env.DATABASE_URL,
432
+ host: fromOptions.host ?? process.env.TANGO_DB_HOST,
433
+ port: fromOptions.port ?? readNumber(process.env.TANGO_DB_PORT),
434
+ database: fromOptions.database ?? process.env.TANGO_DB_NAME,
435
+ user: fromOptions.user ?? process.env.TANGO_DB_USER,
436
+ password: fromOptions.password ?? process.env.TANGO_DB_PASSWORD,
437
+ maxConnections: fromOptions.maxConnections ?? 10
438
+ };
439
+ return {
440
+ filename: opts.sqliteFile ?? fromOptions.filename ?? process.env.TANGO_SQLITE_FILENAME ?? ":memory:",
441
+ maxConnections: fromOptions.maxConnections ?? 1
442
+ };
443
+ }
444
+
445
+ //#endregion
446
+ //#region src/integration/strategies/PostgresHarnessStrategy.ts
447
+ var PostgresHarnessStrategy = class PostgresHarnessStrategy {
448
+ static BRAND = "tango.testing.postgres_harness_strategy";
449
+ __tangoBrand = PostgresHarnessStrategy.BRAND;
450
+ dialect = Dialect.Postgres;
451
+ capabilities = {
452
+ transactionalDDL: true,
453
+ supportsSchemas: true,
454
+ supportsConcurrentIndex: true,
455
+ supportsDeferredFkValidation: true,
456
+ supportsJsonb: true
457
+ };
458
+ /**
459
+ * Narrow an unknown value to the PostgreSQL integration harness strategy.
460
+ */
461
+ static isPostgresHarnessStrategy(value) {
462
+ return typeof value === "object" && value !== null && value.__tangoBrand === PostgresHarnessStrategy.BRAND;
463
+ }
464
+ static buildSchemaName(explicitSchema) {
465
+ if (explicitSchema) return explicitSchema;
466
+ const random = Math.random().toString(36).slice(2, 8);
467
+ return `tango_test_${Date.now()}_${random}`;
468
+ }
469
+ /**
470
+ * Create a configured Postgres integration harness instance.
471
+ */
472
+ async create(options = {}) {
473
+ const config = resolveAdapterConfig(Dialect.Postgres, {
474
+ config: options.config,
475
+ tangoConfigLoader: options.tangoConfigLoader
476
+ });
477
+ const adapter = new PostgresAdapter();
478
+ const schemaName = PostgresHarnessStrategy.buildSchemaName(options.schema);
479
+ const resetMode = options.resetMode ?? ResetMode.DropSchema;
480
+ let client = null;
481
+ const ensureSearchPath = async () => {
482
+ const dbClient = client;
483
+ const schema = quoteSqlIdentifier$2(validateSqlIdentifier$2(schemaName, "schema"), "postgres");
484
+ await dbClient.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
485
+ await dbClient.query(`SET search_path TO ${schema}`);
486
+ };
487
+ const recreateSchema = async () => {
488
+ const dbClient = client;
489
+ const schema = quoteSqlIdentifier$2(validateSqlIdentifier$2(schemaName, "schema"), "postgres");
490
+ await dbClient.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
491
+ await dbClient.query(`CREATE SCHEMA ${schema}`);
492
+ await dbClient.query(`SET search_path TO ${schema}`);
493
+ };
494
+ const harness = {
495
+ dialect: Dialect.Postgres,
496
+ capabilities: this.capabilities,
497
+ resetMode,
498
+ get dbClient() {
499
+ if (!client) throw new Error("Postgres harness not initialized. Call setup() first.");
500
+ return client;
501
+ },
502
+ async setup() {
503
+ client = await adapter.connect(config);
504
+ await ensureSearchPath();
505
+ },
506
+ async reset() {
507
+ if (!client) throw new Error("Postgres harness not initialized. Call setup() first.");
508
+ if (resetMode === ResetMode.DropSchema || resetMode === ResetMode.Transaction) {
509
+ await recreateSchema();
510
+ return;
511
+ }
512
+ const { rows } = await client.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_type = 'BASE TABLE'`, [schemaName]);
513
+ for (const row of rows) {
514
+ const schema = quoteSqlIdentifier$2(validateSqlIdentifier$2(schemaName, "schema"), "postgres");
515
+ const table = quoteSqlIdentifier$2(validateSqlIdentifier$2(String(row.table_name), "table"), "postgres");
516
+ await client.query(`TRUNCATE TABLE ${schema}.${table} RESTART IDENTITY CASCADE`);
517
+ }
518
+ await client.query(`SET search_path TO ${quoteSqlIdentifier$2(validateSqlIdentifier$2(schemaName, "schema"), "postgres")}`);
519
+ },
520
+ async teardown() {
521
+ if (!client) return;
522
+ try {
523
+ await client.query(`DROP SCHEMA IF EXISTS ${quoteSqlIdentifier$2(validateSqlIdentifier$2(schemaName, "schema"), "postgres")} CASCADE`);
524
+ } finally {
525
+ await client.close();
526
+ client = null;
527
+ }
528
+ },
529
+ migrationRunner(migrationsDir) {
530
+ if (!client) throw new Error("Postgres harness not initialized. Call setup() first.");
531
+ return new MigrationRunner$1(client, "postgres", migrationsDir);
532
+ }
533
+ };
534
+ return harness;
535
+ }
536
+ };
537
+
538
+ //#endregion
539
+ //#region src/integration/strategies/SqliteHarnessStrategy.ts
540
+ var SqliteHarnessStrategy = class SqliteHarnessStrategy {
541
+ static BRAND = "tango.testing.sqlite_harness_strategy";
542
+ __tangoBrand = SqliteHarnessStrategy.BRAND;
543
+ dialect = Dialect.Sqlite;
544
+ capabilities = {
545
+ transactionalDDL: true,
546
+ supportsSchemas: false,
547
+ supportsConcurrentIndex: false,
548
+ supportsDeferredFkValidation: false,
549
+ supportsJsonb: false
550
+ };
551
+ /**
552
+ * Narrow an unknown value to the SQLite integration harness strategy.
553
+ */
554
+ static isSqliteHarnessStrategy(value) {
555
+ return typeof value === "object" && value !== null && value.__tangoBrand === SqliteHarnessStrategy.BRAND;
556
+ }
557
+ static async dropAllTables(client) {
558
+ const { rows } = await client.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
559
+ for (const row of rows) {
560
+ const table = quoteSqlIdentifier$1(validateSqlIdentifier$1(String(row.name), "table"), "sqlite");
561
+ await client.query(`DROP TABLE IF EXISTS ${table}`);
562
+ }
563
+ }
564
+ /**
565
+ * Create a configured SQLite integration harness instance.
566
+ */
567
+ async create(options = {}) {
568
+ const config = resolveAdapterConfig(Dialect.Sqlite, {
569
+ config: options.config,
570
+ tangoConfigLoader: options.tangoConfigLoader,
571
+ sqliteFile: options.sqliteFile
572
+ });
573
+ const adapter = new SqliteAdapter();
574
+ const resetMode = options.resetMode ?? ResetMode.DropSchema;
575
+ let client = null;
576
+ const reconnect = async () => {
577
+ client = await adapter.connect(config);
578
+ return client;
579
+ };
580
+ const harness = {
581
+ dialect: Dialect.Sqlite,
582
+ capabilities: this.capabilities,
583
+ resetMode,
584
+ get dbClient() {
585
+ if (!client) throw new Error("Sqlite harness not initialized. Call setup() first.");
586
+ return client;
587
+ },
588
+ async setup() {
589
+ await reconnect();
590
+ },
591
+ async reset() {
592
+ if (!client) throw new Error("Sqlite harness not initialized. Call setup() first.");
593
+ if (resetMode === ResetMode.DropSchema && config.filename && config.filename !== ":memory:") {
594
+ await client.close();
595
+ await rm(config.filename, { force: true });
596
+ await reconnect();
597
+ return;
598
+ }
599
+ await SqliteHarnessStrategy.dropAllTables(client);
600
+ },
601
+ async teardown() {
602
+ if (client) {
603
+ await client.close();
604
+ client = null;
605
+ }
606
+ if (config.filename && config.filename !== ":memory:") await rm(config.filename, { force: true });
607
+ },
608
+ migrationRunner(migrationsDir) {
609
+ if (!client) throw new Error("Sqlite harness not initialized. Call setup() first.");
610
+ return new MigrationRunner(client, "sqlite", migrationsDir);
611
+ }
612
+ };
613
+ return harness;
614
+ }
615
+ };
616
+
617
+ //#endregion
618
+ //#region src/integration/TestHarness.ts
619
+ var TestHarness = class TestHarness {
620
+ static BRAND = "tango.testing.test_harness";
621
+ static defaultRegistry = null;
622
+ __tangoBrand = TestHarness.BRAND;
623
+ /**
624
+ * Narrow an unknown value to `TestHarness`.
625
+ */
626
+ static isTestHarness(value) {
627
+ return typeof value === "object" && value !== null && value.__tangoBrand === TestHarness.BRAND;
628
+ }
629
+ /**
630
+ * Register a harness strategy on the shared default registry.
631
+ */
632
+ static registerStrategy(strategy) {
633
+ this.ensureRegistry().register(strategy);
634
+ }
635
+ /**
636
+ * Return the shared harness strategy registry.
637
+ */
638
+ static getRegistry() {
639
+ return this.ensureRegistry();
640
+ }
641
+ /**
642
+ * Create a dialect-specific harness from the registry.
643
+ */
644
+ static async forDialect(args, registry) {
645
+ const selectedRegistry = registry ?? this.ensureRegistry();
646
+ const strategy = selectedRegistry.get(args.dialect);
647
+ return strategy.create(args.options);
648
+ }
649
+ /**
650
+ * Convenience helper for a SQLite test harness.
651
+ */
652
+ static async sqlite(options) {
653
+ return this.forDialect({
654
+ dialect: Dialect.Sqlite,
655
+ options
656
+ });
657
+ }
658
+ /**
659
+ * Convenience helper for a Postgres test harness.
660
+ */
661
+ static async postgres(options) {
662
+ return this.forDialect({
663
+ dialect: Dialect.Postgres,
664
+ options
665
+ });
666
+ }
667
+ static ensureRegistry() {
668
+ if (this.defaultRegistry) return this.defaultRegistry;
669
+ const registry = new HarnessStrategyRegistry();
670
+ registry.register(new SqliteHarnessStrategy());
671
+ registry.register(new PostgresHarnessStrategy());
672
+ this.defaultRegistry = registry;
673
+ return registry;
674
+ }
675
+ };
676
+
677
+ //#endregion
678
+ //#region src/integration/orm/seedTable.ts
679
+ async function seedTable(harness, table, rows) {
680
+ if (rows.length === 0) return;
681
+ const columns = Object.keys(rows[0] ?? {});
682
+ if (columns.length === 0) return;
683
+ const dialect = harness.dialect;
684
+ const safeTable = quoteSqlIdentifier(validateSqlIdentifier(table, "table"), dialect);
685
+ const safeColumns = columns.map((column) => quoteSqlIdentifier(validateSqlIdentifier(column, "column", columns), dialect));
686
+ for (const row of rows) {
687
+ const values = columns.map((column) => {
688
+ const value = row[column];
689
+ if (harness.dialect === Dialect.Sqlite && typeof value === "boolean") return value ? 1 : 0;
690
+ return value;
691
+ });
692
+ const placeholders = harness.dialect === Dialect.Postgres ? columns.map((_, index) => `$${index + 1}`).join(", ") : columns.map(() => "?").join(", ");
693
+ await harness.dbClient.query(`INSERT INTO ${safeTable} (${safeColumns.join(", ")}) VALUES (${placeholders})`, values);
694
+ }
695
+ }
696
+
697
+ //#endregion
698
+ //#region src/integration/orm/createQuerySetFixture.ts
699
+ function createQuerySetFixture(input) {
700
+ const executor = {
701
+ meta: input.meta,
702
+ client: input.harness.dbClient,
703
+ dialect: input.harness.dialect,
704
+ run: async (compiled) => {
705
+ const result = await input.harness.dbClient.query(compiled.sql, compiled.params);
706
+ return result.rows;
707
+ }
708
+ };
709
+ return new QuerySet(executor);
710
+ }
711
+
712
+ //#endregion
713
+ //#region src/integration/orm/expectQueryResult.ts
714
+ async function expectQueryResult(actual, expected) {
715
+ const resolved = await actual;
716
+ if (JSON.stringify(resolved) !== JSON.stringify(expected)) throw new Error(`Expected query result ${JSON.stringify(expected)}, got ${JSON.stringify(resolved)}`);
717
+ }
718
+
719
+ //#endregion
720
+ //#region src/integration/index.ts
721
+ var integration_exports = {};
722
+ __export(integration_exports, {
723
+ AppProcessHarness: () => AppProcessHarness,
724
+ Dialect: () => Dialect,
725
+ HarnessStrategyRegistry: () => HarnessStrategyRegistry,
726
+ ResetMode: () => ResetMode,
727
+ TestHarness: () => TestHarness,
728
+ aTangoConfig: () => aTangoConfig,
729
+ anIntegrationHarness: () => anIntegrationHarness,
730
+ applyAndVerifyMigrations: () => applyAndVerifyMigrations,
731
+ assertMigrationPlan: () => assertMigrationPlan,
732
+ conformance: () => conformance_exports,
733
+ createQuerySetFixture: () => createQuerySetFixture,
734
+ domain: () => domain_exports,
735
+ expectQueryResult: () => expectQueryResult,
736
+ introspectSchema: () => introspectSchema,
737
+ migrations: () => migrations_exports,
738
+ runDialectConformanceSuite: () => runDialectConformanceSuite,
739
+ runtime: () => runtime_exports,
740
+ seedTable: () => seedTable,
741
+ setupTestTangoRuntime: () => setupTestTangoRuntime,
742
+ smoke: () => smoke_exports
743
+ });
744
+
745
+ //#endregion
746
+ export { AppProcessHarness, Dialect, HarnessStrategyRegistry, ResetMode, TestHarness, aTangoConfig, anIntegrationHarness, applyAndVerifyMigrations, assertMigrationPlan, conformance_exports, createQuerySetFixture, domain_exports, expectQueryResult, integration_exports, introspectSchema, migrations_exports, runDialectConformanceSuite, runtime_exports, seedTable, setupTestTangoRuntime, smoke_exports };
747
+ //# sourceMappingURL=integration-BrJw6NzG.js.map