@bongos/core 1.17.2 → 1.18.0
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 +426 -96
- package/.devcontainer/healthcheck.sh +0 -0
- package/.devcontainer/init-firewall.sh +0 -0
- package/.devcontainer/post-create.sh +0 -0
- package/GOVERNANCE.md +1 -1
- package/README.md +26 -22
- package/bin/bongos.js +0 -0
- 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/docker-entrypoint.sh +0 -0
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +94 -0
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +43 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +68 -370
- package/docs/api-reference.md +7 -18
- package/docs/file-map.md +7 -25
- package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
- package/docs/onboarding/diagrams/assertions.json +5 -9
- package/modules/hall-ui/public/builders.js +132 -645
- package/modules/hall-ui/public/goal-inbox.js +67 -26
- package/modules/hall-ui/public/goals-page.js +102 -177
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -253
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-kit.js +474 -0
- package/modules/hall-ui/public/hall-widgets.js +24 -20
- package/modules/hall-ui/public/instances-panel.js +24 -3
- package/modules/hall-ui/public/new-project-wizard.js +6 -5
- package/modules/hall-ui/public/settings.css +355 -0
- package/modules/hall-ui/public/settings.html +86 -414
- package/modules/hall-ui/public/settings.js +96 -35
- package/modules/hall-ui/public/style.css +156 -0
- package/modules/hall-ui/public/watch.css +184 -0
- package/modules/hall-ui/public/watch.html +111 -285
- package/modules/hall-ui/public/watch.js +59 -21
- 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/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +83 -7
- package/public-landing/index.html +11 -76
- package/scripts/cron/style_guide_audit.sh +0 -0
- package/scripts/gds/api.js +0 -0
- package/scripts/gds/architect-audit.js +0 -0
- package/scripts/gds/audit-authorship.sh +0 -0
- package/scripts/gds/audit-replay.sh +0 -0
- package/scripts/gds/box.js +0 -0
- package/scripts/gds/capture.js +0 -0
- package/scripts/gds/conductor-dispatch.js +0 -0
- package/scripts/gds/coreB-carve-proof.sh +0 -0
- package/scripts/gds/db-backup-nightly.sh +0 -0
- package/scripts/gds/db-backup-offsite.sh +0 -0
- package/scripts/gds/doctor.js +0 -0
- package/scripts/gds/grade-smoke.js +0 -0
- package/scripts/gds/hooks/pre-push.js +0 -0
- package/scripts/gds/install-git-hooks.js +0 -0
- package/scripts/gds/main-audit.js +0 -0
- package/scripts/gds/oauth-secret.js +0 -0
- package/scripts/gds/onboarding-analyze.js +0 -0
- package/scripts/gds/onboarding-ship.js +0 -0
- package/scripts/gds/paste-token.js +0 -0
- package/scripts/gds/portability-smoke-art.sh +0 -0
- package/scripts/gds/portability-smoke-memory.sh +0 -0
- package/scripts/gds/provision.js +125 -4
- package/scripts/gds/push-main.js +0 -0
- package/scripts/gds/render-ideas.js +0 -0
- package/scripts/gds/security-baseline-check.sh +0 -0
- package/scripts/gds/session.js +0 -0
- package/scripts/gds/ship.js +11 -1
- package/scripts/gds/smoke-batch-claim.sh +0 -0
- package/scripts/gds/smoke-batch-release.sh +0 -0
- package/scripts/gds/smoke-box.sh +0 -0
- package/scripts/gds/smoke-claim-consistency.sh +0 -0
- package/scripts/gds/smoke-dependencies.sh +0 -0
- package/scripts/gds/smoke-write.sh +0 -0
- package/scripts/hall-preview/README.md +103 -0
- package/scripts/hall-preview/fixtures/access-requests.json +20 -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 +68 -0
- package/scripts/hall-preview/fixtures/blockers__detail.json +12 -0
- package/scripts/hall-preview/fixtures/box__me.json +5 -0
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +69 -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 +48 -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 +32 -0
- package/scripts/hall-preview/fixtures/inbox.json +330 -0
- package/scripts/hall-preview/fixtures/inbox__detail.json +15 -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__art-key__own.json +6 -0
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +209 -0
- package/scripts/hall-preview/fixtures/me__recent-ships.json +304 -0
- package/scripts/hall-preview/fixtures/me__render.json +5 -0
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +143 -0
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +67 -0
- package/scripts/hall-preview/fixtures/me__wandering.json +43 -0
- package/scripts/hall-preview/fixtures/override-requests.json +15 -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 +10 -0
- package/scripts/hall-preview/fixtures/security__reports.json +31 -0
- package/scripts/hall-preview/fixtures/sessions__search.json +9 -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 +195 -0
- package/scripts/migrate.sh +29 -4
- package/scripts/migrate_cost_ledger.sh +0 -0
- package/scripts/render-diagrams.sh +0 -0
- package/scripts/security/dep-audit.js +0 -0
- package/scripts/setup-builder.js +0 -0
- package/scripts/setup-builder.sh +0 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
- package/src/bongos/optimizer.js +37 -2
- package/src/module-api.js +20 -1
- package/tests/criterion_suggest.mjs +106 -0
- package/tests/migrate_root_resolution.mjs +97 -0
- package/tests/provision.mjs +69 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/hall-preview/server.js — the hall preview harness (task 2167).
|
|
3
|
+
//
|
|
4
|
+
// The dev-box sandbox previews the GAME only (/builders 404s there, idea 490),
|
|
5
|
+
// so hall-ui changes are reviewed HERE: a tiny express app that serves the real
|
|
6
|
+
// modules/hall-ui/public files through the REAL serving seams (boot-time ?v=
|
|
7
|
+
// stamping, {{token}} replacement, __BRANDING__/__MODULES__ injection, theme
|
|
8
|
+
// markup) with the /api/gds|bongos reads answered from checked-in fixtures.
|
|
9
|
+
// Boots DB-less (pg pools connect lazily). Proven recipe from tasks 1723/1742,
|
|
10
|
+
// finally checked in.
|
|
11
|
+
//
|
|
12
|
+
// node scripts/hall-preview/server.js [--port 4600] [--brand <file>]
|
|
13
|
+
// [--modules-off dev-box,provisioning] [--empty]
|
|
14
|
+
//
|
|
15
|
+
// --port listen port (default 4600)
|
|
16
|
+
// --brand branding pack file — default config/branding.neutral.json
|
|
17
|
+
// (vanilla); pass config/branding.json for the instance pack
|
|
18
|
+
// --modules-off comma list of module keys to report disabled in
|
|
19
|
+
// window.__MODULES__ (verifies moduleOn() gating)
|
|
20
|
+
// --empty serve every fixture with its arrays emptied — the
|
|
21
|
+
// empty-state screenshot pass
|
|
22
|
+
// --rank archon override the /me fixture's builder.rank so the Archon-gated
|
|
23
|
+
// pages (watch, harbor, gate) render their content in preview
|
|
24
|
+
//
|
|
25
|
+
// Browse http://builders.localhost:<port>/ — Chromium resolves *.localhost,
|
|
26
|
+
// and the `builders.` host prefix exercises the real subdomain serving path.
|
|
27
|
+
// Refresh fixtures from live data: node scripts/hall-preview/refresh-fixtures.js
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const fs = require('fs');
|
|
32
|
+
|
|
33
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
34
|
+
const PUB = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
35
|
+
const FIXTURES = path.join(__dirname, 'fixtures');
|
|
36
|
+
|
|
37
|
+
// ---- flags (before any src/ require: --brand must win the branding resolve) --
|
|
38
|
+
function arg(name) {
|
|
39
|
+
const i = process.argv.indexOf(name);
|
|
40
|
+
return i > -1 ? process.argv[i + 1] : null;
|
|
41
|
+
}
|
|
42
|
+
const PORT = Number(arg('--port')) || 4600;
|
|
43
|
+
const EMPTY = process.argv.includes('--empty');
|
|
44
|
+
// --rank: view the hall as a different rank — the /me fixture's builder.rank is
|
|
45
|
+
// overridden in the response, so the Archon-gated pages (watch, harbor, gate)
|
|
46
|
+
// render their real content instead of the sealed notice. Preview-only theatre:
|
|
47
|
+
// the real pages are server-gated (requireArchonPage / ADR 0016).
|
|
48
|
+
const RANK_OVERRIDE = arg('--rank');
|
|
49
|
+
const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
50
|
+
// --modules-on: force-report modules as enabled — a dev box's local config
|
|
51
|
+
// often lacks prod's instance modules (provisioning, dev-box), and their
|
|
52
|
+
// widgets can't be reviewed without it (task 2169).
|
|
53
|
+
const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
54
|
+
const BRAND_FILE = arg('--brand') || 'config/branding.neutral.json';
|
|
55
|
+
process.env.GDS_BRANDING_FILE = path.resolve(ROOT, BRAND_FILE);
|
|
56
|
+
|
|
57
|
+
/* eslint-disable import/no-dynamic-require */
|
|
58
|
+
// express comes from this tree's node_modules when installed, else node's
|
|
59
|
+
// normal upward walk finds the main checkout's copy (a git worktree under
|
|
60
|
+
// .claude/worktrees/ has no node_modules of its own). The src/ seams below are
|
|
61
|
+
// ALWAYS taken from THIS tree so a worktree's edits are what gets previewed.
|
|
62
|
+
let express;
|
|
63
|
+
try { express = require(path.join(ROOT, 'node_modules', 'express')); }
|
|
64
|
+
catch (_) { express = require('express'); }
|
|
65
|
+
const { buildStampedHtml } = require(path.join(ROOT, 'src', 'asset-version.js'));
|
|
66
|
+
const { injectBrandingGlobal, mountInternalSurfaces } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
|
|
67
|
+
const { clientModules } = require(path.join(ROOT, 'src', 'modules.js'));
|
|
68
|
+
|
|
69
|
+
// ---- fixtures ---------------------------------------------------------------
|
|
70
|
+
// File naming: the API path with slashes as "__" — /me/recent-ships →
|
|
71
|
+
// me__recent-ships.json. Query strings are ignored when matching. Id-bearing
|
|
72
|
+
// paths fall back to the generic names below so one fixture serves any id.
|
|
73
|
+
const FIXTURE_FALLBACKS = [
|
|
74
|
+
[/^goals\/\d+\/conflicts$/, 'goals__conflicts'],
|
|
75
|
+
[/^goals\/\d+\/requests$/, 'goals__requests'],
|
|
76
|
+
[/^goals\/\d+$/, 'goals__detail'],
|
|
77
|
+
[/^tasks\/\d+$/, 'tasks__detail'],
|
|
78
|
+
// the #/idea/N + #/blocker/N deep-link overlay (task-detail.js) — one record
|
|
79
|
+
// fixture serves any id so the overlay seam assertion renders real content.
|
|
80
|
+
[/^inbox\/\d+$/, 'inbox__detail'],
|
|
81
|
+
[/^blockers\/\d+$/, 'blockers__detail'],
|
|
82
|
+
[/^analytics\/builder\/\d+$/, 'analytics__builder'],
|
|
83
|
+
[/^builders\/\d+\/active-claims$/, 'builders__active-claims'],
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function loadFixture(name) {
|
|
87
|
+
try {
|
|
88
|
+
return JSON.parse(fs.readFileSync(path.join(FIXTURES, `${name}.json`), 'utf8'));
|
|
89
|
+
} catch (_) { return undefined; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --empty: the same pages, with nothing in them — every array drains so each
|
|
93
|
+
// page's empty states render. Scalars/strings survive (identity stays real).
|
|
94
|
+
function emptied(v) {
|
|
95
|
+
if (Array.isArray(v)) return [];
|
|
96
|
+
if (v && typeof v === 'object') {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, val] of Object.entries(v)) out[k] = emptied(val);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
return v;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fixtureFor(apiPath) {
|
|
105
|
+
const exact = loadFixture(apiPath.replace(/\//g, '__'));
|
|
106
|
+
if (exact !== undefined) return exact;
|
|
107
|
+
for (const [re, name] of FIXTURE_FALLBACKS) {
|
|
108
|
+
if (re.test(apiPath)) {
|
|
109
|
+
const hit = loadFixture(name);
|
|
110
|
+
if (hit !== undefined) return hit;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- app ----------------------------------------------------------------------
|
|
117
|
+
const app = express();
|
|
118
|
+
|
|
119
|
+
// The API stub — intercepts every /api/gds/*, /api/bongos/*, /api/bongos/v1/*
|
|
120
|
+
// request BEFORE mountInternalSurfaces can reach the real routes (which would
|
|
121
|
+
// touch Postgres — the harness is DB-less by design). A plain function
|
|
122
|
+
// middleware, not a path mount: regex-mount semantics differ across express
|
|
123
|
+
// majors and a silent non-match here means a pg crash.
|
|
124
|
+
app.use((req, res, next) => {
|
|
125
|
+
const m = req.path.match(/^\/api\/(?:gds|bongos)(?:\/v1)?\/(.+)$/);
|
|
126
|
+
if (!m) return next();
|
|
127
|
+
const apiPath = m[1].replace(/\/$/, '');
|
|
128
|
+
|
|
129
|
+
// SSE live channel: emit one nudge (so "live refresh" paths run once), then
|
|
130
|
+
// keep-alive comments. Pages treat this exactly like prod's stream.
|
|
131
|
+
if (apiPath === 'live') {
|
|
132
|
+
res.set({ 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' });
|
|
133
|
+
res.flushHeaders();
|
|
134
|
+
setTimeout(() => res.write('event: nudge\ndata: {}\n\n'), 1200);
|
|
135
|
+
const tick = setInterval(() => res.write(': keep-alive\n\n'), 25000);
|
|
136
|
+
req.on('close', () => clearInterval(tick));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (req.method !== 'GET') {
|
|
141
|
+
return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
|
|
142
|
+
}
|
|
143
|
+
const fix = fixtureFor(apiPath);
|
|
144
|
+
if (fix === undefined) {
|
|
145
|
+
console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
|
|
146
|
+
return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
|
|
147
|
+
}
|
|
148
|
+
let payload = EMPTY ? emptied(fix) : fix;
|
|
149
|
+
if (RANK_OVERRIDE && apiPath === 'me') {
|
|
150
|
+
payload = JSON.parse(JSON.stringify(payload));
|
|
151
|
+
if (payload.builder) payload.builder.rank = RANK_OVERRIDE;
|
|
152
|
+
else if ('rank' in payload) payload.rank = RANK_OVERRIDE;
|
|
153
|
+
}
|
|
154
|
+
res.json(payload);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// The hall pages, pre-auth-gate, through the real seams: boot-time stamped
|
|
158
|
+
// (?v= content hashes), {{token}}-replaced, __BRANDING__/__MODULES__ injected,
|
|
159
|
+
// theme markup appended — byte-for-byte what prod serves after its gates.
|
|
160
|
+
const stamped = buildStampedHtml(PUB, '/builders/');
|
|
161
|
+
const mods = JSON.parse(JSON.stringify(clientModules()));
|
|
162
|
+
for (const key of MODULES_OFF) {
|
|
163
|
+
if (mods.enabled && key in mods.enabled) mods.enabled[key] = false;
|
|
164
|
+
else (mods.enabled = mods.enabled || {})[key] = false;
|
|
165
|
+
}
|
|
166
|
+
for (const key of MODULES_ON) {
|
|
167
|
+
(mods.enabled = mods.enabled || {})[key] = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sendPage(res, name) {
|
|
171
|
+
const html = stamped[name];
|
|
172
|
+
if (!html) return false;
|
|
173
|
+
res.type('html').send(injectBrandingGlobal(html, undefined, mods));
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
app.get('/', (req, res, next) => { if (!sendPage(res, 'index.html')) next(); });
|
|
178
|
+
app.get(/^\/task\/\d+$/, (req, res, next) => { if (!sendPage(res, 'task.html')) next(); });
|
|
179
|
+
app.get(/^\/([a-z-]+)$/, (req, res, next) => {
|
|
180
|
+
if (!sendPage(res, `${req.params[0]}.html`)) next();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Everything else — /builders/*.css|js assets, /lib/bongos-client.js, diagram
|
|
184
|
+
// PNGs — falls through to the real internal-surface mounting. Our handlers
|
|
185
|
+
// above registered first, so the hall's auth gates in there never fire for
|
|
186
|
+
// the page routes we already served.
|
|
187
|
+
mountInternalSurfaces(app);
|
|
188
|
+
|
|
189
|
+
// Loopback only — this is a pre-auth preview of hall pages with fixture data;
|
|
190
|
+
// *.localhost resolves to 127.0.0.1 so nothing else is needed.
|
|
191
|
+
app.listen(PORT, '127.0.0.1', () => {
|
|
192
|
+
console.log(`hall-preview: http://builders.localhost:${PORT}/`);
|
|
193
|
+
console.log(` brand: ${BRAND_FILE}${EMPTY ? ' (EMPTY fixtures)' : ''}${MODULES_OFF.length ? ` modules off: ${MODULES_OFF.join(',')}` : ''}`);
|
|
194
|
+
console.log(' pages:', Object.keys(stamped).map((f) => f.replace(/\.html$/, '')).sort().join(' '));
|
|
195
|
+
});
|
package/scripts/migrate.sh
CHANGED
|
@@ -12,18 +12,43 @@ set -euo pipefail
|
|
|
12
12
|
# alongside the instance, so both resolve to the SAME dir and this is byte-identical to the
|
|
13
13
|
# old `cd "$(dirname "$0")/.."`. Once the core installs from node_modules, resolveCoreRoot()
|
|
14
14
|
# points into the package (core migrations + src/) while resolveInstanceRoot() stays the host
|
|
15
|
-
# repo (env → bongos.config → cwd).
|
|
16
|
-
#
|
|
17
|
-
#
|
|
15
|
+
# repo (env → bongos.config → cwd).
|
|
16
|
+
#
|
|
17
|
+
# CORE root is resolved SCRIPT-relative (the package's own files sit next to this script), so it
|
|
18
|
+
# never depends on the caller's cwd. The INSTANCE root is the OPPOSITE: it MUST come from where
|
|
19
|
+
# migrate was INVOKED. `npm run migrate` from the instance sets INIT_CWD (and $PWD) to the
|
|
20
|
+
# instance dir, where bongos.config.json lives. Resolving the instance root script-relative would
|
|
21
|
+
# `cd` into the core PACKAGE first — whose dir has no bongos.config.json — so with no
|
|
22
|
+
# OTB_INSTANCE_ROOT env it falls back to the package dir as the "instance" and reads the core's
|
|
23
|
+
# VANILLA module config, SILENTLY skipping every host + flag-gated module migration (R89 rehearsal
|
|
24
|
+
# F6, task 2180: literal `npm run migrate` applied 115 rows and created no game schema; from the
|
|
25
|
+
# instance cwd it applied 121). So resolve the instance root from the invocation cwd, preferring an
|
|
26
|
+
# EXPLICIT pointer (OTB_INSTANCE_ROOT env, else the invocation cwd's bongos.config.json).
|
|
27
|
+
# resolveInstanceRootExplicit() returns '' (NOT process.cwd()) when neither is present, so we fall
|
|
28
|
+
# back to the script-relative DEFAULT_ROOT — the monolith case (cwd==core==instance), byte-identical
|
|
29
|
+
# to the old behavior, and a stray cwd can never masquerade as the instance. Fail open to
|
|
30
|
+
# DEFAULT_ROOT if node is unavailable so a deploy never breaks.
|
|
31
|
+
INVOKE_CWD="${INIT_CWD:-$PWD}"
|
|
18
32
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
19
33
|
DEFAULT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
20
34
|
CORE_ROOT="$(cd "$DEFAULT_ROOT" && node -e "process.stdout.write(require('./src/instance-config').resolveCoreRoot())" 2>/dev/null || echo "$DEFAULT_ROOT")"
|
|
21
|
-
INSTANCE_ROOT="$(cd "$
|
|
35
|
+
INSTANCE_ROOT="$(cd "$INVOKE_CWD" && node -e "process.stdout.write(require('$DEFAULT_ROOT/src/instance-config').resolveInstanceRootExplicit() || '')" 2>/dev/null || true)"
|
|
36
|
+
INSTANCE_ROOT="${INSTANCE_ROOT:-$DEFAULT_ROOT}"
|
|
22
37
|
# Run from the instance root so instance-relative paths resolve (the post-deploy art check below)
|
|
23
38
|
# AND so the enabledModules() read finds the instance's config/modules.json (resolveInstanceRoot
|
|
24
39
|
# falls back to cwd).
|
|
25
40
|
cd "$INSTANCE_ROOT"
|
|
26
41
|
|
|
42
|
+
# Test/ops seam (task 2180): print the resolved roots and exit BEFORE any DB work, so a DB-free
|
|
43
|
+
# unit test (tests/migrate_root_resolution.mjs) can assert the INSTANCE root resolves from the
|
|
44
|
+
# invocation cwd (a consumer's bongos.config.json), not script-relative into the core package —
|
|
45
|
+
# the F6 regression. Also handy in the field: `npm run migrate -- --print-roots` shows what
|
|
46
|
+
# migrate sees.
|
|
47
|
+
if [ "${1:-}" = "--print-roots" ]; then
|
|
48
|
+
printf 'INVOKE_CWD=%s\nCORE_ROOT=%s\nINSTANCE_ROOT=%s\n' "$INVOKE_CWD" "$CORE_ROOT" "$INSTANCE_ROOT"
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
27
52
|
DB="${PGDATABASE:-amazonprimea}"
|
|
28
53
|
|
|
29
54
|
# --- Instance-seed policy (task 1704 / ADR 0105) --------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/scripts/setup-builder.js
CHANGED
|
File without changes
|
package/scripts/setup-builder.sh
CHANGED
|
File without changes
|
|
File without changes
|
package/src/bongos/optimizer.js
CHANGED
|
@@ -53,10 +53,45 @@ async function optimizeSession({ builderId, mode, timeBudgetMin, maxParallel })
|
|
|
53
53
|
// tasks_session_fit (derived fits_session_modes[] + credits_per_minute).
|
|
54
54
|
// Also exclude tasks this builder already has an active claim on, mirroring
|
|
55
55
|
// listClaimableTasks in db.js.
|
|
56
|
+
// task 2181 (incident, blocker 74): the view bodies (migration core_190) are
|
|
57
|
+
// inlined so this query takes only ACCESS SHARE on base tables — a detached
|
|
58
|
+
// backend queued an ACCESS EXCLUSIVE DDL on the views and parked every view
|
|
59
|
+
// SELECT behind it. Plan-identical (Postgres inlines simple views); change in
|
|
60
|
+
// lockstep with the views / listClaimableTasks if a migration alters them.
|
|
56
61
|
const { rows } = await pool.query(
|
|
57
62
|
`SELECT ct.*, sf.fits_session_modes, sf.credits_per_minute
|
|
58
|
-
FROM
|
|
59
|
-
|
|
63
|
+
FROM (
|
|
64
|
+
SELECT t.*, pms_calibrated_minutes(t.est_minutes, t.kind) AS est_minutes_calibrated
|
|
65
|
+
FROM tasks t
|
|
66
|
+
WHERE t.status = 'ready'
|
|
67
|
+
AND t.title NOT LIKE '\\_\\_smoke\\_\\_%' ESCAPE '\\'
|
|
68
|
+
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
69
|
+
AND NOT EXISTS (
|
|
70
|
+
SELECT 1 FROM claims c
|
|
71
|
+
JOIN tasks t2 ON t2.id = c.task_id
|
|
72
|
+
WHERE c.released_at IS NULL
|
|
73
|
+
AND t2.touches && t.touches
|
|
74
|
+
)
|
|
75
|
+
) ct
|
|
76
|
+
JOIN (
|
|
77
|
+
SELECT t.id,
|
|
78
|
+
CASE
|
|
79
|
+
WHEN t.automation_tag = 'overnight-safe' AND COALESCE(t.manual_degree, 5) <= 2
|
|
80
|
+
AND NOT EXISTS (SELECT 1 FROM task_blockers tb WHERE tb.task_id = t.id)
|
|
81
|
+
THEN ARRAY['autonomous','check-in','active']
|
|
82
|
+
WHEN t.automation_tag = 'live-only' OR COALESCE(t.manual_degree, 5) >= 4
|
|
83
|
+
THEN ARRAY['active']
|
|
84
|
+
ELSE ARRAY['check-in','active']
|
|
85
|
+
END AS fits_session_modes,
|
|
86
|
+
CASE
|
|
87
|
+
WHEN t.credits_reward > 0
|
|
88
|
+
AND pms_calibrated_minutes(t.est_minutes, t.kind) IS NOT NULL
|
|
89
|
+
AND pms_calibrated_minutes(t.est_minutes, t.kind) > 0
|
|
90
|
+
THEN ROUND(t.credits_reward::numeric / pms_calibrated_minutes(t.est_minutes, t.kind), 3)
|
|
91
|
+
ELSE NULL
|
|
92
|
+
END AS credits_per_minute
|
|
93
|
+
FROM tasks t
|
|
94
|
+
) sf ON sf.id = ct.id
|
|
60
95
|
WHERE $1 = ANY(sf.fits_session_modes)
|
|
61
96
|
AND NOT EXISTS (
|
|
62
97
|
SELECT 1 FROM claims c
|
package/src/module-api.js
CHANGED
|
@@ -199,8 +199,27 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
199
199
|
// plus a THROWAWAY `updateCanary` marker removed in the phase-E follow-up. A
|
|
200
200
|
// backwards-compatible additive change with no API removal is exactly semver
|
|
201
201
|
// PATCH. Every `^1.x` / `^1.17` module pin is satisfied by 1.17.2.
|
|
202
|
+
// 1.17.3 — task 2183 (R89 cutover prereq): republish so the packaged core carries the
|
|
203
|
+
// F6 migrate.sh fix (task 2180). Post-split, `scripts/migrate.sh` cd'd into the
|
|
204
|
+
// installed @bongos/core package before resolving the instance root, so a literal
|
|
205
|
+
// `npm run migrate` read the core's VANILLA module config and SILENTLY skipped
|
|
206
|
+
// every host + flag-gated module migration (rehearsal: 115 rows, no game schema;
|
|
207
|
+
// from the instance cwd, 121). 2180 fixed the script to resolve INSTANCE_ROOT from
|
|
208
|
+
// the invocation cwd (INIT_CWD/$PWD → the instance's bongos.config.json) and added
|
|
209
|
+
// a DB-free `--print-roots` regression guard. Published 1.17.2 PREDATES 2180, so a
|
|
210
|
+
// consumer pinning 1.17.2 would still skip host migrations — hence a PATCH republish
|
|
211
|
+
// at the F6-inclusive main. A functional-file bug fix with no doorway export change
|
|
212
|
+
// is exactly semver PATCH. Every `^1.x` / `^1.17` module pin is satisfied by 1.17.3.
|
|
213
|
+
// 1.18.0 — task 2190 (R89 follow-up): first version cut from the cloud-bongos AUTHORITATIVE
|
|
214
|
+
// source repo. Re-seeds the standalone core (which had frozen at the 2026-07-03
|
|
215
|
+
// v1.14.0 extraction while core dev continued inside amazonprimea through 1.17.3)
|
|
216
|
+
// from amazonprimea's last-with-core commit 345494f2. Content is 1.17.3 PLUS the
|
|
217
|
+
// builders'-hall UI redesign (developed in amazonprimea as tasks 2171/2172/2173/2184) that
|
|
218
|
+
// post-dates the published 1.17.3 tarball — a new user-facing feature set with no
|
|
219
|
+
// doorway export removal, so semver MINOR. NOT yet published to npm (that is the
|
|
220
|
+
// follow-on release task); amazonprimea still runs the vendored 1.17.3.
|
|
202
221
|
// ---------------------------------------------------------------------------
|
|
203
|
-
const CORE_VERSION = '1.
|
|
222
|
+
const CORE_VERSION = '1.18.0'; // see the changelog above — 1.18.0 is the first cut from the cloud-bongos authoritative source (re-seed task 2190); includes the builders'-hall UI redesign beyond published 1.17.3
|
|
204
223
|
|
|
205
224
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
206
225
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// tests/criterion_suggest.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit tests for suggestCriteria() — the pure criterion RECOMMENDER
|
|
4
|
+
// (modules/lifecycle/criterion-suggest.js, task 2165). Given a goal's criteria
|
|
5
|
+
// and a candidate task's title/description, it ranks the criteria by weighted
|
|
6
|
+
// lexical overlap so task-create can suggest which done-when criterion to link
|
|
7
|
+
// (the author confirms — nothing is auto-linked). No DB, so it runs in the
|
|
8
|
+
// DB-free suite.
|
|
9
|
+
//
|
|
10
|
+
// Run: node tests/criterion_suggest.mjs
|
|
11
|
+
|
|
12
|
+
import { strict as assert } from 'node:assert';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { suggestCriteria, tokenize } = require('../modules/lifecycle/criterion-suggest.js');
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
function test(name, fn) {
|
|
21
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
22
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.message}`); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const crit = (id, criterion_id, criterion_md, satisfied = false) =>
|
|
26
|
+
({ id, criterion_id, criterion_md, satisfied });
|
|
27
|
+
|
|
28
|
+
// A small, realistic goal-25-flavoured criterion set.
|
|
29
|
+
const CRITERIA = [
|
|
30
|
+
crit(138, 'bx-hall', 'Core task actions work in the builders hall without invoking the LLM.'),
|
|
31
|
+
crit(139, 'bx-identity', 'A builder can view and edit their own profile and read a leaderboard.'),
|
|
32
|
+
crit(140, 'bx-onramp', 'Newcomers never dead-end: auth and re-auth are browser-clickable.'),
|
|
33
|
+
crit(141, 'bx-box', 'A builder dev box and sandbox is robust: uncommitted work is not lost.'),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
test('ranks the most lexically-relevant criterion first', () => {
|
|
37
|
+
const out = suggestCriteria(CRITERIA, {
|
|
38
|
+
title: 'In-hall confirm button for criteria',
|
|
39
|
+
description: 'Let builders confirm a criterion in the hall without the LLM.',
|
|
40
|
+
});
|
|
41
|
+
assert.ok(out.length >= 1, 'expected at least one suggestion');
|
|
42
|
+
assert.equal(out[0].criterion_id, 'bx-hall', `expected bx-hall first, got ${out[0].criterion_id}`);
|
|
43
|
+
assert.ok(out[0].matched_terms.includes('hall'), 'should report the matched term');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('title terms weigh more than description terms', () => {
|
|
47
|
+
// "profile" only in the title → should beat a body-only "hall" match.
|
|
48
|
+
const out = suggestCriteria(CRITERIA, {
|
|
49
|
+
title: 'Edit your profile',
|
|
50
|
+
description: 'Shown in the hall.',
|
|
51
|
+
});
|
|
52
|
+
assert.equal(out[0].criterion_id, 'bx-identity', `title term should win, got ${out[0].criterion_id}`);
|
|
53
|
+
const identity = out.find((c) => c.criterion_id === 'bx-identity');
|
|
54
|
+
const hall = out.find((c) => c.criterion_id === 'bx-hall');
|
|
55
|
+
assert.ok(hall === undefined || identity.score > hall.score, 'title match must outscore body match');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('returns [] when nothing overlaps (no low-confidence noise)', () => {
|
|
59
|
+
const out = suggestCriteria(CRITERIA, {
|
|
60
|
+
title: 'Migrate the Postgres backup vendor to object storage',
|
|
61
|
+
description: 'Nightly encrypted dumps to a bucket.',
|
|
62
|
+
});
|
|
63
|
+
assert.deepEqual(out, [], 'no shared vocabulary → no suggestion');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('never suggests an already-satisfied criterion', () => {
|
|
67
|
+
const satisfiedHall = [crit(138, 'bx-hall', 'Core task actions work in the builders hall.', true)];
|
|
68
|
+
const out = suggestCriteria(satisfiedHall, { title: 'hall task actions' });
|
|
69
|
+
assert.deepEqual(out, [], 'satisfied criteria are excluded');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('respects the limit and is stable (id tiebreak on equal scores)', () => {
|
|
73
|
+
const tied = [
|
|
74
|
+
crit(20, 'b', 'builder profile'),
|
|
75
|
+
crit(10, 'a', 'builder profile'),
|
|
76
|
+
crit(30, 'c', 'builder profile'),
|
|
77
|
+
];
|
|
78
|
+
const out = suggestCriteria(tied, { title: 'builder profile' }, { limit: 2 });
|
|
79
|
+
assert.equal(out.length, 2, 'limit caps the result');
|
|
80
|
+
assert.deepEqual(out.map((c) => c.id), [10, 20], 'equal scores break ties by ascending id');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('includeZero returns all unsatisfied criteria in score order', () => {
|
|
84
|
+
const out = suggestCriteria(CRITERIA, { title: 'profile' }, { includeZero: true, limit: 10 });
|
|
85
|
+
assert.equal(out.length, 4, 'includeZero keeps the zero-overlap criteria');
|
|
86
|
+
assert.equal(out[0].criterion_id, 'bx-identity', 'best match still leads');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('empty / malformed inputs are safe', () => {
|
|
90
|
+
assert.deepEqual(suggestCriteria([], { title: 'x' }), []);
|
|
91
|
+
assert.deepEqual(suggestCriteria(null, { title: 'x' }), []);
|
|
92
|
+
assert.deepEqual(suggestCriteria(CRITERIA, {}), [], 'no candidate terms → no suggestion');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('tokenize drops stopwords, short tokens, and singularizes', () => {
|
|
96
|
+
const toks = tokenize('The builders can edit profiles and tasks.');
|
|
97
|
+
assert.ok(!toks.includes('the'), 'stopword dropped');
|
|
98
|
+
assert.ok(!toks.includes('can'), 'stopword dropped');
|
|
99
|
+
assert.ok(toks.includes('builder'), 'builders → builder');
|
|
100
|
+
assert.ok(toks.includes('profile'), 'profiles → profile');
|
|
101
|
+
assert.ok(toks.includes('task'), 'tasks → task');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(`criterion-suggest recommender tests: ${passed} passed, ${failed} failed`);
|
|
106
|
+
if (failed > 0) process.exit(1);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// tests/migrate_root_resolution.mjs
|
|
2
|
+
//
|
|
3
|
+
// R89 rehearsal F6 (task 2180): after the core→consumer split, scripts/migrate.sh
|
|
4
|
+
// must resolve the INSTANCE root from the INVOCATION cwd (OTB_INSTANCE_ROOT env,
|
|
5
|
+
// else the invocation cwd's bongos.config.json), NOT script-relative into the
|
|
6
|
+
// installed @bongos/core package. The old script-relative resolve `cd`'d into the
|
|
7
|
+
// package dir first — which has no bongos.config.json — so it silently picked the
|
|
8
|
+
// package as the "instance" and read the core's VANILLA module config, skipping
|
|
9
|
+
// every host + flag-gated module migration (rehearsal: `npm run migrate` applied
|
|
10
|
+
// 115 rows and created NO game schema; from the instance cwd it applied 121).
|
|
11
|
+
//
|
|
12
|
+
// This exercises migrate.sh's `--print-roots` seam (DB-free: it prints the resolved
|
|
13
|
+
// roots and exits before any DB work), so no Postgres is needed. CORE_ROOT must stay
|
|
14
|
+
// script-relative in every case; INSTANCE_ROOT must follow the invocation cwd.
|
|
15
|
+
//
|
|
16
|
+
// Run: node tests/migrate_root_resolution.mjs
|
|
17
|
+
|
|
18
|
+
import { strict as assert } from 'node:assert';
|
|
19
|
+
import { execFileSync } from 'node:child_process';
|
|
20
|
+
import { mkdtempSync, mkdirSync, writeFileSync, realpathSync } from 'node:fs';
|
|
21
|
+
import { tmpdir } from 'node:os';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const MIGRATE = path.join(REPO_ROOT, 'scripts', 'migrate.sh');
|
|
27
|
+
const REPO_REAL = realpathSync(REPO_ROOT);
|
|
28
|
+
|
|
29
|
+
let passed = 0;
|
|
30
|
+
let failed = 0;
|
|
31
|
+
function t(name, fn) {
|
|
32
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
33
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Run `migrate.sh --print-roots` with a controlled cwd + INIT_CWD, and an env that
|
|
37
|
+
// carries NO *_INSTANCE_ROOT override, so we test cwd-based resolution alone.
|
|
38
|
+
function printRoots({ cwd, initCwd } = {}) {
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
for (const k of Object.keys(env)) if (/_INSTANCE_ROOT$/.test(k)) delete env[k];
|
|
41
|
+
delete env.INIT_CWD;
|
|
42
|
+
if (initCwd) env.INIT_CWD = initCwd;
|
|
43
|
+
const out = execFileSync('bash', [MIGRATE, '--print-roots'], { cwd, env, encoding: 'utf8' });
|
|
44
|
+
const roots = {};
|
|
45
|
+
for (const line of out.split('\n')) {
|
|
46
|
+
const m = /^(INVOKE_CWD|CORE_ROOT|INSTANCE_ROOT)=(.*)$/.exec(line);
|
|
47
|
+
if (m) roots[m[1]] = m[2];
|
|
48
|
+
}
|
|
49
|
+
return roots;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A synthetic consumer: a dir with a bongos.config.json pointing at itself, exactly
|
|
53
|
+
// as the post-split amazonprimea repo carries (bongos.config.json { instanceRoot: "." }).
|
|
54
|
+
function makeConsumer() {
|
|
55
|
+
const dir = realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-consumer-')));
|
|
56
|
+
writeFileSync(path.join(dir, 'bongos.config.json'), JSON.stringify({ instanceRoot: '.' }));
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
// A dir with NO bongos.config.json — stands in for the monolith / a stray cwd.
|
|
60
|
+
function makeBare() {
|
|
61
|
+
return realpathSync(mkdtempSync(path.join(tmpdir(), 'r89-bare-')));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---- the F6 regression: instance root follows the invocation cwd ----------
|
|
65
|
+
t('consumer run from its own dir: INSTANCE_ROOT resolves to the consumer (not the core)', () => {
|
|
66
|
+
const consumer = makeConsumer();
|
|
67
|
+
const r = printRoots({ cwd: consumer });
|
|
68
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'instance root must be the consumer dir');
|
|
69
|
+
assert.notEqual(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'must NOT fall back to the script-relative core root');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
t('consumer via npm from elsewhere: INIT_CWD wins over the spawn cwd', () => {
|
|
73
|
+
const consumer = makeConsumer();
|
|
74
|
+
const elsewhere = makeBare();
|
|
75
|
+
// npm sets INIT_CWD to where `npm run` was invoked (the instance), even if a
|
|
76
|
+
// wrapper cd'd somewhere else first.
|
|
77
|
+
const r = printRoots({ cwd: elsewhere, initCwd: consumer });
|
|
78
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), consumer, 'INIT_CWD (the instance) must win');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---- the monolith / no-pointer fallback stays script-relative -------------
|
|
82
|
+
t('no bongos.config + no env: INSTANCE_ROOT falls back to the script-relative repo root', () => {
|
|
83
|
+
const bare = makeBare();
|
|
84
|
+
const r = printRoots({ cwd: bare });
|
|
85
|
+
assert.equal(realpathSync(r.INSTANCE_ROOT), REPO_REAL, 'a stray cwd must NOT masquerade as the instance');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ---- CORE root is always script-relative, regardless of cwd ---------------
|
|
89
|
+
t('CORE_ROOT is always the script-relative repo root, independent of the invocation cwd', () => {
|
|
90
|
+
const consumer = makeConsumer();
|
|
91
|
+
const bare = makeBare();
|
|
92
|
+
assert.equal(realpathSync(printRoots({ cwd: consumer }).CORE_ROOT), REPO_REAL);
|
|
93
|
+
assert.equal(realpathSync(printRoots({ cwd: bare }).CORE_ROOT), REPO_REAL);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
console.log(`\nmigrate_root_resolution: ${passed} passed, ${failed} failed`);
|
|
97
|
+
process.exit(failed ? 1 : 0);
|
package/tests/provision.mjs
CHANGED
|
@@ -100,6 +100,35 @@ t('standalone step generators: root under standaloneBase + pull / npm-ci / insta
|
|
|
100
100
|
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@bongos\/core\/scripts\/migrate\.sh/);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// ── nightly DB backup units (task 2213 — productizes the manual 2210 fix) ──────
|
|
104
|
+
t('backup units: per-instance names + the DB set on the unit (fixes the task-2210 unset GDS_BACKUP_DB)', () => {
|
|
105
|
+
assert.equal(P.backupUnitName(coTenant), 'mercury-db-backup');
|
|
106
|
+
assert.equal(P.backupServicePath(coTenant), '/etc/systemd/system/mercury-db-backup.service');
|
|
107
|
+
assert.equal(P.backupTimerPath(coTenant), '/etc/systemd/system/mercury-db-backup.timer');
|
|
108
|
+
const svc = P.backupService(coTenant);
|
|
109
|
+
// the DB is pinned on the unit → no fallback to the nonexistent amazonprimea default
|
|
110
|
+
assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
|
|
111
|
+
assert.match(svc, /Environment=GDS_BACKUP_DIR=\/var\/backups\/gds/);
|
|
112
|
+
assert.match(svc, new RegExp(`User=${reEsc('lars')}`), 'runs as the app user (mirrors serviceUnit)');
|
|
113
|
+
assert.match(svc, /ReadWritePaths=\/var\/backups\/gds/, 'the only writable path is the backup dir');
|
|
114
|
+
const timer = P.backupTimer(coTenant);
|
|
115
|
+
assert.match(timer, /OnCalendar=\*-\*-\* 04:00:00 UTC/);
|
|
116
|
+
assert.match(timer, /Unit=mercury-db-backup\.service/, 'the timer drives the per-instance service');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
t('backupScriptPath resolves the CONSUMER LAYOUT, never the old /home/lars/amazonprimea monorepo path', () => {
|
|
120
|
+
// standalone → the script inside the instance's OWN pinned @bongos/core
|
|
121
|
+
const standalone = { ...coTenant, hosting_shape: 'standalone' };
|
|
122
|
+
const sPath = P.backupScriptPath(standalone);
|
|
123
|
+
assert.ok(sPath.startsWith(P.standaloneRoot(standalone) + '/'), 'under the instance root');
|
|
124
|
+
assert.match(sPath, /node_modules\/@bongos\/core\/scripts\/gds\/db-backup-nightly\.sh$/);
|
|
125
|
+
// co-tenant → this control-plane checkout's scripts/gds (mirrors serviceUnit's REPO_ROOT split)
|
|
126
|
+
assert.match(P.backupScriptPath(coTenant), /\/scripts\/gds\/db-backup-nightly\.sh$/);
|
|
127
|
+
// the regression this whole task fixes: NEVER the hardcoded old monorepo path
|
|
128
|
+
assert.doesNotMatch(P.backupService(standalone), /\/home\/lars\/amazonprimea/);
|
|
129
|
+
assert.doesNotMatch(P.backupService(coTenant), /\/home\/lars\/amazonprimea/);
|
|
130
|
+
});
|
|
131
|
+
|
|
103
132
|
t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + brand/port/db', () => {
|
|
104
133
|
const b = P.webEnvBody(coTenant); // coTenant.domain === null
|
|
105
134
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=`));
|
|
@@ -280,6 +309,27 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
|
|
|
280
309
|
assert.ok(writes.some((w) => /\/mercury\/web\.env$/.test(w)), 'wrote web.env');
|
|
281
310
|
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote systemd unit');
|
|
282
311
|
assert.ok(writes.some((w) => /\/mercury\.caddy$/.test(w)), 'wrote per-instance Caddy snippet');
|
|
312
|
+
// task 2213: EVERY instance is born with backups — dir + per-instance timer/service
|
|
313
|
+
assert.match(joined, /install -d -o lars -g lars -m 0750 \/var\/backups\/gds/, 'creates the backup dir');
|
|
314
|
+
assert.match(joined, /systemctl enable --now mercury-db-backup\.timer/, 'enables the per-instance backup timer');
|
|
315
|
+
assert.ok(writes.some((w) => /\/mercury-db-backup\.service$/.test(w)), 'wrote backup service unit');
|
|
316
|
+
assert.ok(writes.some((w) => /\/mercury-db-backup\.timer$/.test(w)), 'wrote backup timer unit');
|
|
317
|
+
// the backup timer is enabled AFTER the dir exists (else the first run exits 2)
|
|
318
|
+
assert.ok(idx(/install -d .*\/var\/backups\/gds/) < idx(/enable --now mercury-db-backup\.timer/), 'dir before timer enable');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await ta('provisionInstance(standalone): the backup unit ExecStart points at the instance\'s OWN pinned core (consumer layout)', async () => {
|
|
322
|
+
const { deps, writes } = recorder();
|
|
323
|
+
// capture the unit BODIES, not just paths, so we can assert the ExecStart path
|
|
324
|
+
const bodies = {};
|
|
325
|
+
deps.writeFile = (p, body) => { writes.push(p); bodies[p] = body; return { ok: true }; };
|
|
326
|
+
const inst = { ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury' };
|
|
327
|
+
await P.provisionInstance(inst, deps);
|
|
328
|
+
const svc = bodies[P.backupServicePath(inst)];
|
|
329
|
+
assert.ok(svc, 'wrote the backup service unit');
|
|
330
|
+
const root = P.standaloneRoot(inst);
|
|
331
|
+
assert.match(svc, new RegExp('ExecStart=/bin/bash ' + reEsc(root) + '/node_modules/@bongos/core/scripts/gds/db-backup-nightly\\.sh'));
|
|
332
|
+
assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
|
|
283
333
|
});
|
|
284
334
|
|
|
285
335
|
await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + npm ci) + migrates from the instance root', async () => {
|
|
@@ -427,6 +477,22 @@ await ta('provisionInstance(dedicated) does NOT run local createdb/migrate/seed
|
|
|
427
477
|
assert.ok(!cmds.some((c) => /INSERT INTO versions/.test(c)), 'dedicated does not run the version seed locally (it rides cloud-init)');
|
|
428
478
|
});
|
|
429
479
|
|
|
480
|
+
await ta('dedicatedUserData bakes the nightly backup: unit files (b64) + dir + timer enable (task 2213)', async () => {
|
|
481
|
+
const inst = { ...coTenant, hosting_shape: 'dedicated' };
|
|
482
|
+
const cloudInit = P.dedicatedUserData(inst);
|
|
483
|
+
// the two unit files land via write_files (base64, so no YAML block-scalar escaping)
|
|
484
|
+
assert.match(cloudInit, /write_files:/);
|
|
485
|
+
assert.match(cloudInit, new RegExp('path: ' + reEsc(P.backupServicePath(inst))));
|
|
486
|
+
assert.match(cloudInit, new RegExp('path: ' + reEsc(P.backupTimerPath(inst))));
|
|
487
|
+
assert.match(cloudInit, /encoding: b64/);
|
|
488
|
+
// the base64 content round-trips to a real unit carrying the right DB
|
|
489
|
+
const b64 = cloudInit.match(/content: ([A-Za-z0-9+/=]+)/)[1];
|
|
490
|
+
assert.match(Buffer.from(b64, 'base64').toString('utf8'), /Environment=GDS_BACKUP_DB=mercury/);
|
|
491
|
+
// and the dir is created + the timer enabled on first boot
|
|
492
|
+
assert.match(cloudInit, /install -d -o lars -g lars -m 0750 \/var\/backups\/gds/);
|
|
493
|
+
assert.match(cloudInit, /enable --now mercury mercury-db-backup\.timer/);
|
|
494
|
+
});
|
|
495
|
+
|
|
430
496
|
console.log('\nDNS UPSERT composition (P4, CONFIG override):');
|
|
431
497
|
|
|
432
498
|
// A recorder that also captures each exec's env (so we can assert the DNS hook env).
|
|
@@ -739,6 +805,9 @@ await ta('teardownInstance(privileged): systemctl + rm-under-/etc are sudo-prefi
|
|
|
739
805
|
assert.match(joined, /sudo systemctl disable --now mercury/);
|
|
740
806
|
assert.match(joined, /sudo rm -f \/etc\/systemd\/system\/mercury\.service/);
|
|
741
807
|
assert.match(joined, /sudo systemctl daemon-reload/);
|
|
808
|
+
// task 2213: the backup units come down too (the dumps + dir are left in place)
|
|
809
|
+
assert.match(joined, /sudo systemctl disable --now mercury-db-backup\.timer/);
|
|
810
|
+
assert.match(joined, /sudo rm -f \/etc\/systemd\/system\/mercury-db-backup\.service \/etc\/systemd\/system\/mercury-db-backup\.timer/);
|
|
742
811
|
});
|
|
743
812
|
|
|
744
813
|
await ta('teardownInstance(default): NO sudo (bare, unchanged)', async () => {
|