@ultimat3/testing 15.0.0 → 17.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/CLAUDE.md +25 -0
- package/package.json +12 -12
- package/src/determinism.ts +41 -5
package/CLAUDE.md
CHANGED
|
@@ -92,6 +92,31 @@ is its own entry point and not part of the barrel.
|
|
|
92
92
|
| Globals install all-or-nothing | `installGlobals` saves DESCRIPTORS, not values — a saved value cannot tell "no such global" from "a global holding `undefined`", and the teardown deleted both — and rolls the whole install back if one assignment throws. That rollback is the half with teeth: the install runs BEFORE `mountIsland`'s own `try`, so a getter-only own global among the caller's `globals` used to leave the fake `document` installed for the rest of the process |
|
|
93
93
|
| Which command shards | `bun test` is one process on one database, and that is still what a scaffolded app's `test` script runs. `x verify` DOES shard its parallel test steps, over `ULTIMATE_TEST_WORKER` and one database per worker; `live` and `e2e` stay serial because a replication slot is cluster-scoped and `e2e` has one built `dist/`. Say which command a claim is about |
|
|
94
94
|
|
|
95
|
+
## The frozen instant is screened, and so is the seed — `As of 2026-08-26`
|
|
96
|
+
|
|
97
|
+
`installDeterminism({ now })`, `setFrozenClock`, `frozenClock` and `advanceClock` all write ONE
|
|
98
|
+
module-level number, and `bun test` is one process: `new Date('yesterday').getTime()` is `NaN`, so
|
|
99
|
+
one unreadable instant makes `Date.now()` answer `NaN` and `new Date()` answer `Invalid Date` for
|
|
100
|
+
every file after it, where every `expiresAt > Date.now()` in the framework reads false and no
|
|
101
|
+
assertion anywhere names the clock. Nothing repairs it either — `NaN + ms` is `NaN`, so
|
|
102
|
+
`advanceClock` only carries it forward, which is why that required parameter is screened too. All
|
|
103
|
+
four go through one `instantMs`, and `defineIslandStates` already refused an unpinned `now` at
|
|
104
|
+
declaration (`isPinnedInstant`) — this is that rule where the clock is actually set.
|
|
105
|
+
|
|
106
|
+
**The seed is NOT a bound, and it is screened anyway — measured before deciding.**
|
|
107
|
+
`seededRandom` starts at `seed >>> 0`, so `NaN`, `±Infinity`, `0.5`, `-1` and `2 ** 32` all produce
|
|
108
|
+
the SAME sequence as `seed: 0`. A non-finite seed therefore does not make a run non-deterministic;
|
|
109
|
+
it silently makes it the seed-0 run, and the record of which seed produced it is false — in the
|
|
110
|
+
package whose promise is reproducibility. `preload.ts` already screened its own environment read by
|
|
111
|
+
hand (`Number.isFinite(seed) ? { seed } : {}`) while `harness.ts` passed an app's `seedValue`
|
|
112
|
+
straight through, which is the repair-in-another-file shape. What the screen does NOT claim: `>>>`
|
|
113
|
+
is modulo `2 ** 32`, so a seed above that still wraps onto another one.
|
|
114
|
+
|
|
115
|
+
`finiteOption`/`finiteCount` from `@ultimat3/core` are the one form; `determinism-bounds.test.ts`
|
|
116
|
+
holds both sides, restores the captured instant after EVERY case, and pins seed `0` as legal.
|
|
117
|
+
`retry.ts`'s budget was already screened, and `matcher-visible.ts` passes its `timeout`/`interval`
|
|
118
|
+
into that screen rather than growing a second one.
|
|
119
|
+
|
|
95
120
|
Commands: `bun test`, `bunx tsc --noEmit -p tsconfig.json`.
|
|
96
121
|
|
|
97
122
|
Entry points: `.` (the API), `./preload` (side effects for bunfig) and `./registry-isolation`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0",
|
|
4
4
|
"description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,16 +33,16 @@
|
|
|
33
33
|
"test": "bun test"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ultimat3/cache": "
|
|
37
|
-
"@ultimat3/core": "
|
|
38
|
-
"@ultimat3/db": "
|
|
39
|
-
"@ultimat3/entity": "
|
|
40
|
-
"@ultimat3/i18n": "
|
|
41
|
-
"@ultimat3/jobs": "
|
|
42
|
-
"@ultimat3/mail": "
|
|
43
|
-
"@ultimat3/policy": "
|
|
44
|
-
"@ultimat3/query": "
|
|
45
|
-
"@ultimat3/realtime": "
|
|
46
|
-
"@ultimat3/time": "
|
|
36
|
+
"@ultimat3/cache": "17.0.0",
|
|
37
|
+
"@ultimat3/core": "17.0.0",
|
|
38
|
+
"@ultimat3/db": "17.0.0",
|
|
39
|
+
"@ultimat3/entity": "17.0.0",
|
|
40
|
+
"@ultimat3/i18n": "17.0.0",
|
|
41
|
+
"@ultimat3/jobs": "17.0.0",
|
|
42
|
+
"@ultimat3/mail": "17.0.0",
|
|
43
|
+
"@ultimat3/policy": "17.0.0",
|
|
44
|
+
"@ultimat3/query": "17.0.0",
|
|
45
|
+
"@ultimat3/realtime": "17.0.0",
|
|
46
|
+
"@ultimat3/time": "17.0.0"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/determinism.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// bug: a frozen clock means "advance it", a seeded RNG means "same values every run", and both are
|
|
3
3
|
// restorable so a test that genuinely needs real time can opt out explicitly.
|
|
4
4
|
|
|
5
|
+
import { finiteCount, finiteOption } from '@ultimat3/core';
|
|
5
6
|
import { NondeterministicError } from './errors';
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_SEED = 20260101;
|
|
@@ -16,6 +17,22 @@ const dateGetTime = RealDate.prototype.getTime;
|
|
|
16
17
|
let frozenAt = new RealDate(DEFAULT_NOW).getTime();
|
|
17
18
|
let installed = false;
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The epoch milliseconds `value` names, or a refusal — the ONE writer's screen for `frozenAt`.
|
|
22
|
+
*
|
|
23
|
+
* `new Date('yesterday').getTime()` is `NaN`, and `frozenAt` is process state the preload installs
|
|
24
|
+
* for the whole run: one unreadable instant makes `Date.now()` answer `NaN` and `new Date()`
|
|
25
|
+
* answer `Invalid Date` in every test file after it, where every `expiresAt > Date.now()` reads
|
|
26
|
+
* false and no assertion anywhere names the clock. Nothing can repair it either — `NaN + ms` is
|
|
27
|
+
* `NaN`, so `advanceClock` only carries it forward.
|
|
28
|
+
*
|
|
29
|
+
* `finiteOption` and not `finiteCount`: an instant before 1970 is negative, and `Date.getTime()`
|
|
30
|
+
* is already integral. `defineIslandStates` refuses an unpinned `now` at declaration for the same
|
|
31
|
+
* reason one hop away (`isPinnedInstant`); this is that rule where the clock is actually set.
|
|
32
|
+
*/
|
|
33
|
+
const instantMs = (subject: string, value: string | number): number =>
|
|
34
|
+
finiteOption(subject, 'now', new RealDate(value).getTime());
|
|
35
|
+
|
|
19
36
|
/** mulberry32: 32 bits of state, uniform enough for tests, identical across platforms and runs. */
|
|
20
37
|
export function seededRandom(seed: number): () => number {
|
|
21
38
|
let state = seed >>> 0;
|
|
@@ -81,8 +98,20 @@ export interface DeterminismOptions {
|
|
|
81
98
|
|
|
82
99
|
/** Install the frozen clock and the seeded RNG globally. Idempotent. */
|
|
83
100
|
export function installDeterminism(options: DeterminismOptions = {}): void {
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
// Both screens run BEFORE anything is installed, so a refusal leaves the process on whatever
|
|
102
|
+
// clock and RNG it already had rather than half-way onto a new one.
|
|
103
|
+
const at = instantMs('installDeterminism', options.now ?? DEFAULT_NOW);
|
|
104
|
+
// A seed is not a bound and nothing compares against it — MEASURED, and the answer is why it is
|
|
105
|
+
// screened anyway: `seededRandom` starts at `seed >>> 0`, so `NaN`, `±Infinity`, `0.5`, `-1` and
|
|
106
|
+
// `2 ** 32` all produce the SAME sequence as `seed: 0`. Determinism survives; the record of which
|
|
107
|
+
// seed produced the run does not, and that record is this package's whole promise. The one caller
|
|
108
|
+
// that reads a seed from the environment (`preload.ts`) already screens it by hand, which is the
|
|
109
|
+
// repair-in-another-file shape — `harness.ts` passes an app's `seedValue` through unscreened.
|
|
110
|
+
// What this does NOT claim: `>>>` is modulo 2**32, so a seed above that still wraps onto another.
|
|
111
|
+
const next = seededRandom(
|
|
112
|
+
finiteCount('installDeterminism', 'seed', options.seed ?? DEFAULT_SEED),
|
|
113
|
+
);
|
|
114
|
+
frozenAt = at;
|
|
86
115
|
globalThis.Date = FrozenDate as unknown as DateConstructor;
|
|
87
116
|
Math.random = next;
|
|
88
117
|
installed = true;
|
|
@@ -127,20 +156,27 @@ export const isDeterminismInstalled = (): boolean => installed;
|
|
|
127
156
|
|
|
128
157
|
/** Move the frozen clock forward. The only legal way for time to pass inside a test. */
|
|
129
158
|
export function advanceClock(ms: number): Date {
|
|
130
|
-
|
|
159
|
+
// A required parameter with no default, so no `??` and no ratchet can see it — and it writes the
|
|
160
|
+
// same single number `installDeterminism` does. `frozenAt += NaN` is `NaN` for the rest of the
|
|
161
|
+
// process, which no later `advance` and no later `set` undoes. Negative is legal: a test may
|
|
162
|
+
// move the clock backwards.
|
|
163
|
+
frozenAt += finiteOption('advanceClock', 'ms', ms);
|
|
131
164
|
return new RealDate(frozenAt);
|
|
132
165
|
}
|
|
133
166
|
|
|
134
167
|
export const frozenNow = (): Date => new RealDate(frozenAt);
|
|
135
168
|
|
|
136
169
|
export function setFrozenClock(now: string | number): void {
|
|
137
|
-
frozenAt =
|
|
170
|
+
frozenAt = instantMs('setFrozenClock', now);
|
|
138
171
|
}
|
|
139
172
|
|
|
140
173
|
/** Run `body` with the clock frozen at `now`, then restore whatever was there before. */
|
|
141
174
|
export async function frozenClock<T>(now: string, body: () => T | Promise<T>): Promise<T> {
|
|
175
|
+
// Resolved before `previous` is spent: a refusal here must not run the body and must not leave
|
|
176
|
+
// the outer instant behind a `finally` that restores something already overwritten.
|
|
177
|
+
const at = instantMs('frozenClock', now);
|
|
142
178
|
const previous = frozenAt;
|
|
143
|
-
frozenAt =
|
|
179
|
+
frozenAt = at;
|
|
144
180
|
try {
|
|
145
181
|
return await body();
|
|
146
182
|
} finally {
|