@ultimat3/testing 8.0.0 → 10.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 CHANGED
@@ -43,7 +43,10 @@ is its own entry point and not part of the barrel.
43
43
  | Leaks are the file's, not the next file's | `installRegistryLeakGuard()` runs from the preload and fails the run naming the FILE that left cache tags declared or a cache tier registered after its last test (`X_TEST_REGISTRY_LEAK`). `bun test` is one process, so without it the failure lands on an innocent suite in another package. What a file's MODULE graph declares is its environment; what the file installs after that is its own to undo |
44
44
  | The baseline is not a hook | measured on Bun 1.3.14 the order is onLoad → module eval → file `beforeAll` → describe `beforeAll` → preload `beforeEach`, so a preload hook cannot sample before the file's own `beforeAll` — a `declareTags()` there read as environment and the run went green. The load handler appends the sample to the file's source instead: after evaluation, before any hook the file registers. It is also the only signal carrying file identity, which `bun:test` hooks do not |
45
45
  | 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 |
46
- | 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 |
46
+ | 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 |
47
+ | 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 |
48
+ | 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 unREPORTED |
49
+ | Filled and CLEARED are different questions | and the row above answers only the first. "Idiomatic to leave filled" is about the leak REPORT; it says nothing about a file that calls `clear*()`/`reset*()` and takes a module-scope declaration away from every file after it, which is what the RESTORE half exists for. The restore covers four registries and roughly nine have a reset export — routes (`clearRoutes`, `@ultimat3/render`), jobs and tasks (`@ultimat3/jobs`), actions and queries (`resetRegistry`), models/prompts/agents (`@ultimat3/ai`), mails (`@ultimat3/mail`). The table in `registry-leak-guard.ts` is the list. **Not closed, `As of 2026-08-23`**, and the reason is the shape rather than the difficulty: `jobs`, `action`, `query` and `mail` publish a lister and a reset but no RESTORE, so each needs the pair `@ultimat3/policy` got (`restorePermissions` / `restoreRoles`) before one line here can use it — and it has to land as one change, because anything less means editing `ProcessRegistrySnapshot` twice |
47
50
  | 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 |
48
51
  | That one helper is off the barrel | `@ultimat3/testing/registry-isolation`, its own entry point. It is the only module here that value-imports `@ultimat3/entity` — the restore is handed back synchronously, so it cannot be a dynamic import inside the call — and a static re-export from `src/index.ts` would load the entity registry into every test that imports this package for `expect` |
49
52
  | Teardown restores, never uninstalls | `describeApp`/`testApp` capture the seal and the determinism snapshot before booting and put those back — `restoreDeterminism()` in a scope hands the REAL clock and the REAL `fetch` to every later FILE in the process. `captureDeterminism()` / `restoreCapturedDeterminism()` are the pair for any nested install |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/testing",
3
- "version": "8.0.0",
3
+ "version": "10.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": "8.0.0",
37
- "@ultimat3/core": "8.0.0",
38
- "@ultimat3/db": "8.0.0",
39
- "@ultimat3/entity": "8.0.0",
40
- "@ultimat3/i18n": "8.0.0",
41
- "@ultimat3/jobs": "8.0.0",
42
- "@ultimat3/mail": "8.0.0",
43
- "@ultimat3/policy": "8.0.0",
44
- "@ultimat3/query": "8.0.0",
45
- "@ultimat3/realtime": "8.0.0",
46
- "@ultimat3/time": "8.0.0"
36
+ "@ultimat3/cache": "10.0.0",
37
+ "@ultimat3/core": "10.0.0",
38
+ "@ultimat3/db": "10.0.0",
39
+ "@ultimat3/entity": "10.0.0",
40
+ "@ultimat3/i18n": "10.0.0",
41
+ "@ultimat3/jobs": "10.0.0",
42
+ "@ultimat3/mail": "10.0.0",
43
+ "@ultimat3/policy": "10.0.0",
44
+ "@ultimat3/query": "10.0.0",
45
+ "@ultimat3/realtime": "10.0.0",
46
+ "@ultimat3/time": "10.0.0"
47
47
  }
48
48
  }
package/src/errors.ts CHANGED
@@ -62,7 +62,12 @@ registerErrorCodes(
62
62
  ),
63
63
  );
64
64
 
65
- const docsFor = (code: TestingErrorCode): string => `https://ultimate.dev/errors/${code}`;
65
+ // No `docs:` on the subclasses below. `UltimateError` fills it from `describeErrorCode(code).docs`,
66
+ // which is `@ultimat3/core`'s `ERROR_DOCS_URL` — one page for every code, never one per code, because
67
+ // `wiki/` is the framework's only public documentation surface and a code lives there in a TABLE ROW,
68
+ // which has no anchor. The `https://ultimate.dev/errors/<code>` links this file built until 9.x
69
+ // answered 404, host included, on every error it has ever thrown; restating the replacement here
70
+ // would be the same constant in eight places waiting to drift again.
66
71
 
67
72
  /** A test reached the network without a mock or an allowlist entry. Always a bug, never a flake. */
68
73
  export class NetworkSealedError extends UltimateError {
@@ -73,7 +78,6 @@ export class NetworkSealedError extends UltimateError {
73
78
  input.allowed.length > 0 ? input.allowed.join(', ') : 'none'
74
79
  })`,
75
80
  fix: `mockFetch('${input.url}', () => new Response('{}')) — or allowHost('${hostOf(input.url)}') if it must be real`,
76
- docs: docsFor('X_TEST_NETWORK_SEALED'),
77
81
  });
78
82
  }
79
83
  }
@@ -85,7 +89,6 @@ export class TestDatabaseUnavailableError extends UltimateError {
85
89
  code: 'X_TEST_DB_UNAVAILABLE',
86
90
  cause: input.cause,
87
91
  fix: 'x dev (embedded Postgres), or set TEST_DATABASE_URL to a running Postgres',
88
- docs: docsFor('X_TEST_DB_UNAVAILABLE'),
89
92
  });
90
93
  }
91
94
  }
@@ -97,7 +100,6 @@ export class NondeterministicError extends UltimateError {
97
100
  code: 'X_TEST_NONDETERMINISTIC',
98
101
  cause: `${input.what} produced "${input.first}" then "${input.second}"`,
99
102
  fix: 'wrap the test in frozenClock() / seededRandom(), or remove the wall-clock read',
100
- docs: docsFor('X_TEST_NONDETERMINISTIC'),
101
103
  });
102
104
  }
103
105
  }
@@ -124,7 +126,6 @@ export class FixtureUnknownError extends UltimateError {
124
126
  ? `test requested fixture "${input.name}" but none are registered`
125
127
  : `test requested fixture "${input.name}"; registered: ${input.registered.join(', ')}`,
126
128
  fix: `register it at test setup: defineFixtures({ ${input.name}: () => buildIt() })`,
127
- docs: docsFor('X_TEST_FIXTURE_UNKNOWN'),
128
129
  });
129
130
  }
130
131
  }
@@ -145,7 +146,6 @@ export class FixtureUnavailableError extends UltimateError {
145
146
  code: 'X_TEST_FIXTURE_UNAVAILABLE',
146
147
  cause: `fixture "${input.name}" is declared but nothing in this process drives it — it needs ${input.needs}`,
147
148
  fix: `install one in the test preload: defineFixtures({ ${input.name}: () => yourDriver() })`,
148
- docs: docsFor('X_TEST_FIXTURE_UNAVAILABLE'),
149
149
  });
150
150
  }
151
151
  }
@@ -170,7 +170,6 @@ export class LiveNodeEmptyError extends UltimateError {
170
170
  cause:
171
171
  'no query declared live: true is registered in this process, so the node would serve none',
172
172
  fix: "import the app's api module in the test preload — import './apps/web/api' — then: x queries list --json",
173
- docs: docsFor('X_TEST_LIVE_NODE_EMPTY'),
174
173
  });
175
174
  }
176
175
  }
@@ -189,7 +188,6 @@ export class LiveNodeUpgradeRefusedError extends UltimateError {
189
188
  code: 'X_TEST_LIVE_NODE_UPGRADE_REFUSED',
190
189
  cause: `the sync node answered the upgrade with ${status === undefined ? 'no response' : `HTTP ${String(status)}`} instead of taking it`,
191
190
  fix: 'await node.start() before connect(), and keep the request path at /_x/sync',
192
- docs: docsFor('X_TEST_LIVE_NODE_UPGRADE_REFUSED'),
193
191
  });
194
192
  }
195
193
  }
@@ -208,7 +206,6 @@ export class NetworkOfflineError extends UltimateError {
208
206
  code: 'X_TEST_NETWORK_OFFLINE',
209
207
  cause: `${input.method} ${input.url} while the test network is ${input.mode}`,
210
208
  fix: 'network.online() before the call — or assert the offline path instead of the request',
211
- docs: docsFor('X_TEST_NETWORK_OFFLINE'),
212
209
  });
213
210
  }
214
211
  }
@@ -220,7 +217,6 @@ export class TestEvalThresholdError extends UltimateError {
220
217
  code: 'X_TEST_EVAL_THRESHOLD',
221
218
  cause: `eval "${input.name}" scored below ${input.threshold}: ${input.detail}`,
222
219
  fix: 'improve the prompt under test, or lower the threshold passed to evalTest()',
223
- docs: docsFor('X_TEST_EVAL_THRESHOLD'),
224
220
  });
225
221
  }
226
222
  }
@@ -234,7 +230,6 @@ export class TestSchemaExpectedError extends UltimateError {
234
230
  // Names the call, not the intent: "assert against action.input" left the reader to work out
235
231
  // which call to edit, and a fix is only executable if it can be pasted over the failing line.
236
232
  fix: 'call toRejectInput(action.input) — the schema, not toRejectInput(action) or the query',
237
- docs: docsFor('X_TEST_SCHEMA_EXPECTED'),
238
233
  });
239
234
  }
240
235
  }
@@ -247,7 +242,6 @@ export class TestJobExpectedError extends UltimateError {
247
242
  cause: 'toEmitSteps expects a job declaration built with job(...)',
248
243
  // Same rule as X_TEST_SCHEMA_EXPECTED's: the paste-able call, not a description of it.
249
244
  fix: 'call toEmitSteps(myJob) with the job export, not toEmitSteps(myJob.run)',
250
- docs: docsFor('X_TEST_JOB_EXPECTED'),
251
245
  });
252
246
  }
253
247
  }
@@ -266,7 +260,6 @@ export class FactoryTraitUnknownError extends UltimateError {
266
260
  ? `factory "${input.table}" was asked for trait "${input.trait}" but declares none`
267
261
  : `factory "${input.table}" has no trait "${input.trait}"; declared: ${input.declared.join(', ')}`,
268
262
  fix: `declare it: defineFactory(${input.table}, { traits: { ${input.trait}: { /* columns */ } } })`,
269
- docs: docsFor('X_TEST_FACTORY_TRAIT_UNKNOWN'),
270
263
  });
271
264
  }
272
265
  }
@@ -282,7 +275,6 @@ export class FactoryNotPersistedError extends UltimateError {
282
275
  code: 'X_TEST_FACTORY_NOT_PERSISTED',
283
276
  cause: `factory "${input.table}".create() ran with no persister in this process`,
284
277
  fix: 'usePersister({ insert: (table, row) => repoFor(table).insert(row) }) in the test preload — or build() for an in-memory row',
285
- docs: docsFor('X_TEST_FACTORY_NOT_PERSISTED'),
286
278
  });
287
279
  }
288
280
  }
@@ -298,7 +290,6 @@ export class NetworkRaceError extends UltimateError {
298
290
  code: 'X_TEST_NETWORK_RACE',
299
291
  cause: 'sealed network lost its original fetch mid-request',
300
292
  fix: 'do not call unsealNetwork() while a request from the same test is still in flight',
301
- docs: docsFor('X_TEST_NETWORK_RACE'),
302
293
  });
303
294
  }
304
295
  }
@@ -357,7 +348,6 @@ export class RegistryLeakError extends UltimateError {
357
348
  code: 'X_TEST_REGISTRY_LEAK',
358
349
  cause: input.leaks.map(describeLeak).join('; '),
359
350
  fix: `${input.leaks.map(repairFor).join('; ')} — then re-run: bun test ${files.join(' ')}`,
360
- docs: docsFor('X_TEST_REGISTRY_LEAK'),
361
351
  });
362
352
  }
363
353
  }
@@ -384,7 +374,6 @@ export class IslandNotBuiltError extends UltimateError {
384
374
  input.built.length === 0
385
375
  ? 'x g island <name> --at apps/web/site — then set root to the directory holding apps/'
386
376
  : `mountIsland({ build, root, file: ${renderFixLiteral(input.built[0], ISLAND_PLACEHOLDER)} }) — the path is app-root-relative, not relative to the test, and ${file} is not one of them`,
387
- docs: docsFor('X_TEST_ISLAND_NOT_BUILT'),
388
377
  });
389
378
  }
390
379
  }
@@ -434,7 +423,6 @@ export class IslandMountMissingError extends UltimateError {
434
423
  component === undefined
435
424
  ? `${file} exports nothing a mount could render — x g island <name> --at apps/web/site writes an island whose mount() is already there, and its shape is the one to copy`
436
425
  : `in ${file} add: import { render } from 'solid-js/web'; export function mount(el: HTMLElement, props: Parameters<typeof ${component}>[0]): void { el.textContent = ''; render(() => <${component} {...props} />, el); }`,
437
- docs: docsFor('X_TEST_ISLAND_NO_MOUNT'),
438
426
  });
439
427
  }
440
428
  }
@@ -14,13 +14,38 @@ import { captureProcessRegistries, restoreProcessRegistries } from './registry-s
14
14
  * manifest's entity names, `registerTier` takes `app.config.ts`'s tiers — so "empty again when the
15
15
  * file ends" is the honest invariant for a test. The entity, job, route and permission registries
16
16
  * are not here: `entity()` and `job()` register at module scope, which is how an app declares
17
- * itself, so a file that leaves them filled is idiomatic rather than leaky. A test whose subject is
17
+ * itself, so a file that leaves them FILLED is idiomatic rather than leaky. A test whose subject is
18
18
  * an EMPTY one of those establishes it itself — `isolateEntityRegistry()`.
19
19
  *
20
- * Neither is RESTORED, and that is the same judgement read the other way: `@ultimat3/cache`
21
- * publishes no un-declare for a tag, so there is nothing to put a tag registry back WITH. The
20
+ * Neither is RESTORED, and for the tag registry that is the same judgement read the other way:
21
+ * `@ultimat3/cache` publishes no un-declare, so there is nothing to put one back WITH. The
22
22
  * registries that are restored are `registry-snapshot.ts`'s, and none of them is reported —
23
23
  * repairing a state and then failing the run over it would be two answers to one question.
24
+ *
25
+ * **Filled and CLEARED are different questions, and the paragraph above only answers the first.**
26
+ * "Idiomatic to leave filled" says nothing about a file that calls `clear*()`/`reset*()` and takes
27
+ * a module-scope declaration from every file after it — a module evaluates once per process, so the
28
+ * next file's own `import` is a cache hit that declares nothing. That is what `registry-snapshot.ts`
29
+ * repairs, and it repairs FOUR registries out of the set that has one:
30
+ *
31
+ * | registry | reset export | owner | in the snapshot? |
32
+ * |---|---|---|---|
33
+ * | locales / catalogs | `resetCatalogs` | `@ultimat3/i18n` | yes |
34
+ * | permissions / roles | `restorePermissions` / `restoreRoles` | `@ultimat3/policy` | yes |
35
+ * | routes | `clearRoutes` | `@ultimat3/render` | **no** |
36
+ * | jobs | `resetJobs` | `@ultimat3/jobs` | **no** |
37
+ * | tasks | `resetTasks` | `@ultimat3/jobs` | **no** |
38
+ * | actions | `resetRegistry` | `@ultimat3/action` | **no** |
39
+ * | queries | `resetRegistry` | `@ultimat3/query` | **no** |
40
+ * | models / prompts / agents | `resetModels` / `resetPrompts` / `resetAgents` | `@ultimat3/ai` | **no** |
41
+ * | mails | `resetMails` | `@ultimat3/mail` | **no** |
42
+ * | entities | `clearRegistry` | `@ultimat3/entity` | no, by decision — `registry-isolation.ts` |
43
+ *
44
+ * Every "no" needs the SAME two halves the two "yes" rows have: a reader and a writer in the owning
45
+ * package (`restorePermissions` was added to `@ultimat3/policy` for exactly this), and one line
46
+ * here. `@ultimat3/jobs`, `@ultimat3/action`, `@ultimat3/query` and `@ultimat3/mail` publish a
47
+ * lister and a reset but no restore, so the change is theirs first and this file's second — and it
48
+ * has to land as ONE change, because the cost of the rest is a second edit of this same shape.
24
49
  */
25
50
  export interface RegistrySample {
26
51
  readonly tags: readonly string[];
@@ -138,7 +163,10 @@ export function installRegistryLeakGuard(): void {
138
163
  // loader exists to replace — and, because the first matching handler wins and this one is
139
164
  // registered from the preload, it would shadow render's transform for that file. Routing
140
165
  // `.tsx` through `transformTsx` is not the alternative either: it needs `@ultimat3/render`,
141
- // whose import installs that global loader into every test process in the repo. Zero
166
+ // and the transform itself is on `@ultimat3/render/server`, whose import installs that global
167
+ // loader into every test process in the repo. (The `.` barrel does NOT — `installRenderLoader()`
168
+ // moved to `server.ts:15` in the 9.0.0 split, and this comment claimed otherwise until
169
+ // 2026-08-23. The `server` half is still the one that would have to be imported.) Zero
142
170
  // `.test.tsx` files exist and the convention is `<file>.test.ts`, so the narrower filter
143
171
  // costs nothing today; a `.test.tsx` added later is unguarded rather than mis-compiled.
144
172
  build.onLoad({ filter: /\.test\.ts$/ }, async (args) => {
@@ -2,12 +2,18 @@
2
2
  // `registry-leak-guard.ts` owns WHEN. A snapshot rather than a reset to defaults, because what a
3
3
  // module declares at MODULE scope evaluates once per `bun test` process — a neighbour's clear is
4
4
  // permanent and there is no second evaluation left to redo it.
5
+ //
6
+ // FOUR registries, out of roughly nine that publish a `clear*`/`reset*`. The missing ones and what
7
+ // each still needs are tabulated in `registry-leak-guard.ts` beside `RegistrySample` — every one of
8
+ // them needs a RESTORE in its owning package first, the way `restorePermissions` / `restoreRoles`
9
+ // were added to `@ultimat3/policy` for the two rows below.
5
10
 
6
11
  import type { Catalog, Locale, LocaleConfig } from '@ultimat3/i18n';
7
12
  import {
8
13
  catalogFor,
9
14
  configureLocales,
10
15
  localeConfig,
16
+ mergeCatalogs,
11
17
  registerCatalog,
12
18
  registeredLocales,
13
19
  resetCatalogs,
@@ -46,16 +52,50 @@ export function captureProcessRegistries(): ProcessRegistrySnapshot {
46
52
  }
47
53
 
48
54
  /**
49
- * Idempotent, and a REPLACE on every registry rather than a merge: a snapshot is the whole truth
50
- * about the process at capture time, so anything declared since must go as surely as anything
51
- * cleared since must come back.
55
+ * Idempotent. A REPLACE on the locale config, the permission set and the role map — a snapshot is
56
+ * the whole truth about those at capture time — and, for the catalogs alone, a key-level
57
+ * RECONCILE. See `restoreCatalogs`.
52
58
  */
53
59
  export function restoreProcessRegistries(snapshot: ProcessRegistrySnapshot): void {
54
60
  // A full `LocaleConfig`, so the merge `configureLocales` performs replaces all three fields —
55
61
  // a partial call can never widen `supported` back.
56
62
  configureLocales(snapshot.locales);
57
- resetCatalogs();
58
- for (const [locale, catalog] of snapshot.catalogs) registerCatalog(locale, catalog);
63
+ restoreCatalogs(snapshot.catalogs);
59
64
  restorePermissions(snapshot.permissions);
60
65
  restoreRoles(snapshot.roles, snapshot.roleSites);
61
66
  }
67
+
68
+ /**
69
+ * Repair a clear; never undo a registration. The catalogs are the one registry here restored by
70
+ * MERGE rather than by replacement, and the module cache is why.
71
+ *
72
+ * `registerCatalog` has no inverse — it merges, last wins — so the only thing a file can do that
73
+ * costs the next file anything is `resetCatalogs()`, and that is exactly what this repairs: a key
74
+ * the snapshot holds and the live registry has lost comes back. Everything the live registry still
75
+ * holds is left alone, INCLUDING a key whose value the file changed.
76
+ *
77
+ * Undoing the change is what the first attempt at #312 did, and it is wrong twice over. A key
78
+ * first registered during the file cannot be re-added by anyone: `loadApp()` inside a test body
79
+ * dynamically imports the app's i18n package, `defineCatalogs()` there is MODULE scope — once per
80
+ * `bun test` process — so dropping the app's 519 keys left every later file's own `import` a cache
81
+ * hit that declares nothing and `t('brand.name')` answering `⟦brand.name⟧` for the rest of the run.
82
+ * And a key it OVERRODE is the same declaration read one layer down: the demo app's
83
+ * `admin.denied.body` overrides `@ultimat3/i18n`'s own base string, so reverting to the inherited
84
+ * value rendered the framework's `This account is missing admin:read` in place of the app's copy —
85
+ * a green `⟦…⟧` sweep hiding the identical defect.
86
+ *
87
+ * The cost is stated rather than hidden: a file that deliberately CLOBBERS an inherited key leaves
88
+ * that value for the next file. Its cleanup is the one this repair is built around — `resetCatalogs()`
89
+ * in the file's own `afterAll`, which drops its layer and lets the boundary put back what that clear
90
+ * took from everyone else.
91
+ */
92
+ function restoreCatalogs(snapshot: readonly (readonly [Locale, Catalog])[]): void {
93
+ const inherited = new Map(snapshot);
94
+ const live = new Map(registeredLocales().map((locale) => [locale, catalogFor(locale)] as const));
95
+ // Cleared first so the merge order below is this function's to choose: `registerCatalog` puts
96
+ // its argument last, which would otherwise let the inherited value win the keys it shares.
97
+ resetCatalogs();
98
+ for (const locale of new Set([...inherited.keys(), ...live.keys()])) {
99
+ registerCatalog(locale, mergeCatalogs(inherited.get(locale) ?? {}, live.get(locale) ?? {}));
100
+ }
101
+ }