@endora-commerce/test-kit 0.100.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +108 -0
  3. package/dist/database/index.d.ts +17 -0
  4. package/dist/database/index.d.ts.map +1 -0
  5. package/dist/database/index.js +17 -0
  6. package/dist/database/index.js.map +1 -0
  7. package/dist/database/lease.d.ts +115 -0
  8. package/dist/database/lease.d.ts.map +1 -0
  9. package/dist/database/lease.js +204 -0
  10. package/dist/database/lease.js.map +1 -0
  11. package/dist/database/provision.d.ts +135 -0
  12. package/dist/database/provision.d.ts.map +1 -0
  13. package/dist/database/provision.js +457 -0
  14. package/dist/database/provision.js.map +1 -0
  15. package/dist/database/run-isolation.d.ts +356 -0
  16. package/dist/database/run-isolation.d.ts.map +1 -0
  17. package/dist/database/run-isolation.js +490 -0
  18. package/dist/database/run-isolation.js.map +1 -0
  19. package/dist/server/compose-test-server.d.ts +251 -0
  20. package/dist/server/compose-test-server.d.ts.map +1 -0
  21. package/dist/server/compose-test-server.js +359 -0
  22. package/dist/server/compose-test-server.js.map +1 -0
  23. package/dist/server/composition.d.ts +112 -0
  24. package/dist/server/composition.d.ts.map +1 -0
  25. package/dist/server/composition.js +32 -0
  26. package/dist/server/composition.js.map +1 -0
  27. package/dist/server/index.d.ts +13 -0
  28. package/dist/server/index.d.ts.map +1 -0
  29. package/dist/server/index.js +12 -0
  30. package/dist/server/index.js.map +1 -0
  31. package/dist/support/entity-index.d.ts +91 -0
  32. package/dist/support/entity-index.d.ts.map +1 -0
  33. package/dist/support/entity-index.js +56 -0
  34. package/dist/support/entity-index.js.map +1 -0
  35. package/dist/support/index.d.ts +134 -0
  36. package/dist/support/index.d.ts.map +1 -0
  37. package/dist/support/index.js +153 -0
  38. package/dist/support/index.js.map +1 -0
  39. package/package.json +66 -0
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Per-invocation test isolation (issue #189).
3
+ *
4
+ * ## The defect
5
+ *
6
+ * Isolation in this suite is **per database and per Redis instance**, not per
7
+ * invocation. `helpers/test-server.ts` truncates `SEEDED_TABLES`, composes a
8
+ * server and seeds fixtures on every booting file, so a second `vitest run`
9
+ * against the same `b2b_test` lands its truncate in the middle of the first
10
+ * one's setup. Three agents hit that in one day and every one of them read it
11
+ * as a real failure first: `Hook timed out in 30000ms` in `beforeAll`, then
12
+ * `Cannot read properties of undefined (reading 'app')` in the teardown that
13
+ * never got a handle; a shifting subset of one directory failing with 403s and
14
+ * missing fixtures, a different subset each run; and a role created through the
15
+ * API reading back `null` because another invocation had truncated
16
+ * `admin_roles` between the write and the read.
17
+ *
18
+ * The cost is not the flakes. It is that a false failure is indistinguishable
19
+ * from a true one, so the failure is either chased for an hour or — worse —
20
+ * dismissed as "probably the shared database" when it is real.
21
+ *
22
+ * ## The answer
23
+ *
24
+ * A run gets its **own** database and its **own** Redis logical database,
25
+ * automatically. The escape hatch already existed and was manual — every agent
26
+ * that got clean results set `TEST_DATABASE_URL=…/b2b_<something>_test` by hand
27
+ * — so this makes the thing that already worked the default rather than a thing
28
+ * you have to know. (156 hand-named test databases were standing on the
29
+ * developer machine that reported this, 4.1 GB of them, which is the other half
30
+ * of what "manual" costs.)
31
+ *
32
+ * **Per invocation, not per file.** The harness's per-file cost already
33
+ * dominates the suite (issue #72: a truncate is 1149 ms per composition), so a
34
+ * fresh database per *file* would be unaffordable. Per invocation is the
35
+ * granularity the defect actually has.
36
+ *
37
+ * **Cloned, not migrated.** `<base>_tpl` is the migrated template — created and
38
+ * brought up to date once, under an advisory lock so concurrent invocations
39
+ * cannot race on it — and each run is `create database … template <base>_tpl`,
40
+ * which is a file copy: 0.2 s warm against the 39 MB this schema occupies.
41
+ * Nothing about the migration coverage changes: the template is still migrated
42
+ * from scratch on a fresh cluster, which is what the CI job's comment about
43
+ * exercising the whole chain is protecting.
44
+ *
45
+ * **And it is the template of this run's migration set, not the machine's.**
46
+ * `<base>_tpl_<digest>` (issue #289): the digest covers the ordered migration
47
+ * class names *and* the content of every file that writes into a template, so
48
+ * two branches that differ anywhere a template can see get two templates, and
49
+ * two trees that agree share one and migrate once between them. Before that
50
+ * there was one `<base>_tpl` and a check comparing applied migration *names*
51
+ * against the configured order — which catches a branch that added a migration
52
+ * and cannot catch one that changed what a migration of the same name does.
53
+ * See `templateDigest`. What the name claims is checked against the template's
54
+ * own provenance comment and against `mikro_orm_migrations` before anything is
55
+ * cloned; a template that cannot account for itself is rebuilt, never used.
56
+ *
57
+ * **Names satisfy the existing guard.** `runDatabaseName` refuses to produce a
58
+ * name `TEST_DATABASE_NAME_PATTERN` would not accept, so the harness's refusal
59
+ * to truncate a non-test database is untouched — not loosened, not bypassed.
60
+ * That predicate lives here now and every caller imports it, so there is
61
+ * one spelling of it rather than three.
62
+ *
63
+ * **Redis by logical database index, not by key prefix.** Every Redis client in
64
+ * this tree — 8 in `src/`, 40-odd in `test/` — is built as
65
+ * `new Redis(process.env['REDIS_URL'] ?? 'redis://localhost:6379')`, and
66
+ * ioredis reads the index off the DSN path. Pointing `REDIS_URL` at
67
+ * `…/<index>` therefore isolates all of them, including BullMQ, with no call
68
+ * site changed. A key prefix would have to be threaded through every one of
69
+ * those constructions, and `keys(pattern)` — which `dropStaleCaches` uses — is
70
+ * not a key-taking command as far as ioredis is concerned, so a prefixed client
71
+ * would still scan and delete another run's keys.
72
+ *
73
+ * ## Cleanup
74
+ *
75
+ * The run database is dropped by the lease's own release. A **crashed**
76
+ * run cannot run its teardown, so every invocation also sweeps: run databases
77
+ * carry their creation instant in their name, and one older than
78
+ * `STALE_RUN_DATABASE_MS` with no connection to it is dropped, at most
79
+ * `SWEEP_LIMIT` per invocation. Only names this module generated are ever
80
+ * considered — the 156 hand-named databases and anything else on the cluster
81
+ * are outside the pattern by construction, and `strandedRunDatabases` is
82
+ * unit-tested on exactly that. Redis leases expire on their own TTL.
83
+ *
84
+ * **Templates are collected too**, and have to be: one per migration set means
85
+ * one left behind whenever a branch gains a migration or is rebased.
86
+ * `staleTemplates` is the other half of the bargain keying them made — same
87
+ * three conditions, with the age read from the template's provenance rather
88
+ * than from its name, because what matters about a template is when it was last
89
+ * *used*.
90
+ */
91
+ import { createHash, randomBytes } from 'node:crypto';
92
+ import { hostname } from 'node:os';
93
+ /**
94
+ * A database name that says "this database exists to be wiped".
95
+ *
96
+ * The one spelling of the judgement. `assertTestDatabaseUrl` applies it before
97
+ * a run touches anything, `runDatabaseName` refuses to emit a name that fails
98
+ * it, and the application's own dev-seed guard keeps its copy pinned to this one
99
+ * by a test of its own — `backend/src/seeds/dev-seed-guard.ts` is `src/`, so it
100
+ * cannot import a test's module and has to re-spell the regex.
101
+ */
102
+ export const TEST_DATABASE_NAME_PATTERN = /(^|_)test(_|$)/;
103
+ /** PostgreSQL identifiers are 63 bytes; a longer name is silently truncated. */
104
+ const MAX_IDENTIFIER_LENGTH = 63;
105
+ /** What separates the base name from the run token. */
106
+ const RUN_INFIX = '_r_';
107
+ /**
108
+ * The migrated database a run is cloned from. Nothing runs tests against it.
109
+ *
110
+ * The suffix is followed by the migration set's digest — see `templateDigest`.
111
+ * The bare `<base>_tpl` was the pre-#289 name and is nobody's template now.
112
+ */
113
+ const TEMPLATE_SUFFIX = '_tpl';
114
+ /** How many hex characters of the digest a template name carries. */
115
+ export const TEMPLATE_DIGEST_LENGTH = 12;
116
+ /** How old a run database must be before a sweep may take it for stranded. */
117
+ export const STALE_RUN_DATABASE_MS = 4 * 60 * 60 * 1000;
118
+ /**
119
+ * How long a template survives with nothing using it.
120
+ *
121
+ * Templates are keyed by the migration set, so a branch that adds a migration
122
+ * leaves the previous set's template behind the moment it is rebased or merged.
123
+ * A day of nobody asking for a set is the point at which keeping its 19 MB
124
+ * costs more than the one migration pass rebuilding it would.
125
+ */
126
+ export const STALE_TEMPLATE_MS = 24 * 60 * 60 * 1000;
127
+ /** How many stranded databases one invocation drops, so a sweep cannot become the run's cost. */
128
+ export const SWEEP_LIMIT = 25;
129
+ /** The environment variable that turns per-invocation isolation off. */
130
+ export const ISOLATION_ENV = 'BACKEND_TEST_ISOLATION';
131
+ /** Set this to keep the run database after the run, for a post-mortem. */
132
+ export const KEEP_DATABASE_ENV = 'BACKEND_TEST_KEEP_DATABASE';
133
+ /**
134
+ * The base DSN, exported to the workers by the lease on the isolated
135
+ * path only.
136
+ *
137
+ * Its **presence is the statement** "this invocation has a migrated template,
138
+ * and a file that needs a database of its own may clone it" — which is what
139
+ * `setupMigratorTestDb` asks. Nothing derives it from the run database's name:
140
+ * a run database is `<base>_r_<stamp>_<token>` and taking the base back out of
141
+ * one would be a second, weaker spelling of `runDatabaseName`.
142
+ */
143
+ export const BASE_DATABASE_URL_ENV = 'BACKEND_TEST_BASE_URL';
144
+ /**
145
+ * The template this run was cloned from, exported to the workers alongside it.
146
+ *
147
+ * A name and not a derivation, because since #289 there is more than one
148
+ * template on the cluster and which one is this run's is a fact about the run,
149
+ * not about the base: another invocation may build a template of its own while
150
+ * this one is still going, and a file that re-derived "the template" mid-run
151
+ * would clone whichever set happened to be newest.
152
+ */
153
+ export const TEMPLATE_DATABASE_ENV = 'BACKEND_TEST_TEMPLATE';
154
+ export function isolationMode(env = process.env) {
155
+ const raw = env[ISOLATION_ENV]?.trim();
156
+ if (raw === undefined || raw === '')
157
+ return 'per-invocation';
158
+ if (raw === 'per-invocation' || raw === 'shared')
159
+ return raw;
160
+ throw new Error(`${ISOLATION_ENV} must be "per-invocation" or "shared" (got "${raw}"). ` +
161
+ `"shared" is the pre-#189 behaviour: every invocation on one database.`);
162
+ }
163
+ /**
164
+ * Why this run is sharing the base database rather than taking one of its own,
165
+ * or `undefined` when it is not.
166
+ */
167
+ export function sharedDatabaseReason(env, baseName) {
168
+ if (isolationMode(env) === 'shared')
169
+ return 'explicit';
170
+ if (!TEST_DATABASE_NAME_PATTERN.test(baseName))
171
+ return 'name-override';
172
+ return undefined;
173
+ }
174
+ /** Whether this run keeps its database for a post-mortem instead of dropping it. */
175
+ export function keepRunDatabase(env = process.env) {
176
+ const raw = env[KEEP_DATABASE_ENV]?.trim();
177
+ return raw !== undefined && raw !== '' && raw !== '0' && raw !== 'false';
178
+ }
179
+ function assertSafeIdentifier(name) {
180
+ if (!/^[A-Za-z0-9_]+$/.test(name)) {
181
+ throw new Error(`unsafe database name "${name}" — a generated name is interpolated into ` +
182
+ `CREATE/DROP DATABASE, which cannot parameterise it.`);
183
+ }
184
+ }
185
+ function assertUsableDatabaseName(name) {
186
+ assertSafeIdentifier(name);
187
+ if (name.length > MAX_IDENTIFIER_LENGTH) {
188
+ throw new Error(`generated database name "${name}" is ${name.length} bytes; PostgreSQL truncates ` +
189
+ `identifiers at ${MAX_IDENTIFIER_LENGTH}, and a truncated name would collide with ` +
190
+ `another run's. Shorten the base name in TEST_DATABASE_URL.`);
191
+ }
192
+ if (!TEST_DATABASE_NAME_PATTERN.test(name)) {
193
+ throw new Error(`generated database name "${name}" is not a test database by the convention ` +
194
+ `the base DSN is judged by before a run touches anything. Isolation may not widen that ` +
195
+ `judgement — point TEST_DATABASE_URL at a database whose name carries "test".`);
196
+ }
197
+ return name;
198
+ }
199
+ /**
200
+ * The migrated source every run of *this migration set* is cloned from.
201
+ *
202
+ * The digest is in the name because that is what makes two different migration
203
+ * sets two different databases: a template is shared exactly by the runs whose
204
+ * platform it actually is, and never by the runs that merely share a cluster
205
+ * with it. See `templateDigest` for what "the same set" means, and issue #289
206
+ * for what one shared `<base>_tpl` cost.
207
+ */
208
+ export function templateDatabaseName(base, digest) {
209
+ assertSafeIdentifier(base);
210
+ if (!new RegExp(`^[0-9a-f]{${TEMPLATE_DIGEST_LENGTH}}$`).test(digest)) {
211
+ throw new Error(`"${digest}" is not a migration-set digest — expected ${TEMPLATE_DIGEST_LENGTH} hex ` +
212
+ `characters from templateDigest(). A template whose name does not carry its identity ` +
213
+ `is the one issue #289 is about.`);
214
+ }
215
+ return assertUsableDatabaseName(`${base}${TEMPLATE_SUFFIX}_${digest}`);
216
+ }
217
+ /**
218
+ * The lock every invocation of this base takes before it touches a template.
219
+ *
220
+ * One key for the whole family rather than one per digest: a sweep walks the
221
+ * templates of *other* digests, so it has to exclude an invocation that is
222
+ * half-way through building one. It is also, deliberately, the exact string the
223
+ * pre-#289 code derived its key from, so an invocation running that code still
224
+ * serializes against this one while the two versions coexist on a machine.
225
+ */
226
+ export function templateFamilyName(base) {
227
+ assertSafeIdentifier(base);
228
+ return `${base}${TEMPLATE_SUFFIX}`;
229
+ }
230
+ /** The digest a template name carries, or `undefined` for a name this module did not make. */
231
+ export function parseTemplateDatabaseName(base, name) {
232
+ if (!/^[A-Za-z0-9_]+$/.test(base))
233
+ return undefined;
234
+ const match = new RegExp(`^${base}${TEMPLATE_SUFFIX}_([0-9a-f]{${TEMPLATE_DIGEST_LENGTH}})$`).exec(name);
235
+ return match ? { digest: match[1] } : undefined;
236
+ }
237
+ /**
238
+ * What makes two runs' templates the same template.
239
+ *
240
+ * Issue #189 keyed the template on the base database alone and compared the
241
+ * *names* of the applied migrations against this run's order before cloning it.
242
+ * That catches a template another branch added a migration to. It does not
243
+ * catch — and cannot — a branch that changed what a migration of the same name
244
+ * *does*, which is the ordinary shape of two agents iterating on the same
245
+ * feature, and it is how a run whose tree contained no such string failed
246
+ * against a payment method row called `paypal_checkout` seeded from a branch it
247
+ * had never seen (issue #289).
248
+ *
249
+ * So the identity is the whole input: the ordered class names — order included,
250
+ * because a manifest `dependencies` edit reorders migrations without touching
251
+ * one of them — and the content of every file that writes into the template,
252
+ * which is every migration source plus the handful of files that seed the
253
+ * platform invariants. Two trees that agree on all of it share a template and
254
+ * pay one migration pass between them; two that do not cannot collide, because
255
+ * they are not looking at the same database at all.
256
+ *
257
+ * It **throws on an empty input** rather than digesting nothing: every empty
258
+ * input hashes to the same value, so a caller that read no migrations would
259
+ * quietly name the same template as a caller that read all of them — a template
260
+ * of unknown provenance, which is the defect and not a fix for it.
261
+ */
262
+ export function templateDigest(inputs) {
263
+ if (inputs.migrations.length === 0) {
264
+ throw new Error('refusing to identify a migration template from no migrations — the digest would be the ' +
265
+ 'one every empty read produces, and every such run would share one template.');
266
+ }
267
+ if (inputs.sources.length === 0) {
268
+ throw new Error('refusing to identify a migration template from no source files — the migration names ' +
269
+ 'alone cannot see a branch that changed what a migration of the same name does.');
270
+ }
271
+ const hash = createHash('sha256');
272
+ hash.update('migrations\n');
273
+ for (const name of inputs.migrations)
274
+ hash.update(`${name}\n`);
275
+ hash.update('sources\n');
276
+ for (const source of [...inputs.sources].sort((a, b) => (a.path < b.path ? -1 : 1))) {
277
+ hash.update(`${source.path} ${source.sha256}\n`);
278
+ }
279
+ return hash.digest('hex').slice(0, TEMPLATE_DIGEST_LENGTH);
280
+ }
281
+ /** The marker that says the comment is ours and is the shape this version reads. */
282
+ const PROVENANCE_MARKER = 'b2b-test-template v1';
283
+ export function formatTemplateProvenance(provenance) {
284
+ return (`${PROVENANCE_MARKER} digest=${provenance.digest} migrations=${provenance.migrations} ` +
285
+ `lastUsedAt=${provenance.lastUsedAt.toISOString()}`);
286
+ }
287
+ /**
288
+ * What the template says it is, or `undefined` when it says nothing this
289
+ * version understands.
290
+ *
291
+ * Every "undefined" is a template a run must rebuild rather than clone: an
292
+ * older version's template, a hand-made database that happens to fit the name,
293
+ * one whose build died before it could sign itself. "I cannot tell what this
294
+ * holds" and "this holds the wrong thing" get the same answer on purpose.
295
+ */
296
+ export function parseTemplateProvenance(comment) {
297
+ if (!comment || !comment.startsWith(PROVENANCE_MARKER))
298
+ return undefined;
299
+ const digest = /\bdigest=([0-9a-f]+)/.exec(comment)?.[1];
300
+ const migrations = /\bmigrations=(\d+)/.exec(comment)?.[1];
301
+ const lastUsedAt = /\blastUsedAt=(\S+)/.exec(comment)?.[1];
302
+ if (digest === undefined || migrations === undefined || lastUsedAt === undefined) {
303
+ return undefined;
304
+ }
305
+ const at = new Date(lastUsedAt);
306
+ if (Number.isNaN(at.getTime()))
307
+ return undefined;
308
+ return { digest, migrations: Number.parseInt(migrations, 10), lastUsedAt: at };
309
+ }
310
+ /**
311
+ * The templates a sweep may drop.
312
+ *
313
+ * Keying templates by their migration set is what stops two branches sharing
314
+ * one; the cost of it is that a branch leaves its set's template behind when it
315
+ * merges, so something has to collect them. Same three conditions as the run
316
+ * database sweep — a name this module generated, nothing connected, nobody
317
+ * using it lately — with the age read from the template's own provenance
318
+ * comment rather than from its name, because a template's useful age is when it
319
+ * was last *used* and not when it was built.
320
+ *
321
+ * A template with no readable provenance is stale immediately. It is a database
322
+ * no run would clone anyway (provisioning rebuilds one it cannot identify), so
323
+ * leaving it costs disk and buys nothing. The pre-#289 `<base>_tpl` is not in
324
+ * the population at all — `parseTemplateDatabaseName` refuses it — because an
325
+ * invocation running older code clones that one *during* its run, and dropping
326
+ * it under such a run would fail it for a reason in nobody's diff.
327
+ */
328
+ export function staleTemplates(input) {
329
+ const selected = [];
330
+ for (const name of input.names) {
331
+ if (selected.length >= input.limit)
332
+ break;
333
+ if (name === input.keep)
334
+ continue;
335
+ if (input.busy.has(name))
336
+ continue;
337
+ if (!parseTemplateDatabaseName(input.base, name))
338
+ continue;
339
+ const lastUsed = input.lastUsed.get(name);
340
+ if (lastUsed && input.now.getTime() - lastUsed.getTime() < input.maxAgeMs)
341
+ continue;
342
+ selected.push(name);
343
+ }
344
+ return selected;
345
+ }
346
+ export function randomRunToken() {
347
+ return randomBytes(3).toString('hex');
348
+ }
349
+ /**
350
+ * `<base>_r_<epoch-seconds base36>_<6 hex>`.
351
+ *
352
+ * The instant is in the name because `pg_database` has no creation timestamp
353
+ * and the sweep needs an age. The random tail is what makes two invocations
354
+ * starting in the same second distinct.
355
+ */
356
+ export function runDatabaseName(base, createdAt = new Date(), token = randomRunToken()) {
357
+ assertSafeIdentifier(base);
358
+ const stamp = Math.floor(createdAt.getTime() / 1000).toString(36);
359
+ return assertUsableDatabaseName(`${base}${RUN_INFIX}${stamp}_${token}`);
360
+ }
361
+ export function parseRunDatabaseName(base, name) {
362
+ if (!/^[A-Za-z0-9_]+$/.test(base))
363
+ return undefined;
364
+ const match = new RegExp(`^${base}${RUN_INFIX}([0-9a-z]+)_([0-9a-f]{6})$`).exec(name);
365
+ if (!match)
366
+ return undefined;
367
+ const seconds = Number.parseInt(match[1], 36);
368
+ if (!Number.isFinite(seconds))
369
+ return undefined;
370
+ return { createdAt: new Date(seconds * 1000) };
371
+ }
372
+ /**
373
+ * The stranded run databases a sweep may drop.
374
+ *
375
+ * Three conditions, and each one is load-bearing: the name is one this module
376
+ * generated (so no foreign database is ever a candidate), it is older than the
377
+ * threshold (so a live run is not taken for a corpse — the complete suite takes
378
+ * the better part of an hour), and nothing is connected to it right now (a
379
+ * booting file closes the ORM between files, so idleness alone would not be
380
+ * enough, which is why it is `and` rather than `or`).
381
+ */
382
+ export function strandedRunDatabases(input) {
383
+ const selected = [];
384
+ for (const name of input.names) {
385
+ if (selected.length >= input.limit)
386
+ break;
387
+ if (name === input.keep)
388
+ continue;
389
+ if (input.busy.has(name))
390
+ continue;
391
+ const parsed = parseRunDatabaseName(input.base, name);
392
+ if (!parsed)
393
+ continue;
394
+ if (input.now.getTime() - parsed.createdAt.getTime() < input.maxAgeMs)
395
+ continue;
396
+ selected.push(name);
397
+ }
398
+ return selected;
399
+ }
400
+ /** The same DSN, pointed at another database on the same server. */
401
+ export function withDatabase(url, database) {
402
+ const parsed = new URL(url);
403
+ parsed.pathname = `/${database}`;
404
+ return parsed.toString();
405
+ }
406
+ export function databaseNameOf(url) {
407
+ return decodeURIComponent(new URL(url).pathname).replace(/^\//, '');
408
+ }
409
+ /** The same Redis DSN, pointed at a logical database index. */
410
+ export function redisUrlWithDatabase(url, index) {
411
+ if (!Number.isInteger(index) || index < 0) {
412
+ throw new Error(`redis database index must be a non-negative integer (got ${index})`);
413
+ }
414
+ const parsed = new URL(url);
415
+ parsed.pathname = `/${index}`;
416
+ return parsed.toString();
417
+ }
418
+ /** Whether a Redis DSN already names a logical database — an explicit choice this module leaves alone. */
419
+ export function redisUrlNamesDatabase(url) {
420
+ const path = new URL(url).pathname.replace(/^\//, '');
421
+ return path !== '' && /^\d+$/.test(path);
422
+ }
423
+ /** Identifies this invocation in a Redis lease and in the log lines. */
424
+ export function runIdentity() {
425
+ return `${hostname()}:${process.pid}:${randomRunToken()}`;
426
+ }
427
+ /**
428
+ * A 32-bit key for `pg_advisory_lock`, derived from the template name so two
429
+ * bases do not serialize against each other. FNV-1a, folded into a signed int.
430
+ */
431
+ export function advisoryLockKey(name) {
432
+ let hash = 0x811c9dc5;
433
+ for (let i = 0; i < name.length; i += 1) {
434
+ hash ^= name.charCodeAt(i);
435
+ hash = Math.imul(hash, 0x01000193);
436
+ }
437
+ return hash | 0;
438
+ }
439
+ /**
440
+ * Whether the template is a database this run's migrations could have produced.
441
+ *
442
+ * The template is **long-lived and shared by every branch on the machine**, and
443
+ * it is only ever moved forward by `migrator.up()`, which appends. That is
444
+ * enough exactly when what is already applied is a **prefix** of the order this
445
+ * run configures: then the pending ones land after it, and the result is the
446
+ * order a database migrated from scratch today would have. Anything else —
447
+ * another branch's migration, or one of ours applied somewhere the order does
448
+ * not put it — is a database no `up()` can repair, so it is rebuilt.
449
+ *
450
+ * Both shapes are ordinary here, not exotic. Five agents share one cluster, so
451
+ * a template routinely carries a migration the branch running has never heard
452
+ * of; and `orderMigrations` places a migration by its module's position in the
453
+ * dependency graph, so a migration added anywhere but the last module lands at
454
+ * the **end** of an incrementally-migrated template and in the **middle** of a
455
+ * fresh one.
456
+ *
457
+ * That is not cosmetic. It broke
458
+ * `test/integration/catalog/attributes-migration-parity.test.ts`, which drives
459
+ * the real migrator: umzug reverts the last migration in *configured* order,
460
+ * while the test reads the last one in *applied* (`id`) order, and the two stop
461
+ * agreeing the moment the template stops being a prefix. The test then walked
462
+ * `down()` past its own target, never reached its termination condition, timed
463
+ * out, and left the run database with most of its schema reverted.
464
+ */
465
+ export function templateDrift(applied, expected, templateName = 'the migration template') {
466
+ const known = new Set(expected);
467
+ for (const [index, name] of applied.entries()) {
468
+ if (name === expected[index])
469
+ continue;
470
+ if (!known.has(name)) {
471
+ return {
472
+ kind: 'unknown-migration',
473
+ name,
474
+ message: `${templateName} has "${name}" applied, which this run's migration registry does ` +
475
+ `not contain — another branch migrated it. Rebuilding it from empty.`,
476
+ };
477
+ }
478
+ const inPosition = expected[index];
479
+ return {
480
+ kind: 'out-of-order',
481
+ name,
482
+ ...(inPosition === undefined ? {} : { expected: inPosition }),
483
+ message: `${templateName} has "${name}" applied at position ${index + 1}, where this run's ` +
484
+ `order has ${inPosition === undefined ? 'nothing' : `"${inPosition}"`} — an append ` +
485
+ `cannot move it. Rebuilding it from empty.`,
486
+ };
487
+ }
488
+ return undefined;
489
+ }
490
+ //# sourceMappingURL=run-isolation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-isolation.js","sourceRoot":"","sources":["../../src/database/run-isolation.ts"],"names":[],"mappings":"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"}