@bongos/core 1.17.1 → 1.17.3
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/.bongos-core.json +521 -281
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +9 -6
- package/docs/api/openapi.json +65 -26
- package/docs/api-reference.md +3 -2
- package/docs/architecture.md +2 -2
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +18 -17
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +6 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/private-npm-distribution.md +17 -0
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +55 -0
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/modules/discord/module.json +2 -1
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/hall-ui/public/builders.js +137 -620
- package/modules/hall-ui/public/goal-inbox.js +232 -0
- package/modules/hall-ui/public/goals-page.js +4 -1
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -5
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +7 -5
- package/modules/hall-ui/public/style.css +168 -0
- package/modules/hall-ui/public/work.css +194 -0
- package/modules/hall-ui/public/work.html +46 -374
- package/modules/hall-ui/public/work.js +137 -102
- package/modules/lifecycle/criterion-suggest.js +80 -0
- package/modules/lifecycle/db.js +42 -5
- package/modules/lifecycle/routes/goals.js +26 -0
- package/modules/lifecycle/routes/tasks.js +21 -0
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/secret-scrub.js +10 -0
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +30 -9
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/init.js +59 -3
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/onboard.js +4 -0
- package/scripts/gds/package-core.js +18 -3
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +116 -9
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +1 -0
- package/scripts/hall-preview/fixtures/achievements.json +76 -0
- package/scripts/hall-preview/fixtures/analytics__builder.json +251 -0
- package/scripts/hall-preview/fixtures/audit-log.json +1 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/builders__active-claims.json +1 -0
- package/scripts/hall-preview/fixtures/builders__directory.json +81 -0
- package/scripts/hall-preview/fixtures/builders__roster.json +1 -0
- package/scripts/hall-preview/fixtures/goals.json +502 -0
- package/scripts/hall-preview/fixtures/goals__25.json +96 -0
- package/scripts/hall-preview/fixtures/goals__26.json +67 -0
- package/scripts/hall-preview/fixtures/goals__3.json +103 -0
- package/scripts/hall-preview/fixtures/goals__35.json +49 -0
- package/scripts/hall-preview/fixtures/goals__conflicts.json +154 -0
- package/scripts/hall-preview/fixtures/goals__detail.json +96 -0
- package/scripts/hall-preview/fixtures/goals__inbox.json +5 -0
- package/scripts/hall-preview/fixtures/goals__requests.json +3 -0
- package/scripts/hall-preview/fixtures/grades__by-builder.json +1 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/instance.json +124 -0
- package/scripts/hall-preview/fixtures/leaderboard.json +184 -0
- package/scripts/hall-preview/fixtures/me.json +476 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/override-requests.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +1 -0
- package/scripts/hall-preview/fixtures/provisioning__instances.json +1 -0
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +516 -0
- package/scripts/hall-preview/fixtures/public__bounty-table.json +37 -0
- package/scripts/hall-preview/fixtures/public__cost-summary.json +742 -0
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +41 -0
- package/scripts/hall-preview/fixtures/public__grades.json +50 -0
- package/scripts/hall-preview/fixtures/public__progress.json +124 -0
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +426 -0
- package/scripts/hall-preview/fixtures/public__repo-health.json +160 -0
- package/scripts/hall-preview/fixtures/public__repo-info.json +5 -0
- package/scripts/hall-preview/fixtures/security__docs.json +1 -0
- package/scripts/hall-preview/fixtures/security__reports.json +1 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +1 -0
- package/scripts/hall-preview/fixtures/tasks.json +2420 -0
- package/scripts/hall-preview/fixtures/tasks__claimable.json +2555 -0
- package/scripts/hall-preview/fixtures/tasks__detail.json +111 -0
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +330 -0
- package/scripts/hall-preview/fixtures/versions.json +89 -0
- package/scripts/hall-preview/refresh-fixtures.js +77 -0
- package/scripts/hall-preview/server.js +178 -0
- package/scripts/migrate.sh +29 -4
- package/src/bongos/auth.js +14 -1
- package/src/bongos/optimizer.js +37 -2
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/module-api.js +12 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +20 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +25 -2
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +47 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/onboard.mjs +13 -0
- package/tests/package_core.mjs +8 -0
- package/tests/provisioning_github_app_routes.mjs +15 -0
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +131 -0
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
package/server.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
const http = require('http');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const express = require('express');
|
|
4
|
-
const { Server, matchMaker } = require('colyseus');
|
|
5
|
-
const { WebSocketTransport } = require('@colyseus/ws-transport');
|
|
6
|
-
const { isApiShapedPath, apiNotFoundBody } = require('./src/bongos/api-path-404');
|
|
7
|
-
const uptimePoller = require('./src/bongos/uptime-poller');
|
|
8
|
-
const moduleLoader = require('./src/module-loader/loader');
|
|
9
|
-
const { mountInternalSurfaces, isStatusHost } = require('./src/bongos/serve-internal');
|
|
10
|
-
const { isModuleEnabled } = require('./src/modules');
|
|
11
|
-
const seams = require('./src/module-seams');
|
|
12
|
-
|
|
13
|
-
const PORT = Number(process.env.PORT) || 3000;
|
|
14
|
-
const HOST = process.env.HOST || '127.0.0.1';
|
|
15
|
-
|
|
16
|
-
const app = express();
|
|
17
|
-
app.disable('x-powered-by');
|
|
18
|
-
// Behind Cloudflare → Caddy → Node. Trust the immediate proxy so
|
|
19
|
-
// req.secure / x-forwarded-proto reflect reality for OAuth callbacks.
|
|
20
|
-
app.set('trust proxy', 1);
|
|
21
|
-
|
|
22
|
-
// All non-game surfaces — /healthz, /version, /api/gds, the Dev Box download
|
|
23
|
-
// redirects, and the status + builders hosts — are served by the shared internal
|
|
24
|
-
// surface module (ADR 0062 R51, task 1190). This also calls mountModuleRoutes,
|
|
25
|
-
// which runs each enabled module's route factory — including the game module's,
|
|
26
|
-
// which registers the game.staticRoot / game.registerRooms / game.precreateWorldRoom
|
|
27
|
-
// seam ports before the code below resolves them (ADR 0083 / BV1.R49).
|
|
28
|
-
// Mounted before the game tail below so /builders/* + the status host
|
|
29
|
-
// short-circuit ahead of the game static middleware.
|
|
30
|
-
mountInternalSurfaces(app);
|
|
31
|
-
|
|
32
|
-
app.use((req, res, next) => {
|
|
33
|
-
// From here down: only the game host. Skip if request was for the status
|
|
34
|
-
// host (it should already be handled, but defense in depth).
|
|
35
|
-
if (isStatusHost(req)) return res.status(404).type('text/plain').send('not found');
|
|
36
|
-
next();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
app.use(
|
|
40
|
-
'/vendor/phaser.min.js',
|
|
41
|
-
express.static(path.join(__dirname, 'node_modules/phaser/dist/phaser.min.js'), {
|
|
42
|
-
maxAge: '1d',
|
|
43
|
-
})
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
app.use(
|
|
47
|
-
'/vendor/colyseus.js',
|
|
48
|
-
express.static(path.join(__dirname, 'node_modules/colyseus.js/dist/colyseus.js'), {
|
|
49
|
-
maxAge: '1d',
|
|
50
|
-
})
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// Game static files — served from the game module's public/ dir (modules/game/public/).
|
|
54
|
-
// The path is provided by the game module via the game.staticRoot seam port, registered
|
|
55
|
-
// by modules/game/routes/game.js when the loader runs mountModuleRoutes above.
|
|
56
|
-
// If the game module is disabled (vanilla Cloud Bongos boot) these blocks are simply
|
|
57
|
-
// skipped — no game assets served, no 404.html, no client JS. The platform API still
|
|
58
|
-
// works via mountInternalSurfaces above.
|
|
59
|
-
const _gameStaticRoot = seams.resolveOptional('game.staticRoot');
|
|
60
|
-
if (_gameStaticRoot) {
|
|
61
|
-
const gamePublic = _gameStaticRoot();
|
|
62
|
-
|
|
63
|
-
// Tile-asset cache strategy (task #55 + #160 + #176).
|
|
64
|
-
//
|
|
65
|
-
// Three buckets:
|
|
66
|
-
// 1. Content-hashed atlas (atlas-<sha8>.png + atlas-<sha8>.json if any)
|
|
67
|
-
// Filename changes with content → safe to cache forever. Marked
|
|
68
|
-
// immutable so browsers / CF skip revalidation entirely.
|
|
69
|
-
// 2. Stable-URL files paired with bucket 1 (atlas-index.json, manifest.json,
|
|
70
|
-
// atlas.json). URLs don't change across deploys but contents do, so
|
|
71
|
-
// they get a short TTL — a deploy is visible within ~1 minute. atlas.json
|
|
72
|
-
// lives here because tilePalette.js still hardcodes its URL; until the
|
|
73
|
-
// build hashes it (idea_inbox follow-up), the short TTL is what prevents
|
|
74
|
-
// the post-deploy "new PNG + stale frame coords" mismatch window.
|
|
75
|
-
// 3. Legacy unhashed per-tile PNGs + everything else under /assets/tiles
|
|
76
|
-
// Short TTL (5 min) until they migrate to content-hashed names. The
|
|
77
|
-
// old 1-day TTL is what caused the 2026-05-10 stale-atlas incident.
|
|
78
|
-
app.use(
|
|
79
|
-
'/assets/tiles',
|
|
80
|
-
express.static(path.join(gamePublic, 'assets/tiles'), {
|
|
81
|
-
setHeaders: (res, filePath) => {
|
|
82
|
-
const name = path.basename(filePath);
|
|
83
|
-
if (/^atlas-[a-f0-9]{8,}\.(png|json)$/.test(name)) {
|
|
84
|
-
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
85
|
-
} else if (
|
|
86
|
-
name === 'atlas-index.json' ||
|
|
87
|
-
name === 'manifest.json' ||
|
|
88
|
-
name === 'atlas.json'
|
|
89
|
-
) {
|
|
90
|
-
res.setHeader('Cache-Control', 'public, max-age=60');
|
|
91
|
-
} else {
|
|
92
|
-
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
})
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// HTML + game JS — short TTL so deploys are visible quickly. .js files in
|
|
99
|
-
// game/ are not content-hashed today (V1 shortcut), so a long TTL would
|
|
100
|
-
// compound the stale-art class of bugs into stale-code bugs.
|
|
101
|
-
app.use(
|
|
102
|
-
express.static(gamePublic, {
|
|
103
|
-
extensions: ['html'],
|
|
104
|
-
setHeaders: (res, filePath) => {
|
|
105
|
-
if (/\.(html|js|json)$/.test(filePath)) {
|
|
106
|
-
res.setHeader('Cache-Control', 'public, max-age=60');
|
|
107
|
-
} else {
|
|
108
|
-
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
})
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Themed 404 for the game host. Anything that didn't resolve to an API route,
|
|
116
|
-
// the status/builders hosts, or a static file lands here.
|
|
117
|
-
// The status host has its own plain-text 404 above; we don't poach it.
|
|
118
|
-
// API-SHAPED paths get a JSON 404 with a corrective hint — never the HTML island
|
|
119
|
-
// page — so a caller that missed the /api/gds mount (curl /tasks, /gds/*, an
|
|
120
|
-
// /api/* typo) gets a self-describing error instead of HTML that JSON.parse-faults
|
|
121
|
-
// on the client (V4.R14 / #952). The match is broader than the old `/api/` test:
|
|
122
|
-
// it also catches the bare GDS resource roots a human hits without the prefix.
|
|
123
|
-
app.use((req, res) => {
|
|
124
|
-
if (isStatusHost(req)) return res.status(404).type('text/plain').send('not found');
|
|
125
|
-
res.status(404);
|
|
126
|
-
if (isApiShapedPath(req.path)) {
|
|
127
|
-
return res.json(apiNotFoundBody(req.path));
|
|
128
|
-
}
|
|
129
|
-
if (req.method === 'GET' && req.accepts('html')) {
|
|
130
|
-
res.setHeader('Cache-Control', 'public, max-age=60');
|
|
131
|
-
const gameStaticRoot = seams.resolveOptional('game.staticRoot');
|
|
132
|
-
if (gameStaticRoot) {
|
|
133
|
-
return res.sendFile(path.join(gameStaticRoot(), '404.html'));
|
|
134
|
-
}
|
|
135
|
-
return res.type('text/plain').send('404 — not found');
|
|
136
|
-
}
|
|
137
|
-
return res.type('text/plain').send('404 — you have washed ashore on the wrong island');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const httpServer = http.createServer(app);
|
|
141
|
-
|
|
142
|
-
const gameServer = new Server({
|
|
143
|
-
transport: new WebSocketTransport({ server: httpServer }),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// The PRODUCT/game rooms (world + queue) — registered via the game module's seam
|
|
147
|
-
// port (game.registerRooms), wired by modules/game/routes/game.js at loader mount
|
|
148
|
-
// time. Only called when the game module is enabled (OTB does; vanilla skips).
|
|
149
|
-
// The game-less platform entrypoint (src/platform-server.js) never constructs a
|
|
150
|
-
// Colyseus server at all. The builders' hall live channel is NOT a Colyseus room —
|
|
151
|
-
// it rides the SSE endpoint (src/bongos/routes/live.js, ADR 0069).
|
|
152
|
-
const _registerRooms = seams.resolveOptional('game.registerRooms');
|
|
153
|
-
if (_registerRooms) {
|
|
154
|
-
_registerRooms(gameServer);
|
|
155
|
-
} else {
|
|
156
|
-
console.log('[game] game module disabled — no Colyseus rooms registered');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
uptimePoller.start();
|
|
160
|
-
|
|
161
|
-
// task 1134 — the server-owned publish reconciler (drives 'confirmed' tasks whose
|
|
162
|
-
// branch has merged + deployed to 'shipped' on a timer, ADR 0055). CARVED into the
|
|
163
|
-
// lifecycle module at BV1.R86 (ADR 0093 §1): it now starts via the loader's
|
|
164
|
-
// startModulePollers (a declared poller — modules/lifecycle/pollers/publish-
|
|
165
|
-
// reconciler.js), in the listen callback below alongside the Discord bot + routine
|
|
166
|
-
// timers — so server.js no longer reaches into the lifecycle module to start it.
|
|
167
|
-
// No-op without a push credential (local dev/tests); kill-switch via
|
|
168
|
-
// PUBLISH_RECONCILER_DISABLED. Unref'd timers, best-effort — never takes the
|
|
169
|
-
// game/API down.
|
|
170
|
-
|
|
171
|
-
// task 1382 (ADR 0078 amendment) — server-internal routine timers (peer-vote-tally,
|
|
172
|
-
// weekly, in-process; no model, no external cron). CARVED into the autonomy module
|
|
173
|
-
// at BV1.R83 (ADR 0093 §1): it now starts via the loader's startModulePollers (a
|
|
174
|
-
// declared poller — modules/autonomy/pollers/routine-timers.js), in the listen
|
|
175
|
-
// callback below alongside the Discord bot poller. server.js is still the only
|
|
176
|
-
// startModulePollers caller (not the game-less platform entrypoint), so a second
|
|
177
|
-
// always-on instance can't double-run. Kill-switch ROUTINE_TIMERS_DISABLED stays.
|
|
178
|
-
|
|
179
|
-
httpServer.listen(PORT, HOST, async () => {
|
|
180
|
-
console.log(`Amazonprimea listening on ${HOST}:${PORT}`);
|
|
181
|
-
// Pre-create the singleton WorldRoom so clients can use
|
|
182
|
-
// `client.join('world')` (existing-only, no auto-create on overflow). The
|
|
183
|
-
// precreate function is provided by the game module via the seam port
|
|
184
|
-
// game.precreateWorldRoom; idempotent + failure-tolerant.
|
|
185
|
-
const _precreate = seams.resolveOptional('game.precreateWorldRoom');
|
|
186
|
-
if (_precreate) {
|
|
187
|
-
await _precreate(matchMaker);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Start each ENABLED module's web-process background pollers through the loader
|
|
191
|
-
// — module-agnostic: it requires modules/<key>/pollers/<name>.js and fires
|
|
192
|
-
// start(). The Discord bot (ADR 0033) is one such poller; a vanilla instance
|
|
193
|
-
// with discord off simply has no poller to start, and never reads a webhook/bot
|
|
194
|
-
// token. Non-blocking + failure-isolated by the loader (a poller problem must
|
|
195
|
-
// never take the game/API down) — the module-agnostic successor to the
|
|
196
|
-
// hardcoded discordBot.start() block (ADR 0083 / BV1.R47, task 1416).
|
|
197
|
-
moduleLoader.startModulePollers({}, { isEnabled: isModuleEnabled });
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Graceful shutdown — stop the in-process pollers, then stop each module poller
|
|
201
|
-
// (e.g. close the Discord gateway socket cleanly on a systemd restart / Ctrl-C so
|
|
202
|
-
// we don't leave a zombie session for Discord to reap). stopModulePollers awaits
|
|
203
|
-
// each stop and isolates errors, so a stuck poller never blocks the exit (BV1.R47).
|
|
204
|
-
for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
205
|
-
process.on(sig, () => {
|
|
206
|
-
// The publish reconciler + routine timers now stop via
|
|
207
|
-
// moduleLoader.stopModulePollers() — the lifecycle module's publish-reconciler
|
|
208
|
-
// poller (carved BV1.R86) + the autonomy module's routine-timers poller
|
|
209
|
-
// (carved BV1.R83 / ADR 0093 §1).
|
|
210
|
-
moduleLoader.stopModulePollers().finally(() => process.exit(0));
|
|
211
|
-
});
|
|
212
|
-
}
|
package/tests/art_key.mjs
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// tests/art_key.mjs
|
|
2
|
-
//
|
|
3
|
-
// Unit smoke for modules/art-pipeline/art-key.js (task 1281, moved BV1.R48) — the GDS-delivered SHARED Gemini
|
|
4
|
-
// key for newcomers. Covers the pure entitlement decision (xenos in; thetes+ out;
|
|
5
|
-
// unknown rank fails closed), the env/file key loader, and the describeArtKey
|
|
6
|
-
// payload shape. The route (routes/me.js) + the local sync (fetch-art-key.js) and
|
|
7
|
-
// the pipeline tier (gen_api.py) are exercised by the endpoint smoke + manual
|
|
8
|
-
// resolution check; here we cover the logic without pg or network.
|
|
9
|
-
//
|
|
10
|
-
// Hermetic: the "not configured" cases unset SHARED_ART_GEMINI_KEY and repoint
|
|
11
|
-
// HOME at an empty temp dir so the ~/.config/otb/gemini-shared.json fallback can't
|
|
12
|
-
// resolve to a real operator file on the test machine.
|
|
13
|
-
//
|
|
14
|
-
// Run: node tests/art_key.mjs
|
|
15
|
-
|
|
16
|
-
import { strict as assert } from 'node:assert';
|
|
17
|
-
import { createRequire } from 'node:module';
|
|
18
|
-
import { mkdtempSync } from 'node:fs';
|
|
19
|
-
import { tmpdir } from 'node:os';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const a = require('../modules/art-pipeline/art-key.js');
|
|
24
|
-
const db = require('../modules/lifecycle/db.js');
|
|
25
|
-
|
|
26
|
-
let passed = 0;
|
|
27
|
-
let failed = 0;
|
|
28
|
-
function t(name, fn) {
|
|
29
|
-
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
30
|
-
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Run a thunk with SHARED_ART_GEMINI_KEY + HOME forced to known values, restoring
|
|
34
|
-
// both afterward so cases don't leak into each other.
|
|
35
|
-
function withEnv({ key, emptyHome }, fn) {
|
|
36
|
-
const origKey = process.env.SHARED_ART_GEMINI_KEY;
|
|
37
|
-
const origHome = process.env.HOME;
|
|
38
|
-
if (key === null) delete process.env.SHARED_ART_GEMINI_KEY;
|
|
39
|
-
else if (key !== undefined) process.env.SHARED_ART_GEMINI_KEY = key;
|
|
40
|
-
if (emptyHome) process.env.HOME = mkdtempSync(join(tmpdir(), 'otb-artkey-'));
|
|
41
|
-
try { fn(); }
|
|
42
|
-
finally {
|
|
43
|
-
if (origKey === undefined) delete process.env.SHARED_ART_GEMINI_KEY;
|
|
44
|
-
else process.env.SHARED_ART_GEMINI_KEY = origKey;
|
|
45
|
-
process.env.HOME = origHome;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.log('entitlement boundary:');
|
|
50
|
-
|
|
51
|
-
t('SHARED_ART_KEY_MAX_TIER is the xenos tier (1)', () => {
|
|
52
|
-
assert.equal(a.SHARED_ART_KEY_MAX_TIER, db.REQUIRES_RANK_TIER.xenos);
|
|
53
|
-
assert.equal(a.SHARED_ART_KEY_MAX_TIER, 1);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
t('xenos is entitled (reason newcomer)', () => {
|
|
57
|
-
assert.deepEqual(a.entitlementFor('xenos'), { entitled: true, reason: 'newcomer' });
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
t('thetes / metic / archon are NOT entitled (reason rank_above_newcomer)', () => {
|
|
61
|
-
for (const rank of ['thetes', 'metic', 'archon']) {
|
|
62
|
-
assert.deepEqual(a.entitlementFor(rank), { entitled: false, reason: 'rank_above_newcomer' }, rank);
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
t('unknown / absent rank fails closed (reason unknown_rank)', () => {
|
|
67
|
-
for (const rank of [undefined, null, '', 'wizard', 'helot']) {
|
|
68
|
-
assert.deepEqual(a.entitlementFor(rank), { entitled: false, reason: 'unknown_rank' }, String(rank));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
console.log('\nkey loader (env wins; trimmed):');
|
|
73
|
-
|
|
74
|
-
t('loadSharedGeminiKey returns the env key, trimmed', () => {
|
|
75
|
-
withEnv({ key: ' AIza-test-123 ' }, () => {
|
|
76
|
-
assert.equal(a.loadSharedGeminiKey(), 'AIza-test-123');
|
|
77
|
-
assert.equal(a.sharedGeminiKeyConfigured(), true);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
t('not configured when env unset and no operator file (empty HOME)', () => {
|
|
82
|
-
withEnv({ key: null, emptyHome: true }, () => {
|
|
83
|
-
assert.equal(a.loadSharedGeminiKey(), null);
|
|
84
|
-
assert.equal(a.sharedGeminiKeyConfigured(), false);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
console.log('\ndescribeArtKey (the GET /me/art-key payload, sans secret):');
|
|
89
|
-
|
|
90
|
-
t('xenos + configured: entitled, configured, shared_key placeholder is null', () => {
|
|
91
|
-
withEnv({ key: 'AIza-shared' }, () => {
|
|
92
|
-
const d = a.describeArtKey({ rank: 'xenos' });
|
|
93
|
-
assert.equal(d.entitled, true);
|
|
94
|
-
assert.equal(d.configured, true);
|
|
95
|
-
assert.equal(d.reason, 'newcomer');
|
|
96
|
-
assert.equal(d.rank, 'xenos');
|
|
97
|
-
// The pure describe never embeds the secret — the route attaches it.
|
|
98
|
-
assert.equal(d.shared_key, null);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
t('thetes: not entitled even when a key is configured (the withdraw signal)', () => {
|
|
103
|
-
withEnv({ key: 'AIza-shared' }, () => {
|
|
104
|
-
const d = a.describeArtKey({ rank: 'thetes' });
|
|
105
|
-
assert.equal(d.entitled, false);
|
|
106
|
-
assert.equal(d.reason, 'rank_above_newcomer');
|
|
107
|
-
assert.equal(d.configured, true);
|
|
108
|
-
assert.equal(d.shared_key, null);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
t('xenos but unconfigured: entitled yet configured=false (sync no-ops)', () => {
|
|
113
|
-
withEnv({ key: null, emptyHome: true }, () => {
|
|
114
|
-
const d = a.describeArtKey({ rank: 'xenos' });
|
|
115
|
-
assert.equal(d.entitled, true);
|
|
116
|
-
assert.equal(d.configured, false);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
console.log(`\n${passed} passed, ${failed} failed`);
|
|
121
|
-
process.exit(failed === 0 ? 0 : 1);
|
package/tests/day_cycle.mjs
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
// tests/day_cycle.mjs
|
|
2
|
-
//
|
|
3
|
-
// The pure math behind the #639 time-of-day lighting. No Phaser, no DOM — the
|
|
4
|
-
// fx/lighting.js glue is a thin shell over these functions, so getting the
|
|
5
|
-
// math right here is most of the feature.
|
|
6
|
-
//
|
|
7
|
-
// Run: node tests/day_cycle.mjs
|
|
8
|
-
|
|
9
|
-
import { strict as assert } from 'node:assert';
|
|
10
|
-
import {
|
|
11
|
-
DEFAULT_PERIOD_MS,
|
|
12
|
-
KEYFRAMES,
|
|
13
|
-
SKY_PHASES,
|
|
14
|
-
wrap01,
|
|
15
|
-
phaseFromClock,
|
|
16
|
-
lerpColor,
|
|
17
|
-
bracket,
|
|
18
|
-
tintAt,
|
|
19
|
-
parseSkyOverride,
|
|
20
|
-
} from '../modules/game/public/game/fx/dayCycle.js';
|
|
21
|
-
|
|
22
|
-
let passed = 0;
|
|
23
|
-
function test(name, fn) {
|
|
24
|
-
try {
|
|
25
|
-
fn();
|
|
26
|
-
console.log(` ok ${name}`);
|
|
27
|
-
passed++;
|
|
28
|
-
} catch (e) {
|
|
29
|
-
console.error(` FAIL ${name}: ${e.message}`);
|
|
30
|
-
process.exitCode = 1;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const chan = (c) => [(c >> 16) & 0xff, (c >> 8) & 0xff, c & 0xff];
|
|
35
|
-
|
|
36
|
-
// ---------- wrap01 -------------------------------------------------------
|
|
37
|
-
|
|
38
|
-
test('wrap01 maps any real into [0,1)', () => {
|
|
39
|
-
assert.equal(wrap01(0), 0);
|
|
40
|
-
assert.equal(wrap01(0.5), 0.5);
|
|
41
|
-
assert.equal(wrap01(1), 0);
|
|
42
|
-
assert.ok(Math.abs(wrap01(1.25) - 0.25) < 1e-9);
|
|
43
|
-
assert.ok(Math.abs(wrap01(-0.25) - 0.75) < 1e-9);
|
|
44
|
-
assert.ok(Math.abs(wrap01(-2.1) - 0.9) < 1e-9);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// ---------- phaseFromClock ----------------------------------------------
|
|
48
|
-
|
|
49
|
-
test('phaseFromClock is 0 at the epoch and 0.5 at half a period', () => {
|
|
50
|
-
assert.equal(phaseFromClock(0), 0);
|
|
51
|
-
assert.equal(phaseFromClock(DEFAULT_PERIOD_MS / 2), 0.5);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('phaseFromClock wraps each period', () => {
|
|
55
|
-
assert.equal(phaseFromClock(DEFAULT_PERIOD_MS), 0);
|
|
56
|
-
assert.ok(Math.abs(phaseFromClock(DEFAULT_PERIOD_MS * 1.25) - 0.25) < 1e-9);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('phaseFromClock is deterministic — shared across clients by construction', () => {
|
|
60
|
-
const now = 1_700_000_123_456;
|
|
61
|
-
assert.equal(phaseFromClock(now), phaseFromClock(now));
|
|
62
|
-
// Two clients a few seconds apart see ~the same phase (period is minutes).
|
|
63
|
-
const skew = phaseFromClock(now + 3000) - phaseFromClock(now);
|
|
64
|
-
assert.ok(Math.abs(skew) < 0.01, `3s skew tiny: ${skew}`);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('phaseFromClock guards a non-positive period', () => {
|
|
68
|
-
assert.equal(phaseFromClock(1234, 0), 0);
|
|
69
|
-
assert.equal(phaseFromClock(1234, -5), 0);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// ---------- lerpColor ----------------------------------------------------
|
|
73
|
-
|
|
74
|
-
test('lerpColor returns the endpoints at f=0 and f=1', () => {
|
|
75
|
-
assert.equal(lerpColor(0x000000, 0xffffff, 0), 0x000000);
|
|
76
|
-
assert.equal(lerpColor(0x000000, 0xffffff, 1), 0xffffff);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('lerpColor blends channel-by-channel at the midpoint', () => {
|
|
80
|
-
// (0,128,255) is the per-channel midpoint of black and 0x00ffff... use a
|
|
81
|
-
// clean case: midpoint of 0x000000 and 0x804020 is (64,32,16).
|
|
82
|
-
assert.deepEqual(chan(lerpColor(0x000000, 0x804020, 0.5)), [64, 32, 16]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('lerpColor clamps f outside [0,1]', () => {
|
|
86
|
-
assert.equal(lerpColor(0x102030, 0xa0b0c0, -1), 0x102030);
|
|
87
|
-
assert.equal(lerpColor(0x102030, 0xa0b0c0, 2), 0xa0b0c0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// ---------- bracket / tintAt --------------------------------------------
|
|
91
|
-
|
|
92
|
-
test('tintAt hits each keyframe exactly at its phase', () => {
|
|
93
|
-
for (const kf of KEYFRAMES) {
|
|
94
|
-
const { color, alpha } = tintAt(kf.t);
|
|
95
|
-
assert.equal(color, kf.color, `${kf.name}@${kf.t} color`);
|
|
96
|
-
assert.ok(Math.abs(alpha - kf.alpha) < 1e-9, `${kf.name}@${kf.t} alpha`);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('high noon is fully transparent (base art at full strength)', () => {
|
|
101
|
-
assert.equal(tintAt(SKY_PHASES.midday).alpha, 0);
|
|
102
|
-
// The whole noon plateau holds at alpha 0.
|
|
103
|
-
assert.equal(tintAt(0.45).alpha, 0);
|
|
104
|
-
assert.equal(tintAt(0.50).alpha, 0);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('golden hour is the warm hero state', () => {
|
|
108
|
-
const t = tintAt(SKY_PHASES.golden);
|
|
109
|
-
const [r, g, b] = chan(t.color);
|
|
110
|
-
assert.ok(r > g && g > b, `amber: r>g>b got ${r},${g},${b}`);
|
|
111
|
-
assert.ok(t.alpha > 0.3, `visible: ${t.alpha}`);
|
|
112
|
-
assert.equal(t.name, 'golden');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('night is a dark blue-dominant tint', () => {
|
|
116
|
-
const [r, g, b] = chan(tintAt(0).color);
|
|
117
|
-
assert.ok(b > r && b > g, `blue-dominant: ${r},${g},${b}`);
|
|
118
|
-
assert.ok(tintAt(0).alpha > 0.5, 'strong');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('the wrap segment interpolates nightfall → deep night across midnight', () => {
|
|
122
|
-
// phase 0.94 sits between the 0.88 nightfall keyframe and the 0.00 deep-night
|
|
123
|
-
// keyframe (read circularly). Alpha must land strictly between the two.
|
|
124
|
-
const { alpha } = tintAt(0.94);
|
|
125
|
-
assert.ok(alpha > 0.64 && alpha < 0.72, `between nightfall and deep night: ${alpha}`);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test('bracket picks the wrap segment for late-night phases', () => {
|
|
129
|
-
const b = bracket(0.95);
|
|
130
|
-
assert.equal(b.from.t, 0.88);
|
|
131
|
-
assert.equal(b.to.t, 0.00);
|
|
132
|
-
assert.ok(b.f > 0 && b.f < 1);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('tintAt is continuous and total over the whole cycle (no gaps/throws)', () => {
|
|
136
|
-
let prev = tintAt(0);
|
|
137
|
-
for (let i = 1; i <= 1000; i++) {
|
|
138
|
-
const p = i / 1000;
|
|
139
|
-
const cur = tintAt(p);
|
|
140
|
-
assert.ok(cur.alpha >= 0 && cur.alpha <= 1, `alpha in range @${p}`);
|
|
141
|
-
// No discontinuous jump bigger than the largest keyframe-to-keyframe step.
|
|
142
|
-
assert.ok(Math.abs(cur.alpha - prev.alpha) < 0.05, `smooth @${p}`);
|
|
143
|
-
prev = cur;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('tintAt wraps: phase 1.0 equals phase 0.0', () => {
|
|
148
|
-
assert.deepEqual(tintAt(1.0), tintAt(0.0));
|
|
149
|
-
assert.deepEqual(tintAt(1.5), tintAt(0.5));
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// ---------- parseSkyOverride --------------------------------------------
|
|
153
|
-
|
|
154
|
-
test('parseSkyOverride: absent / blank => null (run the live clock)', () => {
|
|
155
|
-
assert.equal(parseSkyOverride(null), null);
|
|
156
|
-
assert.equal(parseSkyOverride(undefined), null);
|
|
157
|
-
assert.equal(parseSkyOverride(''), null);
|
|
158
|
-
assert.equal(parseSkyOverride(' '), null);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('parseSkyOverride: mood names (case-insensitive) => their phase', () => {
|
|
162
|
-
assert.equal(parseSkyOverride('golden'), SKY_PHASES.golden);
|
|
163
|
-
assert.equal(parseSkyOverride('NIGHT'), SKY_PHASES.night);
|
|
164
|
-
assert.equal(parseSkyOverride('Dusk'), SKY_PHASES.dusk);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('parseSkyOverride: raw fractions, wrapped into [0,1)', () => {
|
|
168
|
-
assert.equal(parseSkyOverride('0.68'), 0.68);
|
|
169
|
-
assert.equal(parseSkyOverride('0'), 0);
|
|
170
|
-
assert.ok(Math.abs(parseSkyOverride('1.25') - 0.25) < 1e-9);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('parseSkyOverride: garbage => null', () => {
|
|
174
|
-
assert.equal(parseSkyOverride('banana'), null);
|
|
175
|
-
assert.equal(parseSkyOverride('NaN'), null);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
// ---------- data sanity --------------------------------------------------
|
|
179
|
-
|
|
180
|
-
test('KEYFRAMES are sorted, valid colours, alphas in [0,1]', () => {
|
|
181
|
-
for (let i = 0; i < KEYFRAMES.length; i++) {
|
|
182
|
-
const kf = KEYFRAMES[i];
|
|
183
|
-
assert.ok(kf.t >= 0 && kf.t < 1, `t in [0,1): ${kf.t}`);
|
|
184
|
-
assert.ok(kf.alpha >= 0 && kf.alpha <= 1, `alpha in [0,1]: ${kf.alpha}`);
|
|
185
|
-
assert.ok(Number.isInteger(kf.color) && kf.color >= 0 && kf.color <= 0xffffff, 'color 24-bit');
|
|
186
|
-
if (i > 0) assert.ok(kf.t > KEYFRAMES[i - 1].t, `strictly ascending @${i}`);
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('SKY_PHASES all resolve into [0,1) and round-trip through tintAt', () => {
|
|
191
|
-
for (const [name, p] of Object.entries(SKY_PHASES)) {
|
|
192
|
-
assert.ok(p >= 0 && p < 1, `${name} phase in range`);
|
|
193
|
-
assert.doesNotThrow(() => tintAt(p), `${name} tints`);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
console.log(`\nday_cycle: ${passed} passed`);
|