@bongos/core 1.19.376 → 1.19.377
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 +57 -17
- 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/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 +3 -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/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/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.377",
|
|
6
|
+
"core_contract": "1.19.377",
|
|
7
|
+
"source_commit": "90c7b43a6f0265aaf5d5c117cbe2721c18bd6c35",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-26T23:56:55.300Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 335,
|
|
13
13
|
"agent_docs_stubbed": 22,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1565,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1922,
|
|
20
|
+
"tree_sha256": "ef3ea529acb199c38d7dc4bb8bc657b694727f8e8ffaa8fdf2af230913416726",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4154,6 +4154,16 @@
|
|
|
4154
4154
|
"mode": "0000644",
|
|
4155
4155
|
"sha256": "0027e5e8a3b718a16dcae5ad4387b4773525d429944e712228fbd38a5335e23a"
|
|
4156
4156
|
},
|
|
4157
|
+
{
|
|
4158
|
+
"path": "modules/lifecycle/kickoff-checklist.js",
|
|
4159
|
+
"mode": "0000644",
|
|
4160
|
+
"sha256": "829dd13fb87033dba1db8d3003a0359e1be8bb97198df419edb2617558bfd984"
|
|
4161
|
+
},
|
|
4162
|
+
{
|
|
4163
|
+
"path": "modules/lifecycle/kickoff-seed.js",
|
|
4164
|
+
"mode": "0000644",
|
|
4165
|
+
"sha256": "f26a507ea55169df9f4841edb2347610f3dea9b7eac729c034c038a0edd1c7af"
|
|
4166
|
+
},
|
|
4157
4167
|
{
|
|
4158
4168
|
"path": "modules/lifecycle/lifecycle.js",
|
|
4159
4169
|
"mode": "0000644",
|
|
@@ -4187,7 +4197,7 @@
|
|
|
4187
4197
|
{
|
|
4188
4198
|
"path": "modules/lifecycle/module.json",
|
|
4189
4199
|
"mode": "0000644",
|
|
4190
|
-
"sha256": "
|
|
4200
|
+
"sha256": "401e4121a3f335ae0ae532b6b7ccff63e668b3a53520a0ff3f0d1a4223b02020"
|
|
4191
4201
|
},
|
|
4192
4202
|
{
|
|
4193
4203
|
"path": "modules/lifecycle/pollers/publish-reconciler.js",
|
|
@@ -4244,6 +4254,11 @@
|
|
|
4244
4254
|
"mode": "0000644",
|
|
4245
4255
|
"sha256": "0e9430101ce99d0a84f1893d13bcf1434af13325c2ce531e693b6bce42cb9385"
|
|
4246
4256
|
},
|
|
4257
|
+
{
|
|
4258
|
+
"path": "modules/lifecycle/routes/kickoff-seed.js",
|
|
4259
|
+
"mode": "0000644",
|
|
4260
|
+
"sha256": "f5090caaf0d0f32feff5bf4263e65ecf831ad6fd10ae5b053788c987e1a86399"
|
|
4261
|
+
},
|
|
4247
4262
|
{
|
|
4248
4263
|
"path": "modules/lifecycle/routes/lifecycle.js",
|
|
4249
4264
|
"mode": "0000644",
|
|
@@ -4927,12 +4942,12 @@
|
|
|
4927
4942
|
{
|
|
4928
4943
|
"path": "package-lock.json",
|
|
4929
4944
|
"mode": "0000644",
|
|
4930
|
-
"sha256": "
|
|
4945
|
+
"sha256": "e5e8b0e3c6879a8636f96f6bdce1c4fd8d9b06521e431bd6051bb2be2ea96518"
|
|
4931
4946
|
},
|
|
4932
4947
|
{
|
|
4933
4948
|
"path": "package.json",
|
|
4934
4949
|
"mode": "0000644",
|
|
4935
|
-
"sha256": "
|
|
4950
|
+
"sha256": "3057a201c65b95c06512dfbf5642b4d3de10c513382e771b478f0eb43dad3ff8"
|
|
4936
4951
|
},
|
|
4937
4952
|
{
|
|
4938
4953
|
"path": "public-docs/index.html",
|
|
@@ -5627,7 +5642,7 @@
|
|
|
5627
5642
|
{
|
|
5628
5643
|
"path": "scripts/gds/fitness.js",
|
|
5629
5644
|
"mode": "0000644",
|
|
5630
|
-
"sha256": "
|
|
5645
|
+
"sha256": "4dec7f6bee219ca3c11dbf5b9c3380de286264a66830fd8e2d37018a989eb83e"
|
|
5631
5646
|
},
|
|
5632
5647
|
{
|
|
5633
5648
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -5747,7 +5762,7 @@
|
|
|
5747
5762
|
{
|
|
5748
5763
|
"path": "scripts/gds/init.js",
|
|
5749
5764
|
"mode": "0000644",
|
|
5750
|
-
"sha256": "
|
|
5765
|
+
"sha256": "280197b62032374c11824c4f97ec713725e28e2b57cfd6c8aa42d493b02c4ed7"
|
|
5751
5766
|
},
|
|
5752
5767
|
{
|
|
5753
5768
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -6399,6 +6414,11 @@
|
|
|
6399
6414
|
"mode": "0000644",
|
|
6400
6415
|
"sha256": "88b9b721b3911957b13aa5e8023e6d41534e34afd4bbc70f40435efc52e2017b"
|
|
6401
6416
|
},
|
|
6417
|
+
{
|
|
6418
|
+
"path": "scripts/gds/test-path-guard.js",
|
|
6419
|
+
"mode": "0000644",
|
|
6420
|
+
"sha256": "d3e542cf977f9aadad8f856defa6d809f3604e184cd261fbee7738aa1da736ea"
|
|
6421
|
+
},
|
|
6402
6422
|
{
|
|
6403
6423
|
"path": "scripts/gds/triage.js",
|
|
6404
6424
|
"mode": "0000644",
|
|
@@ -6542,7 +6562,7 @@
|
|
|
6542
6562
|
{
|
|
6543
6563
|
"path": "src/bongos/auth-admission.js",
|
|
6544
6564
|
"mode": "0000644",
|
|
6545
|
-
"sha256": "
|
|
6565
|
+
"sha256": "9978b10e2e675b6f7c1211dd74c8cf302395b2f47062eba5df376203ee77d341"
|
|
6546
6566
|
},
|
|
6547
6567
|
{
|
|
6548
6568
|
"path": "src/bongos/auth-config.js",
|
|
@@ -6817,7 +6837,7 @@
|
|
|
6817
6837
|
{
|
|
6818
6838
|
"path": "src/module-api.js",
|
|
6819
6839
|
"mode": "0000644",
|
|
6820
|
-
"sha256": "
|
|
6840
|
+
"sha256": "5287468c8e1a2ba691e02ff2150bcb425b00db6c0ce676b063d32cb20ef640c5"
|
|
6821
6841
|
},
|
|
6822
6842
|
{
|
|
6823
6843
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7694,6 +7714,11 @@
|
|
|
7694
7714
|
"mode": "0000644",
|
|
7695
7715
|
"sha256": "5466ddf0b7e0298206d46da7b84cfd3826e0f549395088bf5ed91397d15f1746"
|
|
7696
7716
|
},
|
|
7717
|
+
{
|
|
7718
|
+
"path": "tests/first_admin_kickoff_seed.mjs",
|
|
7719
|
+
"mode": "0000644",
|
|
7720
|
+
"sha256": "8006f4696e6bae21b84c9e8ec25cb7a7d34c5899f187d2ac59b2a6dcbb6f92d1"
|
|
7721
|
+
},
|
|
7697
7722
|
{
|
|
7698
7723
|
"path": "tests/first_run_engine_check.mjs",
|
|
7699
7724
|
"mode": "0000644",
|
|
@@ -8374,6 +8399,16 @@
|
|
|
8374
8399
|
"mode": "0000644",
|
|
8375
8400
|
"sha256": "db9ad86df4c9593db89f91ef2306c30eb33ae56bebe5b210a4b3a1912f67d42c"
|
|
8376
8401
|
},
|
|
8402
|
+
{
|
|
8403
|
+
"path": "tests/kickoff_checklist.mjs",
|
|
8404
|
+
"mode": "0000644",
|
|
8405
|
+
"sha256": "d0849053675cb4705fe6774b1a8ac6b3edb5b43a758775446fad257be6cb9ba3"
|
|
8406
|
+
},
|
|
8407
|
+
{
|
|
8408
|
+
"path": "tests/kickoff_seed.mjs",
|
|
8409
|
+
"mode": "0000644",
|
|
8410
|
+
"sha256": "416d413757a13fbddb5841ea8a2c503077d7e6067bc1c58ce1f4d416b8d8487f"
|
|
8411
|
+
},
|
|
8377
8412
|
{
|
|
8378
8413
|
"path": "tests/known_failing_ratchet.mjs",
|
|
8379
8414
|
"mode": "0000644",
|
|
@@ -8832,7 +8867,7 @@
|
|
|
8832
8867
|
{
|
|
8833
8868
|
"path": "tests/provisioning_onboard_mode.mjs",
|
|
8834
8869
|
"mode": "0000644",
|
|
8835
|
-
"sha256": "
|
|
8870
|
+
"sha256": "97d04ee39cc06b720d4a5b42f3e3cc614f64d9462b27471a1af7598b5e2929be"
|
|
8836
8871
|
},
|
|
8837
8872
|
{
|
|
8838
8873
|
"path": "tests/provisioning_settings.mjs",
|
|
@@ -9057,7 +9092,7 @@
|
|
|
9057
9092
|
{
|
|
9058
9093
|
"path": "tests/search_ingest.mjs",
|
|
9059
9094
|
"mode": "0000644",
|
|
9060
|
-
"sha256": "
|
|
9095
|
+
"sha256": "8d685a46ab9f4cf9aaa71b9ee78774d95c16daa27c15a229b8628a97688b9a55"
|
|
9061
9096
|
},
|
|
9062
9097
|
{
|
|
9063
9098
|
"path": "tests/search_ingest_db.mjs",
|
|
@@ -9192,7 +9227,7 @@
|
|
|
9192
9227
|
{
|
|
9193
9228
|
"path": "tests/setup_smoke.mjs",
|
|
9194
9229
|
"mode": "0000644",
|
|
9195
|
-
"sha256": "
|
|
9230
|
+
"sha256": "c55f6c506758d973b7cbe750a873e8c38d02006e1495f027fedb0ebfc84c7186"
|
|
9196
9231
|
},
|
|
9197
9232
|
{
|
|
9198
9233
|
"path": "tests/ship_api_client_pathspec.mjs",
|
|
@@ -9459,6 +9494,11 @@
|
|
|
9459
9494
|
"mode": "0000644",
|
|
9460
9495
|
"sha256": "3ff18b286a68b244a272b3f2606ae33b298622347aae97305152f235ca57b330"
|
|
9461
9496
|
},
|
|
9497
|
+
{
|
|
9498
|
+
"path": "tests/test_path_guard.mjs",
|
|
9499
|
+
"mode": "0000644",
|
|
9500
|
+
"sha256": "bbb0b063bffc6e394114bc4895da5006d8797a0317ed38f1c7f9b989841d5eab"
|
|
9501
|
+
},
|
|
9462
9502
|
{
|
|
9463
9503
|
"path": "tests/tool-scan-fixtures/README.md",
|
|
9464
9504
|
"mode": "0000644",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// modules/lifecycle/kickoff-checklist.js — the kickoff checklist: the launch steps a fresh
|
|
2
|
+
// instance needs that the platform CANNOT do for its owner, as task specs (task 1003239).
|
|
3
|
+
//
|
|
4
|
+
// ONE list, two filers. `bongos init` files it at scaffold time on the self-host path
|
|
5
|
+
// (scripts/gds/init.js kickoffTasks); the lifecycle module files it at the founder's
|
|
6
|
+
// FIRST SIGN-IN on the control-plane path (modules/lifecycle/kickoff-seed.js), where
|
|
7
|
+
// the scaffold ran `init --no-seed` and no Archon existed to file anything — walk
|
|
8
|
+
// finding W4: the founder reached an empty board. Moved here from init.js so both
|
|
9
|
+
// filers read the same steps and the copy cannot drift. It lives with the lifecycle
|
|
10
|
+
// module because tasks are its domain and the kernel doorway must not grow a domain
|
|
11
|
+
// import (ADR 0091 §1); init is a core CLI and, like abandon.js or audit-deps.js,
|
|
12
|
+
// may require module code directly. Pure — imports nothing.
|
|
13
|
+
//
|
|
14
|
+
// Each step becomes a real, claimable task so it is tracked, never silently skipped
|
|
15
|
+
// (grounded in the onboarding/deploy surfaces; the task 1204 mapping pass). Per-module
|
|
16
|
+
// prerequisites are appended only for the modules the owner enabled.
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
// The one kickoff step the provisioning module AUTOMATES (ADR 0111 §3): standing up
|
|
21
|
+
// production hosting/TLS/DNS/managed-DB. Kept as a named builder so init can either
|
|
22
|
+
// FILE it (no provisioning requested, or the request couldn't be enqueued) or OMIT
|
|
23
|
+
// it (a request went in — the control-plane runner covers this leg).
|
|
24
|
+
function productionTopologyKickoff() {
|
|
25
|
+
return {
|
|
26
|
+
title: 'Production topology: TLS, real origins, managed Postgres',
|
|
27
|
+
kind: 'infra',
|
|
28
|
+
description: 'For a real (non-localhost) launch: front the instance with TLS (Caddy/Cloudflare/nginx), point ' +
|
|
29
|
+
'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.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The admission step as the SELF-HOST owner meets it: an env flag on their own box.
|
|
34
|
+
const ADMISSION_SELF_HOST = {
|
|
35
|
+
title: 'Set the admission posture and invite the first builders',
|
|
36
|
+
kind: 'infra',
|
|
37
|
+
description: 'Enrollment is invite-only by default. Beyond you, no one can join until you approve an access request ' +
|
|
38
|
+
'or set <ENVPREFIX>_OPEN_XENOS_ENROLLMENT=1. Decide who can join and invite/approve the first builders.',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// The same decision as a HUB-PROVISIONED founder meets it: the door is the project's
|
|
42
|
+
// "How people join" setting on the hub (task 1003044), never an env var they cannot
|
|
43
|
+
// reach; invites are the Watch page's "Invite by GitHub username". Names only surfaces
|
|
44
|
+
// that exist — a blank board is better than untrue copy (task 1003239).
|
|
45
|
+
const ADMISSION_AT_FIRST_SIGN_IN = {
|
|
46
|
+
title: 'Decide how people join, and invite the first builders',
|
|
47
|
+
kind: 'infra',
|
|
48
|
+
description: 'Unless you opened the door when you created the project, nobody beyond you can join until you invite them. ' +
|
|
49
|
+
'Set "How people join" (open / apply to join / invite-only) from your project\'s manage page on the hub, ' +
|
|
50
|
+
'and invite the first builders by GitHub username from your project\'s Watch page.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const MERGE_GATE = {
|
|
54
|
+
title: 'Arm the merge gate before a second builder joins (ci deploy mode)',
|
|
55
|
+
kind: 'infra',
|
|
56
|
+
description: 'For a shared instance on ci deploy mode, enable branch protection on main + required checks ' +
|
|
57
|
+
'(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.',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const INSTANCE_LOOK = {
|
|
61
|
+
title: 'Fill the instance look with your own LLM',
|
|
62
|
+
// 'ideator' is a DISCIPLINE, not a kind — tasks_kind_check rejects it as a
|
|
63
|
+
// kind and the task silently vanished from the checklist (the one non-infra
|
|
64
|
+
// kickoff step). Deciding the instance look is a decision by an ideator.
|
|
65
|
+
kind: 'decision',
|
|
66
|
+
discipline: 'ideator',
|
|
67
|
+
description: 'init wrote a lean config/branding.json and left the LOOK to you (a locked project rule: brand with your ' +
|
|
68
|
+
'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.',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Steps that are already TRUE the moment a hub-provisioned founder signs in: auth
|
|
72
|
+
// works (they just used it), the sign-in is this one, and the scaffold bound the repo
|
|
73
|
+
// and chose the deploy mode. Filed on the self-host path only.
|
|
74
|
+
function selfHostOnlySteps(spec, origin) {
|
|
75
|
+
return [
|
|
76
|
+
{
|
|
77
|
+
title: 'Register a GitHub OAuth app and set its credentials',
|
|
78
|
+
kind: 'infra',
|
|
79
|
+
description: 'Auth is the hard prerequisite for every sign-in — without it the server returns 503 ' +
|
|
80
|
+
'auth_not_configured and nobody (including you) can log in. Create a GitHub OAuth app, set ' +
|
|
81
|
+
`<ENVPREFIX>_GITHUB_CLIENT_ID/SECRET, tick "Enable Device Flow" (a separate save), and set the callback URL to ${origin}/api/gds/auth/web/callback.`,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
title: `Sign in once as ${spec.firstAdmin} to seat the founding owner`,
|
|
85
|
+
kind: 'infra',
|
|
86
|
+
description: `A fresh database has no owner. Signing in as the configured firstAdmin (${spec.firstAdmin}) ` +
|
|
87
|
+
'the first time auto-seats that account as the founding Archon (owner). Config alone does nothing — you must actually sign in.',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: 'Confirm the GitHub repo binding and deploy mode',
|
|
91
|
+
kind: 'infra',
|
|
92
|
+
description: `This instance ships work to ${spec.repo.owner}/${spec.repo.name}. Confirm the git remote / repo binding ` +
|
|
93
|
+
'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.',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Per-module prerequisites — a key the module cannot run without.
|
|
99
|
+
function moduleKeySteps(spec) {
|
|
100
|
+
const enabled = new Set(Array.isArray(spec.modules) ? spec.modules : []);
|
|
101
|
+
const tasks = [];
|
|
102
|
+
if (enabled.has('art-pipeline')) {
|
|
103
|
+
tasks.push({ title: 'Provide a Google Gemini API key for the art pipeline', kind: 'infra',
|
|
104
|
+
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.' });
|
|
105
|
+
}
|
|
106
|
+
if (enabled.has('discord')) {
|
|
107
|
+
tasks.push({ title: 'Provide Discord bot token + webhooks', kind: 'infra',
|
|
108
|
+
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.' });
|
|
109
|
+
}
|
|
110
|
+
if (enabled.has('dev-box')) {
|
|
111
|
+
tasks.push({ title: 'Provide a DigitalOcean API token for dev boxes', kind: 'infra',
|
|
112
|
+
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.' });
|
|
113
|
+
}
|
|
114
|
+
return tasks;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// kickoffChecklist(spec, opts) → task specs ({ title, kind, description[, discipline] }).
|
|
118
|
+
// provisioned — the owner opted into provisioning at init: the control-plane runner
|
|
119
|
+
// covers production topology, so that step is omitted (ADR 0111 §3).
|
|
120
|
+
// firstSignIn — filed the moment the founder of a hub-provisioned project is seated:
|
|
121
|
+
// only the steps still true after the standup, with the admission door
|
|
122
|
+
// described as the hub setting it now is.
|
|
123
|
+
// Pure — no I/O; the caller files them.
|
|
124
|
+
function kickoffChecklist(spec = {}, { provisioned = false, firstSignIn = false } = {}) {
|
|
125
|
+
const origin = (spec.domains && spec.domains.publicOrigin) || 'http://localhost:3000';
|
|
126
|
+
const tasks = firstSignIn ? [] : selfHostOnlySteps(spec, origin);
|
|
127
|
+
tasks.push(firstSignIn ? ADMISSION_AT_FIRST_SIGN_IN : ADMISSION_SELF_HOST);
|
|
128
|
+
tasks.push(MERGE_GATE);
|
|
129
|
+
// Production topology is either AUTOMATED (the owner opted into provisioning — the
|
|
130
|
+
// control-plane runner stands up TLS/DNS/managed-DB) or a tracked manual step.
|
|
131
|
+
// Keep it in its infra slot, before the look step, when it's still manual.
|
|
132
|
+
if (!provisioned && !firstSignIn) tasks.push(productionTopologyKickoff());
|
|
133
|
+
tasks.push(INSTANCE_LOOK);
|
|
134
|
+
tasks.push(...moduleKeySteps(spec));
|
|
135
|
+
return tasks.map((t) => ({ ...t }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { kickoffChecklist, productionTopologyKickoff };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// modules/lifecycle/kickoff-seed.js — file the kickoff checklist at the founder's first
|
|
2
|
+
// sign-in (task 1003239; behind the `lifecycle.kickoffSeed` port).
|
|
3
|
+
//
|
|
4
|
+
// WHY HERE, WHY THEN. `bongos init` files the kickoff checklist as tasks — but the
|
|
5
|
+
// control-plane scaffold runs `init --no-seed` (scripts/gds/provision-repo.js), and a
|
|
6
|
+
// hub-provisioned project has no Archon until its founder signs in, so nothing could
|
|
7
|
+
// be filed earlier: the founder reached an EMPTY board (walk finding W4). The kernel's
|
|
8
|
+
// first-admin seating (src/bongos/auth-admission.js) resolves this port the moment the
|
|
9
|
+
// owner is seated — the first moment the checklist CAN be filed.
|
|
10
|
+
//
|
|
11
|
+
// IDEMPOTENT BY EMPTINESS, not by path. It seeds only when the tasks table is empty:
|
|
12
|
+
// the self-host path already filed its checklist before anyone signed in, so it is
|
|
13
|
+
// skipped there with no "was I provisioned" marker to keep true. The caller treats a
|
|
14
|
+
// failure as best-effort — a seed hiccup must never fail the sign-in that just seated
|
|
15
|
+
// the owner.
|
|
16
|
+
//
|
|
17
|
+
// THE BOUNDARY (ADR 0093 §1): core is reached only through ../../src/module-api.js.
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const api = require('../../src/module-api');
|
|
22
|
+
const db = require('./db');
|
|
23
|
+
const { kickoffChecklist } = require('./kickoff-checklist');
|
|
24
|
+
|
|
25
|
+
// The spec the checklist reads, from the instance's own branding pack. `modules` is
|
|
26
|
+
// empty on purpose: a hub-provisioned project is scaffolded with every optional module
|
|
27
|
+
// off, and its founder cannot have enabled one before their first sign-in.
|
|
28
|
+
function specFromBranding(b = api.branding()) {
|
|
29
|
+
return {
|
|
30
|
+
firstAdmin: b.firstAdmin,
|
|
31
|
+
repo: b.repo || {},
|
|
32
|
+
domains: { publicOrigin: b.domains && b.domains.publicOrigin },
|
|
33
|
+
modules: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The version the checklist lands under: the building one, else the first by id. The
|
|
38
|
+
// scaffold seeds exactly one (provision-repo.js seedFirstVersionCmd).
|
|
39
|
+
const FIRST_VERSION_SQL = `SELECT id FROM versions ORDER BY (status = 'building') DESC, id ASC LIMIT 1`;
|
|
40
|
+
|
|
41
|
+
// seedKickoffIfEmpty({ builderId }, deps) → { seeded, versionId } | { seeded: 0, skipped }.
|
|
42
|
+
// deps.pool / deps.createTask / deps.spec are test seams; production reads the doorway.
|
|
43
|
+
async function seedKickoffIfEmpty({ builderId = null } = {}, deps = {}) {
|
|
44
|
+
const pool = deps.pool || api.pool;
|
|
45
|
+
const insert = deps.createTask || ((row) => db.createTask(row));
|
|
46
|
+
const { rows: [{ n }] } = await pool.query('SELECT count(*)::int AS n FROM tasks');
|
|
47
|
+
if (n > 0) return { seeded: 0, skipped: 'tasks_exist' };
|
|
48
|
+
const { rows: versions } = await pool.query(FIRST_VERSION_SQL);
|
|
49
|
+
if (!versions.length) return { seeded: 0, skipped: 'no_version' };
|
|
50
|
+
const versionId = versions[0].id;
|
|
51
|
+
const steps = kickoffChecklist(deps.spec || specFromBranding(), { firstSignIn: true });
|
|
52
|
+
let seeded = 0;
|
|
53
|
+
for (const t of steps) {
|
|
54
|
+
await insert({
|
|
55
|
+
versionId,
|
|
56
|
+
title: t.title,
|
|
57
|
+
description: t.description || '',
|
|
58
|
+
status: 'ready',
|
|
59
|
+
kind: t.kind || 'infra',
|
|
60
|
+
discipline: t.discipline || 'engineer',
|
|
61
|
+
source: 'first-sign-in',
|
|
62
|
+
createdBy: builderId,
|
|
63
|
+
});
|
|
64
|
+
seeded += 1;
|
|
65
|
+
}
|
|
66
|
+
return { seeded, versionId };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = { seedKickoffIfEmpty, specFromBranding, FIRST_VERSION_SQL };
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
"help-requests",
|
|
22
22
|
"recommendations",
|
|
23
23
|
"closeness",
|
|
24
|
-
"visuals"
|
|
24
|
+
"visuals",
|
|
25
|
+
"kickoff-seed"
|
|
25
26
|
],
|
|
26
27
|
"pollers": [
|
|
27
28
|
"publish-reconciler"
|
|
28
29
|
]
|
|
29
30
|
},
|
|
30
31
|
"provides": [
|
|
31
|
-
"lifecycle"
|
|
32
|
+
"lifecycle",
|
|
33
|
+
"lifecycle.kickoffSeed"
|
|
32
34
|
],
|
|
33
35
|
"consumes": []
|
|
34
36
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/lifecycle/routes/kickoff-seed.js — registers the `lifecycle.kickoffSeed` port
|
|
4
|
+
// (task 1003239): the founder's first board, filed the moment the kernel seats them.
|
|
5
|
+
//
|
|
6
|
+
// Resolved by ONE caller — the first-admin bootstrap in src/bongos/auth-admission.js —
|
|
7
|
+
// through resolveOptional, so an instance without this module signs in exactly as
|
|
8
|
+
// before. A route factory for a routeless capability, for the reason routes/lifecycle.js
|
|
9
|
+
// and routes/rank-sync.js give: mountModuleRoutes is the boot hook that runs in the live
|
|
10
|
+
// server AND whenever a test rebuilds the router. The router stays EMPTY — seeding has
|
|
11
|
+
// no HTTP surface, and an endpoint here would let anyone re-file a founder's checklist.
|
|
12
|
+
|
|
13
|
+
const express = require('express');
|
|
14
|
+
const api = require('../../../src/module-api');
|
|
15
|
+
const seed = require('../kickoff-seed');
|
|
16
|
+
|
|
17
|
+
// Delegates at call time (never spreads) so a stubbed seed still bites.
|
|
18
|
+
const kickoffSeedPort = {
|
|
19
|
+
seedIfEmpty: (...a) => seed.seedKickoffIfEmpty(...a),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
module.exports = function buildLifecycleKickoffSeedRouter() {
|
|
23
|
+
if (!api.hasProvider('lifecycle.kickoffSeed')) {
|
|
24
|
+
api.registerProvider('lifecycle.kickoffSeed', kickoffSeedPort);
|
|
25
|
+
}
|
|
26
|
+
return express.Router();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports.kickoffSeedPort = kickoffSeedPort;
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.377",
|
|
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.377",
|
|
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.377",
|
|
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,10 @@ 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)
|
|
1195
1197
|
// ---------------------------------------------------------------------------
|
|
1196
|
-
const CORE_VERSION = '1.19.
|
|
1198
|
+
const CORE_VERSION = '1.19.377'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1197
1199
|
|
|
1198
1200
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1199
1201
|
// 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
|
+
});
|
|
@@ -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) => {
|
package/tests/setup_smoke.mjs
CHANGED
|
@@ -22,8 +22,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import os from 'node:os';
|
|
24
24
|
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
25
26
|
|
|
26
|
-
const REPO_ROOT = path.resolve(path.dirname(
|
|
27
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
28
|
const SETUP_JS = path.join(REPO_ROOT, 'scripts/gds/setup.js');
|
|
28
29
|
|
|
29
30
|
const tests = [];
|
|
@@ -36,7 +37,8 @@ function makeFreshHome() {
|
|
|
36
37
|
|
|
37
38
|
function runSetup(args, env = {}) {
|
|
38
39
|
return spawnSync('node', [SETUP_JS, ...args], {
|
|
39
|
-
|
|
40
|
+
// os.homedir() reads USERPROFILE on Windows, so a HOME override alone leaks the real home there.
|
|
41
|
+
env: { ...process.env, ...env, ...(env.HOME ? { USERPROFILE: env.HOME } : {}) },
|
|
40
42
|
encoding: 'utf8',
|
|
41
43
|
timeout: 15000,
|
|
42
44
|
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// tests/test_path_guard.mjs — the guard that keeps a test runnable on a checkout
|
|
2
|
+
// whose path has a space in it (task 1002889).
|
|
3
|
+
//
|
|
4
|
+
// Four tests built their repo root from URL.pathname and threw MODULE_NOT_FOUND at
|
|
5
|
+
// load on the owner's Windows checkout, while CI (no space in its path) stayed
|
|
6
|
+
// green. The guard is tested on synthetic sources, not only the live tree: a scan
|
|
7
|
+
// that passes because the tree is clean today proves nothing about what it REFUSES.
|
|
8
|
+
//
|
|
9
|
+
// The defective form is spelled by concatenation below so this file never trips the
|
|
10
|
+
// guard it tests.
|
|
11
|
+
//
|
|
12
|
+
// Of the four files the task named, tests/provisioning_github_app_routes.mjs had
|
|
13
|
+
// already been converted before this landed (915d273b, task 1003236 — fileURLToPath
|
|
14
|
+
// at its line 17), so it is absent from this change on purpose; the live-tree case
|
|
15
|
+
// at the bottom is what proves all four are clean.
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/test_path_guard.mjs
|
|
18
|
+
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const { checkTestsUseFileUrlToPath, scanTests } = require('../scripts/gds/test-path-guard.js');
|
|
24
|
+
|
|
25
|
+
let passed = 0;
|
|
26
|
+
let failed = 0;
|
|
27
|
+
function test(name, fn) {
|
|
28
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
29
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const from = (map) => ({
|
|
33
|
+
files: Object.keys(map),
|
|
34
|
+
read: (rel) => (rel in map ? map[rel] : null),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const BAD_TAIL = 'import.meta.url)' + '.pathname';
|
|
38
|
+
|
|
39
|
+
// --- THE DEFECT ------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
test('the shipped defect is refused, with its line and the fix', () => {
|
|
42
|
+
const { violations } = scanTests(...Object.values(from({
|
|
43
|
+
'tests/bad.mjs':
|
|
44
|
+
"import { createRequire } from 'node:module';\n"
|
|
45
|
+
+ `const ROOT = new URL('..', ${BAD_TAIL};\n`
|
|
46
|
+
+ 'const mod = require(`${ROOT}/modules/x.js`);\n',
|
|
47
|
+
})));
|
|
48
|
+
assert.equal(violations.length, 1);
|
|
49
|
+
assert.match(violations[0], /bad\.mjs:2/, 'names the file and line');
|
|
50
|
+
assert.match(violations[0], /%20/, 'says WHY it breaks');
|
|
51
|
+
assert.match(violations[0], /fileURLToPath/, 'and what to write instead');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the dirname form is the same defect', () => {
|
|
55
|
+
const { violations } = scanTests(...Object.values(from({
|
|
56
|
+
'tests/smoke.mjs': `const REPO_ROOT = path.resolve(path.dirname(new URL(${BAD_TAIL}), '..');\n`,
|
|
57
|
+
})));
|
|
58
|
+
assert.equal(violations.length, 1);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('every site in a file is reported, not just the first', () => {
|
|
62
|
+
const { violations } = scanTests(...Object.values(from({
|
|
63
|
+
'tests/two.mjs':
|
|
64
|
+
`const a = path.resolve(new URL('..', ${BAD_TAIL});\n`
|
|
65
|
+
+ 'x();\n'
|
|
66
|
+
+ `const b = path.resolve(new URL('..', ${BAD_TAIL});\n`,
|
|
67
|
+
})));
|
|
68
|
+
assert.equal(violations.length, 2);
|
|
69
|
+
assert.match(violations[1], /two\.mjs:3/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// --- WHAT IT MUST NOT FLAG -------------------------------------------------
|
|
73
|
+
|
|
74
|
+
test('fileURLToPath and a relative createRequire are the correct forms — never flagged', () => {
|
|
75
|
+
const { violations } = scanTests(...Object.values(from({
|
|
76
|
+
'tests/good_a.mjs':
|
|
77
|
+
"import { fileURLToPath } from 'node:url';\n"
|
|
78
|
+
+ "const ROOT = fileURLToPath(new URL('..', import.meta.url));\n",
|
|
79
|
+
'tests/good_b.mjs':
|
|
80
|
+
"const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');\n",
|
|
81
|
+
'tests/good_c.mjs':
|
|
82
|
+
"const require = createRequire(import.meta.url);\n"
|
|
83
|
+
+ "const mod = require('../modules/x.js');\n",
|
|
84
|
+
})));
|
|
85
|
+
assert.deepEqual(violations, []);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('a request URL parsed for its pathname is not the defect', () => {
|
|
89
|
+
// http tests routinely do this; the guard is about import.meta.url only.
|
|
90
|
+
const { violations } = scanTests(...Object.values(from({
|
|
91
|
+
'tests/routes.mjs': "const { pathname } = new URL(req.url, 'http://x');\nconst p = new URL(req.url, base).pathname;\n",
|
|
92
|
+
})));
|
|
93
|
+
assert.deepEqual(violations, []);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// --- THE SCAN MUST NOT SILENTLY SUCCEED ------------------------------------
|
|
97
|
+
|
|
98
|
+
test('an empty enumeration is a hard FAIL, not a clean pass', () => {
|
|
99
|
+
const r = checkTestsUseFileUrlToPath({ files: [], read: () => null });
|
|
100
|
+
assert.equal(r.hardFail, true);
|
|
101
|
+
assert.match(r.violations[0], /scan defect/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('an unreadable file is skipped without being counted as scanned', () => {
|
|
105
|
+
const { scanned } = scanTests(['gone.mjs'], () => null);
|
|
106
|
+
assert.equal(scanned, 0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('the check reports hardFail so CI actually stops', () => {
|
|
110
|
+
const r = checkTestsUseFileUrlToPath(from({
|
|
111
|
+
'tests/bad.mjs': `const ROOT = new URL('..', ${BAD_TAIL};\n`,
|
|
112
|
+
}));
|
|
113
|
+
assert.equal(r.ok, false);
|
|
114
|
+
assert.equal(r.hardFail, true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// --- THE LIVE TREE ---------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
test('the tracked tests/ tree is clean (the four sites of task 1002889 are fixed)', () => {
|
|
120
|
+
const r = checkTestsUseFileUrlToPath();
|
|
121
|
+
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
122
|
+
assert.ok(/^\d+ test file\(s\) scanned/.test(r.note) && !r.note.startsWith('0 '), r.note);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
console.log(`\ntest_path_guard: ${passed} passed, ${failed} failed`);
|
|
126
|
+
process.exit(failed ? 1 : 0);
|