@bongos/core 1.19.376 → 1.19.378
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 +64 -24
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +5 -0
- package/docs/api-reference.md +1 -1
- package/modules/lifecycle/db-tasks.js +8 -1
- package/modules/lifecycle/db.js +2 -0
- package/modules/lifecycle/kickoff-checklist.js +138 -0
- package/modules/lifecycle/kickoff-seed.js +69 -0
- package/modules/lifecycle/module.json +4 -2
- package/modules/lifecycle/routes/kickoff-seed.js +29 -0
- package/modules/lifecycle/routes/task-write-routes.js +17 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +1 -0
- package/scripts/gds/init.js +9 -79
- package/scripts/gds/test-path-guard.js +99 -0
- package/src/bongos/auth-admission.js +23 -0
- package/src/module-api.js +5 -1
- package/tests/first_admin_kickoff_seed.mjs +56 -0
- package/tests/kickoff_checklist.mjs +107 -0
- package/tests/kickoff_seed.mjs +93 -0
- package/tests/migration_allocation.mjs +11 -0
- package/tests/provisioning_onboard_mode.mjs +3 -4
- package/tests/search_ingest.mjs +3 -2
- package/tests/setup_smoke.mjs +4 -2
- package/tests/test_path_guard.mjs +126 -0
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.378",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.378",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.378",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -1308,6 +1308,7 @@ const CHECKS = [
|
|
|
1308
1308
|
require('./routine-schedule.js').checkRoutineCadencesScheduled, // Check 25 — task 1003209: a routine that declares a cadence must have a scheduler firing it AT that period (ADR 0188)
|
|
1309
1309
|
require('./baseline-staleness.js').checkBaselineFloors, // Check 26 — task 1003210: a shrink-only baseline that never shrinks is unmanaged debt; a floor exempting one must say why
|
|
1310
1310
|
require('./client-baseurl-guard.js').checkClientBaseUrl,
|
|
1311
|
+
require('./test-path-guard.js').checkTestsUseFileUrlToPath, // Check 30 — task 1002889: a test builds its repo root with fileURLToPath, never URL.pathname (a %20 or a Windows drive slash makes require() throw at load; CI's spaceless checkout can't see it)
|
|
1311
1312
|
// Check 29 — task 1003275 / ADR 0189: a work category ORIENTS a human and steers
|
|
1312
1313
|
// Claude; it authorises nothing. The owner said so twice, and the goal's own prose
|
|
1313
1314
|
// predicts the failure: a second thing that looks like a hierarchy gets mistaken
|
package/scripts/gds/init.js
CHANGED
|
@@ -218,18 +218,11 @@ function provisioningRequest(spec) {
|
|
|
218
218
|
return body;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// The
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
return {
|
|
227
|
-
title: 'Production topology: TLS, real origins, managed Postgres',
|
|
228
|
-
kind: 'infra',
|
|
229
|
-
description: 'For a real (non-localhost) launch: front the instance with TLS (Caddy/Cloudflare/nginx), point ' +
|
|
230
|
-
'publicOrigin/buildersOrigin/statusOrigin at the real domain (the OAuth callback depends on this), and move off the bundled container DB volume to managed/backed-up Postgres.',
|
|
231
|
-
};
|
|
232
|
-
}
|
|
221
|
+
// The kickoff checklist itself (and the production-topology step the provisioning
|
|
222
|
+
// module can automate) now live in modules/lifecycle/kickoff-checklist.js — ONE list shared
|
|
223
|
+
// with the lifecycle module, which files the same steps at the founder's first
|
|
224
|
+
// sign-in on the control-plane path (task 1003239). init keeps its names.
|
|
225
|
+
const { kickoffChecklist, productionTopologyKickoff } = require('../../modules/lifecycle/kickoff-checklist');
|
|
233
226
|
|
|
234
227
|
// The lean instance branding.json: ONLY the fields the owner supplied. Everything
|
|
235
228
|
// else (theme, copy, rank labels, box subdomain pattern) resolves from
|
|
@@ -325,74 +318,11 @@ function mergeCoreDependency(pkg, { corePkg = CORE_PKG, coreRange, name = 'cloud
|
|
|
325
318
|
return { pkg: next, action: 'added' };
|
|
326
319
|
}
|
|
327
320
|
|
|
328
|
-
// The kickoff checklist, as tasks
|
|
329
|
-
//
|
|
330
|
-
// the
|
|
331
|
-
// a real, claimable task so it's tracked, never silently skipped. Per-module
|
|
332
|
-
// prerequisites are appended only for the modules the owner enabled.
|
|
321
|
+
// The kickoff checklist, as tasks — the steps init CANNOT do for you (the shared
|
|
322
|
+
// list, self-host shape). Production topology is omitted when the owner opted into
|
|
323
|
+
// provisioning: the control-plane runner covers that leg (ADR 0111 §3).
|
|
333
324
|
function kickoffTasks(spec) {
|
|
334
|
-
|
|
335
|
-
const tasks = [
|
|
336
|
-
{
|
|
337
|
-
title: 'Register a GitHub OAuth app and set its credentials',
|
|
338
|
-
kind: 'infra',
|
|
339
|
-
description: 'Auth is the hard prerequisite for every sign-in — without it the server returns 503 ' +
|
|
340
|
-
'auth_not_configured and nobody (including you) can log in. Create a GitHub OAuth app, set ' +
|
|
341
|
-
`<ENVPREFIX>_GITHUB_CLIENT_ID/SECRET, tick "Enable Device Flow" (a separate save), and set the callback URL to ${origin}/api/gds/auth/web/callback.`,
|
|
342
|
-
},
|
|
343
|
-
{
|
|
344
|
-
title: `Sign in once as ${spec.firstAdmin} to seat the founding owner`,
|
|
345
|
-
kind: 'infra',
|
|
346
|
-
description: `A fresh database has no owner. Signing in as the configured firstAdmin (${spec.firstAdmin}) ` +
|
|
347
|
-
'the first time auto-seats that account as the founding Archon (owner). Config alone does nothing — you must actually sign in.',
|
|
348
|
-
},
|
|
349
|
-
{
|
|
350
|
-
title: 'Confirm the GitHub repo binding and deploy mode',
|
|
351
|
-
kind: 'infra',
|
|
352
|
-
description: `This instance ships work to ${spec.repo.owner}/${spec.repo.name}. Confirm the git remote / repo binding ` +
|
|
353
|
-
'and pick deploy mode in config/deploy.json (ci = server-mediated PR auto-merge, or laptop). A wrong/absent repo binding lets a task grade but never merge.',
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
title: 'Set the admission posture and invite the first builders',
|
|
357
|
-
kind: 'infra',
|
|
358
|
-
description: 'Enrollment is invite-only by default. Beyond you, no one can join until you approve an access request ' +
|
|
359
|
-
'or set <ENVPREFIX>_OPEN_XENOS_ENROLLMENT=1. Decide who can join and invite/approve the first builders.',
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
title: 'Arm the merge gate before a second builder joins (ci deploy mode)',
|
|
363
|
-
kind: 'infra',
|
|
364
|
-
description: 'For a shared instance on ci deploy mode, enable branch protection on main + required checks ' +
|
|
365
|
-
'(unit / secret-scan / gate-review), so untrusted builders cannot push straight to production. No review requirement — gates are CI checks, not human approvals (ADR 0162). Skippable for a solo/local instance.',
|
|
366
|
-
},
|
|
367
|
-
];
|
|
368
|
-
// Production topology is either AUTOMATED (the owner opted into provisioning — the
|
|
369
|
-
// control-plane runner stands up TLS/DNS/managed-DB) or a tracked manual step.
|
|
370
|
-
// Keep it in its infra slot, before the look step, when it's still manual. (ADR 0111 §3)
|
|
371
|
-
if (!provisioningRequest(spec)) tasks.push(productionTopologyKickoff());
|
|
372
|
-
tasks.push({
|
|
373
|
-
title: 'Fill the instance look with your own LLM',
|
|
374
|
-
// 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
|
|
375
|
-
// kind and the task silently vanished from the checklist (the one non-infra
|
|
376
|
-
// kickoff step). Deciding the instance look is a decision by an ideator.
|
|
377
|
-
kind: 'decision',
|
|
378
|
-
discipline: 'ideator',
|
|
379
|
-
description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
|
|
380
|
-
'own LLM, not a generator). Hand docs/branding-fill-prompt.md to your AI to design a palette/fonts/wordmark, or keep the vanilla look.',
|
|
381
|
-
});
|
|
382
|
-
const enabled = new Set(Array.isArray(spec.modules) ? spec.modules : []);
|
|
383
|
-
if (enabled.has('art-pipeline')) {
|
|
384
|
-
tasks.push({ title: 'Provide a Google Gemini API key for the art pipeline', kind: 'infra',
|
|
385
|
-
description: 'The art-pipeline module needs a Google AI Studio (Gemini) key (GOOGLE_AI_STUDIO_KEY, a shared key, or per-builder keys) or it cannot generate art.' });
|
|
386
|
-
}
|
|
387
|
-
if (enabled.has('discord')) {
|
|
388
|
-
tasks.push({ title: 'Provide Discord bot token + webhooks', kind: 'infra',
|
|
389
|
-
description: 'The discord module needs a bot token + webhook URLs + DISCORD_BOT_ENABLED=true. Note: outbound/link surfaces fire on credentials even if the module flag is later turned off.' });
|
|
390
|
-
}
|
|
391
|
-
if (enabled.has('dev-box')) {
|
|
392
|
-
tasks.push({ title: 'Provide a DigitalOcean API token for dev boxes', kind: 'infra',
|
|
393
|
-
description: 'The dev-box module needs a DigitalOcean API token (and optionally Cloudflare) on the operator control plane before any builder box can be provisioned.' });
|
|
394
|
-
}
|
|
395
|
-
return tasks;
|
|
325
|
+
return kickoffChecklist(spec, { provisioned: !!provisioningRequest(spec) });
|
|
396
326
|
}
|
|
397
327
|
|
|
398
328
|
// Normalize a raw spec (from --from or the interview) to canonical shape +
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/test-path-guard.js — a test must not build a filesystem path from
|
|
3
|
+
// `URL.pathname` (task 1002889).
|
|
4
|
+
//
|
|
5
|
+
// THE BUG THIS EXISTS TO STOP. `new URL('..', import.meta.url).pathname` is a URL
|
|
6
|
+
// path, not a filesystem path: percent-encoding survives (a space stays `%20`) and
|
|
7
|
+
// on Windows it carries a leading slash before the drive letter. Handed to
|
|
8
|
+
// require() or fs it resolves nowhere — four tests threw MODULE_NOT_FOUND at load
|
|
9
|
+
// on the owner's checkout, whose path has a space in it.
|
|
10
|
+
//
|
|
11
|
+
// WHY A FITNESS CHECK AND NOT A TEST. CI checks out to a path with no space, so the
|
|
12
|
+
// broken form passes there and the suite reads green while being unrunnable on a
|
|
13
|
+
// Windows dev machine. CI can never catch the regression on its own; a static scan
|
|
14
|
+
// at a different altitude can.
|
|
15
|
+
//
|
|
16
|
+
// THE FIX IT POINTS AT. `fileURLToPath(new URL('..', import.meta.url))` from
|
|
17
|
+
// node:url (tests/fitness.mjs), or a plain relative require through
|
|
18
|
+
// createRequire(import.meta.url) (tests/provisioning_teardown_intent.mjs).
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const { execFileSync } = require('node:child_process');
|
|
25
|
+
|
|
26
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
27
|
+
const NAME = 'a test builds its repo root with fileURLToPath, never URL.pathname (task 1002889)';
|
|
28
|
+
|
|
29
|
+
// `import.meta.url).pathname` — the URL-path-as-filesystem-path defect, in both its
|
|
30
|
+
// forms: new URL('..', import.meta.url).pathname and new URL(import.meta.url).pathname.
|
|
31
|
+
const PATHNAME_ROOT = /import\.meta\.url\s*\)\s*\.pathname\b/;
|
|
32
|
+
|
|
33
|
+
// Every tracked test file. Only tests use import.meta.url this way; source is CJS.
|
|
34
|
+
function trackedTestFiles() {
|
|
35
|
+
try {
|
|
36
|
+
const out = execFileSync('git', ['ls-files', 'tests/*.mjs', 'tests/*.js', 'tests/**/*.mjs', 'tests/**/*.js'],
|
|
37
|
+
{ cwd: REPO_ROOT, encoding: 'utf8' });
|
|
38
|
+
return [...new Set(out.split('\n').map((s) => s.trim()).filter(Boolean))];
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function scanTests(files, read) {
|
|
45
|
+
const violations = [];
|
|
46
|
+
let scanned = 0;
|
|
47
|
+
for (const rel of files) {
|
|
48
|
+
const src = read(rel);
|
|
49
|
+
if (src == null) continue;
|
|
50
|
+
scanned += 1;
|
|
51
|
+
const lines = src.split('\n');
|
|
52
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
53
|
+
if (!PATHNAME_ROOT.test(lines[i])) continue;
|
|
54
|
+
violations.push(
|
|
55
|
+
`${rel}:${i + 1} builds a filesystem path from URL.pathname — a space becomes %20 and a Windows `
|
|
56
|
+
+ 'drive gains a leading slash, so require()/fs throw MODULE_NOT_FOUND at load. Use '
|
|
57
|
+
+ "fileURLToPath(new URL('..', import.meta.url)) from node:url, or a relative require via "
|
|
58
|
+
+ 'createRequire(import.meta.url).',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { violations, scanned };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function checkTestsUseFileUrlToPath({ files, read } = {}) {
|
|
66
|
+
const list = files || trackedTestFiles();
|
|
67
|
+
if (!list.length) {
|
|
68
|
+
return {
|
|
69
|
+
name: NAME, ok: false, hardFail: true,
|
|
70
|
+
violations: ['scan defect — enumerated 0 tracked test files; a broken enumeration, not a clean result.'],
|
|
71
|
+
warnings: [], note: 'static scan of tests/ for a URL.pathname used as a filesystem path.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const reader = read || ((rel) => {
|
|
75
|
+
try { return fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); } catch { return null; }
|
|
76
|
+
});
|
|
77
|
+
const { violations, scanned } = scanTests(list, reader);
|
|
78
|
+
return {
|
|
79
|
+
name: NAME,
|
|
80
|
+
ok: violations.length === 0,
|
|
81
|
+
hardFail: violations.length > 0,
|
|
82
|
+
violations,
|
|
83
|
+
warnings: [],
|
|
84
|
+
note: `${scanned} test file(s) scanned: none may hand \`import.meta.url).pathname\` to require()/fs — `
|
|
85
|
+
+ 'it keeps %20 and a Windows drive slash, so the file is unrunnable on a checkout with a space '
|
|
86
|
+
+ 'in its path while CI (no space) stays green. fileURLToPath() or a relative require is the form.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function main() {
|
|
91
|
+
const r = checkTestsUseFileUrlToPath();
|
|
92
|
+
for (const v of r.violations) console.error(`✗ ${v}`);
|
|
93
|
+
console.log(`${r.hardFail ? 'FAIL' : 'PASS'} ${r.note}`);
|
|
94
|
+
process.exit(r.hardFail ? 1 : 0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (require.main === module) main();
|
|
98
|
+
|
|
99
|
+
module.exports = { checkTestsUseFileUrlToPath, scanTests, trackedTestFiles, PATHNAME_ROOT };
|
|
@@ -261,6 +261,7 @@ async function maybeSeatFirstAdmin(builder) {
|
|
|
261
261
|
const seated = await seatFirstAdminIfUnclaimed(builder.id, builder.github_login);
|
|
262
262
|
if (seated) {
|
|
263
263
|
log.info(`[auth] first-admin bootstrap: seated ${builder.github_login} as Archon (configured firstAdmin, no prior Archon).`);
|
|
264
|
+
await seedKickoffChecklist(builder.id);
|
|
264
265
|
return { ...builder, rank: 'archon' };
|
|
265
266
|
}
|
|
266
267
|
} catch (err) {
|
|
@@ -269,6 +270,27 @@ async function maybeSeatFirstAdmin(builder) {
|
|
|
269
270
|
return builder;
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
// seedKickoffChecklist — the founder's first board (task 1003239). A hub-provisioned
|
|
274
|
+
// project reaches its first sign-in with an EMPTY tasks table (the scaffold ran init
|
|
275
|
+
// --no-seed; no Archon existed to file anything), so the moment the founder is seated
|
|
276
|
+
// is the first moment the kickoff checklist CAN be filed. Resolved through the
|
|
277
|
+
// `lifecycle.kickoffSeed` port — this is a KERNEL_FILE and may not require a module
|
|
278
|
+
// (ADR 0083 / ADR 0091 §1); the port itself skips a board that already has tasks.
|
|
279
|
+
// BEST-EFFORT: a seed hiccup must never fail the sign-in that just seated the owner.
|
|
280
|
+
// deps.port is a test seam; production resolves the seam registry.
|
|
281
|
+
async function seedKickoffChecklist(builderId, deps = {}) {
|
|
282
|
+
const port = deps.port !== undefined ? deps.port : seams.resolveOptional('lifecycle.kickoffSeed');
|
|
283
|
+
if (!port) return null;
|
|
284
|
+
try {
|
|
285
|
+
const r = await port.seedIfEmpty({ builderId });
|
|
286
|
+
if (r && r.seeded) log.info(`[auth] first-admin bootstrap: filed ${r.seeded} kickoff task(s) under ${r.versionId} — the founder's first board.`);
|
|
287
|
+
return r;
|
|
288
|
+
} catch (err) {
|
|
289
|
+
log.error(`[auth] first-admin bootstrap: kickoff seed failed (non-fatal): ${err.message}`);
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
272
294
|
// safeSyncRankRole — give a builder the seeded rank-role matching their rank at
|
|
273
295
|
// REGISTRATION (BV1.R95b, ADR 0151). governance_002's builder→role backfill is
|
|
274
296
|
// run-once, so before this every builder admitted after that migration had no role
|
|
@@ -303,6 +325,7 @@ module.exports = {
|
|
|
303
325
|
firstTimeAdmissionBlocked,
|
|
304
326
|
joinabilityMode,
|
|
305
327
|
maybeSeatFirstAdmin,
|
|
328
|
+
seedKickoffChecklist,
|
|
306
329
|
membershipKind,
|
|
307
330
|
openEnrollmentEnabled,
|
|
308
331
|
reportActivityRollup,
|
package/src/module-api.js
CHANGED
|
@@ -1192,8 +1192,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1192
1192
|
// landed since 1.19.374 with no explicit bump. run 32909701598. (task 1002620)
|
|
1193
1193
|
// 1.19.376 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1194
1194
|
// landed since 1.19.375 with no explicit bump. run 32910331206. (task 1002620)
|
|
1195
|
+
// 1.19.377 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1196
|
+
// landed since 1.19.376 with no explicit bump. run 33025011131. (task 1002620)
|
|
1197
|
+
// 1.19.378 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1198
|
+
// landed since 1.19.377 with no explicit bump. run 33025512093. (task 1002620)
|
|
1195
1199
|
// ---------------------------------------------------------------------------
|
|
1196
|
-
const CORE_VERSION = '1.19.
|
|
1200
|
+
const CORE_VERSION = '1.19.378'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1197
1201
|
|
|
1198
1202
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1199
1203
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// tests/first_admin_kickoff_seed.mjs — the kernel side of task 1003239: the first-admin
|
|
2
|
+
// bootstrap files the founder's first board through the `lifecycle.kickoffSeed` port,
|
|
3
|
+
// and NOTHING about that can fail the sign-in that just seated the owner.
|
|
4
|
+
//
|
|
5
|
+
// DB-free: the helper is exercised with an injected port (deps.port), the same seam the
|
|
6
|
+
// production call resolves from the registry. Covers the three shapes the port can take
|
|
7
|
+
// at runtime — absent (an instance without the lifecycle module), throwing, seeding.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/first_admin_kickoff_seed.mjs
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { seedKickoffChecklist } = require('../src/bongos/auth-admission.js');
|
|
17
|
+
const seams = require('../src/module-seams.js');
|
|
18
|
+
|
|
19
|
+
test('no port registered → a quiet null, never a throw (an instance without lifecycle signs in as before)', async () => {
|
|
20
|
+
assert.equal(seams.hasProvider('lifecycle.kickoffSeed'), false, 'nothing in-process registered the port');
|
|
21
|
+
assert.equal(await seedKickoffChecklist(1), null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('an absent port passed explicitly is the same quiet null', async () => {
|
|
25
|
+
assert.equal(await seedKickoffChecklist(1, { port: null }), null);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('a throwing port is swallowed — best-effort, the seat stands', async () => {
|
|
29
|
+
const port = { seedIfEmpty: async () => { throw new Error('db went away'); } };
|
|
30
|
+
assert.equal(await seedKickoffChecklist(1, { port }), null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a seeding port is handed the seated builder id and its result flows back', async () => {
|
|
34
|
+
const calls = [];
|
|
35
|
+
const port = { seedIfEmpty: async (arg) => { calls.push(arg); return { seeded: 3, versionId: 'V1' }; } };
|
|
36
|
+
const r = await seedKickoffChecklist(42, { port });
|
|
37
|
+
assert.deepEqual(calls, [{ builderId: 42 }]);
|
|
38
|
+
assert.deepEqual(r, { seeded: 3, versionId: 'V1' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a skipping port (board already populated) flows back untouched', async () => {
|
|
42
|
+
const port = { seedIfEmpty: async () => ({ seeded: 0, skipped: 'tasks_exist' }) };
|
|
43
|
+
assert.deepEqual(await seedKickoffChecklist(42, { port }), { seeded: 0, skipped: 'tasks_exist' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('production resolves the port from the seam registry', async () => {
|
|
47
|
+
const calls = [];
|
|
48
|
+
seams.registerProvider('lifecycle.kickoffSeed', { seedIfEmpty: async (arg) => { calls.push(arg); return { seeded: 1, versionId: 'V1' }; } });
|
|
49
|
+
try {
|
|
50
|
+
const r = await seedKickoffChecklist(9);
|
|
51
|
+
assert.deepEqual(calls, [{ builderId: 9 }]);
|
|
52
|
+
assert.equal(r.seeded, 1);
|
|
53
|
+
} finally {
|
|
54
|
+
seams._reset();
|
|
55
|
+
}
|
|
56
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// tests/kickoff_checklist.mjs — the shared kickoff checklist (task 1003239).
|
|
2
|
+
//
|
|
3
|
+
// One list, two filers: `bongos init` at scaffold time (self-host) and the lifecycle
|
|
4
|
+
// module at the founder's first sign-in (hub-provisioned, where init ran --no-seed).
|
|
5
|
+
// The first-sign-in shape must contain only what is still TRUE after a hub standup,
|
|
6
|
+
// and must never name a surface that does not exist — the task's own rule: a blank
|
|
7
|
+
// board beats untrue copy.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/kickoff_checklist.mjs
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { kickoffChecklist, productionTopologyKickoff } = require('../modules/lifecycle/kickoff-checklist.js');
|
|
17
|
+
const { VALID_KINDS, VALID_DISCIPLINES } = require('../modules/lifecycle/task-classifier.js');
|
|
18
|
+
|
|
19
|
+
const spec = () => ({
|
|
20
|
+
firstAdmin: 'founder', repo: { owner: 'o', name: 'n' },
|
|
21
|
+
domains: { publicOrigin: 'https://p.example' }, modules: [],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('default (self-host): the seven owner-manual steps, topology included', () => {
|
|
25
|
+
const t = kickoffChecklist(spec());
|
|
26
|
+
assert.equal(t.length, 7);
|
|
27
|
+
assert.ok(t.some((x) => /OAuth/i.test(x.title)), 'OAuth app step');
|
|
28
|
+
assert.ok(t.some((x) => /founding owner/i.test(x.title)), 'first sign-in step');
|
|
29
|
+
assert.ok(t.some((x) => /repo binding/i.test(x.title)), 'repo binding step');
|
|
30
|
+
assert.ok(t.some((x) => /Production topology/i.test(x.title)), 'topology step');
|
|
31
|
+
assert.ok(t.every((x) => x.title && x.description), 'every step has a title + description');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('provisioned: only the topology step is omitted', () => {
|
|
35
|
+
const t = kickoffChecklist(spec(), { provisioned: true });
|
|
36
|
+
assert.equal(t.length, 6);
|
|
37
|
+
assert.ok(!t.some((x) => /Production topology/i.test(x.title)));
|
|
38
|
+
assert.ok(t.some((x) => /OAuth/i.test(x.title)), 'the self-host owner still registers their OAuth app');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('firstSignIn: only what is still true after a hub standup', () => {
|
|
42
|
+
const t = kickoffChecklist(spec(), { firstSignIn: true });
|
|
43
|
+
assert.deepEqual(
|
|
44
|
+
t.map((x) => x.title),
|
|
45
|
+
[
|
|
46
|
+
'Decide how people join, and invite the first builders',
|
|
47
|
+
'Arm the merge gate before a second builder joins (ci deploy mode)',
|
|
48
|
+
'Fill the instance look with your own LLM',
|
|
49
|
+
],
|
|
50
|
+
);
|
|
51
|
+
for (const x of t) {
|
|
52
|
+
assert.ok(!/OAuth|founding owner|repo binding|topology/i.test(x.title + x.description),
|
|
53
|
+
`"${x.title}" names a step the standup already did`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('firstSignIn: the admission step points at surfaces that exist, never an env var', () => {
|
|
58
|
+
const join = kickoffChecklist(spec(), { firstSignIn: true })[0];
|
|
59
|
+
assert.ok(!/ENVPREFIX|OPEN_XENOS_ENROLLMENT|config\//.test(join.description), 'no env var or config file a hub founder cannot reach');
|
|
60
|
+
assert.match(join.description, /How people join/, 'the hub setting (task 1003044)');
|
|
61
|
+
assert.match(join.description, /manage page/, 'where the setting lives');
|
|
62
|
+
assert.match(join.description, /Watch page/, 'where invites live');
|
|
63
|
+
assert.match(join.description, /GitHub username/, 'how an invite is addressed');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('firstSignIn: per-module keys still follow the enabled modules', () => {
|
|
67
|
+
const t = kickoffChecklist({ ...spec(), modules: ['art-pipeline', 'discord'] }, { firstSignIn: true });
|
|
68
|
+
assert.equal(t.length, 5);
|
|
69
|
+
assert.ok(t.some((x) => /Gemini/i.test(x.title)));
|
|
70
|
+
assert.ok(t.some((x) => /Discord/i.test(x.title)));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('firstSignIn: never reads spec.repo or spec.firstAdmin (the steps that need them are omitted)', () => {
|
|
74
|
+
assert.doesNotThrow(() => kickoffChecklist({}, { firstSignIn: true }));
|
|
75
|
+
assert.equal(kickoffChecklist({}, { firstSignIn: true }).length, 3);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('every variant satisfies the DB CHECK vocabulary (tasks_kind_check / tasks_discipline_check)', () => {
|
|
79
|
+
const full = { ...spec(), modules: ['art-pipeline', 'discord', 'dev-box'] };
|
|
80
|
+
const all = [
|
|
81
|
+
...kickoffChecklist(full),
|
|
82
|
+
...kickoffChecklist(full, { provisioned: true }),
|
|
83
|
+
...kickoffChecklist(full, { firstSignIn: true }),
|
|
84
|
+
productionTopologyKickoff(),
|
|
85
|
+
];
|
|
86
|
+
for (const t of all) {
|
|
87
|
+
assert.ok(VALID_KINDS.has(t.kind), `"${t.title}": kind '${t.kind}' violates tasks_kind_check`);
|
|
88
|
+
if (t.discipline !== undefined) {
|
|
89
|
+
assert.ok(VALID_DISCIPLINES.has(t.discipline), `"${t.title}": discipline '${t.discipline}' violates tasks_discipline_check`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('the look step is a decision BY an ideator in every variant', () => {
|
|
95
|
+
for (const opts of [{}, { provisioned: true }, { firstSignIn: true }]) {
|
|
96
|
+
const look = kickoffChecklist(spec(), opts).find((t) => /instance look/i.test(t.title));
|
|
97
|
+
assert.ok(look, `look step present for ${JSON.stringify(opts)}`);
|
|
98
|
+
assert.equal(look.kind, 'decision');
|
|
99
|
+
assert.equal(look.discipline, 'ideator');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('returns fresh objects — a filer mutating one cannot poison the next call', () => {
|
|
104
|
+
const a = kickoffChecklist(spec(), { firstSignIn: true });
|
|
105
|
+
a[0].title = 'mutated';
|
|
106
|
+
assert.notEqual(kickoffChecklist(spec(), { firstSignIn: true })[0].title, 'mutated');
|
|
107
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// tests/kickoff_seed.mjs — the lifecycle module files the kickoff checklist at the
|
|
2
|
+
// founder's first sign-in, and only onto an EMPTY board (task 1003239).
|
|
3
|
+
//
|
|
4
|
+
// The seed is idempotent by emptiness, not by path: the self-host path already filed
|
|
5
|
+
// its checklist before anyone signed in, so it must be skipped there without any
|
|
6
|
+
// "was I provisioned" marker. Every case runs against an in-memory pool + insert —
|
|
7
|
+
// no database.
|
|
8
|
+
//
|
|
9
|
+
// Run: node --test tests/kickoff_seed.mjs
|
|
10
|
+
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { seedKickoffIfEmpty, specFromBranding } = require('../modules/lifecycle/kickoff-seed.js');
|
|
17
|
+
const buildRouter = require('../modules/lifecycle/routes/kickoff-seed.js');
|
|
18
|
+
const api = require('../src/module-api.js');
|
|
19
|
+
|
|
20
|
+
const spec = { firstAdmin: 'founder', repo: { owner: 'o', name: 'n' }, domains: { publicOrigin: 'https://p.example' }, modules: [] };
|
|
21
|
+
|
|
22
|
+
function fakePool({ taskCount, versions }) {
|
|
23
|
+
const queries = [];
|
|
24
|
+
return {
|
|
25
|
+
queries,
|
|
26
|
+
query: async (sql) => {
|
|
27
|
+
queries.push(sql);
|
|
28
|
+
if (/FROM tasks/.test(sql)) return { rows: [{ n: taskCount }] };
|
|
29
|
+
if (/FROM versions/.test(sql)) return { rows: versions };
|
|
30
|
+
throw new Error(`unexpected query: ${sql}`);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('an empty board gets the first-sign-in checklist, as ready tasks attributed to the founder', async () => {
|
|
36
|
+
const inserted = [];
|
|
37
|
+
const pool = fakePool({ taskCount: 0, versions: [{ id: 'V1' }] });
|
|
38
|
+
const r = await seedKickoffIfEmpty({ builderId: 7 }, { pool, spec, createTask: async (row) => { inserted.push(row); return row; } });
|
|
39
|
+
assert.deepEqual(r, { seeded: 3, versionId: 'V1' });
|
|
40
|
+
assert.equal(inserted.length, 3);
|
|
41
|
+
for (const row of inserted) {
|
|
42
|
+
assert.equal(row.versionId, 'V1');
|
|
43
|
+
assert.equal(row.status, 'ready', 'claimable from the first board, not backlog');
|
|
44
|
+
assert.equal(row.source, 'first-sign-in');
|
|
45
|
+
assert.equal(row.createdBy, 7);
|
|
46
|
+
assert.ok(row.title && row.description);
|
|
47
|
+
}
|
|
48
|
+
assert.match(inserted[0].title, /how people join/i);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a board that already has tasks is left alone (the self-host path seeded at init)', async () => {
|
|
52
|
+
const inserted = [];
|
|
53
|
+
const pool = fakePool({ taskCount: 12, versions: [{ id: 'V1' }] });
|
|
54
|
+
const r = await seedKickoffIfEmpty({ builderId: 7 }, { pool, spec, createTask: async (row) => inserted.push(row) });
|
|
55
|
+
assert.deepEqual(r, { seeded: 0, skipped: 'tasks_exist' });
|
|
56
|
+
assert.equal(inserted.length, 0);
|
|
57
|
+
assert.ok(!pool.queries.some((q) => /FROM versions/.test(q)), 'never even looks for a version');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('no version to file under → skipped, nothing inserted', async () => {
|
|
61
|
+
const inserted = [];
|
|
62
|
+
const pool = fakePool({ taskCount: 0, versions: [] });
|
|
63
|
+
const r = await seedKickoffIfEmpty({ builderId: 7 }, { pool, spec, createTask: async (row) => inserted.push(row) });
|
|
64
|
+
assert.deepEqual(r, { seeded: 0, skipped: 'no_version' });
|
|
65
|
+
assert.equal(inserted.length, 0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('the version query prefers the building one, then the first by id', async () => {
|
|
69
|
+
const pool = fakePool({ taskCount: 0, versions: [{ id: 'V1' }] });
|
|
70
|
+
await seedKickoffIfEmpty({ builderId: 1 }, { pool, spec, createTask: async () => {} });
|
|
71
|
+
const q = pool.queries.find((s) => /FROM versions/.test(s));
|
|
72
|
+
assert.match(q, /status = 'building'/);
|
|
73
|
+
assert.match(q, /LIMIT 1/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('specFromBranding reads the instance pack and keeps modules empty', () => {
|
|
77
|
+
const s = specFromBranding({ firstAdmin: 'f', repo: { owner: 'a', name: 'b' }, domains: { publicOrigin: 'https://x' } });
|
|
78
|
+
assert.deepEqual(s, { firstAdmin: 'f', repo: { owner: 'a', name: 'b' }, domains: { publicOrigin: 'https://x' }, modules: [] });
|
|
79
|
+
assert.deepEqual(specFromBranding({}).repo, {});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the route factory registers the port once, returns an empty router, and the port delegates', async () => {
|
|
83
|
+
const before = api.hasProvider('lifecycle.kickoffSeed');
|
|
84
|
+
const router = buildRouter();
|
|
85
|
+
buildRouter(); // a rebuild must not trip the single-provider guard
|
|
86
|
+
assert.ok(api.hasProvider('lifecycle.kickoffSeed'));
|
|
87
|
+
assert.equal(typeof router, 'function');
|
|
88
|
+
assert.equal(router.stack.length, 0, 'seeding has no HTTP surface');
|
|
89
|
+
const port = api.resolveOptional('lifecycle.kickoffSeed');
|
|
90
|
+
const r = await port.seedIfEmpty({ builderId: 3 }, { pool: fakePool({ taskCount: 1, versions: [] }), spec });
|
|
91
|
+
assert.deepEqual(r, { seeded: 0, skipped: 'tasks_exist' });
|
|
92
|
+
assert.equal(before, false, 'this test registered it, nothing else in-process had');
|
|
93
|
+
});
|
|
@@ -203,6 +203,7 @@ function shouldReserveMigrationNumber(task) {
|
|
|
203
203
|
[() => db.updateTaskNeedsMigration(1, false), 'needs_migration'],
|
|
204
204
|
[() => db.updateTaskModuleKey(1, null), 'module_key'],
|
|
205
205
|
[() => db.updateTaskDescription(1, 'edited body'), 'description'], // task 1002647
|
|
206
|
+
[() => db.updateTaskTitle(1, 'corrected title'), 'title'], // task 1002792
|
|
206
207
|
];
|
|
207
208
|
for (const [call, column] of setters) {
|
|
208
209
|
const before = seen.length;
|
|
@@ -218,6 +219,16 @@ function shouldReserveMigrationNumber(task) {
|
|
|
218
219
|
}
|
|
219
220
|
});
|
|
220
221
|
|
|
222
|
+
await test('PATCH /tasks/:id accepts title so a wrong title is correctable (task 1002792)', () => {
|
|
223
|
+
assert.ok(
|
|
224
|
+
/title: \{ type: 'string', minLength: 1, maxLength: LIMITS\.TITLE \}/.test(routesSrc),
|
|
225
|
+
'the PATCH schema must declare title with the create-time limits — a title can be rewritten, never blanked'
|
|
226
|
+
);
|
|
227
|
+
assert.ok(/const hasTitle = Object\.prototype\.hasOwnProperty\.call\(body, 'title'\)/.test(routesSrc), 'title is gated like every other field');
|
|
228
|
+
assert.ok(/&& !hasTitle\) \{/.test(routesSrc), 'a title-only PATCH is a supported request, not no_supported_fields');
|
|
229
|
+
assert.ok(/db\.updateTaskTitle\(id, body\.title\.trim\(\)\)/.test(routesSrc), 'the write goes through the allowlisted setter');
|
|
230
|
+
});
|
|
231
|
+
|
|
221
232
|
await test('PATCH /tasks/:id accepts needs_migration so a wrong flag is correctable', () => {
|
|
222
233
|
assert.ok(
|
|
223
234
|
/needs_migration: \{ type: 'boolean' \}/.test(routesSrc),
|
|
@@ -21,9 +21,8 @@ import { createRequire } from 'node:module';
|
|
|
21
21
|
import http from 'node:http';
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
|
-
const ROOT = new URL('..', import.meta.url).pathname;
|
|
25
24
|
|
|
26
|
-
const provisioning = require(
|
|
25
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
27
26
|
|
|
28
27
|
let passed = 0, failed = 0;
|
|
29
28
|
function t(name, fn) {
|
|
@@ -99,11 +98,11 @@ provisioning.enqueueIntent = async () => ({ intent: { id: 1, state: 'pending' },
|
|
|
99
98
|
provisioning.recordEvent = async () => {};
|
|
100
99
|
provisioning.getInstanceBySlug = async () => null;
|
|
101
100
|
|
|
102
|
-
const api = require(
|
|
101
|
+
const api = require('../src/module-api.js');
|
|
103
102
|
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
104
103
|
|
|
105
104
|
const express = require('express');
|
|
106
|
-
const provisioningRoutes = require(
|
|
105
|
+
const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
|
|
107
106
|
const app = express();
|
|
108
107
|
app.use(express.json());
|
|
109
108
|
app.use((_req, res, next) => {
|
package/tests/search_ingest.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { strict as assert } from 'node:assert';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
const require = createRequire(import.meta.url);
|
|
16
17
|
const ingest = require('../scripts/gds/search-ingest-md.js');
|
|
@@ -61,7 +62,7 @@ test('SENSITIVE_DOC_GLOBS is the curated source of truth (subtrees end with /)',
|
|
|
61
62
|
|
|
62
63
|
test('collectMarkdown finds the repo corpus but excludes node_modules/.git/worktrees/public', () => {
|
|
63
64
|
const path = require('node:path');
|
|
64
|
-
const repoRoot = path.resolve(new URL('..', import.meta.url)
|
|
65
|
+
const repoRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
65
66
|
const real = collectMarkdown(repoRoot, repoRoot);
|
|
66
67
|
// Assert on paths RELATIVE to the repo root — the suite may run from inside a
|
|
67
68
|
// git worktree (…/.claude/worktrees/<name>/…), so the ABSOLUTE paths would all
|
|
@@ -77,7 +78,7 @@ test('collectMarkdown finds the repo corpus but excludes node_modules/.git/workt
|
|
|
77
78
|
|
|
78
79
|
test('chunkFile yields well-formed rows (source_kind doc, hash, rank, sequential index)', () => {
|
|
79
80
|
const path = require('node:path');
|
|
80
|
-
const repoRoot = path.resolve(new URL('..', import.meta.url)
|
|
81
|
+
const repoRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
81
82
|
const rows = chunkFile(path.join(repoRoot, 'docs', 'adr', '0016-trust-boundary-server-enforced-permissions.md'));
|
|
82
83
|
assert.ok(rows.length > 0);
|
|
83
84
|
rows.forEach((r, i) => {
|