@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -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/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -35,6 +35,7 @@ import {
35
35
  resolveHostKeyContext,
36
36
  resolveReplicaCount,
37
37
  syncDeployIdSources,
38
+ syncDeployIdSourcesBack,
38
39
  syncPrivateConf,
39
40
  updatePrivateEngineTestRepo,
40
41
  updatePrivateTemplateRepo,
@@ -712,6 +713,23 @@ describe('private conf sync', () => {
712
713
  expect(copied).to.include('./engine-private/catalog/items.json -> ../engine-core-private/catalog/items.json');
713
714
  });
714
715
 
716
+ // Regression: an instance backup that dropped an item left the item's stale atlas in the
717
+ // mirror, and the pod restored it over the current one from another instance.
718
+ it('replaces each payload entry in the checkout instead of merging into it', () => {
719
+ const calls = [];
720
+ checkout({ present: true });
721
+ vi.spyOn(fs, 'readdirSync').mockReturnValue([]);
722
+ vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
723
+ vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
724
+ withEnv({ GITHUB_USERNAME: 'fixture-org' }, () => syncPrivateConf('dd-core', ['cyberia-instances/TEST']));
725
+ const remove = calls.indexOf('remove ../engine-core-private/cyberia-instances/TEST');
726
+ const copy = calls.indexOf(
727
+ 'copy ./engine-private/cyberia-instances/TEST -> ../engine-core-private/cyberia-instances/TEST',
728
+ );
729
+ expect(remove).to.be.greaterThan(-1);
730
+ expect(copy).to.be.greaterThan(remove);
731
+ });
732
+
715
733
  it('mirrors the cron conf the checkout declares', () => {
716
734
  const copied = [];
717
735
  checkout({ present: true, cronId: 'dd-cron' });
@@ -740,17 +758,50 @@ describe('private conf sync', () => {
740
758
  expect(syncDeployIdSources([])).to.equal(false);
741
759
  });
742
760
 
743
- it('moves every declared source that is present, skipping the rest', () => {
744
- const moved = [];
745
- vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => `${filePath}`.includes('present'));
746
- vi.spyOn(fs, 'moveSync').mockImplementation((src, dest) => moved.push(`${src} -> ${dest}`));
761
+ it('copies in only the sources the engine tree lacks, and never moves', () => {
762
+ // Regression: the repo copy used to be moved over the engine copy, so a build discarded
763
+ // every edit made in the engine tree and left the repo reading as wholesale deletions.
764
+ const calls = [];
765
+ const present = new Set(['../engine-x/src/new.js', '../engine-x/src/edited.js', 'src/api/edited.js']);
766
+ vi.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
767
+ vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => present.has(`${filePath}`));
768
+ vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
769
+ vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
770
+ vi.spyOn(fs, 'moveSync').mockImplementation(() => calls.push('move'));
747
771
  expect(
748
772
  syncDeployIdSources([
749
- ['src/present.js', 'src/api/present.js'],
750
- ['src/absent.js', 'src/api/absent.js'],
773
+ ['../engine-x/src/new.js', 'src/api/new.js'],
774
+ ['../engine-x/src/edited.js', 'src/api/edited.js'],
775
+ ['../engine-x/src/absent.js', 'src/api/absent.js'],
751
776
  ]),
752
777
  ).to.equal(true);
753
- expect(moved).to.deep.equal(['src/present.js -> src/api/present.js']);
778
+ expect(calls).to.deep.equal([
779
+ // The engine copy wins: it is mirrored out before anything is copied in.
780
+ 'remove ../engine-x/src/edited.js',
781
+ 'copy src/api/edited.js -> ../engine-x/src/edited.js',
782
+ 'copy ../engine-x/src/new.js -> src/api/new.js',
783
+ ]);
784
+ });
785
+
786
+ it('mirrors every source the tree holds back to its repo, replacing it whole', () => {
787
+ // The engine ignores the moved-in paths, so the source repo is the only place a change to
788
+ // them is diffed; replacing rather than overlaying makes deletions reach that diff too.
789
+ const calls = [];
790
+ vi.spyOn(fs, 'existsSync').mockImplementation((filePath) => `${filePath}`.includes('present'));
791
+ vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
792
+ vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
793
+ vi.spyOn(fs, 'moveSync').mockImplementation(() => calls.push('move'));
794
+ expect(
795
+ syncDeployIdSourcesBack([
796
+ ['../engine-x/src/present.js', 'src/api/present.js'],
797
+ ['../engine-x/src/absent.js', 'src/api/absent.js'],
798
+ ]),
799
+ ).to.deep.equal(['../engine-x/src/present.js']);
800
+ expect(calls).to.deep.equal([
801
+ 'remove ../engine-x/src/present.js',
802
+ 'copy src/api/present.js -> ../engine-x/src/present.js',
803
+ ]);
804
+ expect(syncDeployIdSourcesBack()).to.deep.equal([]);
754
805
  });
755
806
  });
756
807
 
@@ -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
+ });