@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # `@endora-commerce/test-kit`
2
+
3
+ Composes an Endora Commerce platform for a test, from a composition its caller supplies.
4
+
5
+ `specs/109-backend-test-kit/` is the feature; `contracts/test-kit-package.md` is normative.
6
+
7
+ ---
8
+
9
+ ## Why it exists
10
+
11
+ `backend/test/helpers/test-server.ts` is 2993 lines and composes a platform by **finding**
12
+ one: the generated module list, the application's ORM configuration, the resolved manifest
13
+ registry, the overlay loader and the package loader. Every one of those is a fact about
14
+ *this repository's tree*, so a package cannot have any of them — which is why a module
15
+ package's server-bound test had nowhere to run but `backend/test`, and why 211 such files
16
+ are still there.
17
+
18
+ 68 % of a server-bound test file's wall clock is that composition (116.6 s of a 171 s run,
19
+ median 4700 ms). The kit does not make it cheaper. What it makes is **reachable**: the same
20
+ composition, from a package, for a module that this repository has never heard of.
21
+
22
+ ## The inversion
23
+
24
+ `composeTestServer` takes a `PlatformComposition` and never builds one. Its four members
25
+ are the four things only the caller knows:
26
+
27
+ | Member | What the caller supplies |
28
+ | --- | --- |
29
+ | `modules` | the module entries to compose — this repository's `MODULES` plus overlay plus discovered packages, or a third party's own set |
30
+ | `orm` | an opener and a closer |
31
+ | `manifests` | the loaded manifest registry the lifecycle needs |
32
+ | `testSupport` | the contributions of exactly the modules in `modules` |
33
+
34
+ Everything else it does is host-shaped and true of any platform: the container, the ORM
35
+ registrations, the audit writer, the event and command buses, the two Redis clients, the
36
+ registry cache, the settings and sales-channel kernels, one `composeModules` pass, one
37
+ contribution window, the tenancy request-scope hook, one boot phase, `buildServer`, and
38
+ `teardownTestServer`.
39
+
40
+ It does **not** read `process.env.DEPLOYMENT`, walk `node_modules` or read a generated
41
+ artefact. Each of those is a fact about the caller's process (D-104's predicate), and a kit
42
+ that answered them would answer them differently from the platform that composes for real.
43
+
44
+ ```ts
45
+ import { composeTestServer, teardownTestServer } from '@endora-commerce/test-kit/server';
46
+
47
+ const handle = await composeTestServer({
48
+ composition: { modules, orm: { open, close }, manifests },
49
+ buildTenantContext: async (request) => resolveTenantContext(actorOf(request)),
50
+ prepareDatabase: async ({ em }) => { /* truncate, seed */ },
51
+ });
52
+
53
+ const response = await handle.app.inject({ method: 'GET', url: '/api/v1/…' });
54
+ const service = handle.container.cradle.myService;
55
+
56
+ await teardownTestServer(handle);
57
+ ```
58
+
59
+ **Nothing on `TestServerHandle` names a module.** The application's own handle carries 29
60
+ module-specific fields today, and every one of them is a container resolution its readers
61
+ can make for themselves: `handle.container.cradle` is how a caller reaches a module's
62
+ service. Draining those fields is Phase 2.
63
+
64
+ ## What it refuses
65
+
66
+ A composition whose `modules` lacks a module declaring `activation.nonDeactivatable` is
67
+ refused by `composeModules` **before the first module registers** — with
68
+ `RequiredModuleAbsentError`, naming the module, the sentence its own manifest gives and the
69
+ remedy. The kit adds no second check and swallows nothing on that path, because a paraphrase
70
+ written here would be a second answer to a question the platform already answers.
71
+
72
+ That matters more than it sounds: 23 modules declare `nonDeactivatable`, and the union with
73
+ each module's transitive `dependencies` closure is a median of 23–24 of 70. So "just my
74
+ module" is not a composition anybody can run, and a stranger's first attempt is the one most
75
+ likely to meet this.
76
+
77
+ ## The subpaths
78
+
79
+ | Subpath | Contents |
80
+ | --- | --- |
81
+ | `./server` | `composeTestServer`, `teardownTestServer`, and the two types they take and return |
82
+ | `./database` | the per-invocation database lease (issue #189), and the naming and selection rules behind it |
83
+ | `./support` | `TestSupportContribution` — what a module publishes at its own `./test-support` |
84
+
85
+ There is **no root export**: a caller names the seam it wants, so a test that needs only the
86
+ lease does not load the server.
87
+
88
+ `./off-state` (`expectModuleAbsent`, `withModuleOff`) is in the contract's table and is not
89
+ here yet. `backend/test/helpers/off-state.ts` is `check:off-state-coverage`'s subject and
90
+ that check's caller walk is `backend/test/**`, so moving the file before the walk follows it
91
+ (T074) would take 46 modules' off-state proofs out of the population that judges them.
92
+
93
+ ## Running its tests
94
+
95
+ ```bash
96
+ pnpm --filter @endora-commerce/test-kit run test # service-free
97
+ pnpm --filter @endora-commerce/test-kit run test:services # needs PostgreSQL and Redis
98
+ ```
99
+
100
+ Two configurations rather than one with a condition, because a condition is a green that can
101
+ quietly become "not looking" (issue #113). CI's `test:frontend` job runs every non-backend
102
+ member's `test` script in an image with no service containers; `test:kit` runs the second
103
+ half beside the services.
104
+
105
+ The service-bound tests compose a fixture platform that is **nobody's module** — two
106
+ synthetic modules, one `@OrgScoped` entity, one route — because the kit may name no module
107
+ package (R1.5). That is the point rather than an inconvenience: what those tests have to
108
+ prove is that the seam works for a *stranger's* composition.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `./database` — the per-invocation database lease (feature 109, R1.1).
3
+ *
4
+ * The cheapest of the kit's subpaths and the one that proves the package
5
+ * shape: the mechanism (issue #189) never had a module coupling, only an
6
+ * application one, and that coupling is now two callbacks the caller supplies.
7
+ *
8
+ * What is published here is the seam plus the naming and selection rules, both
9
+ * halves for the same reason: the two things whose correctness protects the dev
10
+ * database and other people's databases are pure, and a consumer that wants to
11
+ * assert them — as `backend/test/unit/harness/run-isolation.test.ts` does —
12
+ * must be able to name them without a service.
13
+ */
14
+ export { assertTestDatabaseUrl, leaseRunDatabase, type RunDatabaseLease, type RunDatabaseLeaseInput, type TemplateIdentity, } from './lease.js';
15
+ export { BASE_DATABASE_URL_ENV, ISOLATION_ENV, KEEP_DATABASE_ENV, STALE_RUN_DATABASE_MS, STALE_TEMPLATE_MS, SWEEP_LIMIT, TEMPLATE_DATABASE_ENV, TEMPLATE_DIGEST_LENGTH, TEST_DATABASE_NAME_PATTERN, advisoryLockKey, databaseNameOf, formatTemplateProvenance, isolationMode, keepRunDatabase, parseRunDatabaseName, parseTemplateDatabaseName, parseTemplateProvenance, randomRunToken, redisUrlNamesDatabase, redisUrlWithDatabase, runDatabaseName, runIdentity, sharedDatabaseReason, staleTemplates, strandedRunDatabases, templateDatabaseName, templateDigest, templateDrift, templateFamilyName, withDatabase, type IsolationMode, type StaleTemplateSelection, type StrandedSelection, type TemplateDrift, type TemplateInputs, type TemplateProvenance, type TemplateSource, } from './run-isolation.js';
16
+ export { appliedMigrations, cloneTemplateForCaller, dropRunDatabase, leaseRedisDatabase, provisionRunDatabase, sweepStaleTemplates, sweepStrandedRunDatabases, type Log, type ProvisionInput, type RedisLease, type RunDatabase, } from './provision.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,eAAe,EACf,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,GAAG,EACR,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,WAAW,GACjB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `./database` — the per-invocation database lease (feature 109, R1.1).
3
+ *
4
+ * The cheapest of the kit's subpaths and the one that proves the package
5
+ * shape: the mechanism (issue #189) never had a module coupling, only an
6
+ * application one, and that coupling is now two callbacks the caller supplies.
7
+ *
8
+ * What is published here is the seam plus the naming and selection rules, both
9
+ * halves for the same reason: the two things whose correctness protects the dev
10
+ * database and other people's databases are pure, and a consumer that wants to
11
+ * assert them — as `backend/test/unit/harness/run-isolation.test.ts` does —
12
+ * must be able to name them without a service.
13
+ */
14
+ export { assertTestDatabaseUrl, leaseRunDatabase, } from './lease.js';
15
+ export { BASE_DATABASE_URL_ENV, ISOLATION_ENV, KEEP_DATABASE_ENV, STALE_RUN_DATABASE_MS, STALE_TEMPLATE_MS, SWEEP_LIMIT, TEMPLATE_DATABASE_ENV, TEMPLATE_DIGEST_LENGTH, TEST_DATABASE_NAME_PATTERN, advisoryLockKey, databaseNameOf, formatTemplateProvenance, isolationMode, keepRunDatabase, parseRunDatabaseName, parseTemplateDatabaseName, parseTemplateProvenance, randomRunToken, redisUrlNamesDatabase, redisUrlWithDatabase, runDatabaseName, runIdentity, sharedDatabaseReason, staleTemplates, strandedRunDatabases, templateDatabaseName, templateDigest, templateDrift, templateFamilyName, withDatabase, } from './run-isolation.js';
16
+ export { appliedMigrations, cloneTemplateForCaller, dropRunDatabase, leaseRedisDatabase, provisionRunDatabase, sweepStaleTemplates, sweepStrandedRunDatabases, } from './provision.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACL,qBAAqB,EACrB,gBAAgB,GAIjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,qBAAqB,EACrB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,iBAAiB,EACjB,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,0BAA0B,EAC1B,eAAe,EACf,cAAc,EACd,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,cAAc,EACd,qBAAqB,EACrB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACpB,oBAAoB,EACpB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,YAAY,GAQb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,eAAe,EACf,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,yBAAyB,GAK1B,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The per-invocation database lease, as a seam a caller drives (feature 109,
3
+ * T022; the mechanism is issue #189's).
4
+ *
5
+ * ## What it does, unchanged
6
+ *
7
+ * One `vitest run` gets its **own** database — `<base>_r_<stamp>_<rand>`, a
8
+ * `create database … template` clone of the migrated `<base>_tpl_<digest>` —
9
+ * and its **own** Redis logical database, leased in index 0. Both are released
10
+ * when the run ends and swept if it crashed. Before that, isolation used to be
11
+ * per database and per Redis instance and never per invocation, so a second
12
+ * `vitest run` landed its truncate inside the first one's setup, and every
13
+ * symptom of that is indistinguishable from a real failure.
14
+ *
15
+ * ## What moved, and why this is a seam rather than a script
16
+ *
17
+ * It was `backend/test/global-setup.ts`, whose isolated path could not be
18
+ * reached from outside `backend`. The two things in it that are *this
19
+ * repository's* — which migrations exist, and what "migrated" means for its
20
+ * schema — are the two the caller supplies:
21
+ *
22
+ * - **`identity`** is the digest over the caller's whole migration input.
23
+ * Two callers whose input differs get two templates and cannot contaminate
24
+ * each other; two that agree share one and pay one migration pass between
25
+ * them.
26
+ * - **`migrateTemplate`** is applied to the template while `DATABASE_URL`
27
+ * names it. It is a callback rather than a value for a reason that survives
28
+ * the move: an ORM configuration captures `DATABASE_URL` at import, once
29
+ * per process, so the import has to happen after this function has pointed
30
+ * that variable at the template.
31
+ *
32
+ * Nothing here reads a generated artefact, walks `node_modules` or consults
33
+ * `process.env.DEPLOYMENT`. Every one of those is a fact about the caller's
34
+ * process (R2.2), and a kit that answered them would answer them differently
35
+ * from the platform that composes for real.
36
+ *
37
+ * ## The two documented overrides behave exactly as they did
38
+ *
39
+ * `BACKEND_TEST_ISOLATION=shared` restores the pre-#189 behaviour of every
40
+ * invocation sharing one database, and `BACKEND_TEST_KEEP_DATABASE=1` keeps the
41
+ * run's database for a post-mortem instead of dropping it. Both are read here,
42
+ * from the same two constants the naming rules use — see
43
+ * `backend/test/README.md` § *One database per invocation*, which is still the
44
+ * document that describes them.
45
+ */
46
+ import { type Log, type ProvisionInput } from './provision.js';
47
+ /** The migration set this run's platform is, exactly as `provisionRunDatabase` takes it. */
48
+ export type TemplateIdentity = ProvisionInput['identity'];
49
+ export interface RunDatabaseLeaseInput {
50
+ /**
51
+ * The base test DSN. The run database and the template are **named from it**,
52
+ * and it is never itself what the workers connect to on the isolated path.
53
+ */
54
+ readonly baseUrl: string;
55
+ /** Which migration set this run has — the caller's, because the migrations are. */
56
+ readonly identity: TemplateIdentity;
57
+ /**
58
+ * Apply the caller's schema to the template database at this URL.
59
+ *
60
+ * Called with `DATABASE_URL` already pointing at the template, so an ORM
61
+ * configuration imported inside it captures the right database.
62
+ */
63
+ readonly migrateTemplate: (templateUrl: string) => Promise<void>;
64
+ /**
65
+ * The migration class names this run applies, in order.
66
+ *
67
+ * Used on the **shared** path only, and only to warn: a shared database
68
+ * accumulates a migration from another branch, and a test that drives the
69
+ * migrator then reads that as a failure of the code under test. Nothing here
70
+ * rebuilds a shared database — it is the operator's, named by them.
71
+ */
72
+ readonly configuredMigrationNames?: () => Promise<readonly string[]>;
73
+ /** Defaults to `REDIS_URL`, or `redis://localhost:6379`. */
74
+ readonly redisUrl?: string;
75
+ /** Defaults to `process.env`, which is also what the lease writes into. */
76
+ readonly env?: NodeJS.ProcessEnv;
77
+ readonly log?: Log;
78
+ }
79
+ export interface RunDatabaseLease {
80
+ /** Which path this lease took. `shared` means nothing was provisioned. */
81
+ readonly mode: 'per-invocation' | 'shared';
82
+ /** The DSN every worker of this invocation will use — also written to `DATABASE_URL`. */
83
+ readonly databaseUrl: string;
84
+ /** The run database's name, or `undefined` on the shared path. */
85
+ readonly databaseName: string | undefined;
86
+ /** The template this run was cloned from, or `undefined` on the shared path. */
87
+ readonly templateName: string | undefined;
88
+ /** The Redis DSN this invocation owns, or `undefined` when the caller's already named one. */
89
+ readonly redisUrl: string | undefined;
90
+ /** Drop the run database and release the Redis index. Idempotent. */
91
+ readonly release: () => Promise<void>;
92
+ }
93
+ /**
94
+ * The `_test` judgement, applied to the base DSN before anything is created.
95
+ *
96
+ * Exported because it is the guard a caller wants **before** it decides to
97
+ * lease at all, and because a caller that resolves its own DSN should not
98
+ * reimplement the refusal. `ALLOW_NON_TEST_DATABASE_URL=1` is the documented
99
+ * override and stands down the isolation with it (see `sharedDatabaseReason`):
100
+ * a name derived from a base that already breaks the convention would break it
101
+ * too, and isolation refuses to widen the judgement.
102
+ */
103
+ export declare function assertTestDatabaseUrl(url: string, env?: NodeJS.ProcessEnv): string;
104
+ /**
105
+ * Give this invocation its own database and its own Redis logical database.
106
+ *
107
+ * Writes `DATABASE_URL`, `BACKEND_TEST_BASE_URL`, `BACKEND_TEST_TEMPLATE` and
108
+ * `REDIS_URL` into `env` (which defaults to `process.env`), because a vitest
109
+ * worker inherits the parent's environment and that inheritance **is** the
110
+ * mechanism by which a run's actual DSN reaches the files that use it. A caller
111
+ * that spawns a CLI from a test reads `DATABASE_URL`, never `TEST_DATABASE_URL`
112
+ * — the second names the base.
113
+ */
114
+ export declare function leaseRunDatabase(input: RunDatabaseLeaseInput): Promise<RunDatabaseLease>;
115
+ //# sourceMappingURL=lease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../../src/database/lease.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAgBH,OAAO,EAML,KAAK,GAAG,EACR,KAAK,cAAc,EAEpB,MAAM,gBAAgB,CAAC;AAExB,4FAA4F;AAC5F,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,UAAU,CAAC,CAAC;AAE1D,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE;;;;;;;OAOG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IACrE,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,gBAAgB,GAAG,QAAQ,CAAC;IAC3C,yFAAyF;IACzF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,gFAAgF;IAChF,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1C,8FAA8F;IAC9F,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAID;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAU/F;AA6BD;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA8G9F"}
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The per-invocation database lease, as a seam a caller drives (feature 109,
3
+ * T022; the mechanism is issue #189's).
4
+ *
5
+ * ## What it does, unchanged
6
+ *
7
+ * One `vitest run` gets its **own** database — `<base>_r_<stamp>_<rand>`, a
8
+ * `create database … template` clone of the migrated `<base>_tpl_<digest>` —
9
+ * and its **own** Redis logical database, leased in index 0. Both are released
10
+ * when the run ends and swept if it crashed. Before that, isolation used to be
11
+ * per database and per Redis instance and never per invocation, so a second
12
+ * `vitest run` landed its truncate inside the first one's setup, and every
13
+ * symptom of that is indistinguishable from a real failure.
14
+ *
15
+ * ## What moved, and why this is a seam rather than a script
16
+ *
17
+ * It was `backend/test/global-setup.ts`, whose isolated path could not be
18
+ * reached from outside `backend`. The two things in it that are *this
19
+ * repository's* — which migrations exist, and what "migrated" means for its
20
+ * schema — are the two the caller supplies:
21
+ *
22
+ * - **`identity`** is the digest over the caller's whole migration input.
23
+ * Two callers whose input differs get two templates and cannot contaminate
24
+ * each other; two that agree share one and pay one migration pass between
25
+ * them.
26
+ * - **`migrateTemplate`** is applied to the template while `DATABASE_URL`
27
+ * names it. It is a callback rather than a value for a reason that survives
28
+ * the move: an ORM configuration captures `DATABASE_URL` at import, once
29
+ * per process, so the import has to happen after this function has pointed
30
+ * that variable at the template.
31
+ *
32
+ * Nothing here reads a generated artefact, walks `node_modules` or consults
33
+ * `process.env.DEPLOYMENT`. Every one of those is a fact about the caller's
34
+ * process (R2.2), and a kit that answered them would answer them differently
35
+ * from the platform that composes for real.
36
+ *
37
+ * ## The two documented overrides behave exactly as they did
38
+ *
39
+ * `BACKEND_TEST_ISOLATION=shared` restores the pre-#189 behaviour of every
40
+ * invocation sharing one database, and `BACKEND_TEST_KEEP_DATABASE=1` keeps the
41
+ * run's database for a post-mortem instead of dropping it. Both are read here,
42
+ * from the same two constants the naming rules use — see
43
+ * `backend/test/README.md` § *One database per invocation*, which is still the
44
+ * document that describes them.
45
+ */
46
+ import { BASE_DATABASE_URL_ENV, ISOLATION_ENV, KEEP_DATABASE_ENV, TEMPLATE_DATABASE_ENV, TEST_DATABASE_NAME_PATTERN, databaseNameOf, keepRunDatabase, redisUrlNamesDatabase, runIdentity, sharedDatabaseReason, templateDrift, withDatabase, } from './run-isolation.js';
47
+ import { appliedMigrations, dropRunDatabase, leaseRedisDatabase, provisionRunDatabase, sweepStrandedRunDatabases, } from './provision.js';
48
+ const DEFAULT_REDIS_URL = 'redis://localhost:6379';
49
+ /**
50
+ * The `_test` judgement, applied to the base DSN before anything is created.
51
+ *
52
+ * Exported because it is the guard a caller wants **before** it decides to
53
+ * lease at all, and because a caller that resolves its own DSN should not
54
+ * reimplement the refusal. `ALLOW_NON_TEST_DATABASE_URL=1` is the documented
55
+ * override and stands down the isolation with it (see `sharedDatabaseReason`):
56
+ * a name derived from a base that already breaks the convention would break it
57
+ * too, and isolation refuses to widen the judgement.
58
+ */
59
+ export function assertTestDatabaseUrl(url, env = process.env) {
60
+ const dbName = databaseNameOf(url);
61
+ if (!TEST_DATABASE_NAME_PATTERN.test(dbName) && !env['ALLOW_NON_TEST_DATABASE_URL']) {
62
+ throw new Error(`Refusing to run tests against database "${dbName}" — name must contain "_test" ` +
63
+ `(e.g. b2b_test). Set ALLOW_NON_TEST_DATABASE_URL=1 to override. ` +
64
+ `Resolved URL: ${url}`);
65
+ }
66
+ return url;
67
+ }
68
+ /** Create the base database if it is not there, so a first run on a fresh cluster works. */
69
+ async function ensureDatabaseExists(testUrl, log) {
70
+ const dbName = databaseNameOf(testUrl);
71
+ const { Client: PgClient } = await import('pg');
72
+ const admin = new PgClient({ connectionString: withDatabase(testUrl, 'postgres') });
73
+ await admin.connect();
74
+ try {
75
+ const { rowCount } = await admin.query('select 1 from pg_database where datname = $1', [
76
+ dbName,
77
+ ]);
78
+ // `pg` types `rowCount` as `number | null`; for this existence probe `0`
79
+ // and `null` both honestly mean "no such database", and the value reaches
80
+ // nothing but this comparison.
81
+ if (rowCount === null || rowCount === 0) {
82
+ // A `pg_database` name cannot be parameterised in `create database`; the
83
+ // `_test` judgement above plus URL parsing keeps this from being
84
+ // injectable, and the re-check refuses anything the two let through.
85
+ const safeName = dbName.replace(/[^a-zA-Z0-9_]/g, '');
86
+ if (safeName !== dbName)
87
+ throw new Error(`Unsafe DB name: "${dbName}"`);
88
+ await admin.query(`create database "${safeName}"`);
89
+ log(`[test-kit] created database ${safeName}`);
90
+ }
91
+ }
92
+ finally {
93
+ await admin.end();
94
+ }
95
+ }
96
+ /**
97
+ * Give this invocation its own database and its own Redis logical database.
98
+ *
99
+ * Writes `DATABASE_URL`, `BACKEND_TEST_BASE_URL`, `BACKEND_TEST_TEMPLATE` and
100
+ * `REDIS_URL` into `env` (which defaults to `process.env`), because a vitest
101
+ * worker inherits the parent's environment and that inheritance **is** the
102
+ * mechanism by which a run's actual DSN reaches the files that use it. A caller
103
+ * that spawns a CLI from a test reads `DATABASE_URL`, never `TEST_DATABASE_URL`
104
+ * — the second names the base.
105
+ */
106
+ export async function leaseRunDatabase(input) {
107
+ const env = input.env ?? process.env;
108
+ const log = input.log ?? ((message) => process.stdout.write(`${message}\n`));
109
+ const baseUrl = assertTestDatabaseUrl(input.baseUrl, env);
110
+ // The pre-#189 behaviour, on either of two grounds. The first is an explicit
111
+ // opt-out, worth having for a post-mortem — the run database is dropped when
112
+ // the run ends, and sometimes what you want is the database a failing run
113
+ // left behind, under a name you already know. The second is
114
+ // ALLOW_NON_TEST_DATABASE_URL: that override is somebody deliberately
115
+ // pointing the suite at a database whose name breaks the convention, and a
116
+ // name derived from it would break it too.
117
+ const shared = sharedDatabaseReason(env, databaseNameOf(baseUrl));
118
+ if (shared !== undefined) {
119
+ env['DATABASE_URL'] = baseUrl;
120
+ await ensureDatabaseExists(baseUrl, log);
121
+ await input.migrateTemplate(baseUrl);
122
+ log(`[test-kit] ${shared === 'explicit' ? `${ISOLATION_ENV}=shared` : 'ALLOW_NON_TEST_DATABASE_URL'}` +
123
+ ` — this invocation shares ${databaseNameOf(baseUrl)} with every other one.`);
124
+ // The shared database accumulates the same way a template does — a
125
+ // migration from another branch, or one of ours appended where the order
126
+ // does not put it — and a test that drives the migrator reads that as a
127
+ // failure of the code under test. This path only *says* so: the database is
128
+ // the operator's, named by them or kept by them for a post-mortem, so it is
129
+ // not this lease's to drop.
130
+ if (input.configuredMigrationNames !== undefined) {
131
+ const drift = templateDrift(await appliedMigrations(baseUrl), await input.configuredMigrationNames(), databaseNameOf(baseUrl));
132
+ if (drift) {
133
+ log(`[test-kit] WARNING: ${drift.message.replace(' Rebuilding it from empty.', '')} ` +
134
+ `Nothing here rebuilds it, because you named it. A test that drives the migrator ` +
135
+ `will fail against it for that reason and not for its own — drop it and re-run, ` +
136
+ `or drop ${ISOLATION_ENV}=shared and let the isolated path rebuild its template.`);
137
+ }
138
+ }
139
+ return {
140
+ mode: 'shared',
141
+ databaseUrl: baseUrl,
142
+ databaseName: undefined,
143
+ templateName: undefined,
144
+ redisUrl: undefined,
145
+ release: async () => {
146
+ /* nothing was provisioned, so there is nothing to give back */
147
+ },
148
+ };
149
+ }
150
+ const run = await provisionRunDatabase({
151
+ baseUrl,
152
+ identity: input.identity,
153
+ migrateTemplate: async (templateUrl) => {
154
+ env['DATABASE_URL'] = templateUrl;
155
+ await input.migrateTemplate(templateUrl);
156
+ },
157
+ log,
158
+ });
159
+ env['DATABASE_URL'] = run.url;
160
+ // A file that drives the real migrator may not do it to the database its
161
+ // neighbours share — the run database is this invocation's only copy, so a
162
+ // migration sequence that dies half-way takes the whole invocation with it.
163
+ // Exporting the base and the template *name* is what lets such a file clone
164
+ // the same template this run was cloned from: by name, because by the time it
165
+ // asks, another invocation may have built a template of its own.
166
+ env[BASE_DATABASE_URL_ENV] = baseUrl;
167
+ env[TEMPLATE_DATABASE_ENV] = run.template;
168
+ // The same defect on the other service: a batch that "passed alone" would
169
+ // still have collided on Redis keys. An explicit index in REDIS_URL is
170
+ // somebody's deliberate choice and is left alone.
171
+ const redisBaseUrl = input.redisUrl ?? env['REDIS_URL'] ?? DEFAULT_REDIS_URL;
172
+ let lease;
173
+ if (redisUrlNamesDatabase(redisBaseUrl)) {
174
+ log(`[test-kit] REDIS_URL already names a logical database — leaving it alone.`);
175
+ }
176
+ else {
177
+ lease = await leaseRedisDatabase(redisBaseUrl, runIdentity(), log);
178
+ env['REDIS_URL'] = lease.url;
179
+ }
180
+ // Runs that crashed before their release. Best-effort, capped, and only ever
181
+ // over names this mechanism generated.
182
+ await sweepStrandedRunDatabases(baseUrl, run.name, log);
183
+ let released = false;
184
+ return {
185
+ mode: 'per-invocation',
186
+ databaseUrl: run.url,
187
+ databaseName: run.name,
188
+ templateName: run.template,
189
+ redisUrl: lease?.url,
190
+ release: async () => {
191
+ if (released)
192
+ return;
193
+ released = true;
194
+ await lease?.release();
195
+ if (keepRunDatabase(env)) {
196
+ log(`[test-kit] ${KEEP_DATABASE_ENV} is set — keeping ${run.name}. ` +
197
+ `Drop it yourself, or leave it for the sweep in ~4 h.`);
198
+ return;
199
+ }
200
+ await dropRunDatabase(baseUrl, run.name, log);
201
+ },
202
+ };
203
+ }
204
+ //# sourceMappingURL=lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.js","sourceRoot":"","sources":["../../src/database/lease.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * The half of per-invocation isolation (issue #189) that talks to a server.
3
+ *
4
+ * Split from `run-isolation.ts` so the naming and selection rules — the two
5
+ * things whose correctness protects the dev database and other people's
6
+ * databases — are unit-testable with no service at all, and run in the fast
7
+ * unit job. Nothing here is imported by a test file: `leaseRunDatabase` in
8
+ * `./lease.ts` is the only caller, and a vitest `globalSetup` calls that once
9
+ * per invocation in the parent process.
10
+ *
11
+ * `pg` and `ioredis` are imported dynamically for the same reason: a run that
12
+ * declared `BACKEND_TEST_SERVICES=none` never reaches this module, and should
13
+ * not pay to load a driver it will not dial.
14
+ */
15
+ import type { Client } from 'pg';
16
+ export type Log = (message: string) => void;
17
+ /**
18
+ * The migration names applied to a database, in the order they were applied.
19
+ *
20
+ * `mikro_orm_migrations` is missing on a template that has just been created
21
+ * and on one that has never been migrated; both mean "nothing applied", which
22
+ * is a prefix of every order and so needs no rebuild.
23
+ */
24
+ export declare function appliedMigrations(databaseUrl: string): Promise<string[]>;
25
+ export interface RunDatabase {
26
+ /** The DSN every worker of this invocation will use. */
27
+ readonly url: string;
28
+ readonly name: string;
29
+ readonly template: string;
30
+ }
31
+ export interface ProvisionInput {
32
+ /** The DSN naming the base database, from TEST_DATABASE_URL or the default. */
33
+ readonly baseUrl: string;
34
+ /**
35
+ * Which migration set this run has — the caller's, because the migrations are.
36
+ *
37
+ * A **value**, unlike the callback this used to be, because a caller reads its
38
+ * own migration order out of whatever module assigns it and hands the answer
39
+ * over. That reading is what the callback existed to delay: **an ORM
40
+ * configuration captures `DATABASE_URL` at import**, once per process, and the
41
+ * parent used to import it while that variable was unset, whose fallback is
42
+ * the caller's *dev* database. `migrateTemplate` is still a callback for
43
+ * exactly that reason.
44
+ */
45
+ readonly identity: {
46
+ readonly digest: string;
47
+ readonly migrations: readonly string[];
48
+ readonly migrationFiles: number;
49
+ /**
50
+ * What each producer contributed and what the walk found of it. Printed
51
+ * rather than merely floored: a shortfall stops the run by name, but only
52
+ * this line says an origin was read at all, and "the walk saw 7% of itself"
53
+ * is the failure a number with no population behind it cannot report.
54
+ */
55
+ readonly origins: readonly {
56
+ readonly origin: string;
57
+ readonly registered: number;
58
+ readonly migrationFiles: number;
59
+ }[];
60
+ };
61
+ /** Applies pending migrations to the template and establishes the platform invariants. */
62
+ readonly migrateTemplate: (templateUrl: string) => Promise<void>;
63
+ readonly log?: Log;
64
+ }
65
+ /**
66
+ * Give this invocation its own database.
67
+ *
68
+ * Everything between taking the advisory lock and releasing it is serialized
69
+ * across invocations: identify the template, build it if it is not there or not
70
+ * what it claims, migrate it, disconnect from it, clone, collect the templates
71
+ * nobody uses any more. Holding the lock across the clone as well is deliberate
72
+ * — the clone is a fraction of a second, and it is what guarantees the template
73
+ * is idle at the moment the next invocation's migration opens a connection to
74
+ * it.
75
+ *
76
+ * The lock is taken on the `postgres` maintenance database: PostgreSQL advisory
77
+ * locks are scoped to a database, so every waiter has to agree on which, and it
78
+ * must not be the template — a session holding the lock there is exactly the
79
+ * session that would make the clone illegal. It is one key for the whole
80
+ * `<base>_tpl*` family rather than one per digest, because the sweep walks the
81
+ * templates of other digests (see `templateFamilyName`).
82
+ *
83
+ * **Which template, is the question this function exists to answer** (issue
84
+ * #289). It is `<base>_tpl_<digest>`, the digest being over this run's whole
85
+ * migration input, so a template is shared only by runs whose platform it
86
+ * actually is. Two things then have to agree before it is cloned: the
87
+ * provenance comment the build wrote on it, and the migration names it actually
88
+ * holds. Either disagreeing rebuilds it from empty — including "it says
89
+ * nothing", because a template that cannot say what it holds is the defect.
90
+ */
91
+ export declare function provisionRunDatabase(input: ProvisionInput): Promise<RunDatabase>;
92
+ /**
93
+ * A database of this run's template, for a caller that is not the invocation.
94
+ *
95
+ * The template is named by the caller, not re-derived here, and that is the
96
+ * point (issue #289): the lease resolved which template this run's
97
+ * platform is and exported the name, so a file cloning one mid-run gets the
98
+ * database the run itself was cloned from — not whatever `<base>_tpl` has
99
+ * become while the run has been going, which with several branches on one
100
+ * cluster is routinely another branch's schema.
101
+ *
102
+ * The advisory lock is the same one provisioning takes, and for the same
103
+ * reason: it is what keeps the template idle at the moment a clone starts.
104
+ * `setupMigratorTestDb` is the only caller — a test file that drives the real
105
+ * migrator and must not do it to the database its neighbours share.
106
+ */
107
+ export declare function cloneTemplateForCaller(baseUrl: string, template: string): Promise<RunDatabase>;
108
+ /**
109
+ * Drop the templates of migration sets nobody runs any more.
110
+ *
111
+ * Keying a template by its migration set is what stops two branches sharing
112
+ * one; this is the other half of that bargain, because a branch leaves its
113
+ * set's template behind the moment it gains a migration or is rebased. Called
114
+ * under the provisioning lock, so no template can be half-built while it runs.
115
+ *
116
+ * Best-effort and never fatal: a sweep that fails is a disk-space problem, not
117
+ * a reason to fail somebody's test run.
118
+ */
119
+ export declare function sweepStaleTemplates(admin: Client, base: string, keep: string, log?: Log): Promise<string[]>;
120
+ export declare function dropRunDatabase(baseUrl: string, name: string, log?: Log): Promise<void>;
121
+ /**
122
+ * Drop the run databases of invocations that crashed before their teardown.
123
+ *
124
+ * Best-effort and never fatal: a sweep that fails is a disk-space problem, not
125
+ * a reason to fail somebody's test run.
126
+ */
127
+ export declare function sweepStrandedRunDatabases(baseUrl: string, keep: string, log?: Log): Promise<string[]>;
128
+ export interface RedisLease {
129
+ readonly url: string;
130
+ readonly index: number;
131
+ readonly runId: string;
132
+ readonly release: () => Promise<void>;
133
+ }
134
+ export declare function leaseRedisDatabase(redisUrl: string, runId: string, log?: Log): Promise<RedisLease>;
135
+ //# sourceMappingURL=provision.d.ts.map