@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.
- package/.env.example +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +10 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +1 -1
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/AGENTS.md +61 -12
- package/CHANGELOG.md +159 -161
- package/CLI-HELP.md +20 -13
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +2 -2
- package/README.md +28 -28
- package/bin/build.js +96 -21
- package/bin/cyberia.js +1247 -1373
- package/bin/index.js +1247 -1373
- package/compose.env +17 -5
- package/conf.js +7 -4
- package/deploy/cyberia-client/deploy.sh +10 -11
- package/deploy/cyberia-client/package.sh +0 -1
- package/deploy/cyberia-client/state.sh +3 -2
- package/deploy/cyberia-server/deploy.sh +10 -11
- package/deploy/cyberia-server/package.sh +0 -1
- package/deploy/cyberia-server/state.sh +3 -2
- package/deploy/dd-cyberia/deploy.sh +5 -5
- package/deploy/dd-cyberia/init.sh +15 -14
- package/deploy/dd-cyberia/package.sh +0 -1
- package/deploy/dd-cyberia/state.sh +2 -2
- package/deploy/dd-cyberia/sync-deploy.sh +139 -75
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +92 -76
- package/deploy/lib/host.sh +28 -19
- package/deploy/lib/state.sh +4 -1
- package/deploy/pwa-microservices-template/deploy.sh +10 -8
- package/deploy/release/deploy.sh +0 -2
- package/deployment.yaml +1 -1
- package/docker-compose.yml +23 -17
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +2 -2
- package/hardhat/package.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/package.json +19 -17
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
- package/src/api/cyberia-action/cyberia-action.router.js +2 -2
- package/src/api/cyberia-action/cyberia-action.service.js +3 -4
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
- package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
- package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
- package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
- package/src/api/cyberia-map/cyberia-map.service.js +6 -2
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
- package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +10 -3
- package/src/api/object-layer/object-layer.service.js +113 -120
- package/src/cli/app.js +12 -1
- package/src/cli/client.js +2 -2
- package/src/cli/cluster.js +80 -18
- package/src/cli/deploy.js +215 -34
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +158 -342
- package/src/cli/index.js +22 -10
- package/src/cli/release.js +3 -1
- package/src/cli/repository.js +35 -9
- package/src/cli/run.js +80 -199
- package/src/cli/test.js +4 -0
- package/src/cli/wireguard.js +32 -31
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +8 -1
- package/src/client/components/core/Docs.js +77 -100
- package/src/client/components/core/Modal.js +77 -23
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +1 -1
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client/ssr/views/Cyberia404.js +3 -3
- package/src/client-builder/client-build-docs.js +52 -52
- package/src/client-builder/client-build.js +67 -44
- package/src/client-builder/client-bundle.js +347 -0
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/grpc/cyberia/grpc-server.js +5 -7
- package/src/index.js +1 -1
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +5 -14
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +3 -2
- package/src/projects/cyberia/gemini-client.js +3 -3
- package/src/projects/cyberia/generate-saga.js +4 -11
- package/src/projects/cyberia/hot-reload-trigger.js +17 -28
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +177 -257
- package/src/projects/cyberia/map-preview-generator.js +32 -68
- package/src/projects/cyberia/object-layer.js +152 -319
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +2 -18
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/runtime/cyberia-client/Dockerfile +8 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -2
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/server/build/coverage.js +113 -42
- package/src/server/build/package.js +55 -12
- package/src/server/build/testing.js +59 -9
- package/src/server/network/middlewares.js +8 -2
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/ops/logger.js +70 -35
- package/src/server/runtime/conf.js +40 -9
- package/src/server/runtime/start.js +11 -2
- package/src/server/storage/data-query.js +16 -0
- package/src/server/storage/downloader.js +62 -14
- package/src/server/storage/zip.js +153 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
- package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/client-build-docs.test.js +97 -70
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-resolution.test.js +58 -7
- package/test/unit/coverage-artifact.test.js +120 -29
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/deploy-log-table.test.js +76 -5
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/logger-redaction.test.js +29 -0
- package/test/unit/package.test.js +70 -16
- package/test/unit/prepare-host.test.js +84 -1
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
- package/test/unit/test-tiers.test.js +27 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +11 -3
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
|
@@ -8,112 +8,148 @@ import { buildCoverage, buildDocs, buildSwaggerUiOptions } from '../../src/clien
|
|
|
8
8
|
|
|
9
9
|
// swagger-autogen is a code generator that walks the real router files and
|
|
10
10
|
// writes into ./public. The doc it is handed is what this module is responsible
|
|
11
|
-
// for, so the generator itself is replaced and its arguments are asserted
|
|
11
|
+
// for, so the generator itself is replaced and its arguments are asserted; it
|
|
12
|
+
// writes the document a test stages as its output, so the post-process has one.
|
|
12
13
|
const generated = vi.hoisted(() => []);
|
|
13
|
-
vi.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
const staged = vi.hoisted(() => ({ swaggerJson: null }));
|
|
15
|
+
vi.mock('swagger-autogen', async () => {
|
|
16
|
+
const fs = (await import('fs-extra')).default;
|
|
17
|
+
return {
|
|
18
|
+
default: (options) => async (outputFile, routes, doc) => {
|
|
19
|
+
generated.push({ options, outputFile, routes, doc });
|
|
20
|
+
if (staged.swaggerJson) fs.outputJsonSync(outputFile, staged.swaggerJson);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
18
24
|
|
|
19
25
|
describe('client coverage build', () => {
|
|
20
26
|
let fixturePath;
|
|
27
|
+
let cwd;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
fixturePath = fs.mkdtempSync('/tmp/engine-coverage-');
|
|
31
|
+
// Suite reports resolve against the engine root, so the fixture stands in for it.
|
|
32
|
+
cwd = process.cwd();
|
|
33
|
+
process.chdir(fixturePath);
|
|
34
|
+
});
|
|
21
35
|
|
|
22
36
|
afterEach(() => {
|
|
23
|
-
|
|
24
|
-
fixturePath
|
|
37
|
+
process.chdir(cwd);
|
|
38
|
+
fs.removeSync(fixturePath);
|
|
25
39
|
vi.restoreAllMocks();
|
|
26
40
|
});
|
|
27
41
|
|
|
28
|
-
it('publishes
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
it('publishes each declared report at its own coverage route', async () => {
|
|
43
|
+
fs.outputFileSync(`${fixturePath}/coverage/cyberia/index.html`, '<!doctype html><title>cyberia</title>');
|
|
44
|
+
fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/index.html`, '<!doctype html><title>hardhat</title>');
|
|
45
|
+
fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/base.css`, 'body {}');
|
|
31
46
|
const docsDestination = `${fixturePath}/public/docs/`;
|
|
32
|
-
fs.outputFileSync(`${reportPath}/index.html`, '<!doctype html><title>Coverage</title>');
|
|
33
|
-
fs.outputFileSync(`${reportPath}/base.css`, 'body {}');
|
|
34
47
|
|
|
35
|
-
await buildCoverage({
|
|
48
|
+
await buildCoverage({
|
|
49
|
+
docs: {
|
|
50
|
+
coverage: [
|
|
51
|
+
{ id: 'cyberia', suite: 'cyberia' },
|
|
52
|
+
{ id: 'hardhat', path: `${fixturePath}/hardhat` },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
docsDestination,
|
|
56
|
+
});
|
|
36
57
|
|
|
37
|
-
expect(fs.
|
|
38
|
-
expect(fs.
|
|
39
|
-
expect(fs.existsSync(`${docsDestination}coverage/
|
|
58
|
+
expect(fs.readFileSync(`${docsDestination}coverage/cyberia/index.html`, 'utf8')).to.include('cyberia');
|
|
59
|
+
expect(fs.readFileSync(`${docsDestination}coverage/hardhat/index.html`, 'utf8')).to.include('hardhat');
|
|
60
|
+
expect(fs.existsSync(`${docsDestination}coverage/hardhat/base.css`)).to.equal(true);
|
|
61
|
+
expect(fs.existsSync(`${docsDestination}coverage/hardhat/html`)).to.equal(false);
|
|
40
62
|
});
|
|
41
63
|
|
|
42
|
-
it('
|
|
43
|
-
|
|
44
|
-
fs.outputFileSync(`${fixturePath}/coverage/
|
|
45
|
-
fs.outputFileSync(`${fixturePath}/coverage/lcov-report/index.html`, '<!doctype html><title>lcov</title>');
|
|
64
|
+
it('drops a previously published report the conf no longer declares', async () => {
|
|
65
|
+
fs.outputFileSync(`${fixturePath}/out/coverage/legacy/index.html`, '<!doctype html>');
|
|
66
|
+
fs.outputFileSync(`${fixturePath}/coverage/unit/index.html`, '<!doctype html><title>unit</title>');
|
|
46
67
|
|
|
47
|
-
await buildCoverage({
|
|
68
|
+
await buildCoverage({
|
|
69
|
+
docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
|
|
70
|
+
docsDestination: `${fixturePath}/out/`,
|
|
71
|
+
});
|
|
48
72
|
|
|
49
|
-
expect(fs.
|
|
73
|
+
expect(fs.existsSync(`${fixturePath}/out/coverage/legacy`)).to.equal(false);
|
|
74
|
+
expect(fs.existsSync(`${fixturePath}/out/coverage/engine/index.html`)).to.equal(true);
|
|
50
75
|
});
|
|
51
76
|
|
|
52
|
-
it(
|
|
53
|
-
|
|
54
|
-
fs.outputFileSync(`${fixturePath}/coverage/index.html`, '<!doctype html><title>flat</title>');
|
|
77
|
+
it("publishes the run a report names, never another selection's", async () => {
|
|
78
|
+
fs.outputFileSync(`${fixturePath}/coverage/unit-infra-app-cyberia/index.html`, '<!doctype html><title>all</title>');
|
|
55
79
|
|
|
56
80
|
await buildCoverage({
|
|
57
|
-
docs: {
|
|
81
|
+
docs: { coverage: [{ id: 'engine', suite: 'unit,infra,app' }] },
|
|
58
82
|
docsDestination: `${fixturePath}/out/`,
|
|
59
83
|
});
|
|
60
84
|
|
|
61
|
-
|
|
85
|
+
const page = fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8');
|
|
86
|
+
expect(page).to.include('Coverage report unavailable');
|
|
87
|
+
expect(page).to.include('node bin test unit,infra,app');
|
|
62
88
|
});
|
|
63
89
|
|
|
64
90
|
it('publishes the unavailable page for a coverage directory with no HTML index', async () => {
|
|
65
|
-
|
|
66
|
-
fs.outputFileSync(`${fixturePath}/coverage/lcov.info`, 'TN:\n');
|
|
91
|
+
fs.outputFileSync(`${fixturePath}/hardhat/coverage/lcov.info`, 'TN:\n');
|
|
67
92
|
|
|
68
|
-
await buildCoverage({
|
|
93
|
+
await buildCoverage({
|
|
94
|
+
docs: { coverage: [{ id: 'hardhat', path: `${fixturePath}/hardhat` }] },
|
|
95
|
+
docsDestination: `${fixturePath}/out/`,
|
|
96
|
+
});
|
|
69
97
|
|
|
70
|
-
expect(fs.readFileSync(`${fixturePath}/out/coverage/index.html`, 'utf8')).to.include(
|
|
98
|
+
expect(fs.readFileSync(`${fixturePath}/out/coverage/hardhat/index.html`, 'utf8')).to.include(
|
|
99
|
+
'Coverage report unavailable',
|
|
100
|
+
);
|
|
71
101
|
});
|
|
72
102
|
|
|
73
103
|
it('publishes the report an assembled deploy artifact bundled', async () => {
|
|
74
|
-
fixturePath = fs.mkdtempSync('/tmp/engine-coverage-');
|
|
75
104
|
fs.outputFileSync(
|
|
76
|
-
`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/index.html`,
|
|
105
|
+
`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/engine/index.html`,
|
|
77
106
|
'<!doctype html><title>bundled</title>',
|
|
78
107
|
);
|
|
79
108
|
|
|
80
|
-
await buildCoverage({
|
|
109
|
+
await buildCoverage({
|
|
110
|
+
docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
|
|
111
|
+
docsDestination: `${fixturePath}/out/`,
|
|
112
|
+
});
|
|
81
113
|
|
|
82
|
-
expect(fs.readFileSync(`${fixturePath}/out/coverage/index.html`, 'utf8')).to.include('bundled');
|
|
114
|
+
expect(fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8')).to.include('bundled');
|
|
83
115
|
});
|
|
84
116
|
|
|
85
117
|
it('prefers a freshly generated report over the bundled artifact', async () => {
|
|
86
|
-
|
|
87
|
-
fs.outputFileSync(`${fixturePath}/coverage/lcov-report/index.html`, '<!doctype html><title>fresh</title>');
|
|
118
|
+
fs.outputFileSync(`${fixturePath}/coverage/unit/index.html`, '<!doctype html><title>fresh</title>');
|
|
88
119
|
fs.outputFileSync(
|
|
89
|
-
`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/index.html`,
|
|
120
|
+
`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/engine/index.html`,
|
|
90
121
|
'<!doctype html><title>bundled</title>',
|
|
91
122
|
);
|
|
92
123
|
|
|
93
|
-
await buildCoverage({
|
|
124
|
+
await buildCoverage({
|
|
125
|
+
docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
|
|
126
|
+
docsDestination: `${fixturePath}/out/`,
|
|
127
|
+
});
|
|
94
128
|
|
|
95
|
-
expect(fs.readFileSync(`${fixturePath}/out/coverage/index.html`, 'utf8')).to.include('fresh');
|
|
129
|
+
expect(fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8')).to.include('fresh');
|
|
96
130
|
});
|
|
97
131
|
|
|
98
132
|
// Regression: the build used to shell out to `npm test` when no report was present. Inside a
|
|
99
133
|
// pod that spent minutes of the build phase on a test runner, and the suite's expected
|
|
100
134
|
// non-zero exits latched `container-status=error`, failing a healthy rollout.
|
|
101
135
|
it('never runs a test runner to produce a missing report', async () => {
|
|
102
|
-
fixturePath = fs.mkdtempSync('/tmp/engine-coverage-');
|
|
103
136
|
fs.outputJsonSync(`${fixturePath}/package.json`, {
|
|
104
137
|
scripts: { coverage: 'vitest run --coverage', test: 'vitest run' },
|
|
105
138
|
});
|
|
106
139
|
const exec = vi.spyOn(shell, 'exec');
|
|
107
140
|
|
|
108
|
-
await buildCoverage({
|
|
141
|
+
await buildCoverage({
|
|
142
|
+
docs: { coverage: [{ id: 'engine', suite: 'unit' }] },
|
|
143
|
+
docsDestination: `${fixturePath}/out/`,
|
|
144
|
+
});
|
|
109
145
|
|
|
110
146
|
expect(exec.mock.calls.length).to.equal(0);
|
|
111
|
-
expect(fs.readFileSync(`${fixturePath}/out/coverage/index.html`, 'utf8')).to.include(
|
|
147
|
+
expect(fs.readFileSync(`${fixturePath}/out/coverage/engine/index.html`, 'utf8')).to.include(
|
|
148
|
+
'Coverage report unavailable',
|
|
149
|
+
);
|
|
112
150
|
});
|
|
113
151
|
|
|
114
|
-
it('publishes nothing for a deploy that declares no coverage
|
|
115
|
-
fixturePath = fs.mkdtempSync('/tmp/engine-coverage-');
|
|
116
|
-
|
|
152
|
+
it('publishes nothing for a deploy that declares no coverage report', async () => {
|
|
117
153
|
await buildCoverage({ docs: {}, docsDestination: `${fixturePath}/out/` });
|
|
118
154
|
|
|
119
155
|
expect(fs.existsSync(`${fixturePath}/out`)).to.equal(false);
|
|
@@ -137,7 +173,7 @@ describe('docs build', () => {
|
|
|
137
173
|
publicClientId: 'fixture',
|
|
138
174
|
rootClientPath: `${fixturePath}/client`,
|
|
139
175
|
packageData: PACKAGE_DATA,
|
|
140
|
-
docs: { jsJsonPath: `${fixturePath}/typedoc.json
|
|
176
|
+
docs: { jsJsonPath: `${fixturePath}/typedoc.json` },
|
|
141
177
|
...overrides,
|
|
142
178
|
});
|
|
143
179
|
|
|
@@ -146,21 +182,16 @@ describe('docs build', () => {
|
|
|
146
182
|
fixturePath = fs.mkdtempSync('/tmp/engine-docs-');
|
|
147
183
|
publicRoot = `./public/${HOST}`;
|
|
148
184
|
if (fs.existsSync(publicRoot)) throw new Error(`Refusing to write fixtures into an existing site: ${publicRoot}`);
|
|
149
|
-
|
|
185
|
+
staged.swaggerJson = null;
|
|
150
186
|
});
|
|
151
187
|
|
|
152
188
|
afterEach(() => {
|
|
153
|
-
vi.useRealTimers();
|
|
154
189
|
vi.restoreAllMocks();
|
|
155
190
|
fs.removeSync(fixturePath);
|
|
156
191
|
fs.removeSync(publicRoot);
|
|
157
192
|
fs.removeSync('.typedoc.tmp.json');
|
|
158
193
|
});
|
|
159
194
|
|
|
160
|
-
const flushApiDocs = async () => {
|
|
161
|
-
await vi.advanceTimersByTimeAsync(1);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
195
|
it('renders the typedoc runtime config without mutating the base config on disk', async () => {
|
|
165
196
|
const baseConfig = { tsconfig: './tsconfig.docs.json', name: 'Base', entryPoints: ['./src'] };
|
|
166
197
|
fs.outputJsonSync(`${fixturePath}/typedoc.json`, baseConfig);
|
|
@@ -174,11 +205,9 @@ describe('docs build', () => {
|
|
|
174
205
|
await runBuildDocs({
|
|
175
206
|
docs: {
|
|
176
207
|
jsJsonPath: `${fixturePath}/typedoc.json`,
|
|
177
|
-
coveragePath: `${fixturePath}/nothing-here`,
|
|
178
208
|
references: [`${fixturePath}/reference-a.md`, `${fixturePath}/missing.md`],
|
|
179
209
|
},
|
|
180
210
|
});
|
|
181
|
-
await flushApiDocs();
|
|
182
211
|
|
|
183
212
|
expect(tmpConfig.name).to.equal('Fixture API');
|
|
184
213
|
expect(tmpConfig.out).to.equal(`./public/${HOST}/docs/engine/1.2.3/`);
|
|
@@ -198,7 +227,6 @@ describe('docs build', () => {
|
|
|
198
227
|
});
|
|
199
228
|
|
|
200
229
|
await runBuildDocs({ metadata: {}, docs: { jsJsonPath: `${fixturePath}/typedoc.json`, references: [] } });
|
|
201
|
-
await flushApiDocs();
|
|
202
230
|
|
|
203
231
|
expect(tmpConfig.name).to.equal('Base');
|
|
204
232
|
expect(tmpConfig).not.to.have.property('projectDocuments');
|
|
@@ -207,14 +235,12 @@ describe('docs build', () => {
|
|
|
207
235
|
it('skips the typedoc build when the deploy declares no config', async () => {
|
|
208
236
|
const exec = vi.spyOn(shell, 'exec');
|
|
209
237
|
await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
210
|
-
await flushApiDocs();
|
|
211
238
|
expect(exec.mock.calls.length).to.equal(0);
|
|
212
239
|
});
|
|
213
240
|
|
|
214
241
|
it('documents only the routers it has schemas for, at the deploy version', async () => {
|
|
215
242
|
const exec = vi.spyOn(shell, 'exec').mockReturnValue({ code: 0, stdout: '', stderr: '' });
|
|
216
243
|
await runBuildDocs();
|
|
217
|
-
await flushApiDocs();
|
|
218
244
|
|
|
219
245
|
expect(exec.mock.calls.length).to.equal(0);
|
|
220
246
|
expect(generated.length).to.equal(1);
|
|
@@ -228,7 +254,6 @@ describe('docs build', () => {
|
|
|
228
254
|
|
|
229
255
|
it('falls back to a generic title and empty description without metadata', async () => {
|
|
230
256
|
await runBuildDocs({ metadata: {}, docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
231
|
-
await flushApiDocs();
|
|
232
257
|
expect(generated[0].doc.info.title).to.equal('REST API');
|
|
233
258
|
expect(generated[0].doc.info.description).to.equal('');
|
|
234
259
|
});
|
|
@@ -240,14 +265,12 @@ describe('docs build', () => {
|
|
|
240
265
|
try {
|
|
241
266
|
process.env.NODE_ENV = 'development';
|
|
242
267
|
await runBuildDocs({ path: '/store', docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
243
|
-
await flushApiDocs();
|
|
244
268
|
expect(generated[0].doc.servers[0].url).to.equal('http://localhost:4000/store/api');
|
|
245
269
|
expect(generated[0].outputFile).to.equal(`./public/${HOST}/store/swagger-output.json`);
|
|
246
270
|
|
|
247
271
|
generated.length = 0;
|
|
248
272
|
process.env.NODE_ENV = 'production';
|
|
249
273
|
await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
250
|
-
await flushApiDocs();
|
|
251
274
|
expect(generated[0].doc.servers[0].url).to.equal(`https://${HOST}/api`);
|
|
252
275
|
} finally {
|
|
253
276
|
process.env.NODE_ENV = previousEnv;
|
|
@@ -267,7 +290,6 @@ describe('docs build', () => {
|
|
|
267
290
|
apiBaseProxyPath: '/',
|
|
268
291
|
docs: { jsJsonPath: `${fixturePath}/absent.json` },
|
|
269
292
|
});
|
|
270
|
-
await flushApiDocs();
|
|
271
293
|
expect(generated[0].doc.servers[0].url).to.equal('http://localhost:4017/api');
|
|
272
294
|
|
|
273
295
|
generated.length = 0;
|
|
@@ -278,7 +300,6 @@ describe('docs build', () => {
|
|
|
278
300
|
apiBaseProxyPath: '/store',
|
|
279
301
|
docs: { jsJsonPath: `${fixturePath}/absent.json` },
|
|
280
302
|
});
|
|
281
|
-
await flushApiDocs();
|
|
282
303
|
expect(generated[0].doc.servers[0].url).to.equal('https://api.fixture.test/store/api');
|
|
283
304
|
} finally {
|
|
284
305
|
process.env.NODE_ENV = previousEnv;
|
|
@@ -292,8 +313,7 @@ describe('docs build', () => {
|
|
|
292
313
|
// did not emit, and has to strip the OAS-2 `in: body` parameter it did.
|
|
293
314
|
it('injects the request bodies into the generated document', async () => {
|
|
294
315
|
const outputFile = `./public/${HOST}/swagger-output.json`;
|
|
295
|
-
|
|
296
|
-
fs.outputJsonSync(outputFile, {
|
|
316
|
+
staged.swaggerJson = {
|
|
297
317
|
paths: {
|
|
298
318
|
'/user': {
|
|
299
319
|
post: {
|
|
@@ -305,8 +325,8 @@ describe('docs build', () => {
|
|
|
305
325
|
},
|
|
306
326
|
'/user/{id}': { get: {} },
|
|
307
327
|
},
|
|
308
|
-
}
|
|
309
|
-
await
|
|
328
|
+
};
|
|
329
|
+
await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
310
330
|
|
|
311
331
|
const patched = fs.readJsonSync(outputFile);
|
|
312
332
|
expect(patched.paths['/user'].post.requestBody.content['application/json'].schema.$ref).to.equal(
|
|
@@ -318,11 +338,18 @@ describe('docs build', () => {
|
|
|
318
338
|
|
|
319
339
|
it('leaves the generated document alone when no operation matches', async () => {
|
|
320
340
|
const outputFile = `./public/${HOST}/swagger-output.json`;
|
|
341
|
+
staged.swaggerJson = { paths: { '/health': { get: {} } } };
|
|
321
342
|
await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
322
|
-
fs.outputJsonSync(outputFile, { paths: { '/health': { get: {} } } });
|
|
323
|
-
await flushApiDocs();
|
|
324
343
|
expect(fs.readJsonSync(outputFile)).to.deep.equal({ paths: { '/health': { get: {} } } });
|
|
325
344
|
});
|
|
345
|
+
|
|
346
|
+
// Regression: the spec was generated on a timer the build never awaited, so a bundle zipped
|
|
347
|
+
// right after the build shipped without it and the pod restoring it served no api-docs.
|
|
348
|
+
it('has written the spec by the time the build returns', async () => {
|
|
349
|
+
staged.swaggerJson = { paths: {} };
|
|
350
|
+
await runBuildDocs({ docs: { jsJsonPath: `${fixturePath}/absent.json` } });
|
|
351
|
+
expect(fs.existsSync(`./public/${HOST}/swagger-output.json`)).to.equal(true);
|
|
352
|
+
});
|
|
326
353
|
});
|
|
327
354
|
|
|
328
355
|
describe('swagger UI options', () => {
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module client-bundle.test
|
|
5
|
+
* @description Covers the client bundle transport: the route set push and pull agree on, the
|
|
6
|
+
* uniform host filter and split option both runners parse, and the pull path that merges split
|
|
7
|
+
* parts, extracts them in-process, and installs the result under the served public directory.
|
|
8
|
+
*
|
|
9
|
+
* The shell boundary is mocked so the deterministic parts — route selection, artifact
|
|
10
|
+
* resolution, and the local zip round trip — run for real.
|
|
11
|
+
*
|
|
12
|
+
* Uses 'chai' for assertions.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { expect } from 'chai';
|
|
16
|
+
import fs from 'fs-extra';
|
|
17
|
+
import { vi } from 'vitest';
|
|
18
|
+
|
|
19
|
+
const shellCommands = [];
|
|
20
|
+
vi.mock('../../src/server/runtime/process.js', async (importOriginal) => ({
|
|
21
|
+
...(await importOriginal()),
|
|
22
|
+
shellExec: (command) => {
|
|
23
|
+
shellCommands.push(command);
|
|
24
|
+
return '';
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
CLIENT_BUNDLE_DIRECTORY,
|
|
30
|
+
clientBundleTrackedKeys,
|
|
31
|
+
clientBundleManifestKey,
|
|
32
|
+
clientBundleArtifactPaths,
|
|
33
|
+
clientBundleEntriesFactory,
|
|
34
|
+
clientBundleHostFilter,
|
|
35
|
+
clientBundleSplitFlag,
|
|
36
|
+
clientBundleStorageFilePath,
|
|
37
|
+
pullClientBundle,
|
|
38
|
+
pushClientBundle,
|
|
39
|
+
} = await import('../../src/client-builder/client-bundle.js');
|
|
40
|
+
const { zipFromLocalFiles } = await import('../../src/server/storage/zip.js');
|
|
41
|
+
const { program } = await import('../../src/cli/index.js');
|
|
42
|
+
|
|
43
|
+
const CONF_SERVER = {
|
|
44
|
+
'a.com': {
|
|
45
|
+
'/': { client: 'A' },
|
|
46
|
+
'/peer': { client: 'P' },
|
|
47
|
+
'/old': { redirect: '/' },
|
|
48
|
+
'/frozen': { disabledRebuild: true },
|
|
49
|
+
'/multi': { client: 'M', replicas: ['/multi-1', '/multi-2'] },
|
|
50
|
+
'/single': { client: 'S', singleReplica: true, replicas: ['/single-1'] },
|
|
51
|
+
},
|
|
52
|
+
'b.com': { '/': { client: 'B' } },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
describe('client bundle runner input', () => {
|
|
56
|
+
it('parses the comma-separated host filter both runners share', () => {
|
|
57
|
+
expect(clientBundleHostFilter(' a.com , b.com ,, ')).to.deep.equal(['a.com', 'b.com']);
|
|
58
|
+
expect(clientBundleHostFilter('')).to.deep.equal([]);
|
|
59
|
+
expect(clientBundleHostFilter(undefined)).to.deep.equal([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('resolves the split option, defaulting an unusable value rather than failing the build', () => {
|
|
63
|
+
expect(clientBundleSplitFlag(undefined)).to.equal('--split 8');
|
|
64
|
+
expect(clientBundleSplitFlag('16')).to.equal('--split 16');
|
|
65
|
+
expect(clientBundleSplitFlag('none')).to.equal('');
|
|
66
|
+
expect(clientBundleSplitFlag('0')).to.equal('--split 8');
|
|
67
|
+
expect(clientBundleSplitFlag('abc')).to.equal('--split 8');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('names one storage manifest per deployment', () => {
|
|
71
|
+
expect(clientBundleStorageFilePath('dd-test')).to.equal('engine-private/conf/dd-test/storage.bundle.json');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('client bundle route selection', () => {
|
|
76
|
+
let fixturePath;
|
|
77
|
+
let previousCwd;
|
|
78
|
+
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
previousCwd = process.cwd();
|
|
81
|
+
fixturePath = fs.mkdtempSync('/tmp/engine-client-bundle-');
|
|
82
|
+
process.chdir(fixturePath);
|
|
83
|
+
fs.mkdirSync('engine-private/conf/dd-test', { recursive: true });
|
|
84
|
+
fs.writeFileSync('engine-private/conf/dd-test/conf.server.json', JSON.stringify(CONF_SERVER), 'utf8');
|
|
85
|
+
shellCommands.length = 0;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
process.chdir(previousCwd);
|
|
90
|
+
fs.removeSync(fixturePath);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const buildIds = (options) => clientBundleEntriesFactory(options).map((entry) => entry.buildId);
|
|
94
|
+
|
|
95
|
+
it('selects every route the client build emits a bundle for, replicas included', () => {
|
|
96
|
+
expect(buildIds({ deployId: 'dd-test' })).to.deep.equal([
|
|
97
|
+
'a.com-',
|
|
98
|
+
'a.com-peer',
|
|
99
|
+
'a.com-multi',
|
|
100
|
+
'a.com-multi-1',
|
|
101
|
+
'a.com-multi-2',
|
|
102
|
+
'b.com-',
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('skips the routes the client build skips', () => {
|
|
107
|
+
const selected = buildIds({ deployId: 'dd-test' });
|
|
108
|
+
expect(selected).to.not.include('a.com-old');
|
|
109
|
+
expect(selected).to.not.include('a.com-frozen');
|
|
110
|
+
expect(selected).to.not.include('a.com-single');
|
|
111
|
+
expect(selected).to.not.include('a.com-single-1');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('maps a route to its served public directory', () => {
|
|
115
|
+
const entries = clientBundleEntriesFactory({ deployId: 'dd-test', hosts: ['a.com'] });
|
|
116
|
+
const byId = Object.fromEntries(entries.map((entry) => [entry.buildId, entry.publicPath]));
|
|
117
|
+
expect(byId['a.com-']).to.equal('public/a.com');
|
|
118
|
+
expect(byId['a.com-peer']).to.equal('public/a.com/peer');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('applies the host filter, and resolves nothing for an unknown host or deployment', () => {
|
|
122
|
+
expect(buildIds({ deployId: 'dd-test', hosts: ['b.com'] })).to.deep.equal(['b.com-']);
|
|
123
|
+
expect(buildIds({ deployId: 'dd-test', hosts: ['absent.com'] })).to.deep.equal([]);
|
|
124
|
+
expect(buildIds({ deployId: 'dd-absent' })).to.deep.equal([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('resolves split parts ahead of a single zip, and nothing when neither is present', () => {
|
|
128
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
129
|
+
for (const part of ['001', '002']) fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/a.com-.zip.part${part}`, 'x');
|
|
130
|
+
fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`, 'y');
|
|
131
|
+
|
|
132
|
+
expect(clientBundleArtifactPaths(`${CLIENT_BUNDLE_DIRECTORY}/a.com-.zip`).artifactPaths).to.have.lengthOf(2);
|
|
133
|
+
expect(clientBundleArtifactPaths(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`).artifactPaths).to.deep.equal([
|
|
134
|
+
`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`,
|
|
135
|
+
]);
|
|
136
|
+
expect(clientBundleArtifactPaths(`${CLIENT_BUNDLE_DIRECTORY}/absent-.zip`).artifactPaths).to.deep.equal([]);
|
|
137
|
+
expect(clientBundleArtifactPaths('./no-such-dir/x.zip').artifactPaths).to.deep.equal([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('uploads every artifact of every resolved route, and reports the routes with none', () => {
|
|
141
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
142
|
+
for (const part of ['001', '002']) fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${part}`, 'x');
|
|
143
|
+
|
|
144
|
+
const result = pushClientBundle({ deployId: 'dd-test', hosts: ['b.com'], split: '4' });
|
|
145
|
+
|
|
146
|
+
expect(result).to.deep.equal({ pushed: 1, skipped: 0 });
|
|
147
|
+
expect(shellCommands.some((command) => command.includes('--build-zip --split 4'))).to.equal(true);
|
|
148
|
+
const uploads = shellCommands.filter((command) => command.includes(' fs '));
|
|
149
|
+
expect(uploads).to.have.lengthOf(2);
|
|
150
|
+
expect(uploads.every((command) => command.includes('--storage-id bundle') && command.includes('--force'))).to.equal(
|
|
151
|
+
true,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
shellCommands.length = 0;
|
|
155
|
+
expect(pushClientBundle({ deployId: 'dd-test', hosts: ['a.com'] })).to.deep.equal({ pushed: 0, skipped: 5 });
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('shells out only to subcommands the CLI still registers', async () => {
|
|
159
|
+
// Regression: both directions called `underpost env`, a deprecated alias removed when the
|
|
160
|
+
// env and config commands were folded into the host configuration store. Nothing failed at
|
|
161
|
+
// import time, so bundle mode stayed broken until a deployment ran it.
|
|
162
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
163
|
+
fs.writeFileSync('bundled.html', '<h1>bundled</h1>', 'utf8');
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`,
|
|
166
|
+
await zipFromLocalFiles([{ localPath: 'bundled.html', entryName: 'index.html' }]),
|
|
167
|
+
);
|
|
168
|
+
pushClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
|
|
169
|
+
await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
|
|
170
|
+
|
|
171
|
+
const pull = shellCommands.find((command) => command.includes(' fs ') && command.includes('--pull'));
|
|
172
|
+
expect(pull).to.include('--tracked').and.to.include('--storage-id bundle');
|
|
173
|
+
expect(pull).not.to.include('--storage-file-path');
|
|
174
|
+
|
|
175
|
+
const registered = program.commands.map((command) => command.name());
|
|
176
|
+
const invoked = [
|
|
177
|
+
...new Set(
|
|
178
|
+
shellCommands
|
|
179
|
+
.map((command) => /(?:^|\s)(?:node\s+\S*bin\S*|underpost)\s+([a-z-]+)/.exec(command)?.[1])
|
|
180
|
+
.filter(Boolean),
|
|
181
|
+
),
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
expect(invoked).to.not.be.empty;
|
|
185
|
+
for (const subcommand of invoked) expect(registered, subcommand).to.include(subcommand);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('untracks bundle parts a smaller build no longer produces', async () => {
|
|
189
|
+
const manifest = {
|
|
190
|
+
'build/b.com-.zip.part001': { bytes: 1 },
|
|
191
|
+
'build/b.com-.zip.part002': { bytes: 1 },
|
|
192
|
+
'build/b.com-.zip.part003': { bytes: 1 },
|
|
193
|
+
'build/other.com-.zip.part003': { bytes: 1 },
|
|
194
|
+
};
|
|
195
|
+
expect(clientBundleTrackedKeys(manifest, 'b.com-')).to.deep.equal([
|
|
196
|
+
'build/b.com-.zip.part001',
|
|
197
|
+
'build/b.com-.zip.part002',
|
|
198
|
+
'build/b.com-.zip.part003',
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
202
|
+
for (const part of ['001', '002']) fs.writeFileSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${part}`, 'x');
|
|
203
|
+
fs.writeFileSync('engine-private/conf/dd-test/storage.bundle.json', JSON.stringify(manifest), 'utf8');
|
|
204
|
+
|
|
205
|
+
shellCommands.length = 0;
|
|
206
|
+
expect(fs.existsSync('build/b.com-.zip.part003')).to.equal(false);
|
|
207
|
+
expect(pushClientBundle({ deployId: 'dd-test', hosts: ['b.com'] })).to.deep.equal({ pushed: 1, skipped: 0 });
|
|
208
|
+
|
|
209
|
+
const removals = shellCommands.filter((command) => command.includes(' --rm'));
|
|
210
|
+
expect(removals, 'exactly the part this build did not produce').to.have.lengthOf(1);
|
|
211
|
+
expect(removals[0]).to.include(" fs --tracked --key 'build/b.com-.zip.part003'");
|
|
212
|
+
expect(removals[0]).to.include('--deploy-id dd-test --storage-id bundle --rm');
|
|
213
|
+
expect(removals[0]).to.not.include('other.com');
|
|
214
|
+
const uploads = shellCommands.filter((command) => command.includes(' fs ') && command.includes('--force'));
|
|
215
|
+
expect(uploads).to.have.lengthOf(2);
|
|
216
|
+
expect(shellCommands.indexOf(removals[0])).to.be.greaterThan(shellCommands.indexOf(uploads[1]));
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('merges, extracts, and installs a split bundle into the public directory', async () => {
|
|
220
|
+
fs.mkdirSync('source/nested', { recursive: true });
|
|
221
|
+
fs.writeFileSync('source/index.html', '<h1>bundle</h1>'.repeat(100), 'utf8');
|
|
222
|
+
fs.writeFileSync('source/nested/app.js', 'console.log(1);'.repeat(100), 'utf8');
|
|
223
|
+
const archive = await zipFromLocalFiles([
|
|
224
|
+
{ localPath: 'source/index.html', entryName: 'index.html' },
|
|
225
|
+
{ localPath: 'source/nested/app.js', entryName: 'nested/app.js' },
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
229
|
+
const partSize = Math.ceil(archive.length / 3);
|
|
230
|
+
for (let offset = 0, index = 1; offset < archive.length; offset += partSize, index++) {
|
|
231
|
+
const name = `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${String(index).padStart(3, '0')}`;
|
|
232
|
+
fs.writeFileSync(name, archive.subarray(offset, offset + partSize));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const result = await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
|
|
236
|
+
|
|
237
|
+
expect(result).to.deep.equal({ pulled: 1, skipped: 0 });
|
|
238
|
+
expect(fs.readFileSync('public/b.com/nested/app.js', 'utf8')).to.equal(
|
|
239
|
+
fs.readFileSync('source/nested/app.js', 'utf8'),
|
|
240
|
+
);
|
|
241
|
+
expect(fs.existsSync(`${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip`)).to.equal(false);
|
|
242
|
+
expect(fs.readdirSync(CLIENT_BUNDLE_DIRECTORY).filter((name) => name.includes('.part'))).to.deep.equal([]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('maps a pulled part back to the storage key that produced it', () => {
|
|
246
|
+
// A pull keeping the downloaded archive writes `<key>.zip`; an unwrapped part is the key.
|
|
247
|
+
expect(clientBundleManifestKey('build/a.com-.zip.part001.zip')).to.equal('build/a.com-.zip.part001');
|
|
248
|
+
expect(clientBundleManifestKey('./build/a.com-.zip.part001.zip')).to.equal('build/a.com-.zip.part001');
|
|
249
|
+
expect(clientBundleManifestKey('build/a.com-.zip.part001')).to.equal('build/a.com-.zip.part001');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('refuses a bundle that reassembles to a different size than was pushed', async () => {
|
|
253
|
+
// Regression: one part's upload failed silently, so the store kept the previous build's copy.
|
|
254
|
+
// The parts merged without complaint and only the assembled archive was corrupt, 190MB later.
|
|
255
|
+
fs.writeFileSync('source.html', '<h1>bundle</h1>'.repeat(100), 'utf8');
|
|
256
|
+
const archive = await zipFromLocalFiles([{ localPath: 'source.html', entryName: 'index.html' }]);
|
|
257
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
258
|
+
|
|
259
|
+
const partSize = Math.ceil(archive.length / 2);
|
|
260
|
+
const partPaths = [];
|
|
261
|
+
for (let offset = 0, index = 1; offset < archive.length; offset += partSize, index++) {
|
|
262
|
+
const name = `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${String(index).padStart(3, '0')}`;
|
|
263
|
+
fs.writeFileSync(name, archive.subarray(offset, offset + partSize));
|
|
264
|
+
partPaths.push(name.replace(/^\.\//, ''));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The manifest records a larger second part, as a build whose last part never replaced the
|
|
268
|
+
// previous one would.
|
|
269
|
+
fs.writeFileSync(
|
|
270
|
+
'engine-private/conf/dd-test/storage.bundle.json',
|
|
271
|
+
JSON.stringify({
|
|
272
|
+
[partPaths[0]]: { bytes: fs.statSync(partPaths[0]).size },
|
|
273
|
+
[partPaths[1]]: { bytes: fs.statSync(partPaths[1]).size + 4096 },
|
|
274
|
+
}),
|
|
275
|
+
'utf8',
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
let thrown;
|
|
279
|
+
await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] }).catch((error) => (thrown = error));
|
|
280
|
+
|
|
281
|
+
expect(thrown?.message).to.match(/reassembled to \d+ bytes, expected \d+/);
|
|
282
|
+
expect(fs.existsSync('public/b.com'), 'a bundle that failed verification must not be installed').to.equal(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('installs a bundle whose parts match the pushed sizes', async () => {
|
|
286
|
+
fs.writeFileSync('source.html', '<h1>bundle</h1>'.repeat(100), 'utf8');
|
|
287
|
+
const archive = await zipFromLocalFiles([{ localPath: 'source.html', entryName: 'index.html' }]);
|
|
288
|
+
fs.mkdirSync(CLIENT_BUNDLE_DIRECTORY, { recursive: true });
|
|
289
|
+
|
|
290
|
+
const partSize = Math.ceil(archive.length / 2);
|
|
291
|
+
const manifest = {};
|
|
292
|
+
for (let offset = 0, index = 1; offset < archive.length; offset += partSize, index++) {
|
|
293
|
+
const name = `${CLIENT_BUNDLE_DIRECTORY}/b.com-.zip.part${String(index).padStart(3, '0')}`;
|
|
294
|
+
fs.writeFileSync(name, archive.subarray(offset, offset + partSize));
|
|
295
|
+
manifest[name.replace(/^\.\//, '')] = { bytes: fs.statSync(name).size };
|
|
296
|
+
}
|
|
297
|
+
fs.writeFileSync('engine-private/conf/dd-test/storage.bundle.json', JSON.stringify(manifest), 'utf8');
|
|
298
|
+
|
|
299
|
+
expect(await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] })).to.deep.equal({ pulled: 1, skipped: 0 });
|
|
300
|
+
expect(fs.existsSync('public/b.com/index.html')).to.equal(true);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('skips a route whose bundle never arrived rather than clearing its public directory', async () => {
|
|
304
|
+
fs.mkdirSync('public/b.com', { recursive: true });
|
|
305
|
+
fs.writeFileSync('public/b.com/index.html', 'live', 'utf8');
|
|
306
|
+
|
|
307
|
+
const result = await pullClientBundle({ deployId: 'dd-test', hosts: ['b.com'] });
|
|
308
|
+
|
|
309
|
+
expect(result).to.deep.equal({ pulled: 0, skipped: 1 });
|
|
310
|
+
expect(fs.readFileSync('public/b.com/index.html', 'utf8')).to.equal('live');
|
|
311
|
+
expect(shellCommands.some((command) => command.startsWith('sudo rm -rf'))).to.equal(false);
|
|
312
|
+
});
|
|
313
|
+
});
|