@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
@@ -14,6 +14,7 @@ import {
14
14
  deployPackagePathFactory,
15
15
  productDevDependenciesFactory,
16
16
  productPackageOptionsFactory,
17
+ publishedProductPackageJson,
17
18
  stagePackageArchive,
18
19
  syncDeployPackages,
19
20
  } from '../../src/server/build/package.js';
@@ -38,7 +39,7 @@ describe('generated product package dependencies', () => {
38
39
  },
39
40
  productDependencies: {
40
41
  'runtime-shared': '3.0.0',
41
- underpost: '^3.3.0',
42
+ nodemailer: '^10.0.0',
42
43
  },
43
44
  productDevDependencies: {
44
45
  'product-dev-only': '4.0.0',
@@ -72,7 +73,7 @@ describe('generated product package dependencies', () => {
72
73
  },
73
74
  catalog: { description: 'Cyberia CLI', keywords: ['cyberia'] },
74
75
  confName: 'dd-cyberia',
75
- customDependencies: { underpost: '^3.3.0' },
76
+ customDependencies: { nodemailer: '^10.0.0' },
76
77
  customScripts: { build: 'node bin client' },
77
78
  customBin: { cyberia: 'bin/index.js' },
78
79
  });
@@ -82,7 +83,7 @@ describe('generated product package dependencies', () => {
82
83
  description: 'Cyberia CLI',
83
84
  keywords: ['cyberia'],
84
85
  bin: { cyberia: 'bin/index.js' },
85
- dependencies: { underpost: '^3.3.0' },
86
+ dependencies: { nodemailer: '^10.0.0' },
86
87
  devDependencies: { express: '5.2.1', vitest: '4.1.11' },
87
88
  scripts: { test: 'node bin test', build: 'node bin client' },
88
89
  });
@@ -265,25 +266,78 @@ describe('generated deploy package manifests', () => {
265
266
  });
266
267
 
267
268
  describe('product manifest overrides read the catalog', () => {
268
- it('carries the published engine CLI alongside the catalog pins', () => {
269
- expect(
270
- productPackageOptionsFactory({
271
- catalog: {
272
- packageDependencies: { ethers: '~6.16.0' },
273
- packageBin: { cyberia: 'bin/index.js' },
274
- packageScripts: { 'docker:up': 'node bin docker-compose --up' },
275
- },
276
- underpostVersion: 'v3.3.0',
277
- }),
278
- ).to.deep.equal({
279
- customDependencies: { underpost: '^3.3.0', ethers: '~6.16.0' },
269
+ it('carries the engine runtime dependencies alongside the catalog pins', () => {
270
+ // Regression: the product depended on the published `underpost` package instead, which
271
+ // installed a second engine whose nested `nodemailer` `src/` could not resolve, while the
272
+ // hoistable copy survived only as a dev entry a production install omits.
273
+ const options = productPackageOptionsFactory({
274
+ catalog: {
275
+ packageDependencies: { ethers: '~6.16.0' },
276
+ packageBin: { cyberia: 'bin/index.js' },
277
+ packageScripts: { 'docker:up': 'node bin docker-compose --up' },
278
+ },
279
+ engineDependencies: { express: '^5.2.1', nodemailer: '^10.0.0' },
280
+ });
281
+
282
+ expect(options).to.deep.equal({
283
+ customDependencies: { express: '^5.2.1', nodemailer: '^10.0.0', ethers: '~6.16.0' },
280
284
  customScripts: { 'docker:up': 'node bin docker-compose --up' },
281
285
  customBin: { cyberia: 'bin/index.js' },
282
286
  });
287
+ expect(options.customDependencies).to.not.have.property('underpost');
288
+ });
289
+
290
+ it('lets a catalog pin override the engine version of the same dependency', () => {
291
+ expect(
292
+ productPackageOptionsFactory({
293
+ catalog: { packageDependencies: { sharp: '^0.35.3' } },
294
+ engineDependencies: { sharp: '^0.34.0' },
295
+ }).customDependencies,
296
+ ).to.deep.equal({ sharp: '^0.35.3' });
283
297
  });
284
298
 
285
299
  it('leaves the base template manifest alone for a catalog that declares no package', () => {
286
- expect(productPackageOptionsFactory({ catalog: {}, underpostVersion: 'v3.3.0' })).to.deep.equal({});
300
+ expect(productPackageOptionsFactory({ catalog: {}, engineDependencies: { express: '^5.2.1' } })).to.deep.equal({});
301
+ });
302
+ });
303
+
304
+ describe('the manifest a product publishes to npm', () => {
305
+ it('trades the engine runtime set for the published CLI and the catalog pins', () => {
306
+ const published = publishedProductPackageJson({
307
+ packageJson: {
308
+ name: 'cyberia',
309
+ version: '3.3.73',
310
+ dependencies: { express: '^5.2.1', nodemailer: '^10.0.0', sharp: '^0.35.3' },
311
+ devDependencies: { vitest: '5.0.0' },
312
+ },
313
+ catalog: { packageDependencies: { sharp: '^0.35.3', ethers: '~6.16.0' } },
314
+ });
315
+
316
+ expect(published.dependencies).to.deep.equal({ underpost: '^3.3.73', sharp: '^0.35.3', ethers: '~6.16.0' });
317
+ expect(published.devDependencies).to.deep.equal({
318
+ express: '^5.2.1',
319
+ nodemailer: '^10.0.0',
320
+ vitest: '5.0.0',
321
+ });
322
+ expect(published.name).to.equal('cyberia');
323
+ });
324
+
325
+ it('pins the engine CLI at the version it is given, tag stripped', () => {
326
+ expect(
327
+ publishedProductPackageJson({ packageJson: { version: '3.3.73' }, underpostVersion: 'v4.0.0' }).dependencies,
328
+ ).to.deep.equal({ underpost: '^4.0.0' });
329
+ });
330
+
331
+ it('refuses a manifest it cannot pin the engine CLI from', () => {
332
+ expect(() => publishedProductPackageJson()).to.throw('packageJson');
333
+ expect(() => publishedProductPackageJson({ packageJson: { name: 'cyberia' } })).to.throw('version');
334
+ });
335
+
336
+ it('leaves the repository manifest as the checkout installs from', () => {
337
+ const packageJson = { version: '1.0.0', dependencies: { express: '^5.2.1' } };
338
+ publishedProductPackageJson({ packageJson });
339
+
340
+ expect(packageJson).to.deep.equal({ version: '1.0.0', dependencies: { express: '^5.2.1' } });
287
341
  });
288
342
  });
289
343
 
@@ -81,7 +81,12 @@ describe('a node installs the deploy own package manifest', () => {
81
81
  try {
82
82
  const emitted = run(`install_deploy_dependencies ${root} dd-absent`).trim().split('\n');
83
83
  expect(emitted[0]).to.equal(`Install dependencies :: sudo -n -- /bin/bash -lc cd ${root} && npm install`);
84
- expect(emitted[1]).to.include(`Link underpost CLI :: sudo -n -- /bin/bash -lc cd ${root} && npm link --force`);
84
+ // The link is a symlink onto the checkout's entrypoint, and a restored checkout can leave
85
+ // that file without its executable mode, so the link step restores it before linking.
86
+ expect(emitted[1]).to.include(
87
+ `Link underpost CLI :: sudo -n -- /bin/bash -lc cd ${root} && chmod +x ./bin/index.js && npm link --force`,
88
+ );
89
+ expect(emitted[1]).to.include('test -x ./bin/index.js');
85
90
  } finally {
86
91
  fs.removeSync(root);
87
92
  }
@@ -297,3 +302,81 @@ describe('prepare_host brings a node up in an order it can recover from', () =>
297
302
  expect(emitted.at(-1)).to.include('node bin host load');
298
303
  });
299
304
  });
305
+
306
+ const cyberiaDeployScript = path.join(deployRoot, 'dd-cyberia/sync-deploy.sh');
307
+
308
+ describe.skipIf(!fs.existsSync(cyberiaDeployScript))('a pod lands its private configuration at engine-private', () => {
309
+ const source = fs.existsSync(cyberiaDeployScript) ? fs.readFileSync(cyberiaDeployScript, 'utf8') : '';
310
+
311
+ it('names the pod conf repository once and derives the directory the clone lands in', () => {
312
+ expect(source).to.match(/^POD_SRC_PRIVATE_REPO="\$\{POD_SRC_PRIVATE_REPO:-[^"]+\}"$/m);
313
+ expect(source).to.include('underpost clone ${POD_SRC_PRIVATE_REPO}');
314
+ expect(source).to.include('POD_SRC_PRIVATE_DIR="${POD_SRC_PRIVATE_REPO##*/}"');
315
+ expect(source).to.include('sudo mv ./${POD_SRC_PRIVATE_DIR} ./engine-private');
316
+ });
317
+
318
+ it('clears the destination first, so the checkout replaces engine-private instead of nesting in it', () => {
319
+ const cleared = source.indexOf('sudo rm -rf ./engine-private');
320
+ const moved = source.indexOf('sudo mv ./${POD_SRC_PRIVATE_DIR} ./engine-private');
321
+ expect(cleared).to.be.greaterThan(-1);
322
+ expect(cleared).to.be.lessThan(moved);
323
+ });
324
+ });
325
+
326
+ // Every script under deploy/ is the same shape on purpose: one entry point, one constants block,
327
+ // and no value repeated across files. A second shape is how the drift starts, so it is asserted
328
+ // here rather than left to review.
329
+ describe.skipIf(!shipsDeployIds)('every deploy script is written to one shape', () => {
330
+ const libDirectory = path.join(deployRoot, 'lib');
331
+ const scripts = fs
332
+ .readdirSync(deployRoot)
333
+ .filter((entry) => fs.statSync(path.join(deployRoot, entry)).isDirectory() && entry !== 'lib')
334
+ .flatMap((directory) =>
335
+ fs
336
+ .readdirSync(path.join(deployRoot, directory))
337
+ .filter((file) => file.endsWith('.sh'))
338
+ .map((file) => ({
339
+ id: `${directory}/${file}`,
340
+ source: fs.readFileSync(path.join(deployRoot, directory, file), 'utf8'),
341
+ })),
342
+ );
343
+ const libraries = fs
344
+ .readdirSync(libDirectory)
345
+ .filter((file) => file.endsWith('.sh'))
346
+ .map((file) => ({ id: `lib/${file}`, source: fs.readFileSync(path.join(libDirectory, file), 'utf8') }));
347
+
348
+ it('opens and closes every entry point the same way', () => {
349
+ expect(scripts).to.not.be.empty;
350
+ for (const { id, source } of scripts) {
351
+ expect(source.startsWith('#!/bin/bash\nset -euo pipefail\n'), id).to.equal(true);
352
+ expect(source, id).to.include('source "$SCRIPT_DIR/../lib/github-actions-logging.sh"');
353
+ expect(source.trimEnd().endsWith('main "$@"'), id).to.equal(true);
354
+ }
355
+ });
356
+
357
+ it('leaves the shared values to lib/config.sh, so no script carries a copy', () => {
358
+ // The engine root, the ingress node, the fleet's node names, the ssh key and the WordPress
359
+ // image were each pasted into a dozen scripts before this.
360
+ for (const { id, source } of scripts) {
361
+ expect(source, id).to.not.match(/^ENGINE_ROOT=/m);
362
+ expect(source, id).to.not.match(/^INGRESS_NODE=/m);
363
+ expect(source, id).to.not.include('/home/dd/engine');
364
+ expect(source, id).to.not.include('hp-envy');
365
+ expect(source, id).to.not.include('underpost/wp:');
366
+ }
367
+ });
368
+
369
+ it('states its deploy id once and reads it back from there', () => {
370
+ // A fleet-level script names no deploy of its own; one that names a deploy declares it.
371
+ for (const { id, source } of scripts.filter(({ source: text }) => /dd-[a-z0-9]/.test(text))) {
372
+ expect(source, id).to.match(/^DEPLOY_ID=dd-[a-z0-9-]+$/m);
373
+ const belowDeclaration = source.slice(source.indexOf('\n', source.search(/^DEPLOY_ID=/m)));
374
+ expect(belowDeclaration, id).to.not.match(/dd-[a-z0-9]/);
375
+ }
376
+ });
377
+
378
+ it('carries no library shebang and no trailing whitespace anywhere', () => {
379
+ for (const { id, source } of libraries) expect(source.startsWith('#!'), id).to.equal(false);
380
+ for (const { id, source } of [...scripts, ...libraries]) expect(source, id).to.not.match(/[ \t]+$/m);
381
+ });
382
+ });
@@ -6,10 +6,12 @@ import {
6
6
  TEST_TIERS,
7
7
  UNDERPOST_TESTING,
8
8
  coverageIncludeFactory,
9
+ coverageReportKey,
9
10
  coverageThresholdFactory,
10
11
  resolveTestSelection,
11
12
  testProjectsFactory,
12
13
  testSuiteNames,
14
+ vitestProjectSelector,
13
15
  } from '../../src/server/build/testing.js';
14
16
 
15
17
  // The table declares every tier the platform ships. A product build slices the
@@ -191,3 +193,28 @@ describe('coverage scope', () => {
191
193
  expect(() => coverageIncludeFactory(['--project', 'not-a-tier'])).to.throw(/unknown suite/);
192
194
  });
193
195
  });
196
+
197
+ describe('coverage report directory', () => {
198
+ it('names the report after the suites the selection spans', () => {
199
+ expect(coverageReportKey('unit,infra,app')).to.equal('unit-infra-app');
200
+ expect(coverageReportKey('cyberia')).to.equal('cyberia');
201
+ expect(coverageReportKey('infra:1-security,infra:2-network')).to.equal('infra');
202
+ });
203
+
204
+ it('names every Vitest suite for a full run and none for a delegated one', () => {
205
+ expect(coverageReportKey('')).to.equal(coverageReportKey('all'));
206
+ expect(coverageReportKey('all').split('-')).to.have.members([
207
+ ...new Set(TEST_TIERS.filter(({ delegate }) => !delegate).map(({ name }) => name.split(':')[0])),
208
+ ]);
209
+ expect(coverageReportKey('contracts')).to.equal('');
210
+ });
211
+
212
+ // A deploy names its report by suite and the runner is handed the same tiers as
213
+ // `--project` flags; both must land on one directory.
214
+ it('agrees between a suite selector and the projects the runner was handed', () => {
215
+ const { projects } = resolveTestSelection('unit,infra,app');
216
+ const argv = ['npx', 'vitest', 'run', ...projects.flatMap((project) => ['--project', project]), '--coverage'];
217
+ expect(coverageReportKey(vitestProjectSelector(argv))).to.equal(coverageReportKey('unit,infra,app'));
218
+ expect(vitestProjectSelector(['--project=cyberia'])).to.equal('cyberia');
219
+ });
220
+ });
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module zip-archive.test
5
+ * @description Covers the JSZip-backed archive helpers shared by the file-storage CLI and the
6
+ * client builder: local-file archiving, full and single-entry extraction, entry lookup by
7
+ * basename, and the guard that keeps an archive path from escaping the extraction root.
8
+ *
9
+ * Uses 'chai' for assertions.
10
+ */
11
+
12
+ import { expect } from 'chai';
13
+ import fs from 'fs-extra';
14
+ import JSZip from 'jszip';
15
+ import * as dir from 'path';
16
+ import {
17
+ extractZipEntryTo,
18
+ extractZipTo,
19
+ findZipEntry,
20
+ isZipBuffer,
21
+ loadZip,
22
+ readZipEntry,
23
+ zipFromLocalFiles,
24
+ } from '../../src/server/storage/zip.js';
25
+
26
+ describe('zip archive helpers', () => {
27
+ let fixturePath;
28
+
29
+ beforeEach(() => {
30
+ fixturePath = fs.mkdtempSync('/tmp/engine-zip-archive-');
31
+ fs.mkdirSync(`${fixturePath}/src/nested`, { recursive: true });
32
+ fs.writeFileSync(`${fixturePath}/src/index.html`, '<h1>index</h1>'.repeat(200), 'utf8');
33
+ fs.writeFileSync(`${fixturePath}/src/nested/app.js`, 'console.log(1);'.repeat(200), 'utf8');
34
+ fs.writeFileSync(`${fixturePath}/src/run.sh`, '#!/bin/sh\necho run\n', 'utf8');
35
+ fs.chmodSync(`${fixturePath}/src/run.sh`, 0o755);
36
+ });
37
+
38
+ afterEach(() => {
39
+ fs.removeSync(fixturePath);
40
+ });
41
+
42
+ const buildFixtureArchive = () =>
43
+ zipFromLocalFiles([
44
+ { localPath: `${fixturePath}/src/index.html`, entryName: 'index.html' },
45
+ { localPath: `${fixturePath}/src/nested/app.js`, entryName: 'nested/app.js' },
46
+ { localPath: `${fixturePath}/src/run.sh`, entryName: 'run.sh' },
47
+ ]);
48
+
49
+ it('writes a deflated archive that keeps the requested entry names', async () => {
50
+ const archive = await buildFixtureArchive();
51
+ const rawBytes = ['src/index.html', 'src/nested/app.js', 'src/run.sh'].reduce(
52
+ (total, relativePath) => total + fs.statSync(`${fixturePath}/${relativePath}`).size,
53
+ 0,
54
+ );
55
+
56
+ expect(isZipBuffer(archive)).to.equal(true);
57
+ expect(archive.length).to.be.below(rawBytes);
58
+ const entries = Object.values((await loadZip(archive)).files);
59
+ expect(entries.filter((entry) => entry.dir).map((entry) => entry.name)).to.deep.equal(['nested/']);
60
+ expect(
61
+ entries
62
+ .filter((entry) => !entry.dir)
63
+ .map((entry) => entry.name)
64
+ .sort(),
65
+ ).to.deep.equal(['index.html', 'nested/app.js', 'run.sh']);
66
+ });
67
+
68
+ it('round-trips an archive through extraction, preserving content and executable bits', async () => {
69
+ const outputPath = `${fixturePath}/out`;
70
+ await extractZipTo(await buildFixtureArchive(), outputPath);
71
+
72
+ expect(fs.readFileSync(`${outputPath}/nested/app.js`, 'utf8')).to.equal(
73
+ fs.readFileSync(`${fixturePath}/src/nested/app.js`, 'utf8'),
74
+ );
75
+ expect(fs.statSync(`${outputPath}/run.sh`).mode & 0o777).to.equal(0o755);
76
+ });
77
+
78
+ it('extracts from an archive path as well as from a buffer', async () => {
79
+ const archivePath = `${fixturePath}/build.zip`;
80
+ fs.writeFileSync(archivePath, await buildFixtureArchive());
81
+ await extractZipTo(archivePath, `${fixturePath}/from-path`);
82
+
83
+ expect(fs.existsSync(`${fixturePath}/from-path/nested/app.js`)).to.equal(true);
84
+ });
85
+
86
+ it('resolves an entry by exact name, by basename, and by single-entry fallback', async () => {
87
+ const nested = await loadZip(await buildFixtureArchive());
88
+ expect(findZipEntry(nested, 'nested/app.js').name).to.equal('nested/app.js');
89
+ expect(findZipEntry(nested, 'app.js').name).to.equal('nested/app.js');
90
+ expect(findZipEntry(nested, 'absent.js')).to.equal(null);
91
+
92
+ const single = await loadZip(
93
+ await zipFromLocalFiles([{ localPath: `${fixturePath}/src/index.html`, entryName: 'deep/path/index.html' }]),
94
+ );
95
+ expect(findZipEntry(single, 'absent.js').name).to.equal('deep/path/index.html');
96
+ });
97
+
98
+ it('reads a wrapped entry out of a single-file archive', async () => {
99
+ const inner = await buildFixtureArchive();
100
+ fs.writeFileSync(`${fixturePath}/inner.zip`, inner);
101
+ const wrapper = await zipFromLocalFiles([
102
+ { localPath: `${fixturePath}/inner.zip`, entryName: 'wrapped/inner.zip' },
103
+ ]);
104
+
105
+ expect(await readZipEntry(wrapper, 'inner.zip')).to.deep.equal(inner);
106
+ });
107
+
108
+ it('flattens a single extracted entry into the target directory', async () => {
109
+ const archive = await zipFromLocalFiles([
110
+ { localPath: `${fixturePath}/src/index.html`, entryName: 'deep/path/index.html' },
111
+ ]);
112
+ const targetDir = `${fixturePath}/pull`;
113
+ const written = await extractZipEntryTo(archive, 'index.html', targetDir);
114
+
115
+ expect(written).to.equal(dir.resolve(targetDir, 'index.html'));
116
+ expect(fs.readFileSync(written, 'utf8')).to.equal(fs.readFileSync(`${fixturePath}/src/index.html`, 'utf8'));
117
+ });
118
+
119
+ it('rejects a missing entry rather than writing an empty file', async () => {
120
+ const archive = await buildFixtureArchive();
121
+ let thrown;
122
+ await extractZipEntryTo(archive, 'index.html', `${fixturePath}/missing`).catch((error) => (thrown = error));
123
+ expect(thrown).to.equal(undefined);
124
+
125
+ await extractZipEntryTo(await buildFixtureArchive(), 'absent.js', `${fixturePath}/missing`).catch(
126
+ (error) => (thrown = error),
127
+ );
128
+ expect(thrown?.message).to.match(/Zip entry not found/);
129
+ });
130
+
131
+ it('blocks an archive entry that resolves outside the extraction root', async () => {
132
+ const escaping = new JSZip();
133
+ escaping.file('../escaped.txt', 'escaped');
134
+ const archive = await escaping.generateAsync({ type: 'nodebuffer' });
135
+
136
+ let thrown;
137
+ await extractZipTo(archive, `${fixturePath}/guarded`).catch((error) => (thrown = error));
138
+
139
+ expect(thrown?.message).to.match(/Blocked zip entry outside extraction directory/);
140
+ expect(fs.existsSync(`${fixturePath}/escaped.txt`)).to.equal(false);
141
+ });
142
+ });
package/vitest.config.js CHANGED
@@ -2,13 +2,16 @@ import { defineConfig } from 'vitest/config';
2
2
  import {
3
3
  UNDERPOST_TESTING,
4
4
  coverageIncludeFactory,
5
+ coverageReportKey,
5
6
  coverageThresholdFactory,
6
7
  testProjectsFactory,
8
+ vitestProjectSelector,
7
9
  } from './src/server/build/testing.js';
8
10
 
9
11
  const allureResultsDirectory = process.env[UNDERPOST_TESTING.allureResultsEnvKey];
10
12
  const coverageThreshold = coverageThresholdFactory(process.env);
11
13
  const coverageInclude = coverageIncludeFactory(process.argv);
14
+ const coverageReportDirectory = coverageReportKey(vitestProjectSelector(process.argv));
12
15
 
13
16
  // Spread into every tier: a Vitest project inherits nothing from the root
14
17
  // `test` block, and only `coverage` and `reporters` are read from it.
@@ -33,9 +36,14 @@ export default defineConfig({
33
36
  coverage: {
34
37
  provider: 'v8',
35
38
  reportsDirectory: UNDERPOST_TESTING.coverageDirectory,
36
- // `lcov` is what Coveralls ingests, `json` is what a merged multi-job
37
- // report is assembled from, `text` is what a local run reads.
38
- reporter: ['text', 'lcov', 'json'],
39
+ // `lcovonly` is what Coveralls ingests, `json` is what a merged multi-job
40
+ // report is assembled from, `text` is what a local run reads, and `html` is
41
+ // what a deploy publishes — under the selection's own directory, so the
42
+ // report a deploy declares is the run it names and never the last run made.
43
+ reporter: ['text', 'lcovonly', 'json', ['html', { subdir: coverageReportDirectory }]],
44
+ // The directory holds one report per selection, so a run must not empty it; the
45
+ // runner (`src/cli/test.js`) clears its own selection's report before it starts.
46
+ clean: false,
39
47
  // Written even when the run fails: the coverage workflows upload the report
40
48
  // from a job the threshold step is meant to fail, and without it the badge
41
49
  // freezes at the last passing build instead of moving with the tree.
@@ -1,63 +0,0 @@
1
- /**
2
- * Fallback-world default items — process-local override store.
3
- *
4
- * The procedural fallback world is never persisted, so its default item set
5
- * has no collection to live in. This module holds the set the FallbackWorldEngine
6
- * view composed, in memory, for the lifetime of the engine process:
7
- *
8
- * - written by POST /api/cyberia-instance/fallback-world/hot-reload
9
- * - read by fetchFullInstance()'s fallback branch (instance-data.js) and
10
- * GET /api/cyberia-instance/fallback-world[/default-items]
11
- *
12
- * Deliberately NOT persisted: an engine restart drops back to the code defaults,
13
- * so a stale editor session can never become the permanent shape of the world.
14
- * This keeps the fallback path's "code defaults are authoritative" invariant —
15
- * the only thing an operator can layer on top is this explicit, volatile list.
16
- *
17
- * @module src/api/cyberia-instance/cyberia-fallback-default-items.js
18
- */
19
-
20
- /** @typedef {{ id: string, defaultPlayerInventory: boolean }} FallbackDefaultItem */
21
-
22
- /** @type {FallbackDefaultItem[]} */
23
- let defaultItems = [];
24
-
25
- /**
26
- * Coerce a caller-supplied list into the canonical `{ id, defaultPlayerInventory }`
27
- * shape. Accepts the legacy `string[]` form and drops blank/duplicate ids so the
28
- * world generator never sees a malformed entry.
29
- *
30
- * @param {Array<string|FallbackDefaultItem>} entries
31
- * @returns {FallbackDefaultItem[]}
32
- */
33
- function normalizeFallbackDefaultItems(entries) {
34
- if (!Array.isArray(entries)) return [];
35
- const seen = new Set();
36
- const normalized = [];
37
- for (const entry of entries) {
38
- const raw = typeof entry === 'string' ? { id: entry } : entry;
39
- const id = typeof raw?.id === 'string' ? raw.id.trim() : '';
40
- if (!id || seen.has(id)) continue;
41
- seen.add(id);
42
- normalized.push({ id, defaultPlayerInventory: !!raw.defaultPlayerInventory });
43
- }
44
- return normalized;
45
- }
46
-
47
- /**
48
- * Current override set. Returns a copy so callers cannot mutate the store.
49
- * @returns {FallbackDefaultItem[]}
50
- */
51
- const getFallbackDefaultItems = () => defaultItems.map((entry) => ({ ...entry }));
52
-
53
- /**
54
- * Replace the override set. An empty list clears it, restoring pure code defaults.
55
- * @param {Array<string|FallbackDefaultItem>} entries
56
- * @returns {FallbackDefaultItem[]} The stored set.
57
- */
58
- function setFallbackDefaultItems(entries) {
59
- defaultItems = normalizeFallbackDefaultItems(entries);
60
- return getFallbackDefaultItems();
61
- }
62
-
63
- export { getFallbackDefaultItems, setFallbackDefaultItems, normalizeFallbackDefaultItems };