@geonosis/testbed 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.
- package/LICENSE +202 -0
- package/README.md +437 -0
- package/bin/geonosis-testbed.mjs +4 -0
- package/dist/chunk-ULJUJOSR.js +51 -0
- package/dist/chunk-UZPJWPL3.js +59 -0
- package/dist/cli.js +201 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +235 -0
- package/dist/medusa-built/index.d.ts +82 -0
- package/dist/medusa-built/index.js +91 -0
- package/dist/types-D-uHas5h.d.ts +68 -0
- package/dist/workerd/index.d.ts +117 -0
- package/dist/workerd/index.js +143 -0
- package/package.json +45 -0
- package/templates/ci/db-matrix.yml +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# @geonosis/testbed
|
|
2
|
+
|
|
3
|
+
An integration harness is not its engine. It is the **arrangement around one engine call**: where
|
|
4
|
+
fixtures live and that nothing outside them is readable, what a database is called and how long it
|
|
5
|
+
lives, what the network may reach, and who boots it. Two shipping repos wrote that arrangement
|
|
6
|
+
separately — dielime around `medusaIntegrationTestRunner`, during.day around workerd and a Postgres
|
|
7
|
+
cell — and neither one's arrangement needs its engine in order to be expressed. This package is the
|
|
8
|
+
arrangement; the engines stay peer dependencies of the repo that runs them.
|
|
9
|
+
|
|
10
|
+
What the two harnesses really do, measured file by file, is in
|
|
11
|
+
[`docs/testbed-inventory-2026-08-30.md`](../../docs/testbed-inventory-2026-08-30.md).
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add -D @geonosis/testbed
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## The fixtures law
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { fixturesOnly } from '@geonosis/testbed'
|
|
21
|
+
|
|
22
|
+
const fixture = fixturesOnly(new URL('./__fixtures__', import.meta.url).pathname)
|
|
23
|
+
|
|
24
|
+
fixture('invoices', 'march.yaml') // -> /…/__fixtures__/invoices/march.yaml
|
|
25
|
+
fixture('..', 'Purchase Orders & Specs') // throws
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
during.day's §4.3 — "a test reads and writes ONLY `apps/web/__tests__/__fixtures__/`" — exists
|
|
29
|
+
because the directory next door in that repo holds real customers' documents. Its own guard compares
|
|
30
|
+
strings, which three shapes get past, and all three are refused here:
|
|
31
|
+
|
|
32
|
+
| shape | what a string-prefix guard does |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `fixture('/etc/passwd')` | `resolve` reads /etc/passwd; `join` quietly reads `<root>/etc/passwd` |
|
|
35
|
+
| a sibling named `<root>-production` | `resolved.startsWith(root)` is true, and it is a different directory |
|
|
36
|
+
| a symlink inside the root pointing out | every character of the path is inside the root |
|
|
37
|
+
|
|
38
|
+
The first is refused for naming an absolute path rather than reinterpreted; the second by comparing
|
|
39
|
+
against `root + path.sep`; the third by resolving through `realpath`. A path that does not exist yet
|
|
40
|
+
— a fixture about to be written — is judged as if it had been.
|
|
41
|
+
|
|
42
|
+
## Defining a testbed
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { createHash } from 'node:crypto'
|
|
46
|
+
import { defineTestbed } from '@geonosis/testbed'
|
|
47
|
+
|
|
48
|
+
export const testbed = defineTestbed({
|
|
49
|
+
adapter: myAdapter,
|
|
50
|
+
database: {
|
|
51
|
+
name: (suite) => createHash('sha1').update(suite).digest('hex').slice(0, 12),
|
|
52
|
+
per: 'suite',
|
|
53
|
+
},
|
|
54
|
+
fixtures: new URL('./__fixtures__', import.meta.url).pathname,
|
|
55
|
+
network: 'none',
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`per` is `'suite'` (dielime: one database per `describe`), `'file'`, or `'run'` (during.day: one
|
|
60
|
+
cell for the whole run, tables emptied per test).
|
|
61
|
+
|
|
62
|
+
Everything it can refuse, it refuses **when it is defined**, not at the first boot:
|
|
63
|
+
|
|
64
|
+
- an adapter with no `id` — every diagnostic names it
|
|
65
|
+
- an adapter declaring `snapshot` without `restore`, or the reverse: half of per-test isolation is
|
|
66
|
+
not isolation
|
|
67
|
+
- a `per` it does not know — a config read from JSON has whatever spelling someone typed
|
|
68
|
+
- `network: { allow: [] }` — an allow list of nothing is `'none'` spelt so it reads as permission
|
|
69
|
+
- no fixtures directory
|
|
70
|
+
|
|
71
|
+
And when a database name is derived:
|
|
72
|
+
|
|
73
|
+
- an empty name
|
|
74
|
+
- a name over **63 bytes** — Postgres truncates there, and the truncation is a NOTICE nobody reads
|
|
75
|
+
- a name a **different key already took**. `sha1(description).slice(0, 12)` is 48 bits; two suites
|
|
76
|
+
that collide share a database, and the second one's reset empties the first one's rows with both
|
|
77
|
+
green. Neither source repo notices this; this refuses it.
|
|
78
|
+
|
|
79
|
+
## The adapter contract
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
type TestbedAdapter<Db> = {
|
|
83
|
+
id: string
|
|
84
|
+
boot: (ctx: BootContext) => Promise<{ db: Db; teardown: () => Promise<void> }>
|
|
85
|
+
snapshot?: () => Promise<void>
|
|
86
|
+
restore?: () => Promise<void>
|
|
87
|
+
waitFor?: (kind: string, predicate: () => boolean | Promise<boolean>) => Promise<void>
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`boot` is all that is required. The rest is what a particular engine can do: Medusa snapshots to a
|
|
92
|
+
Postgres TEMPLATE database and restores from it before every test; during.day's cell cannot, and
|
|
93
|
+
truncates instead. The `BootContext` is **frozen**, including its allow-list array — an adapter that
|
|
94
|
+
wrote its own state onto it would carry it into the next boot.
|
|
95
|
+
|
|
96
|
+
## The conformance suite
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { runAdapterConformance } from '@geonosis/testbed'
|
|
100
|
+
|
|
101
|
+
it('conforms', async () => {
|
|
102
|
+
const report = await runAdapterConformance(myAdapter, {
|
|
103
|
+
probe: { read: (db) => db.rows(), write: (db) => db.insert(…) },
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect(report.passed, JSON.stringify(report.checks, null, 2)).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Exported because consumers write adapters this repo never sees, and an adapter that has not been
|
|
111
|
+
through this is a claim. Each check is a property one of the two harnesses really depends on:
|
|
112
|
+
|
|
113
|
+
| check | why |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `boot returns a handle` | a boot that resolves with `db: undefined` fails later, somewhere else |
|
|
116
|
+
| `restore before a snapshot is refused` | Medusa spends a `databaseTemplateReady` flag on exactly this; restoring a template nobody took returns whatever the database happened to hold |
|
|
117
|
+
| `restore returns the snapshot` | a no-op `restore` is invisible without writing a row — see the probe below |
|
|
118
|
+
| `waitFor rejects when it never comes true` | a `waitFor` that resolves on timeout is worse than none |
|
|
119
|
+
| `waitFor resolves when it comes true` | and one that never resolves is a hung suite |
|
|
120
|
+
| `teardown is idempotent` | dielime wires cleanup to `afterAll` **and** to SIGTERM/SIGINT; on a cancelled run both fire |
|
|
121
|
+
| `boots again after teardown` | a run-lifetime adapter has to survive re-entry |
|
|
122
|
+
|
|
123
|
+
It never throws for the adapter's sake — a broken adapter is a failed check carrying its own message
|
|
124
|
+
in `detail`, because a conformance run that dies tells the reader less than one that reports.
|
|
125
|
+
|
|
126
|
+
**The probe.** This package cannot write a row through someone else's handle, and without writing
|
|
127
|
+
one a no-op `restore()` is indistinguishable from a real one. So `restore returns the snapshot` is
|
|
128
|
+
**skipped by name** when no probe is given — `report.skipped` is a separate list from
|
|
129
|
+
`report.checks`, because a check that did not run is not a check that passed. Adapters with no
|
|
130
|
+
`snapshot`/`restore` and no `waitFor` skip those checks and still pass; they are optional, not
|
|
131
|
+
absent.
|
|
132
|
+
|
|
133
|
+
## `@geonosis/testbed/medusa-built`
|
|
134
|
+
|
|
135
|
+
dielime's `runIntegrationTest` — 26 lines that 106 integration suites go through — generalised, and
|
|
136
|
+
carrying the refusals its own comments describe but its code does not make.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { medusaIntegrationTestRunner } from '@medusajs/test-utils'
|
|
140
|
+
import { createMedusaIntegrationTest } from '@geonosis/testbed/medusa-built'
|
|
141
|
+
import { describe } from 'vitest'
|
|
142
|
+
|
|
143
|
+
export const { adapterFor, runIntegrationTest } = createMedusaIntegrationTest({
|
|
144
|
+
describe,
|
|
145
|
+
root: resolve(import.meta.dirname, '../..'),
|
|
146
|
+
runner: medusaIntegrationTestRunner,
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**The engine is injected, never imported.** `@medusajs/test-utils` is a peer of the repo that runs
|
|
151
|
+
it — a package that reached for it could not be installed by a repo on another runtime, and could
|
|
152
|
+
not be tested without a database. `MedusaSuiteLike` is a structural type over what the runner really
|
|
153
|
+
hands a suite.
|
|
154
|
+
|
|
155
|
+
| option | default | why |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| `root` | required | the app directory |
|
|
158
|
+
| `builtDir` | `.medusa/server` | Medusa's own output; the loaders and migrations must run as in production |
|
|
159
|
+
| `databaseName` | `sha1(description).slice(0, 12)` | dielime's derivation, kept exactly — the engine appends `-integration-<worker>` and lowercases |
|
|
160
|
+
| `connectionEnv` | `['DB_HOST', 'DB_PORT', 'DB_USERNAME']` | Medusa's own `getDatabaseURL` keys; `[]` to take its defaults deliberately |
|
|
161
|
+
| `nodeEnv` | `'test'` | `false` when the repo checks it itself |
|
|
162
|
+
| `requireBuilt` | `true` | |
|
|
163
|
+
| `env`, `fixtures`, `waitForMs` | — | `fixtures` is optional: dielime's suites build their world through the booted app |
|
|
164
|
+
|
|
165
|
+
### What it refuses, and why each one is worth a refusal
|
|
166
|
+
|
|
167
|
+
- **an unbuilt server.** "`.medusa/server` must be rebuilt before integration tests" is a law in
|
|
168
|
+
dielime's docs enforced by nothing. Unbuilt, the run dies inside a Medusa loader *after* the
|
|
169
|
+
database has been created.
|
|
170
|
+
- **`NODE_ENV` that is not `test`.** It selects the in-memory cache, event bus and workflow engine,
|
|
171
|
+
and it is read while the app loads — setting it afterwards is too late.
|
|
172
|
+
- **a connection variable that is unset.** This is the sharp one. Medusa **defaults every one** of
|
|
173
|
+
`DB_HOST`/`DB_PORT`/`DB_USERNAME`, so an unset variable is not an error: the suite quietly
|
|
174
|
+
connects to whatever is on the default host and port. dielime's Postgres moved from 5433 to 5434
|
|
175
|
+
because another project's embedded cell took 5433; with the defaults, a stale environment reaches
|
|
176
|
+
the other project's database and the suite goes green against it.
|
|
177
|
+
- **two suites that derive one database name.** 48 bits of sha1. They share a database, and each
|
|
178
|
+
one's reset empties the other's rows, with both green.
|
|
179
|
+
|
|
180
|
+
### The adapter
|
|
181
|
+
|
|
182
|
+
`adapterFor(suiteOptions)` wraps what the runner hands a suite. It **attaches** rather than boots —
|
|
183
|
+
the runner owns the application's lifecycle, creating the database, migrating, snapshotting to a
|
|
184
|
+
Postgres TEMPLATE and restoring from it before every test. Two things it adds:
|
|
185
|
+
|
|
186
|
+
- **`restore()` refuses when this adapter took no `snapshot()`.** The runner keeps a
|
|
187
|
+
`databaseTemplateReady` flag for its own `beforeEach`; the `dbUtils` object it hands the suite
|
|
188
|
+
carries no such guard, so a direct `restore()` returns whichever template was last written.
|
|
189
|
+
- **`waitFor('workflows', predicate)`** drains `utils.waitWorkflowExecutions()` and then reads the
|
|
190
|
+
predicate, on a deadline, rejecting when it never comes true. The engine calls
|
|
191
|
+
`waitWorkflowExecutions` in every `afterEach`; a test that asserts on a workflow's side effect
|
|
192
|
+
needs it *before* the assertion. dielime never names it — `grep` over its tree returns nothing.
|
|
193
|
+
|
|
194
|
+
### The real-engine smoke is the consumer's step
|
|
195
|
+
|
|
196
|
+
This package is proven against a **stub** of `medusaIntegrationTestRunner`, because the contract is
|
|
197
|
+
what it owns. Booting a Medusa app needs a Postgres, a built server and about two minutes, and none
|
|
198
|
+
of those belong in a kit's suite. The consumer's adoption step is: point one existing integration
|
|
199
|
+
suite at `createMedusaIntegrationTest`, run `pnpm test:integration`, and confirm the same pass line
|
|
200
|
+
and the same database name as before.
|
|
201
|
+
|
|
202
|
+
## `@geonosis/testbed/workerd`
|
|
203
|
+
|
|
204
|
+
during.day's harness, in three pieces. Each one is a mechanism its repo built after a specific
|
|
205
|
+
failure, and each is proven here against stubs of the engine — the real workerd pool and a real
|
|
206
|
+
Postgres are the consumer's.
|
|
207
|
+
|
|
208
|
+
### 1. The runner whose exit code lies
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import { runStrict, verdictOf } from '@geonosis/testbed/workerd'
|
|
212
|
+
|
|
213
|
+
const { code, verdict } = await runStrict({ args: ['vitest', 'run', ...process.argv.slice(2)] })
|
|
214
|
+
if (!verdict.ok) {
|
|
215
|
+
process.stderr.write(`\n${verdict.why}\n`)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
process.exit(code)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`@cloudflare/vitest-pool-workers` **exits 0 with failing tests**. Every workerd green in the repo
|
|
222
|
+
this comes from was hollow until something read the report instead of the exit code. `verdictOf`
|
|
223
|
+
refuses three things:
|
|
224
|
+
|
|
225
|
+
| report | verdict |
|
|
226
|
+
|---|---|
|
|
227
|
+
| absent | a crash before the reporter is not a pass |
|
|
228
|
+
| `{ success: false, numFailedTests: 0 }` | the run did not finish; **checked first**, because anything counting failures reads this as a pass |
|
|
229
|
+
| `{ success: true, numFailedTests: n > 0 }` | `n` test(s) failed |
|
|
230
|
+
|
|
231
|
+
A clean report does **not** launder a non-zero exit: `code` is returned unchanged and the caller
|
|
232
|
+
exits with it.
|
|
233
|
+
|
|
234
|
+
This judgement is deliberately **not** shared with `@geonosis/ratchet`'s `testFailures` counter,
|
|
235
|
+
which reads the same field. They answer different questions — "should this process fail" versus "how
|
|
236
|
+
much debt is there" — and they are installed for different reasons; coupling them would put a
|
|
237
|
+
ratchet in every repo that only wanted a harness. The contract is written down here, and each
|
|
238
|
+
package holds it independently. The proof is a real spawned process that lies in each of the three
|
|
239
|
+
ways above, not a mocked one.
|
|
240
|
+
|
|
241
|
+
### 2. The config that has no remote binding to call
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { deriveLocalOnlyConfig } from '@geonosis/testbed/workerd'
|
|
245
|
+
|
|
246
|
+
const configPath = deriveLocalOnlyConfig({
|
|
247
|
+
dropVars: ['LOCAL_AI_URL'],
|
|
248
|
+
source: new URL('./wrangler.jsonc', import.meta.url).pathname,
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
An `ai` binding is **always** remote: the pool resolves the account against `api.cloudflare.com` the
|
|
253
|
+
moment it sees one, before a single test runs — a network call, a credential and a bill in the
|
|
254
|
+
middle of the suite. So the test worker gets the real config with those bindings removed.
|
|
255
|
+
|
|
256
|
+
- **Derived, not duplicated.** Read from the real `wrangler.jsonc` on every start, so a binding added
|
|
257
|
+
there is in the suites the same day. This is what makes "tests never call a model" a property of
|
|
258
|
+
the runtime rather than a promise about the code: in the test worker there is no `env.AI` to call.
|
|
259
|
+
- **Every named environment too.** They inherit nothing per binding, so each repeats it, and a block
|
|
260
|
+
missed is a network call nobody expected.
|
|
261
|
+
- **Written BESIDE the source**, and a `target` containing a path separator is **refused**: every
|
|
262
|
+
relative path in a wrangler config (`main`, `migrations_dir`) resolves against the directory the
|
|
263
|
+
config sits in, so a copy one level deeper resolves `src/index.ts` to a file that is not there.
|
|
264
|
+
- JSONC's line comments and trailing commas are stripped, since `JSON.parse` refuses both.
|
|
265
|
+
|
|
266
|
+
`remoteOnly` defaults to `['ai']` — a platform fact, not a repo's — and is overridable.
|
|
267
|
+
|
|
268
|
+
### 3. One cell for the run
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import EmbeddedPostgres from 'embedded-postgres'
|
|
272
|
+
import { createCellServer, workerdAdapter } from '@geonosis/testbed/workerd'
|
|
273
|
+
|
|
274
|
+
const cell = await createCellServer({
|
|
275
|
+
cluster: ({ databaseDir, port }) =>
|
|
276
|
+
new EmbeddedPostgres({ databaseDir, password: 'postgres', persistent: false, port, user: 'postgres' }),
|
|
277
|
+
prepare: async (connectionString) => {
|
|
278
|
+
await migrateCell(connectionString)
|
|
279
|
+
return appConnectionStringOf(connectionString, 'app-password')
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
`embedded-postgres` is a **peer**, injected as a factory. `CELL_CONNECTION_STRING` (rename it with
|
|
285
|
+
`connectionStringEnv`) takes an externally-owned cell instead — which is how the same suite runs
|
|
286
|
+
against every Postgres major a deployment might use — and that one is prepared and **never
|
|
287
|
+
stopped**: in CI the container is thrown away, and a managed instance is not the suite's to stop.
|
|
288
|
+
Otherwise a throwaway cluster starts on a port found by binding `:0`, in a fresh temp directory.
|
|
289
|
+
|
|
290
|
+
Two things this adds to the shape it came from: `stop()` is idempotent, and **a cluster whose
|
|
291
|
+
`prepare` throws is stopped rather than leaked** — a failed migration in that repo leaves a Postgres
|
|
292
|
+
and its data directory up for the rest of the session.
|
|
293
|
+
|
|
294
|
+
`workerdAdapter({ cell, execute, tables, before })` is the adapter. There is no template database,
|
|
295
|
+
so `restore()` is a `TRUNCATE`, and **empty is the only state a truncate can return to**:
|
|
296
|
+
`snapshot()` therefore does not record the current rows, it *empties* the tables and marks that
|
|
297
|
+
point. That is exactly the split the source repo already has between its two setup files — the
|
|
298
|
+
truncate-per-test suites are the unseeded ones. A suite that seeds re-seeds after each restore.
|
|
299
|
+
|
|
300
|
+
`truncateAllStatement(tables)` is one statement for any number of tables, because a `DELETE` per
|
|
301
|
+
table inside a transaction cost twenty-eight round trips before every test. It refuses an empty
|
|
302
|
+
table list (a reset that resets nothing reads as isolation) and any name that is not a plain
|
|
303
|
+
identifier, since the list is usually derived from a schema object.
|
|
304
|
+
|
|
305
|
+
**No `waitFor`.** The source repo has none — its raised timeouts carry the waiting — so this adapter
|
|
306
|
+
declares none, and the conformance suite reports those checks as `skipped` rather than passed.
|
|
307
|
+
|
|
308
|
+
## The database matrix, and `geonosis-testbed`
|
|
309
|
+
|
|
310
|
+
```jsonc
|
|
311
|
+
// geonosis.json
|
|
312
|
+
{
|
|
313
|
+
"testbed": {
|
|
314
|
+
"matrix": {
|
|
315
|
+
"command": "bun run --cwd packages/db test",
|
|
316
|
+
"connectionEnv": "CELL_CONNECTION_STRING",
|
|
317
|
+
"connectionString": "postgres://postgres:postgres@127.0.0.1:5432/postgres",
|
|
318
|
+
"majors": ["17", "18"],
|
|
319
|
+
"name": "cells",
|
|
320
|
+
"setup": ["bun install --frozen-lockfile"]
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
geonosis-testbed matrix --out .github/workflows/cells.yml
|
|
328
|
+
geonosis-testbed strict -- packages/db # judge the report, not the exit code
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
The rendered workflow is `templates/ci/db-matrix.yml`, which is checked in **as the renderer's own
|
|
332
|
+
output for the documented example** and tested to still equal it — a template committed beside a
|
|
333
|
+
generator drifts from it the first time either is touched, and then the thing people copy is not the
|
|
334
|
+
thing the code produces.
|
|
335
|
+
|
|
336
|
+
**Two jobs, not one, and that is the point.** The source repo runs its data layer against Postgres
|
|
337
|
+
17 and 18 in a matrix, and against an *embedded* cluster in every other job — because the connection
|
|
338
|
+
variable is unset there. The embedded leg is real, it is tested, and it is invisible: nothing in
|
|
339
|
+
that workflow says the suite has a second backend. Here it is `<name>-embedded`, with a name.
|
|
340
|
+
|
|
341
|
+
What the renderer refuses: a matrix with no legs (`embedded: false` and no majors), no command, a
|
|
342
|
+
job name that is not a name, and — the one that matters — **majors with an empty connection string**,
|
|
343
|
+
which would run the embedded cell under a container's name and go green twice against the same
|
|
344
|
+
database while claiming to have tested two.
|
|
345
|
+
|
|
346
|
+
`fail-fast: false` and the `pg_isready` health check are not decoration: one major failing must not
|
|
347
|
+
hide whether the other passes, and without the health check the first connection races the container
|
|
348
|
+
and the suite fails for a reason that is not in the code.
|
|
349
|
+
|
|
350
|
+
## The rule: `no-mock-db-in-integration`
|
|
351
|
+
|
|
352
|
+
Preset **`testbed`**, in `@geonosis/oxlint-plugin-biological-architecture`.
|
|
353
|
+
|
|
354
|
+
```jsonc
|
|
355
|
+
"biological-architecture/no-mock-db-in-integration": ["error", {
|
|
356
|
+
"dbModules": ["@repo/db"],
|
|
357
|
+
"integrationDirs": ["__tests__/integration"],
|
|
358
|
+
"mockNames": []
|
|
359
|
+
}]
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
An integration suite that mocks its database proves that a procedure calls what it says it calls,
|
|
363
|
+
and nothing about what comes back — which is what an integration test exists to check.
|
|
364
|
+
|
|
365
|
+
**Both consumers of this kit pass it today**, measured: dielime's 106 integration files contain zero
|
|
366
|
+
`vi.mock` calls, and during.day's whole tree contains zero. The landmine is a third repo's, where 35
|
|
367
|
+
of 43 test files share one context helper carrying `db: mockDb`, and they are the gate. That is why
|
|
368
|
+
`integrationDirs` and `dbModules` are required with no defaults — a default drawn from two repos
|
|
369
|
+
that do not have the pattern would match nothing and report a clean tree it never looked at.
|
|
370
|
+
|
|
371
|
+
`mockNames` is for the shape no mock call can be seen in: a handle built in a setup file and threaded
|
|
372
|
+
through a helper. It is empty by default; a repo that has one names it.
|
|
373
|
+
|
|
374
|
+
## Adoption status — **NOT adopted** (D-027)
|
|
375
|
+
|
|
376
|
+
Neither consumer depends on this package yet. `grep @geonosis/testbed` in dielime and in during.day
|
|
377
|
+
finds nothing. What follows is what each migration is, written from what each repo measurably has
|
|
378
|
+
today — see [`docs/testbed-inventory-2026-08-30.md`](../../docs/testbed-inventory-2026-08-30.md)
|
|
379
|
+
for the numbers.
|
|
380
|
+
|
|
381
|
+
### dielime
|
|
382
|
+
|
|
383
|
+
Replace `apps/backend/integration-tests/utils/runner.ts` (26 lines, the entry point of all 106
|
|
384
|
+
integration suites) with:
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { medusaIntegrationTestRunner } from '@medusajs/test-utils'
|
|
388
|
+
import { createMedusaIntegrationTest } from '@geonosis/testbed/medusa-built'
|
|
389
|
+
import { describe } from 'vitest'
|
|
390
|
+
|
|
391
|
+
export const { adapterFor, runIntegrationTest } = createMedusaIntegrationTest({
|
|
392
|
+
describe,
|
|
393
|
+
root: resolve(import.meta.dirname, '../..'),
|
|
394
|
+
runner: medusaIntegrationTestRunner,
|
|
395
|
+
})
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Nothing else changes: the default `builtDir` is `.medusa/server`, the default derivation is the
|
|
399
|
+
`sha1(description).slice(0, 12)` it already uses, and the default `connectionEnv` is the `DB_*` set
|
|
400
|
+
its `setup.ts` already fills in. **The same 106 suites must produce the same database names**, which
|
|
401
|
+
is the equivalence check — `moduleName` is printed by the engine's own `Creating database …` line.
|
|
402
|
+
|
|
403
|
+
What it gains on the day it lands: a refusal instead of a loader crash when `.medusa/server` is
|
|
404
|
+
stale, a refusal instead of a silent connection to port 5432 when the `DB_*` environment is not
|
|
405
|
+
set — the failure that moving Postgres from 5433 to 5434 made possible, and which lands on another
|
|
406
|
+
project's database — and a refusal instead of a shared database when two suite descriptions collide
|
|
407
|
+
in 48 bits of sha1.
|
|
408
|
+
|
|
409
|
+
Then `.github/workflows/`: dielime's CI is 26 lines and **has no integration job at all**. 106
|
|
410
|
+
suites exist, none runs on a push. `geonosis-testbed matrix` renders one; `majors` for a Medusa
|
|
411
|
+
backend is the Postgres versions it supports, and the embedded leg does not apply, so
|
|
412
|
+
`embedded: false`.
|
|
413
|
+
|
|
414
|
+
`no-mock-db-in-integration` costs nothing to enable — 0 findings today, proven not silent in
|
|
415
|
+
`proofs/022-W9-consumers/`.
|
|
416
|
+
|
|
417
|
+
### during.day
|
|
418
|
+
|
|
419
|
+
Four replacements, none of which changes what runs:
|
|
420
|
+
|
|
421
|
+
| today | becomes |
|
|
422
|
+
|---|---|
|
|
423
|
+
| `scripts/vitest-strict.ts` (57 lines, the `test` script of 3 packages) | `geonosis-testbed strict`, or `runStrict`/`verdictOf` |
|
|
424
|
+
| `apps/api/testing/local-only-config.ts` (98 lines) | `deriveLocalOnlyConfig({ dropVars: ['LOCAL_AI_URL'], source })` |
|
|
425
|
+
| `packages/db/testing/cell-server.ts` (72 lines) | `createCellServer({ cluster, prepare })` with `EmbeddedPostgres` injected |
|
|
426
|
+
| `packages/db/testing/reset.ts` | `workerdAdapter({ before: ['drop trigger …'], tables })`, or `truncateAllStatement` |
|
|
427
|
+
|
|
428
|
+
Its `.github/workflows/ci.yml` `cells` job becomes `geonosis-testbed matrix` output, with one
|
|
429
|
+
difference that is the reason to do it: the **embedded leg becomes a named job**. Today it is real
|
|
430
|
+
and tested and invisible, running in every job where `CELL_CONNECTION_STRING` happens to be unset.
|
|
431
|
+
|
|
432
|
+
Two behaviour changes it gets for free: `stop()` becomes idempotent, and a cluster whose `prepare`
|
|
433
|
+
throws is **stopped** instead of left running with its data directory for the rest of the session.
|
|
434
|
+
|
|
435
|
+
Nothing its `CLAUDE.md` names is proposed for deletion here, per the 2026-08-30 boundary: `§4.3`'s
|
|
436
|
+
fixtures law stays its law — `fixturesOnly` is that law with three more refusals in it, not a
|
|
437
|
+
replacement for the sentence.
|