@ultimat3/testing
Advanced tools
+2
-0
@@ -46,2 +46,4 @@ # @ultimat3/testing — boundary | ||
| | Reported and restored are different sets | the guard also RESTORES, at the same file boundary, the registries whose module-scope declarations a neighbour's cleanup destroys — the locale config, the catalogs, the permission set and the role map (`registry-snapshot.ts`). A module evaluates once per process, so a later file's own `import` is a cache hit that declares nothing: `clearPermissions()` in one CLI test took `admin:*` from `@ultimat3/admin`'s barrel for the whole run, and a `defineCatalogs()` inside a loaded app narrowed `supported` so `Accept-Language: de-DE` answered `en` in files that never mentioned locales. Nothing restored is reported and nothing reported is restored — a repair followed by a failure over it would be two answers to one question | | ||
| | A catalog restore is a MERGE, never a replace, `As of 2026-08-23` | the other three registries are replaced with the snapshot; the catalogs are not. `registerCatalog` has no inverse, so the only thing a file can cost its neighbour is a `resetCatalogs()` — and that is all this repairs. Everything the live registry still holds survives, a key first registered during the file and an override of a framework base string alike, because both are one-time MODULE-scope declarations: `loadApp()` in a test body dynamically imports the app's i18n package after the file's baseline was sampled, so a replace dropped 519 keys nothing could re-add and `t()` answered `⟦brand.name⟧` for the rest of the process (#312, measured: `bun test apps/admin` in `dummy/social-media-clone`, 4 fail → 0). The override half is the same defect with no `⟦…⟧` to show it — the demo app overrides `admin.denied.body`, and reverting it rendered `@ultimat3/i18n`'s own copy. The cost, stated: a file that CLOBBERS an inherited key owns the cleanup, and the cleanup is `resetCatalogs()` in its own `afterAll`, which this repair is built around | | ||
| | The same is still true of permissions, and it is MEASURED, `As of 2026-08-23` | permissions, roles and the locale config are still replaced with the file's baseline, so an app's `definePermissions()` reached only by a dynamic `loadApp()` is dropped at that file's boundary exactly as the catalogs were. Reproduced: `bun test apps packages` in `dummy/social-media-clone` — one process, unsharded — leaves 6 `.contract.` cases failing on `knownPermissions()` missing `dashboard:read`, and every one of them passes when its file runs alone. The catalog fix took that run from 16 fail to 6; these are the 6. Not fixed here, and the reason is not that it is a different defect — it is the same one — but that the same union rule applied to permissions leaks every permission `packages/policy/src/permissions.test.ts` declares into every later file, and judging that needs a repo-wide `bun test` this package cannot run for itself. Its own piece of work, not a rider on this one. The `unit` step is green over it because `.contract.` is a different step and it shards | | ||
| | Guarded state is boot state | only the two registries whose honest invariant is "clean when the file ends" — `declareTags` and `registerTier` are boot installs. `entity()`, `job()` and `defineRoute()` register at MODULE scope, which is how an app declares itself, so a filled registry there is idiomatic and unguarded | | ||
@@ -48,0 +50,0 @@ | An empty registry is a premise you state | a test whose subject is "nothing is declared" — `x db gen` with nothing to generate — calls `isolateEntityRegistry()` and restores in a `finally`. Inheriting it means the test passes until a neighbouring file imports an entity | |
+12
-12
| { | ||
| "name": "@ultimat3/testing", | ||
| "version": "8.0.0", | ||
| "version": "9.0.0", | ||
| "description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types", | ||
@@ -36,14 +36,14 @@ "license": "MIT", | ||
| "dependencies": { | ||
| "@ultimat3/cache": "8.0.0", | ||
| "@ultimat3/core": "8.0.0", | ||
| "@ultimat3/db": "8.0.0", | ||
| "@ultimat3/entity": "8.0.0", | ||
| "@ultimat3/i18n": "8.0.0", | ||
| "@ultimat3/jobs": "8.0.0", | ||
| "@ultimat3/mail": "8.0.0", | ||
| "@ultimat3/policy": "8.0.0", | ||
| "@ultimat3/query": "8.0.0", | ||
| "@ultimat3/realtime": "8.0.0", | ||
| "@ultimat3/time": "8.0.0" | ||
| "@ultimat3/cache": "9.0.0", | ||
| "@ultimat3/core": "9.0.0", | ||
| "@ultimat3/db": "9.0.0", | ||
| "@ultimat3/entity": "9.0.0", | ||
| "@ultimat3/i18n": "9.0.0", | ||
| "@ultimat3/jobs": "9.0.0", | ||
| "@ultimat3/mail": "9.0.0", | ||
| "@ultimat3/policy": "9.0.0", | ||
| "@ultimat3/query": "9.0.0", | ||
| "@ultimat3/realtime": "9.0.0", | ||
| "@ultimat3/time": "9.0.0" | ||
| } | ||
| } |
@@ -11,2 +11,3 @@ // The process registries a test file inherits, captured and handed back at the file boundary; | ||
| localeConfig, | ||
| mergeCatalogs, | ||
| registerCatalog, | ||
@@ -50,5 +51,5 @@ registeredLocales, | ||
| /** | ||
| * Idempotent, and a REPLACE on every registry rather than a merge: a snapshot is the whole truth | ||
| * about the process at capture time, so anything declared since must go as surely as anything | ||
| * cleared since must come back. | ||
| * Idempotent. A REPLACE on the locale config, the permission set and the role map — a snapshot is | ||
| * the whole truth about those at capture time — and, for the catalogs alone, a key-level | ||
| * RECONCILE. See `restoreCatalogs`. | ||
| */ | ||
@@ -59,6 +60,40 @@ export function restoreProcessRegistries(snapshot: ProcessRegistrySnapshot): void { | ||
| configureLocales(snapshot.locales); | ||
| resetCatalogs(); | ||
| for (const [locale, catalog] of snapshot.catalogs) registerCatalog(locale, catalog); | ||
| restoreCatalogs(snapshot.catalogs); | ||
| restorePermissions(snapshot.permissions); | ||
| restoreRoles(snapshot.roles, snapshot.roleSites); | ||
| } | ||
| /** | ||
| * Repair a clear; never undo a registration. The catalogs are the one registry here restored by | ||
| * MERGE rather than by replacement, and the module cache is why. | ||
| * | ||
| * `registerCatalog` has no inverse — it merges, last wins — so the only thing a file can do that | ||
| * costs the next file anything is `resetCatalogs()`, and that is exactly what this repairs: a key | ||
| * the snapshot holds and the live registry has lost comes back. Everything the live registry still | ||
| * holds is left alone, INCLUDING a key whose value the file changed. | ||
| * | ||
| * Undoing the change is what the first attempt at #312 did, and it is wrong twice over. A key | ||
| * first registered during the file cannot be re-added by anyone: `loadApp()` inside a test body | ||
| * dynamically imports the app's i18n package, `defineCatalogs()` there is MODULE scope — once per | ||
| * `bun test` process — so dropping the app's 519 keys left every later file's own `import` a cache | ||
| * hit that declares nothing and `t('brand.name')` answering `⟦brand.name⟧` for the rest of the run. | ||
| * And a key it OVERRODE is the same declaration read one layer down: the demo app's | ||
| * `admin.denied.body` overrides `@ultimat3/i18n`'s own base string, so reverting to the inherited | ||
| * value rendered the framework's `This account is missing admin:read` in place of the app's copy — | ||
| * a green `⟦…⟧` sweep hiding the identical defect. | ||
| * | ||
| * The cost is stated rather than hidden: a file that deliberately CLOBBERS an inherited key leaves | ||
| * that value for the next file. Its cleanup is the one this repair is built around — `resetCatalogs()` | ||
| * in the file's own `afterAll`, which drops its layer and lets the boundary put back what that clear | ||
| * took from everyone else. | ||
| */ | ||
| function restoreCatalogs(snapshot: readonly (readonly [Locale, Catalog])[]): void { | ||
| const inherited = new Map(snapshot); | ||
| const live = new Map(registeredLocales().map((locale) => [locale, catalogFor(locale)] as const)); | ||
| // Cleared first so the merge order below is this function's to choose: `registerCatalog` puts | ||
| // its argument last, which would otherwise let the inherited value win the keys it shares. | ||
| resetCatalogs(); | ||
| for (const locale of new Set([...inherited.keys(), ...live.keys()])) { | ||
| registerCatalog(locale, mergeCatalogs(inherited.get(locale) ?? {}, live.get(locale) ?? {})); | ||
| } | ||
| } |
236241
1.94%4154
0.83%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated
Updated