@underpostnet/cyberia 3.3.73 → 3.3.76

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.
Files changed (228) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +5 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  10. package/AGENTS.md +61 -12
  11. package/CHANGELOG.md +126 -41
  12. package/CLI-HELP.md +19 -12
  13. package/Dockerfile +1 -1
  14. package/Dockerfile.dev +1 -1
  15. package/Dockerfile.test +2 -2
  16. package/README.md +28 -28
  17. package/bin/build.js +37 -17
  18. package/bin/cyberia.js +1247 -1373
  19. package/bin/index.js +1247 -1373
  20. package/compose.env +17 -5
  21. package/conf.js +7 -3
  22. package/deploy/cyberia-client/deploy.sh +10 -11
  23. package/deploy/cyberia-client/package.sh +0 -1
  24. package/deploy/cyberia-client/state.sh +3 -2
  25. package/deploy/cyberia-server/deploy.sh +10 -11
  26. package/deploy/cyberia-server/package.sh +0 -1
  27. package/deploy/cyberia-server/state.sh +3 -2
  28. package/deploy/dd-cyberia/deploy.sh +5 -5
  29. package/deploy/dd-cyberia/init.sh +15 -14
  30. package/deploy/dd-cyberia/package.sh +0 -1
  31. package/deploy/dd-cyberia/state.sh +2 -2
  32. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  33. package/deploy/lib/config.sh +29 -0
  34. package/deploy/lib/github-actions-logging.sh +92 -76
  35. package/deploy/lib/host.sh +27 -18
  36. package/deploy/lib/state.sh +4 -1
  37. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  38. package/deploy/release/deploy.sh +0 -2
  39. package/deployment.yaml +1 -1
  40. package/docker-compose.yml +23 -17
  41. package/hardhat/package-lock.json +2 -2
  42. package/hardhat/package.json +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  44. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  46. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  47. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  48. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  49. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  50. package/manifests/deployment/mongo-express/service.yaml +16 -0
  51. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  52. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  53. package/package.json +19 -17
  54. package/scripts/test-monitor.sh +3 -3
  55. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  56. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  59. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  60. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  61. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  62. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  63. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  64. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  66. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  67. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  68. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  69. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  70. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  71. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  72. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  73. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  74. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  75. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  76. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  77. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  78. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  79. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  80. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  81. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  82. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  83. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  84. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  85. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  86. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  87. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  88. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  89. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  90. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  91. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  92. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  93. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  94. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  95. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  96. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  97. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  98. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  99. package/src/api/file/file.ref.js +153 -0
  100. package/src/api/file/file.ref.json +11 -2
  101. package/src/api/object-layer/object-layer.model.js +47 -12
  102. package/src/api/object-layer/object-layer.router.js +10 -3
  103. package/src/api/object-layer/object-layer.service.js +113 -120
  104. package/src/cli/app.js +12 -1
  105. package/src/cli/client.js +2 -2
  106. package/src/cli/cluster.js +7 -0
  107. package/src/cli/deploy.js +4 -4
  108. package/src/cli/docker-compose.js +1 -1
  109. package/src/cli/fs-selection.js +115 -0
  110. package/src/cli/fs.js +158 -342
  111. package/src/cli/index.js +22 -10
  112. package/src/cli/repository.js +28 -0
  113. package/src/cli/run.js +71 -197
  114. package/src/cli/test.js +4 -0
  115. package/src/cli/wireguard.js +32 -31
  116. package/src/client/components/core/Docs.js +77 -100
  117. package/src/client/components/core/Modal.js +56 -1
  118. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  119. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  120. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  121. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  122. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  123. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  124. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  125. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  126. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  127. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  128. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  129. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  130. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  131. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  132. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  133. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  134. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  135. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  136. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  137. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  138. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  139. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  140. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  141. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  142. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  143. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  144. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  145. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  146. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  147. package/src/client/services/object-layer/object-layer.management.js +6 -10
  148. package/src/client/services/object-layer/object-layer.service.js +3 -1
  149. package/src/client-builder/client-build-docs.js +52 -52
  150. package/src/client-builder/client-build.js +67 -44
  151. package/src/client-builder/client-bundle.js +347 -0
  152. package/src/db/mongo/MongoExpress.js +187 -0
  153. package/src/grpc/cyberia/grpc-server.js +5 -7
  154. package/src/index.js +1 -1
  155. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  156. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  157. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  158. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  159. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  160. package/src/projects/cyberia/gemini-client.js +3 -3
  161. package/src/projects/cyberia/generate-saga.js +4 -11
  162. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  163. package/src/projects/cyberia/instance-backup.js +210 -0
  164. package/src/projects/cyberia/instance-data.js +177 -257
  165. package/src/projects/cyberia/map-preview-generator.js +32 -68
  166. package/src/projects/cyberia/object-layer.js +152 -319
  167. package/src/projects/cyberia/seed-audio.js +139 -0
  168. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  169. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  170. package/src/projects/cyberia/stat-balance.js +81 -0
  171. package/src/projects/cyberia/stat-commands.js +23 -0
  172. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  173. package/src/runtime/cyberia-client/Dockerfile +8 -4
  174. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  175. package/src/runtime/cyberia-server/Dockerfile +9 -2
  176. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  177. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  178. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  179. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  180. package/src/runtime/engine-cyberia/compose.env +17 -5
  181. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  182. package/src/server/build/coverage.js +113 -42
  183. package/src/server/build/package.js +55 -12
  184. package/src/server/build/testing.js +59 -9
  185. package/src/server/network/middlewares.js +8 -2
  186. package/src/server/ops/logger.js +70 -35
  187. package/src/server/runtime/conf.js +4 -2
  188. package/src/server/storage/data-query.js +16 -0
  189. package/src/server/storage/downloader.js +62 -14
  190. package/src/server/storage/zip.js +153 -0
  191. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  192. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  193. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  194. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  195. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  196. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  197. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  198. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  199. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  200. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  201. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  202. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  203. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  204. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  205. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  206. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  207. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  208. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  209. package/test/unit/client-build-docs.test.js +97 -70
  210. package/test/unit/client-bundle.test.js +313 -0
  211. package/test/unit/conf-resolution.test.js +17 -0
  212. package/test/unit/coverage-artifact.test.js +120 -29
  213. package/test/unit/cyberia/instance-backup.test.js +183 -0
  214. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  215. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  216. package/test/unit/cyberia/stat-balance.test.js +69 -0
  217. package/test/unit/deploy-log-table.test.js +76 -5
  218. package/test/unit/downloader-integrity.test.js +98 -0
  219. package/test/unit/file-reference-registry.test.js +141 -0
  220. package/test/unit/fs-storage-paths.test.js +596 -0
  221. package/test/unit/logger-redaction.test.js +29 -0
  222. package/test/unit/package.test.js +70 -16
  223. package/test/unit/prepare-host.test.js +84 -1
  224. package/test/unit/test-tiers.test.js +27 -0
  225. package/test/unit/zip-archive.test.js +142 -0
  226. package/vitest.config.js +11 -3
  227. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  228. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -712,6 +712,23 @@ describe('private conf sync', () => {
712
712
  expect(copied).to.include('./engine-private/catalog/items.json -> ../engine-core-private/catalog/items.json');
713
713
  });
714
714
 
715
+ // Regression: an instance backup that dropped an item left the item's stale atlas in the
716
+ // mirror, and the pod restored it over the current one from another instance.
717
+ it('replaces each payload entry in the checkout instead of merging into it', () => {
718
+ const calls = [];
719
+ checkout({ present: true });
720
+ vi.spyOn(fs, 'readdirSync').mockReturnValue([]);
721
+ vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
722
+ vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
723
+ withEnv({ GITHUB_USERNAME: 'fixture-org' }, () => syncPrivateConf('dd-core', ['cyberia-instances/TEST']));
724
+ const remove = calls.indexOf('remove ../engine-core-private/cyberia-instances/TEST');
725
+ const copy = calls.indexOf(
726
+ 'copy ./engine-private/cyberia-instances/TEST -> ../engine-core-private/cyberia-instances/TEST',
727
+ );
728
+ expect(remove).to.be.greaterThan(-1);
729
+ expect(copy).to.be.greaterThan(remove);
730
+ });
731
+
715
732
  it('mirrors the cron conf the checkout declares', () => {
716
733
  const copied = [];
717
734
  checkout({ present: true, cronId: 'dd-cron' });
@@ -13,95 +13,186 @@ import { expect } from 'chai';
13
13
  import fs from 'fs-extra';
14
14
  import {
15
15
  COVERAGE_BUNDLE_DIRECTORY,
16
- bundleCoverageReport,
16
+ bundleCoverageReports,
17
17
  coverageReportCandidates,
18
+ coverageReportCommand,
19
+ coverageReportsFactory,
18
20
  coverageUnavailablePage,
21
+ deployCoverageReports,
19
22
  resolveCoverageReportPath,
20
23
  } from '../../src/server/build/coverage.js';
21
24
  import { EXECUTION_PROFILE_ENV_KEY } from '../../src/server/build/execution.js';
22
25
  import { isTestRuntime, runtimeStatusWritable } from '../../src/server/runtime/runtime-status.js';
23
26
 
27
+ describe('coverage report declaration', () => {
28
+ it('reads each declared report with its label defaulting to the id', () => {
29
+ expect(coverageReportsFactory({ coverage: [{ id: 'engine', suite: 'unit' }] })).to.deep.equal([
30
+ { id: 'engine', label: 'engine', suite: 'unit', path: undefined },
31
+ ]);
32
+ });
33
+
34
+ it('declares nothing for a deploy without coverage', () => {
35
+ expect(coverageReportsFactory({})).to.deep.equal([]);
36
+ expect(coverageReportsFactory(undefined)).to.deep.equal([]);
37
+ });
38
+
39
+ it('rejects an entry without exactly one source, a non-slug id, or a repeated id', () => {
40
+ expect(() => coverageReportsFactory({ coverage: [{ id: 'x' }] })).to.throw("exactly one of 'suite' or 'path'");
41
+ expect(() => coverageReportsFactory({ coverage: [{ id: 'x', suite: 'unit', path: './' }] })).to.throw(
42
+ 'exactly one',
43
+ );
44
+ expect(() => coverageReportsFactory({ coverage: [{ id: 'Bad Id', suite: 'unit' }] })).to.throw('slug');
45
+ expect(() =>
46
+ coverageReportsFactory({
47
+ coverage: [
48
+ { id: 'x', suite: 'unit' },
49
+ { id: 'x', suite: 'app' },
50
+ ],
51
+ }),
52
+ ).to.throw('declared twice');
53
+ });
54
+
55
+ it("collects a deploy's reports across routes once each", () => {
56
+ const route = { docs: { coverage: [{ id: 'engine', suite: 'unit' }] } };
57
+ const confServer = { 'a.test': { '/': route, '/store': route }, 'b.test': { '/': {} } };
58
+ expect(deployCoverageReports(confServer).map(({ id }) => id)).to.deep.equal(['engine']);
59
+ });
60
+
61
+ it('refuses one id declared with two sources', () => {
62
+ const confServer = {
63
+ 'a.test': {
64
+ '/': { docs: { coverage: [{ id: 'engine', suite: 'unit' }] } },
65
+ '/store': { docs: { coverage: [{ id: 'engine', suite: 'app' }] } },
66
+ },
67
+ };
68
+ expect(() => deployCoverageReports(confServer)).to.throw('two different sources');
69
+ });
70
+
71
+ it('names the command that produces each kind of report', () => {
72
+ expect(coverageReportCommand({ suite: 'unit,infra,app' })).to.equal('node bin test unit,infra,app');
73
+ expect(coverageReportCommand({ path: './hardhat/' })).to.equal('npm run coverage --prefix ./hardhat');
74
+ });
75
+ });
76
+
24
77
  describe('coverage report resolution', () => {
25
78
  let fixturePath;
79
+ let cwd;
26
80
 
81
+ // The bundled candidate is cwd-relative, and a published deploy artifact (the tree a
82
+ // downstream CI runs this suite in) carries real reports under it; resolve against the
83
+ // fixture, never the tree.
27
84
  beforeEach(() => {
28
85
  fixturePath = fs.mkdtempSync('/tmp/engine-coverage-artifact-');
86
+ cwd = process.cwd();
87
+ process.chdir(fixturePath);
29
88
  });
30
89
 
31
90
  afterEach(() => {
91
+ process.chdir(cwd);
32
92
  fs.removeSync(fixturePath);
33
93
  });
34
94
 
35
- it('looks for a run output first and the bundled artifact last', () => {
36
- expect(coverageReportCandidates('/src/')).to.deep.equal([
95
+ it("looks for a suite's own run output first and the bundled artifact last", () => {
96
+ expect(coverageReportCandidates({ id: 'engine', suite: 'unit,infra,app' })).to.deep.equal([
97
+ 'coverage/unit-infra-app',
98
+ `${COVERAGE_BUNDLE_DIRECTORY}/engine`,
99
+ ]);
100
+ });
101
+
102
+ it("looks through an external tree's report layouts before the bundled artifact", () => {
103
+ expect(coverageReportCandidates({ id: 'hardhat', path: '/src/' })).to.deep.equal([
37
104
  '/src/coverage/html',
38
105
  '/src/coverage/lcov-report',
39
106
  '/src/coverage',
40
- `/src/${COVERAGE_BUNDLE_DIRECTORY}`,
107
+ `${COVERAGE_BUNDLE_DIRECTORY}/hardhat`,
41
108
  ]);
42
109
  });
43
110
 
44
- it('has nowhere to look without a source tree', () => {
45
- expect(coverageReportCandidates(undefined)).to.deep.equal([]);
46
- expect(resolveCoverageReportPath(undefined)).to.equal(undefined);
47
- });
48
-
49
111
  it('ignores a coverage directory that carries no HTML index', () => {
50
112
  fs.outputFileSync(`${fixturePath}/coverage/lcov.info`, 'TN:\n');
51
- expect(resolveCoverageReportPath(fixturePath)).to.equal(undefined);
113
+ expect(resolveCoverageReportPath({ id: 'x', path: fixturePath })).to.equal(undefined);
52
114
  });
53
115
 
54
- it('resolves the bundled artifact when no run wrote a report', () => {
55
- fs.outputFileSync(`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/index.html`, '<!doctype html>');
56
- expect(resolveCoverageReportPath(fixturePath)).to.equal(`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}`);
116
+ it('resolves the report of the selection the deploy names, not the last run made', () => {
117
+ fs.outputFileSync(`${fixturePath}/coverage/unit-infra-app-cyberia/index.html`, '<!doctype html>');
118
+ fs.outputFileSync(`${fixturePath}/coverage/cyberia/index.html`, '<!doctype html>');
119
+ expect(resolveCoverageReportPath({ id: 'x', suite: 'cyberia' })).to.equal('coverage/cyberia');
120
+ expect(resolveCoverageReportPath({ id: 'x', suite: 'unit,infra,app' })).to.equal(undefined);
121
+ });
122
+
123
+ it('falls back to the report a deploy artifact already carries', () => {
124
+ fs.outputFileSync(`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/hardhat/index.html`, '<!doctype html>');
125
+ expect(resolveCoverageReportPath({ id: 'hardhat', path: `${fixturePath}/hardhat` })).to.equal(
126
+ `${COVERAGE_BUNDLE_DIRECTORY}/hardhat`,
127
+ );
57
128
  });
58
129
  });
59
130
 
60
131
  describe('coverage bundling into a deploy artifact', () => {
61
132
  let fixturePath;
133
+ let cwd;
62
134
 
135
+ // The bundled candidate is cwd-relative, and a published deploy artifact (the tree a
136
+ // downstream CI runs this suite in) carries real reports under it; resolve against the
137
+ // fixture, never the tree.
63
138
  beforeEach(() => {
64
139
  fixturePath = fs.mkdtempSync('/tmp/engine-coverage-artifact-');
140
+ cwd = process.cwd();
141
+ process.chdir(fixturePath);
65
142
  });
66
143
 
67
144
  afterEach(() => {
145
+ process.chdir(cwd);
68
146
  fs.removeSync(fixturePath);
69
147
  });
70
148
 
71
- it('carries the report into the artifact at the published path', () => {
72
- fs.outputFileSync(`${fixturePath}/engine/coverage/lcov-report/index.html`, '<!doctype html><title>report</title>');
73
- fs.outputFileSync(`${fixturePath}/engine/coverage/lcov-report/base.css`, 'body {}');
149
+ it('carries each report into the artifact under its own id', () => {
150
+ fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/index.html`, '<!doctype html><title>report</title>');
151
+ fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/base.css`, 'body {}');
74
152
 
75
- const result = bundleCoverageReport(`${fixturePath}/engine`, `${fixturePath}/template`);
153
+ const [result] = bundleCoverageReports(
154
+ [{ id: 'hardhat', path: `${fixturePath}/hardhat` }],
155
+ `${fixturePath}/template`,
156
+ );
76
157
 
77
158
  expect(result.bundled).to.equal(true);
78
- expect(result.to).to.equal(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}`);
159
+ expect(result.to).to.equal(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/hardhat`);
79
160
  expect(fs.readFileSync(`${result.to}/index.html`, 'utf8')).to.include('report');
80
161
  expect(fs.existsSync(`${result.to}/base.css`)).to.equal(true);
81
162
  });
82
163
 
83
164
  it('replaces a previously bundled report rather than merging into it', () => {
84
- fs.outputFileSync(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/stale.html`, 'stale');
85
- fs.outputFileSync(`${fixturePath}/engine/coverage/html/index.html`, '<!doctype html><title>fresh</title>');
165
+ fs.outputFileSync(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/hardhat/stale.html`, 'stale');
166
+ fs.outputFileSync(`${fixturePath}/hardhat/coverage/index.html`, '<!doctype html><title>fresh</title>');
86
167
 
87
- const { to } = bundleCoverageReport(`${fixturePath}/engine`, `${fixturePath}/template`);
168
+ const [{ to }] = bundleCoverageReports(
169
+ [{ id: 'hardhat', path: `${fixturePath}/hardhat` }],
170
+ `${fixturePath}/template`,
171
+ );
88
172
 
89
173
  expect(fs.existsSync(`${to}/stale.html`)).to.equal(false);
90
174
  expect(fs.readFileSync(`${to}/index.html`, 'utf8')).to.include('fresh');
91
175
  });
92
176
 
93
- it('reports that nothing was bundled without creating the directory', () => {
94
- const result = bundleCoverageReport(`${fixturePath}/engine`, `${fixturePath}/template`);
95
-
96
- expect(result.bundled).to.equal(false);
177
+ it('reports what was not bundled without creating its directory', () => {
178
+ const [result] = bundleCoverageReports(
179
+ [{ id: 'hardhat', path: `${fixturePath}/hardhat` }],
180
+ `${fixturePath}/template`,
181
+ );
182
+
183
+ expect(result).to.deep.equal({
184
+ id: 'hardhat',
185
+ bundled: false,
186
+ to: `${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/hardhat`,
187
+ });
97
188
  expect(fs.existsSync(result.to)).to.equal(false);
98
189
  });
99
190
 
100
- it('renders a static unavailable page that never points at a runtime test run', () => {
101
- const page = coverageUnavailablePage();
191
+ it('renders a static unavailable page naming the report and the run that produces it', () => {
192
+ const page = coverageUnavailablePage({ id: 'engine', suite: 'unit,infra,app' });
102
193
  expect(page).to.include('Coverage report unavailable');
103
- expect(page).to.include('npm run test:coverage');
104
- expect(page).to.include(COVERAGE_BUNDLE_DIRECTORY);
194
+ expect(page).to.include('node bin test unit,infra,app');
195
+ expect(page).to.include(`${COVERAGE_BUNDLE_DIRECTORY}/engine`);
105
196
  expect(page).not.to.include('<script');
106
197
  });
107
198
  });
@@ -0,0 +1,183 @@
1
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
2
+ import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ // The restore path only routes documents; the collections are in-memory stand-ins, IPFS is
7
+ // unreachable, and the static frame writer is a no-op, so what is asserted is the routing.
8
+ const models = {};
9
+ vi.mock('../../../src/db/DataBaseProvider.js', () => ({
10
+ DataBaseProviderService: { getModel: (name) => models[name] },
11
+ }));
12
+ vi.mock('../../../src/api/ipfs/ipfs.service.js', () => ({ createPinRecord: async () => ({}) }));
13
+ vi.mock('../../../src/projects/cyberia/ipfs-client.js', () => ({ IpfsClient: { addToIpfs: async () => null } }));
14
+ vi.mock('../../../src/projects/cyberia/object-layer.js', () => ({
15
+ ObjectLayerEngine: { writeStaticFrameAssets: async () => [], computeAndSaveFinalSha256: async () => ({}) },
16
+ }));
17
+ // The generator pulls in jimp, a cyberia product dependency the engine runner does not install;
18
+ // every store method that would reach it is spied out below, so only the constant is needed.
19
+ vi.mock('../../../src/projects/cyberia/atlas-sprite-sheet-generator.js', () => ({
20
+ AtlasSpriteSheetGenerator: {},
21
+ DEFAULT_ATLAS_UPSCALE_FACTOR: 20,
22
+ }));
23
+
24
+ const { AtlasSpriteSheetStore } = await import('../../../src/projects/cyberia/atlas-sprite-sheet-store.js');
25
+ const { atlasBackupFileKey, readObjectLayerBackup, restoreObjectLayerBackup } =
26
+ await import('../../../src/projects/cyberia/instance-backup.js');
27
+
28
+ /** A collection that remembers what was created and hands the last upsert back as the live document. */
29
+ const collection = () => {
30
+ const created = [];
31
+ let live = null;
32
+ const query = (value) => Object.assign(Promise.resolve(value), { populate: () => Promise.resolve(value) });
33
+ return {
34
+ created,
35
+ deleteOne: async () => ({ deletedCount: 0 }),
36
+ create: async (doc) => (created.push(doc), doc),
37
+ findByItemId: () => query(live),
38
+ upsertByItemId: async (doc) => {
39
+ live = { ...doc, markModified() {}, save: async () => live };
40
+ return live;
41
+ },
42
+ get live() {
43
+ return live;
44
+ },
45
+ };
46
+ };
47
+
48
+ let backupDir;
49
+ const write = (rel, value) => writeFile(join(backupDir, rel), Buffer.isBuffer(value) ? value : JSON.stringify(value));
50
+
51
+ beforeAll(async () => {
52
+ backupDir = await mkdtemp(join(tmpdir(), 'cyberia-backup-'));
53
+ for (const dir of ['object-layers', 'render-frames', 'atlas-sprite-sheets', 'files', 'ipfs/content']) {
54
+ await mkdir(join(backupDir, dir), { recursive: true });
55
+ }
56
+ await write('object-layers/hatchet.json', {
57
+ _id: 'ol1',
58
+ cid: 'cid-data',
59
+ objectLayerRenderFramesId: 'rf1',
60
+ atlasSpriteSheetId: 'at1',
61
+ data: { item: { id: 'hatchet', type: 'weapon' }, render: { cid: 'cid-png', metadataCid: 'cid-meta' } },
62
+ });
63
+ await write('render-frames/hatchet.json', { _id: 'rf1', frames: {}, colors: [], frame_duration: 100 });
64
+ await write('atlas-sprite-sheets/hatchet.json', {
65
+ _id: 'at1',
66
+ fileId: 'f-full',
67
+ minifyFileId: 'f-min',
68
+ idlePreviewFileId: 'f-idle',
69
+ cid: 'cid-png',
70
+ });
71
+ // Files are matched on _id, never on name: one is misnamed on purpose, one is a bystander.
72
+ await write('files/whatever.json', {
73
+ _id: 'f-full',
74
+ name: 'hatchet-atlas.png',
75
+ data: { $base64: Buffer.from('PNG').toString('base64') },
76
+ });
77
+ await write('files/atlas-minify-hatchet.json', {
78
+ _id: 'f-min',
79
+ name: 'hatchet-minify.png',
80
+ data: { type: 'Buffer', data: [1, 2, 3] },
81
+ });
82
+ await write('files/atlas-idle-hatchet.json', { _id: 'f-idle', name: 'hatchet-idle.png', data: { $base64: 'AA==' } });
83
+ await write('files/atlas-sword.json', { _id: 'f-other', name: 'sword-atlas.png', data: { $base64: 'AA==' } });
84
+ await write('ipfs/content/cid-data.bin', Buffer.from('data'));
85
+ await write('ipfs/content/cid-png.bin', Buffer.from('png'));
86
+ // cid-meta has no payload on purpose.
87
+ });
88
+
89
+ afterAll(() => rm(backupDir, { recursive: true, force: true }));
90
+
91
+ describe('reading one object layer out of an instance backup', () => {
92
+ it('gathers the object layer and every document it references', () => {
93
+ const backup = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
94
+ expect(backup.objectLayer.data.item.id).toBe('hatchet');
95
+ expect(backup.renderFrames._id).toBe('rf1');
96
+ expect(backup.atlas._id).toBe('at1');
97
+ });
98
+
99
+ it('resolves the atlas render Files by _id and leaves other Files alone', () => {
100
+ const { files } = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
101
+ expect(files.map((f) => f._id).sort()).toEqual(['f-full', 'f-idle', 'f-min']);
102
+ });
103
+
104
+ it('names each exported render after its atlas field', () => {
105
+ expect(atlasBackupFileKey('fileId', 'hatchet')).toBe('atlas-hatchet');
106
+ expect(atlasBackupFileKey('minifyFileId', 'hatchet')).toBe('atlas-minify-hatchet');
107
+ expect(atlasBackupFileKey('idlePreviewFileId', 'hatchet')).toBe('atlas-idle-hatchet');
108
+ });
109
+
110
+ it('decodes both File byte encodings the export can write', () => {
111
+ const { files } = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
112
+ const byId = Object.fromEntries(files.map((f) => [f._id, f.data]));
113
+ expect(Buffer.isBuffer(byId['f-full']) && byId['f-full'].toString()).toBe('PNG');
114
+ expect(Buffer.isBuffer(byId['f-min']) && [...byId['f-min']]).toEqual([1, 2, 3]);
115
+ });
116
+
117
+ it('collects only the payloads the backup actually carries', () => {
118
+ const { payloads } = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
119
+ expect([...payloads.keys()].sort()).toEqual(['cid-data', 'cid-png']);
120
+ expect(payloads.get('cid-png').toString()).toBe('png');
121
+ });
122
+
123
+ it('names the backup and item when the item is missing', () => {
124
+ expect(() => readObjectLayerBackup({ backupDir, itemId: 'ghost' })).toThrow(/no object layer 'ghost'/);
125
+ });
126
+
127
+ it('tolerates an item with no atlas or render frames yet', async () => {
128
+ await write('object-layers/bare.json', { _id: 'ol2', data: { item: { id: 'bare', type: 'floor' } } });
129
+ const backup = readObjectLayerBackup({ backupDir, itemId: 'bare' });
130
+ expect(backup.atlas).toBeNull();
131
+ expect(backup.renderFrames).toBeNull();
132
+ expect(backup.files).toEqual([]);
133
+ expect(backup.payloads.size).toBe(0);
134
+ });
135
+ });
136
+
137
+ describe('restoring one object layer from an instance backup', () => {
138
+ let persist;
139
+ let idle;
140
+
141
+ beforeEach(() => {
142
+ for (const name of ['ObjectLayer', 'ObjectLayerRenderFrames', 'AtlasSpriteSheet', 'File'])
143
+ models[name] = collection();
144
+ persist = vi.spyOn(AtlasSpriteSheetStore, 'persist').mockResolvedValue({
145
+ atlasDoc: { _id: 'at-rebuilt' },
146
+ atlasCid: 'cid-rebuilt-png',
147
+ atlasMetadataCid: 'cid-rebuilt-meta',
148
+ });
149
+ idle = vi.spyOn(AtlasSpriteSheetStore, 'syncIdlePreview').mockResolvedValue({ status: 'unchanged' });
150
+ vi.spyOn(AtlasSpriteSheetStore, 'pruneOrphanRenders').mockResolvedValue(0);
151
+ });
152
+
153
+ afterAll(() => vi.restoreAllMocks());
154
+
155
+ it('keeps the atlas the backup carries when it holds its minified render', async () => {
156
+ await restoreObjectLayerBackup({ backupDir, itemId: 'hatchet', options: {} });
157
+ expect(models.AtlasSpriteSheet.created[0].minifyFileId).toBe('f-min');
158
+ expect(persist).not.toHaveBeenCalled();
159
+ expect(idle).toHaveBeenCalledWith({ itemKey: 'hatchet', options: {} });
160
+ expect(models.ObjectLayer.live.data.render).toEqual({ cid: 'cid-png', metadataCid: 'cid-meta' });
161
+ });
162
+
163
+ // Regression: a backup written before the minified render existed restored an atlas the
164
+ // blob route could not serve, and the client runtime got a 500 for every entity wearing it.
165
+ it('rebuilds the atlas a backup restores without its minified render and relinks the item', async () => {
166
+ await write('object-layers/relic.json', {
167
+ _id: 'ol3',
168
+ objectLayerRenderFramesId: 'rf3',
169
+ atlasSpriteSheetId: 'at3',
170
+ data: { item: { id: 'relic', type: 'skin' }, render: { cid: 'cid-old-png', metadataCid: 'cid-old-meta' } },
171
+ });
172
+ const frames = { _id: 'rf3', frames: { down_idle: [] }, colors: [], frame_duration: 100 };
173
+ await write('render-frames/relic.json', frames);
174
+ await write('atlas-sprite-sheets/relic.json', { _id: 'at3', fileId: 'f-relic', minifyFileId: null });
175
+
176
+ await restoreObjectLayerBackup({ backupDir, itemId: 'relic', options: {} });
177
+
178
+ expect(persist).toHaveBeenCalledWith({ itemKey: 'relic', objectLayerRenderFrames: frames, options: {} });
179
+ const { live } = models.ObjectLayer;
180
+ expect(live.atlasSpriteSheetId).toBe('at-rebuilt');
181
+ expect(live.data.render).toEqual({ cid: 'cid-rebuilt-png', metadataCid: 'cid-rebuilt-meta' });
182
+ });
183
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { fetchInstanceObjectLayerItemIds } from '../../../src/projects/cyberia/instance-data.js';
3
+
4
+ const lean = (value) => ({ lean: async () => value, populate: () => lean(value) });
5
+
6
+ const buildModels = ({ instance, maps = [], storedItemIds = [] }) => ({
7
+ CyberiaInstance: { findOne: () => lean(instance) },
8
+ CyberiaInstanceConf: { findOne: () => lean({ instanceCode: instance?.code, entityDefaults: [] }) },
9
+ CyberiaMap: { find: () => lean(maps) },
10
+ CyberiaEntityTypeDefault: { find: () => lean([]) },
11
+ CyberiaAction: { find: () => lean([]) },
12
+ CyberiaQuest: { find: () => lean([]) },
13
+ CyberiaSkill: { find: () => lean([]) },
14
+ ObjectLayer: {
15
+ find: (filter) => {
16
+ const wanted = new Set(filter['data.item.id'].$in);
17
+ return lean(storedItemIds.filter((id) => wanted.has(id)).map((id) => ({ _id: id, data: { item: { id } } })));
18
+ },
19
+ },
20
+ });
21
+
22
+ describe('instance object layer item ids', () => {
23
+ it('collects the item ids the instance maps place, limited to stored documents', async () => {
24
+ const models = buildModels({
25
+ instance: { _id: 'i1', code: 'FOREST', cyberiaMapCodes: ['forest-1'] },
26
+ maps: [
27
+ {
28
+ code: 'forest-1',
29
+ entities: [{ objectLayerItemIds: ['anon', 'hatchet'] }, { objectLayerItemIds: ['hatchet', '$runtime'] }],
30
+ },
31
+ ],
32
+ storedItemIds: ['anon', 'hatchet', 'unrelated'],
33
+ });
34
+ const itemIds = await fetchInstanceObjectLayerItemIds(models, 'FOREST');
35
+ expect(itemIds).toContain('anon');
36
+ expect(itemIds).toContain('hatchet');
37
+ expect(itemIds).not.toContain('unrelated');
38
+ expect(itemIds).not.toContain('$runtime');
39
+ expect(new Set(itemIds).size).toBe(itemIds.length);
40
+ });
41
+
42
+ it('fails on an unknown instance code instead of returning the fallback world', async () => {
43
+ const models = buildModels({ instance: null });
44
+ await expect(fetchInstanceObjectLayerItemIds(models, 'GHOST')).rejects.toThrow('"GHOST" not found');
45
+ });
46
+ });
@@ -0,0 +1,22 @@
1
+ import { expect } from 'chai';
2
+ import fs from 'fs-extra';
3
+ // Named import: js-yaml's ESM build exports no default.
4
+ import { load } from 'js-yaml';
5
+
6
+ describe('the cyberia publish workflow', () => {
7
+ it('rewrites the published manifest after the install and before every publish', () => {
8
+ // The rewrite is the publish step's alone: a checkout that installed the published shape
9
+ // would run the engine source against a second engine in node_modules.
10
+ const workflow = load(fs.readFileSync('./.github/workflows/publish.cyberia.ci.yml', 'utf8'));
11
+
12
+ for (const [job, { steps }] of Object.entries(workflow.jobs)) {
13
+ const commands = steps.map(({ run }) => `${run ?? ''}`);
14
+ const restore = commands.findIndex((command) => command.includes('publishedProductPackageJson'));
15
+ const install = commands.findIndex((command) => command.trim().startsWith('npm ci'));
16
+ const publish = commands.findIndex((command) => command.includes('npm publish'));
17
+
18
+ expect(restore, `${job}: restores the published manifest`).to.be.greaterThan(install);
19
+ expect(restore, `${job}: restores it before publishing`).to.be.lessThan(publish);
20
+ }
21
+ });
22
+ });
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { balanceStats, resolveStatBounds, statPolicyActive } from '../../../src/projects/cyberia/stat-balance.js';
3
+ import {
4
+ STAT_TYPE_BOUNDS,
5
+ STAT_TYPES,
6
+ statBoundsForType,
7
+ } from '../../../src/client/components/cyberia/SharedDefaultsCyberia.js';
8
+
9
+ const wild = { effect: 60, resistance: -40, agility: 3, range: 25, intelligence: 9, utility: -2 };
10
+ const lcg = (seed) => () => (seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 4294967296;
11
+
12
+ describe('stat balancing policy', () => {
13
+ it('is inert without normalize or random', () => {
14
+ expect(statPolicyActive({})).toBe(false);
15
+ expect(balanceStats({ stats: wild, itemType: 'weapon' })).toEqual(wild);
16
+ });
17
+
18
+ it('clamps into the semantic bounds of the item type', () => {
19
+ expect(balanceStats({ stats: wild, itemType: 'weapon', policy: { normalize: true } })).toEqual({
20
+ effect: 20,
21
+ resistance: -10,
22
+ agility: 3,
23
+ range: 10,
24
+ intelligence: 5,
25
+ utility: 0,
26
+ });
27
+ expect(balanceStats({ stats: wild, itemType: 'skin', policy: { normalize: true } })).toEqual({
28
+ effect: 1,
29
+ resistance: 0,
30
+ agility: 3,
31
+ range: 0,
32
+ intelligence: 2,
33
+ utility: 0,
34
+ });
35
+ });
36
+
37
+ it('keeps the contract bounds for a type without semantic bounds', () => {
38
+ expect(statBoundsForType('unknown')).toEqual(Object.fromEntries(STAT_TYPES.map((key) => [key, [-100, 100]])));
39
+ expect(balanceStats({ stats: wild, itemType: 'unknown', policy: { normalize: true } })).toEqual(wild);
40
+ });
41
+
42
+ it('is idempotent and deterministic', () => {
43
+ const once = balanceStats({ stats: wild, itemType: 'weapon', policy: { normalize: true } });
44
+ expect(balanceStats({ stats: once, itemType: 'weapon', policy: { normalize: true } })).toEqual(once);
45
+ const policy = { normalize: true, random: true, min: 0, max: 8 };
46
+ const first = balanceStats({ stats: wild, itemType: 'weapon', policy, random: lcg(7) });
47
+ expect(balanceStats({ stats: wild, itemType: 'weapon', policy, random: lcg(7) })).toEqual(first);
48
+ for (const key of STAT_TYPES) {
49
+ const [lo, hi] = resolveStatBounds('weapon', policy)[key];
50
+ expect(first[key]).toBeGreaterThanOrEqual(lo);
51
+ expect(first[key]).toBeLessThanOrEqual(hi);
52
+ }
53
+ });
54
+
55
+ it('lets min and max narrow the semantic range and pins a request outside it', () => {
56
+ expect(resolveStatBounds('weapon', { normalize: true, min: 12, max: 30 }).effect).toEqual([12, 20]);
57
+ expect(resolveStatBounds('weapon', { normalize: true, min: 50 }).effect).toEqual([20, 20]);
58
+ expect(resolveStatBounds('weapon', { normalize: true, max: -50 }).resistance).toEqual([-10, -10]);
59
+ expect(resolveStatBounds('weapon', { min: -20, max: 20 }).effect).toEqual([-20, 20]);
60
+ expect(() => resolveStatBounds('weapon', { min: 5, max: 1 })).toThrow('minimum');
61
+ expect(() => resolveStatBounds('weapon', { min: -101 })).toThrow();
62
+ });
63
+
64
+ it('declares every semantic bound inside the contract and in order', () => {
65
+ for (const bounds of Object.values(STAT_TYPE_BOUNDS)) {
66
+ for (const [lo, hi] of Object.values(bounds)) expect(lo <= hi && lo >= -100 && hi <= 100).toBe(true);
67
+ }
68
+ });
69
+ });