@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
|
@@ -712,6 +712,23 @@ describe('private conf sync', () => {
|
|
|
712
712
|
expect(copied).to.include('./engine-private/catalog/items.json -> ../engine-core-private/catalog/items.json');
|
|
713
713
|
});
|
|
714
714
|
|
|
715
|
+
// Regression: an instance backup that dropped an item left the item's stale atlas in the
|
|
716
|
+
// mirror, and the pod restored it over the current one from another instance.
|
|
717
|
+
it('replaces each payload entry in the checkout instead of merging into it', () => {
|
|
718
|
+
const calls = [];
|
|
719
|
+
checkout({ present: true });
|
|
720
|
+
vi.spyOn(fs, 'readdirSync').mockReturnValue([]);
|
|
721
|
+
vi.spyOn(fs, 'removeSync').mockImplementation((target) => calls.push(`remove ${target}`));
|
|
722
|
+
vi.spyOn(fs, 'copySync').mockImplementation((src, dest) => calls.push(`copy ${src} -> ${dest}`));
|
|
723
|
+
withEnv({ GITHUB_USERNAME: 'fixture-org' }, () => syncPrivateConf('dd-core', ['cyberia-instances/TEST']));
|
|
724
|
+
const remove = calls.indexOf('remove ../engine-core-private/cyberia-instances/TEST');
|
|
725
|
+
const copy = calls.indexOf(
|
|
726
|
+
'copy ./engine-private/cyberia-instances/TEST -> ../engine-core-private/cyberia-instances/TEST',
|
|
727
|
+
);
|
|
728
|
+
expect(remove).to.be.greaterThan(-1);
|
|
729
|
+
expect(copy).to.be.greaterThan(remove);
|
|
730
|
+
});
|
|
731
|
+
|
|
715
732
|
it('mirrors the cron conf the checkout declares', () => {
|
|
716
733
|
const copied = [];
|
|
717
734
|
checkout({ present: true, cronId: 'dd-cron' });
|
|
@@ -13,95 +13,186 @@ import { expect } from 'chai';
|
|
|
13
13
|
import fs from 'fs-extra';
|
|
14
14
|
import {
|
|
15
15
|
COVERAGE_BUNDLE_DIRECTORY,
|
|
16
|
-
|
|
16
|
+
bundleCoverageReports,
|
|
17
17
|
coverageReportCandidates,
|
|
18
|
+
coverageReportCommand,
|
|
19
|
+
coverageReportsFactory,
|
|
18
20
|
coverageUnavailablePage,
|
|
21
|
+
deployCoverageReports,
|
|
19
22
|
resolveCoverageReportPath,
|
|
20
23
|
} from '../../src/server/build/coverage.js';
|
|
21
24
|
import { EXECUTION_PROFILE_ENV_KEY } from '../../src/server/build/execution.js';
|
|
22
25
|
import { isTestRuntime, runtimeStatusWritable } from '../../src/server/runtime/runtime-status.js';
|
|
23
26
|
|
|
27
|
+
describe('coverage report declaration', () => {
|
|
28
|
+
it('reads each declared report with its label defaulting to the id', () => {
|
|
29
|
+
expect(coverageReportsFactory({ coverage: [{ id: 'engine', suite: 'unit' }] })).to.deep.equal([
|
|
30
|
+
{ id: 'engine', label: 'engine', suite: 'unit', path: undefined },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('declares nothing for a deploy without coverage', () => {
|
|
35
|
+
expect(coverageReportsFactory({})).to.deep.equal([]);
|
|
36
|
+
expect(coverageReportsFactory(undefined)).to.deep.equal([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects an entry without exactly one source, a non-slug id, or a repeated id', () => {
|
|
40
|
+
expect(() => coverageReportsFactory({ coverage: [{ id: 'x' }] })).to.throw("exactly one of 'suite' or 'path'");
|
|
41
|
+
expect(() => coverageReportsFactory({ coverage: [{ id: 'x', suite: 'unit', path: './' }] })).to.throw(
|
|
42
|
+
'exactly one',
|
|
43
|
+
);
|
|
44
|
+
expect(() => coverageReportsFactory({ coverage: [{ id: 'Bad Id', suite: 'unit' }] })).to.throw('slug');
|
|
45
|
+
expect(() =>
|
|
46
|
+
coverageReportsFactory({
|
|
47
|
+
coverage: [
|
|
48
|
+
{ id: 'x', suite: 'unit' },
|
|
49
|
+
{ id: 'x', suite: 'app' },
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
).to.throw('declared twice');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("collects a deploy's reports across routes once each", () => {
|
|
56
|
+
const route = { docs: { coverage: [{ id: 'engine', suite: 'unit' }] } };
|
|
57
|
+
const confServer = { 'a.test': { '/': route, '/store': route }, 'b.test': { '/': {} } };
|
|
58
|
+
expect(deployCoverageReports(confServer).map(({ id }) => id)).to.deep.equal(['engine']);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('refuses one id declared with two sources', () => {
|
|
62
|
+
const confServer = {
|
|
63
|
+
'a.test': {
|
|
64
|
+
'/': { docs: { coverage: [{ id: 'engine', suite: 'unit' }] } },
|
|
65
|
+
'/store': { docs: { coverage: [{ id: 'engine', suite: 'app' }] } },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
expect(() => deployCoverageReports(confServer)).to.throw('two different sources');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('names the command that produces each kind of report', () => {
|
|
72
|
+
expect(coverageReportCommand({ suite: 'unit,infra,app' })).to.equal('node bin test unit,infra,app');
|
|
73
|
+
expect(coverageReportCommand({ path: './hardhat/' })).to.equal('npm run coverage --prefix ./hardhat');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
24
77
|
describe('coverage report resolution', () => {
|
|
25
78
|
let fixturePath;
|
|
79
|
+
let cwd;
|
|
26
80
|
|
|
81
|
+
// The bundled candidate is cwd-relative, and a published deploy artifact (the tree a
|
|
82
|
+
// downstream CI runs this suite in) carries real reports under it; resolve against the
|
|
83
|
+
// fixture, never the tree.
|
|
27
84
|
beforeEach(() => {
|
|
28
85
|
fixturePath = fs.mkdtempSync('/tmp/engine-coverage-artifact-');
|
|
86
|
+
cwd = process.cwd();
|
|
87
|
+
process.chdir(fixturePath);
|
|
29
88
|
});
|
|
30
89
|
|
|
31
90
|
afterEach(() => {
|
|
91
|
+
process.chdir(cwd);
|
|
32
92
|
fs.removeSync(fixturePath);
|
|
33
93
|
});
|
|
34
94
|
|
|
35
|
-
it(
|
|
36
|
-
expect(coverageReportCandidates('
|
|
95
|
+
it("looks for a suite's own run output first and the bundled artifact last", () => {
|
|
96
|
+
expect(coverageReportCandidates({ id: 'engine', suite: 'unit,infra,app' })).to.deep.equal([
|
|
97
|
+
'coverage/unit-infra-app',
|
|
98
|
+
`${COVERAGE_BUNDLE_DIRECTORY}/engine`,
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("looks through an external tree's report layouts before the bundled artifact", () => {
|
|
103
|
+
expect(coverageReportCandidates({ id: 'hardhat', path: '/src/' })).to.deep.equal([
|
|
37
104
|
'/src/coverage/html',
|
|
38
105
|
'/src/coverage/lcov-report',
|
|
39
106
|
'/src/coverage',
|
|
40
|
-
|
|
107
|
+
`${COVERAGE_BUNDLE_DIRECTORY}/hardhat`,
|
|
41
108
|
]);
|
|
42
109
|
});
|
|
43
110
|
|
|
44
|
-
it('has nowhere to look without a source tree', () => {
|
|
45
|
-
expect(coverageReportCandidates(undefined)).to.deep.equal([]);
|
|
46
|
-
expect(resolveCoverageReportPath(undefined)).to.equal(undefined);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
111
|
it('ignores a coverage directory that carries no HTML index', () => {
|
|
50
112
|
fs.outputFileSync(`${fixturePath}/coverage/lcov.info`, 'TN:\n');
|
|
51
|
-
expect(resolveCoverageReportPath(fixturePath)).to.equal(undefined);
|
|
113
|
+
expect(resolveCoverageReportPath({ id: 'x', path: fixturePath })).to.equal(undefined);
|
|
52
114
|
});
|
|
53
115
|
|
|
54
|
-
it('resolves the
|
|
55
|
-
fs.outputFileSync(`${fixturePath}
|
|
56
|
-
|
|
116
|
+
it('resolves the report of the selection the deploy names, not the last run made', () => {
|
|
117
|
+
fs.outputFileSync(`${fixturePath}/coverage/unit-infra-app-cyberia/index.html`, '<!doctype html>');
|
|
118
|
+
fs.outputFileSync(`${fixturePath}/coverage/cyberia/index.html`, '<!doctype html>');
|
|
119
|
+
expect(resolveCoverageReportPath({ id: 'x', suite: 'cyberia' })).to.equal('coverage/cyberia');
|
|
120
|
+
expect(resolveCoverageReportPath({ id: 'x', suite: 'unit,infra,app' })).to.equal(undefined);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('falls back to the report a deploy artifact already carries', () => {
|
|
124
|
+
fs.outputFileSync(`${fixturePath}/${COVERAGE_BUNDLE_DIRECTORY}/hardhat/index.html`, '<!doctype html>');
|
|
125
|
+
expect(resolveCoverageReportPath({ id: 'hardhat', path: `${fixturePath}/hardhat` })).to.equal(
|
|
126
|
+
`${COVERAGE_BUNDLE_DIRECTORY}/hardhat`,
|
|
127
|
+
);
|
|
57
128
|
});
|
|
58
129
|
});
|
|
59
130
|
|
|
60
131
|
describe('coverage bundling into a deploy artifact', () => {
|
|
61
132
|
let fixturePath;
|
|
133
|
+
let cwd;
|
|
62
134
|
|
|
135
|
+
// The bundled candidate is cwd-relative, and a published deploy artifact (the tree a
|
|
136
|
+
// downstream CI runs this suite in) carries real reports under it; resolve against the
|
|
137
|
+
// fixture, never the tree.
|
|
63
138
|
beforeEach(() => {
|
|
64
139
|
fixturePath = fs.mkdtempSync('/tmp/engine-coverage-artifact-');
|
|
140
|
+
cwd = process.cwd();
|
|
141
|
+
process.chdir(fixturePath);
|
|
65
142
|
});
|
|
66
143
|
|
|
67
144
|
afterEach(() => {
|
|
145
|
+
process.chdir(cwd);
|
|
68
146
|
fs.removeSync(fixturePath);
|
|
69
147
|
});
|
|
70
148
|
|
|
71
|
-
it('carries
|
|
72
|
-
fs.outputFileSync(`${fixturePath}/
|
|
73
|
-
fs.outputFileSync(`${fixturePath}/
|
|
149
|
+
it('carries each report into the artifact under its own id', () => {
|
|
150
|
+
fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/index.html`, '<!doctype html><title>report</title>');
|
|
151
|
+
fs.outputFileSync(`${fixturePath}/hardhat/coverage/html/base.css`, 'body {}');
|
|
74
152
|
|
|
75
|
-
const result =
|
|
153
|
+
const [result] = bundleCoverageReports(
|
|
154
|
+
[{ id: 'hardhat', path: `${fixturePath}/hardhat` }],
|
|
155
|
+
`${fixturePath}/template`,
|
|
156
|
+
);
|
|
76
157
|
|
|
77
158
|
expect(result.bundled).to.equal(true);
|
|
78
|
-
expect(result.to).to.equal(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}`);
|
|
159
|
+
expect(result.to).to.equal(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/hardhat`);
|
|
79
160
|
expect(fs.readFileSync(`${result.to}/index.html`, 'utf8')).to.include('report');
|
|
80
161
|
expect(fs.existsSync(`${result.to}/base.css`)).to.equal(true);
|
|
81
162
|
});
|
|
82
163
|
|
|
83
164
|
it('replaces a previously bundled report rather than merging into it', () => {
|
|
84
|
-
fs.outputFileSync(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/stale.html`, 'stale');
|
|
85
|
-
fs.outputFileSync(`${fixturePath}/
|
|
165
|
+
fs.outputFileSync(`${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/hardhat/stale.html`, 'stale');
|
|
166
|
+
fs.outputFileSync(`${fixturePath}/hardhat/coverage/index.html`, '<!doctype html><title>fresh</title>');
|
|
86
167
|
|
|
87
|
-
const { to } =
|
|
168
|
+
const [{ to }] = bundleCoverageReports(
|
|
169
|
+
[{ id: 'hardhat', path: `${fixturePath}/hardhat` }],
|
|
170
|
+
`${fixturePath}/template`,
|
|
171
|
+
);
|
|
88
172
|
|
|
89
173
|
expect(fs.existsSync(`${to}/stale.html`)).to.equal(false);
|
|
90
174
|
expect(fs.readFileSync(`${to}/index.html`, 'utf8')).to.include('fresh');
|
|
91
175
|
});
|
|
92
176
|
|
|
93
|
-
it('reports
|
|
94
|
-
const result =
|
|
95
|
-
|
|
96
|
-
|
|
177
|
+
it('reports what was not bundled without creating its directory', () => {
|
|
178
|
+
const [result] = bundleCoverageReports(
|
|
179
|
+
[{ id: 'hardhat', path: `${fixturePath}/hardhat` }],
|
|
180
|
+
`${fixturePath}/template`,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(result).to.deep.equal({
|
|
184
|
+
id: 'hardhat',
|
|
185
|
+
bundled: false,
|
|
186
|
+
to: `${fixturePath}/template/${COVERAGE_BUNDLE_DIRECTORY}/hardhat`,
|
|
187
|
+
});
|
|
97
188
|
expect(fs.existsSync(result.to)).to.equal(false);
|
|
98
189
|
});
|
|
99
190
|
|
|
100
|
-
it('renders a static unavailable page
|
|
101
|
-
const page = coverageUnavailablePage();
|
|
191
|
+
it('renders a static unavailable page naming the report and the run that produces it', () => {
|
|
192
|
+
const page = coverageUnavailablePage({ id: 'engine', suite: 'unit,infra,app' });
|
|
102
193
|
expect(page).to.include('Coverage report unavailable');
|
|
103
|
-
expect(page).to.include('
|
|
104
|
-
expect(page).to.include(COVERAGE_BUNDLE_DIRECTORY);
|
|
194
|
+
expect(page).to.include('node bin test unit,infra,app');
|
|
195
|
+
expect(page).to.include(`${COVERAGE_BUNDLE_DIRECTORY}/engine`);
|
|
105
196
|
expect(page).not.to.include('<script');
|
|
106
197
|
});
|
|
107
198
|
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// The restore path only routes documents; the collections are in-memory stand-ins, IPFS is
|
|
7
|
+
// unreachable, and the static frame writer is a no-op, so what is asserted is the routing.
|
|
8
|
+
const models = {};
|
|
9
|
+
vi.mock('../../../src/db/DataBaseProvider.js', () => ({
|
|
10
|
+
DataBaseProviderService: { getModel: (name) => models[name] },
|
|
11
|
+
}));
|
|
12
|
+
vi.mock('../../../src/api/ipfs/ipfs.service.js', () => ({ createPinRecord: async () => ({}) }));
|
|
13
|
+
vi.mock('../../../src/projects/cyberia/ipfs-client.js', () => ({ IpfsClient: { addToIpfs: async () => null } }));
|
|
14
|
+
vi.mock('../../../src/projects/cyberia/object-layer.js', () => ({
|
|
15
|
+
ObjectLayerEngine: { writeStaticFrameAssets: async () => [], computeAndSaveFinalSha256: async () => ({}) },
|
|
16
|
+
}));
|
|
17
|
+
// The generator pulls in jimp, a cyberia product dependency the engine runner does not install;
|
|
18
|
+
// every store method that would reach it is spied out below, so only the constant is needed.
|
|
19
|
+
vi.mock('../../../src/projects/cyberia/atlas-sprite-sheet-generator.js', () => ({
|
|
20
|
+
AtlasSpriteSheetGenerator: {},
|
|
21
|
+
DEFAULT_ATLAS_UPSCALE_FACTOR: 20,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const { AtlasSpriteSheetStore } = await import('../../../src/projects/cyberia/atlas-sprite-sheet-store.js');
|
|
25
|
+
const { atlasBackupFileKey, readObjectLayerBackup, restoreObjectLayerBackup } =
|
|
26
|
+
await import('../../../src/projects/cyberia/instance-backup.js');
|
|
27
|
+
|
|
28
|
+
/** A collection that remembers what was created and hands the last upsert back as the live document. */
|
|
29
|
+
const collection = () => {
|
|
30
|
+
const created = [];
|
|
31
|
+
let live = null;
|
|
32
|
+
const query = (value) => Object.assign(Promise.resolve(value), { populate: () => Promise.resolve(value) });
|
|
33
|
+
return {
|
|
34
|
+
created,
|
|
35
|
+
deleteOne: async () => ({ deletedCount: 0 }),
|
|
36
|
+
create: async (doc) => (created.push(doc), doc),
|
|
37
|
+
findByItemId: () => query(live),
|
|
38
|
+
upsertByItemId: async (doc) => {
|
|
39
|
+
live = { ...doc, markModified() {}, save: async () => live };
|
|
40
|
+
return live;
|
|
41
|
+
},
|
|
42
|
+
get live() {
|
|
43
|
+
return live;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let backupDir;
|
|
49
|
+
const write = (rel, value) => writeFile(join(backupDir, rel), Buffer.isBuffer(value) ? value : JSON.stringify(value));
|
|
50
|
+
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
backupDir = await mkdtemp(join(tmpdir(), 'cyberia-backup-'));
|
|
53
|
+
for (const dir of ['object-layers', 'render-frames', 'atlas-sprite-sheets', 'files', 'ipfs/content']) {
|
|
54
|
+
await mkdir(join(backupDir, dir), { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
await write('object-layers/hatchet.json', {
|
|
57
|
+
_id: 'ol1',
|
|
58
|
+
cid: 'cid-data',
|
|
59
|
+
objectLayerRenderFramesId: 'rf1',
|
|
60
|
+
atlasSpriteSheetId: 'at1',
|
|
61
|
+
data: { item: { id: 'hatchet', type: 'weapon' }, render: { cid: 'cid-png', metadataCid: 'cid-meta' } },
|
|
62
|
+
});
|
|
63
|
+
await write('render-frames/hatchet.json', { _id: 'rf1', frames: {}, colors: [], frame_duration: 100 });
|
|
64
|
+
await write('atlas-sprite-sheets/hatchet.json', {
|
|
65
|
+
_id: 'at1',
|
|
66
|
+
fileId: 'f-full',
|
|
67
|
+
minifyFileId: 'f-min',
|
|
68
|
+
idlePreviewFileId: 'f-idle',
|
|
69
|
+
cid: 'cid-png',
|
|
70
|
+
});
|
|
71
|
+
// Files are matched on _id, never on name: one is misnamed on purpose, one is a bystander.
|
|
72
|
+
await write('files/whatever.json', {
|
|
73
|
+
_id: 'f-full',
|
|
74
|
+
name: 'hatchet-atlas.png',
|
|
75
|
+
data: { $base64: Buffer.from('PNG').toString('base64') },
|
|
76
|
+
});
|
|
77
|
+
await write('files/atlas-minify-hatchet.json', {
|
|
78
|
+
_id: 'f-min',
|
|
79
|
+
name: 'hatchet-minify.png',
|
|
80
|
+
data: { type: 'Buffer', data: [1, 2, 3] },
|
|
81
|
+
});
|
|
82
|
+
await write('files/atlas-idle-hatchet.json', { _id: 'f-idle', name: 'hatchet-idle.png', data: { $base64: 'AA==' } });
|
|
83
|
+
await write('files/atlas-sword.json', { _id: 'f-other', name: 'sword-atlas.png', data: { $base64: 'AA==' } });
|
|
84
|
+
await write('ipfs/content/cid-data.bin', Buffer.from('data'));
|
|
85
|
+
await write('ipfs/content/cid-png.bin', Buffer.from('png'));
|
|
86
|
+
// cid-meta has no payload on purpose.
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterAll(() => rm(backupDir, { recursive: true, force: true }));
|
|
90
|
+
|
|
91
|
+
describe('reading one object layer out of an instance backup', () => {
|
|
92
|
+
it('gathers the object layer and every document it references', () => {
|
|
93
|
+
const backup = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
|
|
94
|
+
expect(backup.objectLayer.data.item.id).toBe('hatchet');
|
|
95
|
+
expect(backup.renderFrames._id).toBe('rf1');
|
|
96
|
+
expect(backup.atlas._id).toBe('at1');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('resolves the atlas render Files by _id and leaves other Files alone', () => {
|
|
100
|
+
const { files } = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
|
|
101
|
+
expect(files.map((f) => f._id).sort()).toEqual(['f-full', 'f-idle', 'f-min']);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('names each exported render after its atlas field', () => {
|
|
105
|
+
expect(atlasBackupFileKey('fileId', 'hatchet')).toBe('atlas-hatchet');
|
|
106
|
+
expect(atlasBackupFileKey('minifyFileId', 'hatchet')).toBe('atlas-minify-hatchet');
|
|
107
|
+
expect(atlasBackupFileKey('idlePreviewFileId', 'hatchet')).toBe('atlas-idle-hatchet');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('decodes both File byte encodings the export can write', () => {
|
|
111
|
+
const { files } = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
|
|
112
|
+
const byId = Object.fromEntries(files.map((f) => [f._id, f.data]));
|
|
113
|
+
expect(Buffer.isBuffer(byId['f-full']) && byId['f-full'].toString()).toBe('PNG');
|
|
114
|
+
expect(Buffer.isBuffer(byId['f-min']) && [...byId['f-min']]).toEqual([1, 2, 3]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('collects only the payloads the backup actually carries', () => {
|
|
118
|
+
const { payloads } = readObjectLayerBackup({ backupDir, itemId: 'hatchet' });
|
|
119
|
+
expect([...payloads.keys()].sort()).toEqual(['cid-data', 'cid-png']);
|
|
120
|
+
expect(payloads.get('cid-png').toString()).toBe('png');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('names the backup and item when the item is missing', () => {
|
|
124
|
+
expect(() => readObjectLayerBackup({ backupDir, itemId: 'ghost' })).toThrow(/no object layer 'ghost'/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('tolerates an item with no atlas or render frames yet', async () => {
|
|
128
|
+
await write('object-layers/bare.json', { _id: 'ol2', data: { item: { id: 'bare', type: 'floor' } } });
|
|
129
|
+
const backup = readObjectLayerBackup({ backupDir, itemId: 'bare' });
|
|
130
|
+
expect(backup.atlas).toBeNull();
|
|
131
|
+
expect(backup.renderFrames).toBeNull();
|
|
132
|
+
expect(backup.files).toEqual([]);
|
|
133
|
+
expect(backup.payloads.size).toBe(0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('restoring one object layer from an instance backup', () => {
|
|
138
|
+
let persist;
|
|
139
|
+
let idle;
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
for (const name of ['ObjectLayer', 'ObjectLayerRenderFrames', 'AtlasSpriteSheet', 'File'])
|
|
143
|
+
models[name] = collection();
|
|
144
|
+
persist = vi.spyOn(AtlasSpriteSheetStore, 'persist').mockResolvedValue({
|
|
145
|
+
atlasDoc: { _id: 'at-rebuilt' },
|
|
146
|
+
atlasCid: 'cid-rebuilt-png',
|
|
147
|
+
atlasMetadataCid: 'cid-rebuilt-meta',
|
|
148
|
+
});
|
|
149
|
+
idle = vi.spyOn(AtlasSpriteSheetStore, 'syncIdlePreview').mockResolvedValue({ status: 'unchanged' });
|
|
150
|
+
vi.spyOn(AtlasSpriteSheetStore, 'pruneOrphanRenders').mockResolvedValue(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
afterAll(() => vi.restoreAllMocks());
|
|
154
|
+
|
|
155
|
+
it('keeps the atlas the backup carries when it holds its minified render', async () => {
|
|
156
|
+
await restoreObjectLayerBackup({ backupDir, itemId: 'hatchet', options: {} });
|
|
157
|
+
expect(models.AtlasSpriteSheet.created[0].minifyFileId).toBe('f-min');
|
|
158
|
+
expect(persist).not.toHaveBeenCalled();
|
|
159
|
+
expect(idle).toHaveBeenCalledWith({ itemKey: 'hatchet', options: {} });
|
|
160
|
+
expect(models.ObjectLayer.live.data.render).toEqual({ cid: 'cid-png', metadataCid: 'cid-meta' });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Regression: a backup written before the minified render existed restored an atlas the
|
|
164
|
+
// blob route could not serve, and the client runtime got a 500 for every entity wearing it.
|
|
165
|
+
it('rebuilds the atlas a backup restores without its minified render and relinks the item', async () => {
|
|
166
|
+
await write('object-layers/relic.json', {
|
|
167
|
+
_id: 'ol3',
|
|
168
|
+
objectLayerRenderFramesId: 'rf3',
|
|
169
|
+
atlasSpriteSheetId: 'at3',
|
|
170
|
+
data: { item: { id: 'relic', type: 'skin' }, render: { cid: 'cid-old-png', metadataCid: 'cid-old-meta' } },
|
|
171
|
+
});
|
|
172
|
+
const frames = { _id: 'rf3', frames: { down_idle: [] }, colors: [], frame_duration: 100 };
|
|
173
|
+
await write('render-frames/relic.json', frames);
|
|
174
|
+
await write('atlas-sprite-sheets/relic.json', { _id: 'at3', fileId: 'f-relic', minifyFileId: null });
|
|
175
|
+
|
|
176
|
+
await restoreObjectLayerBackup({ backupDir, itemId: 'relic', options: {} });
|
|
177
|
+
|
|
178
|
+
expect(persist).toHaveBeenCalledWith({ itemKey: 'relic', objectLayerRenderFrames: frames, options: {} });
|
|
179
|
+
const { live } = models.ObjectLayer;
|
|
180
|
+
expect(live.atlasSpriteSheetId).toBe('at-rebuilt');
|
|
181
|
+
expect(live.data.render).toEqual({ cid: 'cid-rebuilt-png', metadataCid: 'cid-rebuilt-meta' });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { fetchInstanceObjectLayerItemIds } from '../../../src/projects/cyberia/instance-data.js';
|
|
3
|
+
|
|
4
|
+
const lean = (value) => ({ lean: async () => value, populate: () => lean(value) });
|
|
5
|
+
|
|
6
|
+
const buildModels = ({ instance, maps = [], storedItemIds = [] }) => ({
|
|
7
|
+
CyberiaInstance: { findOne: () => lean(instance) },
|
|
8
|
+
CyberiaInstanceConf: { findOne: () => lean({ instanceCode: instance?.code, entityDefaults: [] }) },
|
|
9
|
+
CyberiaMap: { find: () => lean(maps) },
|
|
10
|
+
CyberiaEntityTypeDefault: { find: () => lean([]) },
|
|
11
|
+
CyberiaAction: { find: () => lean([]) },
|
|
12
|
+
CyberiaQuest: { find: () => lean([]) },
|
|
13
|
+
CyberiaSkill: { find: () => lean([]) },
|
|
14
|
+
ObjectLayer: {
|
|
15
|
+
find: (filter) => {
|
|
16
|
+
const wanted = new Set(filter['data.item.id'].$in);
|
|
17
|
+
return lean(storedItemIds.filter((id) => wanted.has(id)).map((id) => ({ _id: id, data: { item: { id } } })));
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('instance object layer item ids', () => {
|
|
23
|
+
it('collects the item ids the instance maps place, limited to stored documents', async () => {
|
|
24
|
+
const models = buildModels({
|
|
25
|
+
instance: { _id: 'i1', code: 'FOREST', cyberiaMapCodes: ['forest-1'] },
|
|
26
|
+
maps: [
|
|
27
|
+
{
|
|
28
|
+
code: 'forest-1',
|
|
29
|
+
entities: [{ objectLayerItemIds: ['anon', 'hatchet'] }, { objectLayerItemIds: ['hatchet', '$runtime'] }],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
storedItemIds: ['anon', 'hatchet', 'unrelated'],
|
|
33
|
+
});
|
|
34
|
+
const itemIds = await fetchInstanceObjectLayerItemIds(models, 'FOREST');
|
|
35
|
+
expect(itemIds).toContain('anon');
|
|
36
|
+
expect(itemIds).toContain('hatchet');
|
|
37
|
+
expect(itemIds).not.toContain('unrelated');
|
|
38
|
+
expect(itemIds).not.toContain('$runtime');
|
|
39
|
+
expect(new Set(itemIds).size).toBe(itemIds.length);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('fails on an unknown instance code instead of returning the fallback world', async () => {
|
|
43
|
+
const models = buildModels({ instance: null });
|
|
44
|
+
await expect(fetchInstanceObjectLayerItemIds(models, 'GHOST')).rejects.toThrow('"GHOST" not found');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
// Named import: js-yaml's ESM build exports no default.
|
|
4
|
+
import { load } from 'js-yaml';
|
|
5
|
+
|
|
6
|
+
describe('the cyberia publish workflow', () => {
|
|
7
|
+
it('rewrites the published manifest after the install and before every publish', () => {
|
|
8
|
+
// The rewrite is the publish step's alone: a checkout that installed the published shape
|
|
9
|
+
// would run the engine source against a second engine in node_modules.
|
|
10
|
+
const workflow = load(fs.readFileSync('./.github/workflows/publish.cyberia.ci.yml', 'utf8'));
|
|
11
|
+
|
|
12
|
+
for (const [job, { steps }] of Object.entries(workflow.jobs)) {
|
|
13
|
+
const commands = steps.map(({ run }) => `${run ?? ''}`);
|
|
14
|
+
const restore = commands.findIndex((command) => command.includes('publishedProductPackageJson'));
|
|
15
|
+
const install = commands.findIndex((command) => command.trim().startsWith('npm ci'));
|
|
16
|
+
const publish = commands.findIndex((command) => command.includes('npm publish'));
|
|
17
|
+
|
|
18
|
+
expect(restore, `${job}: restores the published manifest`).to.be.greaterThan(install);
|
|
19
|
+
expect(restore, `${job}: restores it before publishing`).to.be.lessThan(publish);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { balanceStats, resolveStatBounds, statPolicyActive } from '../../../src/projects/cyberia/stat-balance.js';
|
|
3
|
+
import {
|
|
4
|
+
STAT_TYPE_BOUNDS,
|
|
5
|
+
STAT_TYPES,
|
|
6
|
+
statBoundsForType,
|
|
7
|
+
} from '../../../src/client/components/cyberia/SharedDefaultsCyberia.js';
|
|
8
|
+
|
|
9
|
+
const wild = { effect: 60, resistance: -40, agility: 3, range: 25, intelligence: 9, utility: -2 };
|
|
10
|
+
const lcg = (seed) => () => (seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0) / 4294967296;
|
|
11
|
+
|
|
12
|
+
describe('stat balancing policy', () => {
|
|
13
|
+
it('is inert without normalize or random', () => {
|
|
14
|
+
expect(statPolicyActive({})).toBe(false);
|
|
15
|
+
expect(balanceStats({ stats: wild, itemType: 'weapon' })).toEqual(wild);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('clamps into the semantic bounds of the item type', () => {
|
|
19
|
+
expect(balanceStats({ stats: wild, itemType: 'weapon', policy: { normalize: true } })).toEqual({
|
|
20
|
+
effect: 20,
|
|
21
|
+
resistance: -10,
|
|
22
|
+
agility: 3,
|
|
23
|
+
range: 10,
|
|
24
|
+
intelligence: 5,
|
|
25
|
+
utility: 0,
|
|
26
|
+
});
|
|
27
|
+
expect(balanceStats({ stats: wild, itemType: 'skin', policy: { normalize: true } })).toEqual({
|
|
28
|
+
effect: 1,
|
|
29
|
+
resistance: 0,
|
|
30
|
+
agility: 3,
|
|
31
|
+
range: 0,
|
|
32
|
+
intelligence: 2,
|
|
33
|
+
utility: 0,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('keeps the contract bounds for a type without semantic bounds', () => {
|
|
38
|
+
expect(statBoundsForType('unknown')).toEqual(Object.fromEntries(STAT_TYPES.map((key) => [key, [-100, 100]])));
|
|
39
|
+
expect(balanceStats({ stats: wild, itemType: 'unknown', policy: { normalize: true } })).toEqual(wild);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('is idempotent and deterministic', () => {
|
|
43
|
+
const once = balanceStats({ stats: wild, itemType: 'weapon', policy: { normalize: true } });
|
|
44
|
+
expect(balanceStats({ stats: once, itemType: 'weapon', policy: { normalize: true } })).toEqual(once);
|
|
45
|
+
const policy = { normalize: true, random: true, min: 0, max: 8 };
|
|
46
|
+
const first = balanceStats({ stats: wild, itemType: 'weapon', policy, random: lcg(7) });
|
|
47
|
+
expect(balanceStats({ stats: wild, itemType: 'weapon', policy, random: lcg(7) })).toEqual(first);
|
|
48
|
+
for (const key of STAT_TYPES) {
|
|
49
|
+
const [lo, hi] = resolveStatBounds('weapon', policy)[key];
|
|
50
|
+
expect(first[key]).toBeGreaterThanOrEqual(lo);
|
|
51
|
+
expect(first[key]).toBeLessThanOrEqual(hi);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('lets min and max narrow the semantic range and pins a request outside it', () => {
|
|
56
|
+
expect(resolveStatBounds('weapon', { normalize: true, min: 12, max: 30 }).effect).toEqual([12, 20]);
|
|
57
|
+
expect(resolveStatBounds('weapon', { normalize: true, min: 50 }).effect).toEqual([20, 20]);
|
|
58
|
+
expect(resolveStatBounds('weapon', { normalize: true, max: -50 }).resistance).toEqual([-10, -10]);
|
|
59
|
+
expect(resolveStatBounds('weapon', { min: -20, max: 20 }).effect).toEqual([-20, 20]);
|
|
60
|
+
expect(() => resolveStatBounds('weapon', { min: 5, max: 1 })).toThrow('minimum');
|
|
61
|
+
expect(() => resolveStatBounds('weapon', { min: -101 })).toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('declares every semantic bound inside the contract and in order', () => {
|
|
65
|
+
for (const bounds of Object.values(STAT_TYPE_BOUNDS)) {
|
|
66
|
+
for (const [lo, hi] of Object.values(bounds)) expect(lo <= hi && lo >= -100 && hi <= 100).toBe(true);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|