@underpostnet/cyberia 3.3.73 → 3.3.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +5 -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/publish.cyberia.ci.yml +44 -0
- package/AGENTS.md +61 -12
- package/CHANGELOG.md +126 -41
- package/CLI-HELP.md +19 -12
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +2 -2
- package/README.md +28 -28
- package/bin/build.js +37 -17
- package/bin/cyberia.js +1247 -1373
- package/bin/index.js +1247 -1373
- package/compose.env +17 -5
- package/conf.js +7 -3
- 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 +27 -18
- 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/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 +7 -0
- package/src/cli/deploy.js +4 -4
- 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/repository.js +28 -0
- package/src/cli/run.js +71 -197
- package/src/cli/test.js +4 -0
- package/src/cli/wireguard.js +32 -31
- package/src/client/components/core/Docs.js +77 -100
- package/src/client/components/core/Modal.js +56 -1
- 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/TranslateCyberiaPortal.js +0 -4
- 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-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/ops/logger.js +70 -35
- package/src/server/runtime/conf.js +4 -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/unit/client-build-docs.test.js +97 -70
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-resolution.test.js +17 -0
- package/test/unit/coverage-artifact.test.js +120 -29
- package/test/unit/cyberia/instance-backup.test.js +183 -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-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/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/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
|
@@ -85,7 +85,7 @@ const stream = [
|
|
|
85
85
|
let workDir;
|
|
86
86
|
let streamPath;
|
|
87
87
|
|
|
88
|
-
const runQuiet = ({ tty = false, plain = false, ci = false, exitCode = 0 } = {}) => {
|
|
88
|
+
const runQuiet = ({ tty = false, plain = false, ci = false, debug = false, exitCode = 0 } = {}) => {
|
|
89
89
|
const command = `exit ${exitCode}`;
|
|
90
90
|
const script = [
|
|
91
91
|
'status=0',
|
|
@@ -106,6 +106,8 @@ const runQuiet = ({ tty = false, plain = false, ci = false, exitCode = 0 } = {})
|
|
|
106
106
|
delete env.GITHUB_ACTIONS;
|
|
107
107
|
delete env.RUN_QUIET_CI;
|
|
108
108
|
if (ci) env.RUN_QUIET_CI = 'github';
|
|
109
|
+
delete env.RUN_QUIET_DEBUG;
|
|
110
|
+
if (debug) env.RUN_QUIET_DEBUG = '1';
|
|
109
111
|
|
|
110
112
|
const [file, args] = tty ? ['script', ['-qec', `bash ${scriptPath}`, '/dev/null']] : ['bash', [scriptPath]];
|
|
111
113
|
return execFileSync(file, args, { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
@@ -128,7 +130,7 @@ describe('deploy failure propagation (run_quiet exit status)', () => {
|
|
|
128
130
|
* Runs the helper against an arbitrary stream and reports the exit status rather than
|
|
129
131
|
* throwing, so a failing step is an assertion rather than a caught exception.
|
|
130
132
|
*/
|
|
131
|
-
const runWith = (lines, exitCode = 0) => {
|
|
133
|
+
const runWith = (lines, exitCode = 0, mode = { RUN_QUIET_PLAIN: '1' }) => {
|
|
132
134
|
const logPath = path.join(failDir, 'stream.log');
|
|
133
135
|
fs.writeFileSync(logPath, `${lines.join('\n')}\n`);
|
|
134
136
|
const scriptPath = path.join(failDir, 'run.sh');
|
|
@@ -141,9 +143,12 @@ describe('deploy failure propagation (run_quiet exit status)', () => {
|
|
|
141
143
|
'echo REACHED_NEXT_STEP',
|
|
142
144
|
].join('\n'),
|
|
143
145
|
);
|
|
144
|
-
const env = { ...process.env
|
|
146
|
+
const env = { ...process.env };
|
|
145
147
|
delete env.GITHUB_ACTIONS;
|
|
146
148
|
delete env.RUN_QUIET_CI;
|
|
149
|
+
delete env.RUN_QUIET_PLAIN;
|
|
150
|
+
delete env.RUN_QUIET_DEBUG;
|
|
151
|
+
Object.assign(env, mode);
|
|
147
152
|
try {
|
|
148
153
|
const stdout = execFileSync('bash', [scriptPath], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
149
154
|
return { status: 0, stdout, stderr: '' };
|
|
@@ -158,6 +163,41 @@ describe('deploy failure propagation (run_quiet exit status)', () => {
|
|
|
158
163
|
|
|
159
164
|
afterEach(() => fs.removeSync(failDir));
|
|
160
165
|
|
|
166
|
+
// The failure report names two files and prints nothing out of them, so what the step captured
|
|
167
|
+
// is read back from the file rather than from the transcript.
|
|
168
|
+
const errorLog = (result) => {
|
|
169
|
+
const named = /error trace:\s+(\S+)/.exec(result.stderr);
|
|
170
|
+
expect(named, 'the failure report names an error trace').to.not.equal(null);
|
|
171
|
+
return fs.readFileSync(named[1], 'utf8');
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
it('latches a fatal runtime state in every rendering, including the debug stream', () => {
|
|
175
|
+
// The latch is parsed, not rendered. Turning the display off must not turn it off with it, or
|
|
176
|
+
// an audit run reports success on a deployment that never came up.
|
|
177
|
+
const podFatal = 'Target pod: dd-test-blue | Pod status: Running | Runtime status: error';
|
|
178
|
+
|
|
179
|
+
for (const [name, mode] of [
|
|
180
|
+
['plain', { RUN_QUIET_PLAIN: '1' }],
|
|
181
|
+
['ci', { RUN_QUIET_CI: 'github' }],
|
|
182
|
+
['debug', { RUN_QUIET_DEBUG: '1' }],
|
|
183
|
+
]) {
|
|
184
|
+
const result = runWith([podFatal], 0, mode);
|
|
185
|
+
expect(result.status, name).to.equal(1);
|
|
186
|
+
expect(result.stderr, name).to.include('reported a fatal runtime state but exited 0');
|
|
187
|
+
expect(result.stdout, name).to.not.include('REACHED_NEXT_STEP');
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('latches a fatal runtime state reported only by a monitor event, in the debug stream too', () => {
|
|
192
|
+
// The event arrives as a multi-line JSON block, so the state machine that reads it has to run
|
|
193
|
+
// in a mode that renders none of it.
|
|
194
|
+
const emitted = emit('08:11:33', 'runtime', 'runtime_error', 'error').split('\n');
|
|
195
|
+
const result = runWith(emitted, 0, { RUN_QUIET_DEBUG: '1' });
|
|
196
|
+
|
|
197
|
+
expect(result.status).to.equal(1);
|
|
198
|
+
expect(result.stdout, 'debug shows the event it latched on').to.include('"status": "error"');
|
|
199
|
+
});
|
|
200
|
+
|
|
161
201
|
it('propagates a non-zero command status so `set -e` stops the deployment', () => {
|
|
162
202
|
const result = runWith([podLine(POD_A, 'Running', 'running-deployment')], 7);
|
|
163
203
|
expect(result.status).to.equal(7);
|
|
@@ -184,7 +224,10 @@ describe('deploy failure propagation (run_quiet exit status)', () => {
|
|
|
184
224
|
expect(result.status).to.equal(1);
|
|
185
225
|
expect(result.stdout).to.not.include('REACHED_NEXT_STEP');
|
|
186
226
|
expect(result.stderr).to.include('fatal runtime state but exited 0');
|
|
187
|
-
|
|
227
|
+
// The pod name is captured output: a CI transcript is readable by anyone with the job log,
|
|
228
|
+
// so the latch detail belongs in the error log and nowhere else.
|
|
229
|
+
expect(result.stderr).to.not.include(POD_A);
|
|
230
|
+
expect(errorLog(result)).to.include(`pod ${POD_A}: runtime status=error`);
|
|
188
231
|
});
|
|
189
232
|
|
|
190
233
|
it('fails on a fatal runtime status carried by a monitor event rather than a pod row', () => {
|
|
@@ -196,7 +239,8 @@ describe('deploy failure propagation (run_quiet exit status)', () => {
|
|
|
196
239
|
0,
|
|
197
240
|
);
|
|
198
241
|
expect(result.status).to.equal(1);
|
|
199
|
-
expect(result.stderr).to.include('
|
|
242
|
+
expect(result.stderr).to.not.include('status=error');
|
|
243
|
+
expect(errorLog(result)).to.include('runtime event: status=error');
|
|
200
244
|
});
|
|
201
245
|
|
|
202
246
|
it('never hands off to the traffic switch on a pod reporting the fatal state', () => {
|
|
@@ -318,6 +362,33 @@ describe('deploy log table (run_quiet filter)', () => {
|
|
|
318
362
|
expect(out).to.not.include(CHATTER_LINE);
|
|
319
363
|
});
|
|
320
364
|
|
|
365
|
+
it('streams every line verbatim in debug mode, rendering nothing of its own', () => {
|
|
366
|
+
const out = runQuiet({ debug: true });
|
|
367
|
+
|
|
368
|
+
// What the other modes consume is exactly what an operator opened this session to read: the
|
|
369
|
+
// monitor's own JSON, and the report lines the table replaces.
|
|
370
|
+
expect(out).to.include('deploy-monitor');
|
|
371
|
+
expect(out).to.include('"phase": "kubernetes"');
|
|
372
|
+
expect(out).to.include('Target pod: ');
|
|
373
|
+
expect(out).to.include(READY_LINE);
|
|
374
|
+
expect(out).to.include(CHATTER_LINE);
|
|
375
|
+
|
|
376
|
+
// Nothing is rendered on top of the stream: no table, no CI section, no hand-off.
|
|
377
|
+
expect(out).to.not.include('POD NAME');
|
|
378
|
+
expect(out).to.not.include('refresh #');
|
|
379
|
+
expect(out).to.not.include('::group::');
|
|
380
|
+
expect(out).to.not.include('▶ Switch traffic');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('keeps debug mode a passthrough even where a CI log would fold frames', () => {
|
|
384
|
+
// Debug is the operator asking for the stream, so it wins over the rendering the destination
|
|
385
|
+
// would otherwise get: a collapsed section hides exactly what the run is being watched for.
|
|
386
|
+
const out = runQuiet({ debug: true, ci: true });
|
|
387
|
+
|
|
388
|
+
expect(out).to.not.include('::group::');
|
|
389
|
+
expect(out).to.include('deploy-monitor');
|
|
390
|
+
});
|
|
391
|
+
|
|
321
392
|
it('falls back to plain rows on a terminal when RUN_QUIET_PLAIN is set', function () {
|
|
322
393
|
if (!hasScript) return this.skip();
|
|
323
394
|
const out = runQuiet({ tty: true, plain: true });
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module downloader-integrity.test
|
|
5
|
+
* @description Covers the contract a split-artifact transfer depends on: a download either
|
|
6
|
+
* produces the whole file or produces none. A response that ends early still ends, so the write
|
|
7
|
+
* stream finishes and, without a length check, a short file is reported as a completed download
|
|
8
|
+
* and only surfaces later as a corrupt archive assembled from it.
|
|
9
|
+
*
|
|
10
|
+
* Uses 'chai' for assertions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { expect } from 'chai';
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import http from 'http';
|
|
16
|
+
import Downloader from '../../src/server/storage/downloader.js';
|
|
17
|
+
|
|
18
|
+
const BODY = Buffer.from('a'.repeat(64 * 1024));
|
|
19
|
+
|
|
20
|
+
/** Serves one response shape and resolves the base URL it listens on. */
|
|
21
|
+
const serverFactory = (handler) =>
|
|
22
|
+
new Promise((resolve) => {
|
|
23
|
+
const server = http.createServer(handler);
|
|
24
|
+
server.listen(0, '127.0.0.1', () => resolve({ server, url: `http://127.0.0.1:${server.address().port}/part` }));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('download integrity', () => {
|
|
28
|
+
let fixturePath;
|
|
29
|
+
let running;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
fixturePath = fs.mkdtempSync('/tmp/engine-downloader-');
|
|
33
|
+
running = [];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
for (const server of running) await new Promise((resolve) => server.close(resolve));
|
|
38
|
+
fs.removeSync(fixturePath);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const serve = async (handler) => {
|
|
42
|
+
const { server, url } = await serverFactory(handler);
|
|
43
|
+
running.push(server);
|
|
44
|
+
return url;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
it('saves a complete response and reports the path', async () => {
|
|
48
|
+
const url = await serve((request, response) => {
|
|
49
|
+
response.writeHead(200, { 'content-length': String(BODY.length) });
|
|
50
|
+
response.end(BODY);
|
|
51
|
+
});
|
|
52
|
+
const target = `${fixturePath}/whole.bin`;
|
|
53
|
+
|
|
54
|
+
expect(await Downloader.downloadFile(url, target)).to.equal(target);
|
|
55
|
+
expect(fs.statSync(target).size).to.equal(BODY.length);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('rejects a response that stops short of its declared length, and leaves no partial file', async () => {
|
|
59
|
+
const url = await serve((request, response) => {
|
|
60
|
+
response.writeHead(200, { 'content-length': String(BODY.length) });
|
|
61
|
+
response.write(BODY.subarray(0, 1024));
|
|
62
|
+
// Ending early is what a cut-short transfer looks like to the client: the stream still ends.
|
|
63
|
+
response.destroy();
|
|
64
|
+
});
|
|
65
|
+
const target = `${fixturePath}/short.bin`;
|
|
66
|
+
|
|
67
|
+
let thrown;
|
|
68
|
+
await Downloader.downloadFile(url, target).catch((error) => (thrown = error));
|
|
69
|
+
|
|
70
|
+
expect(thrown, 'a truncated transfer must not resolve').to.not.equal(undefined);
|
|
71
|
+
expect(fs.existsSync(target), 'a partial file is indistinguishable from a whole one').to.equal(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts a response that declares no length, since nothing can be checked against', async () => {
|
|
75
|
+
const url = await serve((request, response) => {
|
|
76
|
+
response.writeHead(200, { 'transfer-encoding': 'chunked' });
|
|
77
|
+
response.end(BODY);
|
|
78
|
+
});
|
|
79
|
+
const target = `${fixturePath}/chunked.bin`;
|
|
80
|
+
|
|
81
|
+
expect(await Downloader.downloadFile(url, target)).to.equal(target);
|
|
82
|
+
expect(fs.statSync(target).size).to.equal(BODY.length);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects a request the server refuses, and leaves no file', async () => {
|
|
86
|
+
const url = await serve((request, response) => {
|
|
87
|
+
response.writeHead(404);
|
|
88
|
+
response.end();
|
|
89
|
+
});
|
|
90
|
+
const target = `${fixturePath}/missing.bin`;
|
|
91
|
+
|
|
92
|
+
let thrown;
|
|
93
|
+
await Downloader.downloadFile(url, target).catch((error) => (thrown = error));
|
|
94
|
+
|
|
95
|
+
expect(thrown).to.not.equal(undefined);
|
|
96
|
+
expect(fs.existsSync(target)).to.equal(false);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module file-reference-registry.test
|
|
5
|
+
* @description Covers `src/api/file/file.ref.js`: the mapping of a model to the fields that hold
|
|
6
|
+
* a File `_id`, and the two deletions built on it. Every writer that removes a document reads
|
|
7
|
+
* this registry to take the blobs it owned with it, so what is asserted here is what keeps the
|
|
8
|
+
* File collection free of unreachable documents.
|
|
9
|
+
*
|
|
10
|
+
* Uses 'chai' for assertions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { expect } from 'chai';
|
|
14
|
+
import {
|
|
15
|
+
deleteOwnedFiles,
|
|
16
|
+
deleteUnreferencedFiles,
|
|
17
|
+
documentFileIds,
|
|
18
|
+
fileRefApis,
|
|
19
|
+
fileRefFields,
|
|
20
|
+
} from '../../src/api/file/file.ref.js';
|
|
21
|
+
|
|
22
|
+
/** Minimal collection: the helpers only run `find(filter, projection).lean()` and `deleteMany`. */
|
|
23
|
+
const collection = (docs) => ({
|
|
24
|
+
docs,
|
|
25
|
+
find(filter) {
|
|
26
|
+
const clauses = filter.$or ?? [filter];
|
|
27
|
+
const matched = docs.filter((doc) =>
|
|
28
|
+
clauses.some((clause) =>
|
|
29
|
+
Object.entries(clause).every(([field, condition]) => {
|
|
30
|
+
const value = doc[field];
|
|
31
|
+
if (value === undefined || value === null) return false;
|
|
32
|
+
return condition.$in.map(String).includes(String(value));
|
|
33
|
+
}),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
return { lean: async () => matched };
|
|
37
|
+
},
|
|
38
|
+
async deleteMany({ _id: { $in } }) {
|
|
39
|
+
const removing = new Set($in.map(String));
|
|
40
|
+
const before = docs.length;
|
|
41
|
+
this.docs = docs = docs.filter((doc) => !removing.has(String(doc._id)));
|
|
42
|
+
return { deletedCount: before - docs.length };
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('the File reference registry', () => {
|
|
47
|
+
it('answers the fields of a registered api and nothing for an unregistered one', () => {
|
|
48
|
+
expect(fileRefFields('atlas-sprite-sheet')).to.deep.equal(['fileId', 'minifyFileId', 'idlePreviewFileId']);
|
|
49
|
+
expect(fileRefFields('cyberia-map')).to.deep.equal(['thumbnail', 'preview']);
|
|
50
|
+
expect(fileRefFields('object-layer')).to.deep.equal([]);
|
|
51
|
+
expect(fileRefApis()).to.include('cyberia-audio');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('reads ids out of documents by field, skipping the ones that hold none', () => {
|
|
55
|
+
const docs = [{ fileId: 'a', minifyFileId: 'b' }, { fileId: 'a' }, { minifyFileId: null }];
|
|
56
|
+
expect(documentFileIds(docs, ['fileId', 'minifyFileId'])).to.deep.equal(['a', 'b']);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('deleting the files a collection owned', () => {
|
|
61
|
+
it('removes the ids no surviving document holds', async () => {
|
|
62
|
+
const files = collection([{ _id: 'atlas-1' }, { _id: 'minify-1' }, { _id: 'unrelated' }]);
|
|
63
|
+
const atlases = collection([]);
|
|
64
|
+
|
|
65
|
+
const removed = await deleteOwnedFiles({
|
|
66
|
+
File: files,
|
|
67
|
+
Owner: atlases,
|
|
68
|
+
fields: ['fileId', 'minifyFileId'],
|
|
69
|
+
ids: ['atlas-1', 'minify-1'],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(removed).to.equal(2);
|
|
73
|
+
expect(files.docs.map((doc) => doc._id)).to.deep.equal(['unrelated']);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('keeps an id a survivor still points at', async () => {
|
|
77
|
+
// Renders are content addressed, so a duplicate document of the same item resolves to the
|
|
78
|
+
// same File id. Dropping the duplicate must not blind the document that remains.
|
|
79
|
+
const files = collection([{ _id: 'shared' }, { _id: 'gone' }]);
|
|
80
|
+
const atlases = collection([{ _id: 'survivor', fileId: 'shared', minifyFileId: null }]);
|
|
81
|
+
|
|
82
|
+
const removed = await deleteOwnedFiles({
|
|
83
|
+
File: files,
|
|
84
|
+
Owner: atlases,
|
|
85
|
+
fields: ['fileId', 'minifyFileId'],
|
|
86
|
+
ids: ['shared', 'gone'],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(removed).to.equal(1);
|
|
90
|
+
expect(files.docs.map((doc) => doc._id)).to.deep.equal(['shared']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('deletes nothing when handed no candidates', async () => {
|
|
94
|
+
const files = collection([{ _id: 'kept' }]);
|
|
95
|
+
expect(await deleteOwnedFiles({ File: files, Owner: collection([]), fields: ['fileId'], ids: [] })).to.equal(0);
|
|
96
|
+
expect(files.docs).to.have.lengthOf(1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('sweeping files no registered model references', () => {
|
|
101
|
+
const loadedModels = (overrides = {}) => {
|
|
102
|
+
const models = Object.fromEntries(fileRefApis().map((api) => [api, collection([])]));
|
|
103
|
+
return { ...models, ...overrides };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
it('refuses to delete while a registered api has no model to read', async () => {
|
|
107
|
+
// An unreadable collection cannot be shown to hold no reference, and a sweep that assumed
|
|
108
|
+
// otherwise would delete a live blob belonging to another domain.
|
|
109
|
+
const files = collection([{ _id: 'candidate' }]);
|
|
110
|
+
const models = loadedModels();
|
|
111
|
+
delete models.document;
|
|
112
|
+
|
|
113
|
+
const result = await deleteUnreferencedFiles({
|
|
114
|
+
File: files,
|
|
115
|
+
ids: ['candidate'],
|
|
116
|
+
getModel: (api) => {
|
|
117
|
+
if (!models[api]) throw new Error(`Model not loaded: ${api}`);
|
|
118
|
+
return models[api];
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(result.skipped).to.equal('document');
|
|
123
|
+
expect(result.removed).to.equal(0);
|
|
124
|
+
expect(files.docs).to.have.lengthOf(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('deletes only the candidates every model agrees it never references', async () => {
|
|
128
|
+
const files = collection([{ _id: 'orphan' }, { _id: 'held' }]);
|
|
129
|
+
const models = loadedModels({ 'cyberia-map': collection([{ _id: 'map', thumbnail: 'held', preview: null }]) });
|
|
130
|
+
|
|
131
|
+
const result = await deleteUnreferencedFiles({
|
|
132
|
+
File: files,
|
|
133
|
+
ids: ['orphan', 'held'],
|
|
134
|
+
getModel: (api) => models[api],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(result.skipped).to.equal('');
|
|
138
|
+
expect(result.removed).to.equal(1);
|
|
139
|
+
expect(files.docs.map((doc) => doc._id)).to.deep.equal(['held']);
|
|
140
|
+
});
|
|
141
|
+
});
|