@zerotal/testing 1.15.0 → 1.15.2
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/CHANGELOG.md +37 -0
- package/package.json +6 -6
- package/src/TestApp.ts +33 -5
- package/src/assertions.ts +2 -2
- package/src/preload.ts +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,43 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.15.2] — 2026-10-23
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`assertDatabaseHas`, `assertDatabaseMissing` and `assertDatabaseCount` run on Bun
|
|
16
|
+
1.4.4.** They counted rows through a hand-built template object whose `raw` was
|
|
17
|
+
enumerable, which Bun 1.4.4 refuses. They use the ORM's `_templateStrings()` now; see
|
|
18
|
+
`@zerotal/orm`.
|
|
19
|
+
|
|
20
|
+
## [1.15.1] — 2026-09-13
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **A file that booted the app with a `setup` callback left every later file without a
|
|
25
|
+
database.** `createTestApp(bootstrap, setup)` opts out of _sharing_, because `setup`
|
|
26
|
+
registers routes and those cannot be added twice to a running server — and
|
|
27
|
+
`FlowBrowser.serve` always passes one. But "not shared" was implemented as "not cached",
|
|
28
|
+
which is a much bigger claim: `bootstrap/app.ts` is module-cached, so the `Application`
|
|
29
|
+
behind it is the one every later file's `bootstrap()` returns.
|
|
30
|
+
|
|
31
|
+
Being invisible to the cache meant nothing knew it was in use, so its `close()` ran the
|
|
32
|
+
full provider teardown. `DatabaseProvider.onStopping` clears the process-global
|
|
33
|
+
connection resolver, and the next file then got that same stopped `Application` back —
|
|
34
|
+
`Application.create()` short-circuits on a module-cached app, so the providers never
|
|
35
|
+
re-registered and nothing put the resolver back. Every query from then on raised
|
|
36
|
+
`[Zerotal ORM] No database connection. Is DatabaseProvider registered?` in files that
|
|
37
|
+
had done nothing wrong.
|
|
38
|
+
|
|
39
|
+
The app is now cached whichever call booted it. `_shared` still means "reached through
|
|
40
|
+
the sharing path", and a second `setup` caller still gets the fresh path it needs — only
|
|
41
|
+
the bookkeeping changed.
|
|
42
|
+
|
|
43
|
+
It read as a CI-only flake for months because the exposing order is a browser-driven
|
|
44
|
+
file before an ordinary one: alphabetical on Windows, arbitrary readdir order on Linux.
|
|
45
|
+
Reproduced locally by renaming the one ordinary file to sort last (62 pass / 49 fail),
|
|
46
|
+
and verified at 111 / 0 across seven runs in both orders.
|
|
47
|
+
|
|
11
48
|
## [1.15.0] — 2026-09-04
|
|
12
49
|
|
|
13
50
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/testing",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@zerotal/core": "1.15.
|
|
36
|
-
"@zerotal/orm": "1.15.
|
|
37
|
-
"@zerotal/queue": "1.15.
|
|
38
|
-
"@zerotal/notifications": "1.15.
|
|
35
|
+
"@zerotal/core": "1.15.2",
|
|
36
|
+
"@zerotal/orm": "1.15.2",
|
|
37
|
+
"@zerotal/queue": "1.15.2",
|
|
38
|
+
"@zerotal/notifications": "1.15.2"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"typescript": "^5.8.0",
|
|
42
|
-
"@zerotal/session": "1.15.
|
|
42
|
+
"@zerotal/session": "1.15.2"
|
|
43
43
|
},
|
|
44
44
|
"description": "Testing utilities for Zerotal — an in-process test app, HTTP helpers, and database refresh.",
|
|
45
45
|
"keywords": [
|
package/src/TestApp.ts
CHANGED
|
@@ -449,7 +449,23 @@ export class TestApp {
|
|
|
449
449
|
* The shared instance is torn down once, by {@link closeSharedTestApps}.
|
|
450
450
|
*/
|
|
451
451
|
async close(): Promise<void> {
|
|
452
|
-
|
|
452
|
+
// `_shared` says this wrapper came from the sharing path. It is not the same
|
|
453
|
+
// question as whether the *Application* is shared — a `bootstrap/app.ts` is
|
|
454
|
+
// module-cached, so a `createTestApp(bootstrap, setup)` call (which opts out of
|
|
455
|
+
// sharing, because `setup` registers routes and those cannot be added twice to
|
|
456
|
+
// a running server) is handed the very same `Application` the shared wrapper
|
|
457
|
+
// holds. `FlowBrowser.serve` always passes a `setup`.
|
|
458
|
+
//
|
|
459
|
+
// Tearing down here would then stop an Application somebody else is still
|
|
460
|
+
// using, closing the process-global database connection out from under every
|
|
461
|
+
// later file: they fail on their first query with "No database connection. Is
|
|
462
|
+
// DatabaseProvider registered?", in files that are entirely correct and merely
|
|
463
|
+
// ran second. The ordering that exposes it — a browser-driven file before an
|
|
464
|
+
// ordinary one — is alphabetical on Windows and arbitrary on Linux, so it read
|
|
465
|
+
// as a CI-only flake for months.
|
|
466
|
+
//
|
|
467
|
+
// So the question to ask is about the Application, not the wrapper.
|
|
468
|
+
if (this._shared || _sharedApps.has(this._app)) {
|
|
453
469
|
resetTestState();
|
|
454
470
|
return;
|
|
455
471
|
}
|
|
@@ -668,10 +684,22 @@ export async function createTestApp(
|
|
|
668
684
|
await app.start(0);
|
|
669
685
|
const testApp = new TestApp(app, errors);
|
|
670
686
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
687
|
+
// Cache it either way. `_shared` still means "reached through the sharing path",
|
|
688
|
+
// and only a `setup`-free call is handed one back — a second `setup` caller has
|
|
689
|
+
// routes to register and needs the fresh path. But the *Application* is
|
|
690
|
+
// module-cached and every later file will ask for it, so it has to be findable
|
|
691
|
+
// regardless of which call happened to boot it first.
|
|
692
|
+
//
|
|
693
|
+
// Without this, an app booted by `FlowBrowser.serve` (which always passes a
|
|
694
|
+
// `setup`) was invisible here, so nothing knew it was in use: its `close()` ran
|
|
695
|
+
// the full teardown, `DatabaseProvider.onStopping` cleared the process-global
|
|
696
|
+
// connection resolver, and the next file's `bootstrap()` returned that same dead
|
|
697
|
+
// Application — `Application.create()` short-circuits on a module-cached app, so
|
|
698
|
+
// the providers never re-registered and nothing ever put the resolver back. Every
|
|
699
|
+
// query after that raised "No database connection. Is DatabaseProvider
|
|
700
|
+
// registered?" in a file that had done nothing wrong.
|
|
701
|
+
testApp._shared = !setup;
|
|
702
|
+
_sharedApps.set(app, testApp);
|
|
675
703
|
return testApp;
|
|
676
704
|
}
|
|
677
705
|
|
package/src/assertions.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _getModelConnection } from "@zerotal/orm";
|
|
1
|
+
import { _getModelConnection, _templateStrings } from "@zerotal/orm";
|
|
2
2
|
|
|
3
3
|
async function countRows(table: string, where: Record<string, unknown>): Promise<number> {
|
|
4
4
|
const conn = _getModelConnection() as unknown as (
|
|
@@ -19,7 +19,7 @@ async function countRows(table: string, where: Record<string, unknown>): Promise
|
|
|
19
19
|
});
|
|
20
20
|
parts.push(sql);
|
|
21
21
|
|
|
22
|
-
const tpl =
|
|
22
|
+
const tpl = _templateStrings(parts);
|
|
23
23
|
const rows = await conn(tpl, ...vals);
|
|
24
24
|
return Number(rows[0]?.cnt ?? 0);
|
|
25
25
|
}
|
package/src/preload.ts
CHANGED
|
@@ -7,8 +7,20 @@
|
|
|
7
7
|
* TestCommand) and calls _setBaseModelConnection + _setDbConnection so that
|
|
8
8
|
* withDatabase() and DB.table() work without any manual beforeAll setup.
|
|
9
9
|
*
|
|
10
|
-
* Each Bun test worker
|
|
11
|
-
*
|
|
10
|
+
* Each Bun test *worker* evaluates this module once — not once per file. Every
|
|
11
|
+
* file that worker runs therefore shares the connection wired here, and shares
|
|
12
|
+
* every other piece of module-level state in the process with it.
|
|
13
|
+
*
|
|
14
|
+
* This used to read "isolated per file — no cross-file leakage", which is the
|
|
15
|
+
* assumption that let five separate cross-file leaks hide: a `document` stub, a
|
|
16
|
+
* runtime-mode variable, an ORM dialect, and two whole booted applications, each
|
|
17
|
+
* set by one file and read by another. Verified rather than assumed — a hook
|
|
18
|
+
* registered here runs once at the end of a run, not once per file.
|
|
19
|
+
*
|
|
20
|
+
* So a test that mutates process-global state has to put it back, and the reason
|
|
21
|
+
* it is easy to skip is that the damage lands somewhere else: the file that
|
|
22
|
+
* breaks is the one that ran next, and which file that is depends on directory
|
|
23
|
+
* order — alphabetical on Windows, arbitrary on Linux.
|
|
12
24
|
*
|
|
13
25
|
* ## Why the runtime check is here and not only in `zt test`
|
|
14
26
|
*
|