@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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import shell from 'shelljs';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
import { gatewayPodFetch, parseRawHttpResponse } from '../../src/server/network/underpost-gateway.js';
|
|
7
|
+
|
|
8
|
+
describe('gateway pod fetch', () => {
|
|
9
|
+
let commands;
|
|
10
|
+
let answer;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
commands = [];
|
|
14
|
+
answer = (command) =>
|
|
15
|
+
command.includes('| nc ') ? 'HTTP/1.1 502 Bad Gateway\r\nServer: nginx\r\n\r\n<h1>maintenance</h1>\n' : '';
|
|
16
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
17
|
+
commands.push(command);
|
|
18
|
+
const stdout = answer(command);
|
|
19
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => vi.restoreAllMocks());
|
|
24
|
+
|
|
25
|
+
it('reads the status and the body of an error response off the socket', () => {
|
|
26
|
+
// Regression: busybox wget discards the body of any non-2xx answer, so the fallback
|
|
27
|
+
// document — delivered with an upstream-failure status by definition — never
|
|
28
|
+
// matched, and the probe polled every attempt against a correct edge.
|
|
29
|
+
const result = gatewayPodFetch({ namespace: 'default', host: 'www.bymyelectrics.com', path: '/' });
|
|
30
|
+
expect(result).to.deep.equal({ status: '502', body: '<h1>maintenance</h1>\n' });
|
|
31
|
+
expect(commands.length).to.equal(1);
|
|
32
|
+
expect(commands[0]).to.include(
|
|
33
|
+
"printf 'GET / HTTP/1.0\\r\\nHost: www.bymyelectrics.com\\r\\n\\r\\n' | nc -w 10 127.0.0.1 80",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('falls back to the wget pair for the status where nc is absent', () => {
|
|
38
|
+
answer = (command) => (command.includes('wget -S') ? 'HTTP/1.1 503 Service Unavailable\n' : '');
|
|
39
|
+
const result = gatewayPodFetch({ namespace: 'default', host: 'healthcare.nexodev.org' });
|
|
40
|
+
expect(result.status).to.equal('503');
|
|
41
|
+
expect(commands.length).to.equal(3);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('parses a raw response byte for byte after the first blank line', () => {
|
|
45
|
+
expect(parseRawHttpResponse('')).to.deep.equal({ status: '', body: '' });
|
|
46
|
+
expect(parseRawHttpResponse('HTTP/1.0 200 OK\r\nX: y\r\n\r\n')).to.deep.equal({ status: '200', body: '' });
|
|
47
|
+
expect(parseRawHttpResponse('HTTP/1.1 504 Gateway Time-out\n\nline\n\nmore')).to.deep.equal({
|
|
48
|
+
status: '504',
|
|
49
|
+
body: 'line\n\nmore',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('gateway fetch', () => {
|
|
55
|
+
afterEach(() => vi.restoreAllMocks());
|
|
56
|
+
|
|
57
|
+
const mock = (answer) => {
|
|
58
|
+
const commands = [];
|
|
59
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
60
|
+
commands.push(command);
|
|
61
|
+
const stdout = answer(command);
|
|
62
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
63
|
+
});
|
|
64
|
+
return commands;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it('asks the gateway Service from this host with curl, keeping the error body', async () => {
|
|
68
|
+
const { gatewayFetch } = await import('../../src/server/network/underpost-gateway.js');
|
|
69
|
+
const commands = mock((command) => {
|
|
70
|
+
if (command.includes('kubectl get svc underpost-gateway-service')) return '10.96.151.82\n';
|
|
71
|
+
if (command.startsWith('curl ')) return 'HTTP/1.1 502 Bad Gateway\r\nServer: nginx\r\n\r\n<h1>maintenance</h1>\n';
|
|
72
|
+
return '';
|
|
73
|
+
});
|
|
74
|
+
const result = gatewayFetch({ namespace: 'default', host: 'healthcare.nexodev.org', path: '/' });
|
|
75
|
+
expect(result).to.deep.equal({ status: '502', body: '<h1>maintenance</h1>\n', via: 'host' });
|
|
76
|
+
expect(commands.find((command) => command.startsWith('curl '))).to.include(
|
|
77
|
+
"-H 'Host: healthcare.nexodev.org' http://10.96.151.82:80/",
|
|
78
|
+
);
|
|
79
|
+
expect(commands.some((command) => command.includes('kubectl exec'))).to.equal(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('falls back to the pod when the Service cannot be reached from here', async () => {
|
|
83
|
+
const { gatewayFetch } = await import('../../src/server/network/underpost-gateway.js');
|
|
84
|
+
mock((command) => {
|
|
85
|
+
if (command.includes('kubectl get svc underpost-gateway-service')) return '10.96.151.82\n';
|
|
86
|
+
if (command.includes('| nc ')) return 'HTTP/1.1 503 Service Unavailable\r\n\r\nbody';
|
|
87
|
+
return '';
|
|
88
|
+
});
|
|
89
|
+
expect(gatewayFetch({ namespace: 'default', host: 'healthcare.nexodev.org' })).to.deep.equal({
|
|
90
|
+
status: '503',
|
|
91
|
+
body: 'body',
|
|
92
|
+
via: 'pod',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
5
|
+
import shell from 'shelljs';
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
import UnderpostCluster from '../../src/cli/cluster.js';
|
|
8
|
+
import UnderpostDeploy, { LETSENCRYPT_ISSUER_MANIFEST, LETSENCRYPT_ISSUER_NAME } from '../../src/cli/deploy.js';
|
|
9
|
+
|
|
10
|
+
describe("Let's Encrypt issuer for the ingress stack in use", () => {
|
|
11
|
+
afterEach(() => vi.restoreAllMocks());
|
|
12
|
+
|
|
13
|
+
describe('acmeSolverFactory', () => {
|
|
14
|
+
it('answers through the Gateway API whenever its data plane is installed', () => {
|
|
15
|
+
// The `cluster` runner installs the issuer in an invocation that carries no stack
|
|
16
|
+
// flag, so the decision has to come from the cluster, not from the flags.
|
|
17
|
+
const presence = { contour: false, gateway: true };
|
|
18
|
+
expect(UnderpostCluster.API.acmeSolverFactory({ options: {}, presence })).to.equal('gateway');
|
|
19
|
+
expect(UnderpostCluster.API.acmeSolverFactory({ options: { gatewayApi: true }, presence })).to.equal('gateway');
|
|
20
|
+
expect(
|
|
21
|
+
UnderpostCluster.API.acmeSolverFactory({
|
|
22
|
+
options: { gatewayApi: true },
|
|
23
|
+
presence: { contour: true, gateway: false },
|
|
24
|
+
}),
|
|
25
|
+
).to.equal('gateway');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps the Contour Ingress solver only where nothing else carries the challenge', () => {
|
|
29
|
+
expect(
|
|
30
|
+
UnderpostCluster.API.acmeSolverFactory({ options: {}, presence: { contour: true, gateway: false } }),
|
|
31
|
+
).to.equal('ingress');
|
|
32
|
+
expect(
|
|
33
|
+
UnderpostCluster.API.acmeSolverFactory({
|
|
34
|
+
options: { contour: true },
|
|
35
|
+
presence: { contour: false, gateway: true },
|
|
36
|
+
}),
|
|
37
|
+
).to.equal('ingress');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('letsEncryptIssuerYamlFactory', () => {
|
|
42
|
+
it('keeps the account from the base manifest and swaps in the Gateway solver', () => {
|
|
43
|
+
const base = yamlLoad(
|
|
44
|
+
UnderpostDeploy.API.letsEncryptIssuerYamlFactory({ solver: 'ingress', basePath: LETSENCRYPT_ISSUER_MANIFEST }),
|
|
45
|
+
);
|
|
46
|
+
const issuer = yamlLoad(
|
|
47
|
+
UnderpostDeploy.API.letsEncryptIssuerYamlFactory({
|
|
48
|
+
solver: 'gateway',
|
|
49
|
+
parentRefs: [
|
|
50
|
+
{ name: 'dd-core-production', namespace: 'default' },
|
|
51
|
+
{ name: 'dd-prototype-production', namespace: 'default' },
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
expect(issuer.kind).to.equal('ClusterIssuer');
|
|
56
|
+
expect(issuer.metadata.name).to.equal(LETSENCRYPT_ISSUER_NAME);
|
|
57
|
+
expect(issuer.spec.acme.email).to.equal(base.spec.acme.email);
|
|
58
|
+
expect(issuer.spec.acme.server).to.equal(base.spec.acme.server);
|
|
59
|
+
expect(issuer.spec.acme.privateKeySecretRef).to.deep.equal(base.spec.acme.privateKeySecretRef);
|
|
60
|
+
expect(issuer.spec.acme.solvers).to.deep.equal([
|
|
61
|
+
{
|
|
62
|
+
http01: {
|
|
63
|
+
gatewayHTTPRoute: {
|
|
64
|
+
parentRefs: [
|
|
65
|
+
{ name: 'dd-core-production', namespace: 'default', kind: 'Gateway' },
|
|
66
|
+
{ name: 'dd-prototype-production', namespace: 'default', kind: 'Gateway' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
expect(base.spec.acme.solvers).to.deep.equal([{ http01: { ingress: { class: 'contour' } } }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('refuses a Gateway solver with nothing to attach the challenge route to', () => {
|
|
76
|
+
expect(() => UnderpostDeploy.API.letsEncryptIssuerYamlFactory({ solver: 'gateway', parentRefs: [] })).to.throw(
|
|
77
|
+
/parentRef/,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('acmeGatewayParentRefsFactory', () => {
|
|
83
|
+
it('unions the live Gateways with the routed deploys, once each, sorted', () => {
|
|
84
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
85
|
+
const stdout = command.startsWith('kubectl get gateway')
|
|
86
|
+
? 'gateway.gateway.networking.k8s.io/dd-core-production\ngateway.gateway.networking.k8s.io/other-production\n'
|
|
87
|
+
: '';
|
|
88
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
89
|
+
});
|
|
90
|
+
vi.spyOn(UnderpostDeploy.API, 'gatewayNameFactory').mockImplementation(
|
|
91
|
+
({ deployId, env }) => `${deployId}-${env}`,
|
|
92
|
+
);
|
|
93
|
+
const refs = UnderpostDeploy.API.acmeGatewayParentRefsFactory({ namespace: 'default', env: 'production' });
|
|
94
|
+
const names = refs.map(({ name }) => name);
|
|
95
|
+
expect(names).to.include('dd-core-production');
|
|
96
|
+
expect(names).to.include('other-production');
|
|
97
|
+
expect(names).to.deep.equal([...new Set(names)].sort());
|
|
98
|
+
expect(refs.every(({ namespace }) => namespace === 'default')).to.equal(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -21,6 +21,35 @@ const renderLog = (message, metadata) => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
describe('central logger redaction', () => {
|
|
24
|
+
it('filters a credential a service reports next to its name, with no delimiter between them', () => {
|
|
25
|
+
// Regression: `Invalid api_key 4778…` reached the terminal in full. Every rule matched on a
|
|
26
|
+
// delimiter — `=`, `:`, a flag — and an error message carries none, so the pair went through.
|
|
27
|
+
expect(redactSensitiveText('Invalid api_key 477826957851424')).to.equal('Invalid api_key [REDACTED]');
|
|
28
|
+
expect(redactSensitiveText('Invalid api_secret 8kQvN2mLp0XyZ_abc')).to.equal('Invalid api_secret [REDACTED]');
|
|
29
|
+
expect(redactSensitiveText('CLOUDINARY_API_KEY 477826957851424')).to.equal('CLOUDINARY_API_KEY [REDACTED]');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('leaves prose and identifiers that merely follow a sensitive word', () => {
|
|
33
|
+
// The name of a thing is not the thing: redacting the next word of every sentence would cost
|
|
34
|
+
// more legibility than it buys protection, so the value has to be shaped like a credential.
|
|
35
|
+
for (const kept of [
|
|
36
|
+
'token verification failed',
|
|
37
|
+
'password changed successfully',
|
|
38
|
+
'authorization required for this route',
|
|
39
|
+
'api_secret mismatch',
|
|
40
|
+
'Target pod: dd-cyberia-production-blue-5b7c67bb8f-kwhz7 | Pod status: Running',
|
|
41
|
+
])
|
|
42
|
+
expect(redactSensitiveText(kept), kept).to.equal(kept);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('reaches a credential that a non-sensitive word precedes', () => {
|
|
46
|
+
// The scan looks ahead rather than consuming: matching `Invalid api_key` as a pair of its own
|
|
47
|
+
// would advance past the real one behind it.
|
|
48
|
+
const rendered = renderLog('cloudinary rejected: Invalid api_key 477826957851424');
|
|
49
|
+
expect(rendered).to.include('[REDACTED]');
|
|
50
|
+
expect(rendered).to.not.include('477826957851424');
|
|
51
|
+
});
|
|
52
|
+
|
|
24
53
|
it('filters common secret forms from free-form messages', () => {
|
|
25
54
|
const rawToken = `ghp_${'A'.repeat(30)}`;
|
|
26
55
|
const input = [
|
|
@@ -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
|
-
|
|
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: {
|
|
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: {
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
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: {},
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -307,7 +307,9 @@ describe('release pipeline commands', () => {
|
|
|
307
307
|
commands = [];
|
|
308
308
|
const { triggerCmd } = await UnderpostRelease.API.ci(deployId, 'fixture message');
|
|
309
309
|
expect(triggerCmd, deployId).to.include('/engine-cyberia');
|
|
310
|
-
|
|
310
|
+
// `--coverage`: the published source must carry the reports the deploy declares, or
|
|
311
|
+
// every pod started from it serves the unavailable page.
|
|
312
|
+
expect(commands, deployId).to.include('node bin/build dd-cyberia --coverage');
|
|
311
313
|
}
|
|
312
314
|
});
|
|
313
315
|
|
|
@@ -37,6 +37,18 @@ describe('start receives every flag its command registers', () => {
|
|
|
37
37
|
expect(buildContract).to.include('{ force: Underpost.state.isInsideContainer() }');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
it('withdraws the ephemeral engine-private clone once a production container is serving', () => {
|
|
41
|
+
// Regression: the domain-split withdraw passed `force: false` to every domain, and only
|
|
42
|
+
// `host clean --force` removes `engine-private` — the clone outlived the boot in the pod.
|
|
43
|
+
const runStart = source.indexOf('async run(');
|
|
44
|
+
const withdraw = source.slice(
|
|
45
|
+
source.indexOf('for (const domain of', runStart),
|
|
46
|
+
source.indexOf('setTimeout', runStart),
|
|
47
|
+
);
|
|
48
|
+
expect(withdraw).to.include('[Underpost.secret, Underpost.host, Underpost.app]');
|
|
49
|
+
expect(withdraw).to.include('force: domain === Underpost.host');
|
|
50
|
+
});
|
|
51
|
+
|
|
40
52
|
it('runs the container in one phase, with no re-entry through the global CLI', () => {
|
|
41
53
|
// The pod's own bootstrap replaces the checkout and links the CLI before `start` is
|
|
42
54
|
// invoked, so re-execing `underpost start` here only pulled and installed a second time.
|
|
@@ -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
|
+
});
|