@bongos/core 1.21.147 → 1.21.149
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 +28 -18
- package/.claude/skills/bongos-feedback/SKILL.md +2 -2
- package/bin/bongos.js +5 -2
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/core-origin-follow.js +185 -0
- package/scripts/gds/core-pin-sync.js +31 -5
- package/scripts/gds/feedback-send.js +59 -14
- package/src/module-api.js +1 -1
- package/tests/core_origin_follow.mjs +287 -0
- package/tests/core_pin_sync.mjs +7 -2
- package/tests/instance_feedback.mjs +45 -1
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.21.
|
|
6
|
-
"core_contract": "1.21.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.21.149",
|
|
6
|
+
"core_contract": "1.21.149",
|
|
7
|
+
"source_commit": "59efe78dbbc530e6743f98d50abc605e2184adcc",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-
|
|
9
|
+
"built_at": "2026-10-19T21:44:07.784Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 608,
|
|
13
13
|
"agent_docs_stubbed": 28,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3168,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3805,
|
|
20
|
+
"tree_sha256": "2fb626947478be776e71613c82d973ebf073298ea8d6e5561f4e604e1438d661",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{
|
|
43
43
|
"path": ".claude/skills/bongos-feedback/SKILL.md",
|
|
44
44
|
"mode": "0000644",
|
|
45
|
-
"sha256": "
|
|
45
|
+
"sha256": "8b295d76d7cc4c9e0f2549db0f1ac857aa8e76a92b3e00898f95820a32c855ba"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"path": ".claude/skills/bug-triage/SKILL.md",
|
|
@@ -312,7 +312,7 @@
|
|
|
312
312
|
{
|
|
313
313
|
"path": "bin/bongos.js",
|
|
314
314
|
"mode": "0000755",
|
|
315
|
-
"sha256": "
|
|
315
|
+
"sha256": "10da188550dd74f74a8598b8815076cf098908196f366aad1f6ca39e16c68c33"
|
|
316
316
|
},
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/README.md",
|
|
@@ -2927,7 +2927,7 @@
|
|
|
2927
2927
|
{
|
|
2928
2928
|
"path": "docs/module-api-changelog.md",
|
|
2929
2929
|
"mode": "0000644",
|
|
2930
|
-
"sha256": "
|
|
2930
|
+
"sha256": "af2424a88f8836e7035c00a52637fbbf137bf1b716130a360868fa50bab54b2d"
|
|
2931
2931
|
},
|
|
2932
2932
|
{
|
|
2933
2933
|
"path": "docs/modules-contract.md",
|
|
@@ -10552,12 +10552,12 @@
|
|
|
10552
10552
|
{
|
|
10553
10553
|
"path": "package-lock.json",
|
|
10554
10554
|
"mode": "0000644",
|
|
10555
|
-
"sha256": "
|
|
10555
|
+
"sha256": "d03c2a175c60400695aecccbe5c9ef87b085521c6bf92176c6d2ed08e514c8a6"
|
|
10556
10556
|
},
|
|
10557
10557
|
{
|
|
10558
10558
|
"path": "package.json",
|
|
10559
10559
|
"mode": "0000644",
|
|
10560
|
-
"sha256": "
|
|
10560
|
+
"sha256": "9d6713f305cdfe492805906778e171c75c42e4fd8dff5935ad9245d185b6ce22"
|
|
10561
10561
|
},
|
|
10562
10562
|
{
|
|
10563
10563
|
"path": "public-docs/index.html",
|
|
@@ -10577,7 +10577,7 @@
|
|
|
10577
10577
|
{
|
|
10578
10578
|
"path": "release-notes.json",
|
|
10579
10579
|
"mode": "0000644",
|
|
10580
|
-
"sha256": "
|
|
10580
|
+
"sha256": "9394c3c784137020e70389b269a68f36f8f2d0757b306a1f64622b8b5886f368"
|
|
10581
10581
|
},
|
|
10582
10582
|
{
|
|
10583
10583
|
"path": "scripts/cron/style_guide_audit.sh",
|
|
@@ -10954,10 +10954,15 @@
|
|
|
10954
10954
|
"mode": "0000644",
|
|
10955
10955
|
"sha256": "9dd80d2c37602c59cb75db0be02cfadc737fc64891a81d03f5be5f0ea5e35a27"
|
|
10956
10956
|
},
|
|
10957
|
+
{
|
|
10958
|
+
"path": "scripts/gds/core-origin-follow.js",
|
|
10959
|
+
"mode": "0000644",
|
|
10960
|
+
"sha256": "5d12058796dc7373584fc8447ee83b8da0820a447e783b2462156f1ec158b8c5"
|
|
10961
|
+
},
|
|
10957
10962
|
{
|
|
10958
10963
|
"path": "scripts/gds/core-pin-sync.js",
|
|
10959
10964
|
"mode": "0000644",
|
|
10960
|
-
"sha256": "
|
|
10965
|
+
"sha256": "9e2e6dd476938df51bf027396df8a92e0ed2ca96a9adf59cb666ad2681cc721e"
|
|
10961
10966
|
},
|
|
10962
10967
|
{
|
|
10963
10968
|
"path": "scripts/gds/coreB-carve-proof.sh",
|
|
@@ -11097,7 +11102,7 @@
|
|
|
11097
11102
|
{
|
|
11098
11103
|
"path": "scripts/gds/feedback-send.js",
|
|
11099
11104
|
"mode": "0000644",
|
|
11100
|
-
"sha256": "
|
|
11105
|
+
"sha256": "e3c82d95c9412126d0c064d40267c7ae7befbc3cebdba99c496c884dceb4a750"
|
|
11101
11106
|
},
|
|
11102
11107
|
{
|
|
11103
11108
|
"path": "scripts/gds/fetch-art-key.js",
|
|
@@ -12952,7 +12957,7 @@
|
|
|
12952
12957
|
{
|
|
12953
12958
|
"path": "src/module-api.js",
|
|
12954
12959
|
"mode": "0000644",
|
|
12955
|
-
"sha256": "
|
|
12960
|
+
"sha256": "b89f1cd4820d3d4257eabf44a666b44c722c9e159dd561bbced94784cceaf8ea"
|
|
12956
12961
|
},
|
|
12957
12962
|
{
|
|
12958
12963
|
"path": "src/module-loader/catalog.js",
|
|
@@ -13974,10 +13979,15 @@
|
|
|
13974
13979
|
"mode": "0000644",
|
|
13975
13980
|
"sha256": "4612301c02a9a6f9927a49f76a2b49d2892e8266580cf31bcd47dd5b36ad71b0"
|
|
13976
13981
|
},
|
|
13982
|
+
{
|
|
13983
|
+
"path": "tests/core_origin_follow.mjs",
|
|
13984
|
+
"mode": "0000644",
|
|
13985
|
+
"sha256": "1b136186f5d91fe9f63034559115ba505959a742712e4103a143a7ede0917869"
|
|
13986
|
+
},
|
|
13977
13987
|
{
|
|
13978
13988
|
"path": "tests/core_pin_sync.mjs",
|
|
13979
13989
|
"mode": "0000644",
|
|
13980
|
-
"sha256": "
|
|
13990
|
+
"sha256": "34d78bb4998adaec12f5666d4ac58815ebceaccff1d560ca978c58ba64190575"
|
|
13981
13991
|
},
|
|
13982
13992
|
{
|
|
13983
13993
|
"path": "tests/core_update_banner.mjs",
|
|
@@ -16087,7 +16097,7 @@
|
|
|
16087
16097
|
{
|
|
16088
16098
|
"path": "tests/instance_feedback.mjs",
|
|
16089
16099
|
"mode": "0000644",
|
|
16090
|
-
"sha256": "
|
|
16100
|
+
"sha256": "a0a558b9d9e0698fa4b6078a2f2e4e2d9679e517149876809c1478743db0f6ff"
|
|
16091
16101
|
},
|
|
16092
16102
|
{
|
|
16093
16103
|
"path": "tests/instance_manifest.mjs",
|
|
@@ -14,7 +14,7 @@ Sends a note about Bongos — the tool — straight to its maintainers (tasks 10
|
|
|
14
14
|
|
|
15
15
|
**When.** File when the builder (a) labels it — "Bongos feedback: …", "tell the Bongos team" — that **is** the go; (b) wishes — "Bongos should…" (**Suggestions count**: send with `--idea`, even if you also fix it in their project); or (c) vents that Bongos is broken — **File it without being asked**, don't argue them out of it. Only about **Bongos itself** (a `bongos`/`/builder-*` command, claim, ship, grade, deploy, upgrade, hall, Settings, sign-in, credits, how it organises work). Not their own project's code, Claude, their laptop or GitHub, nor a rule working as designed. **One filing per distinct problem per session.**
|
|
16
16
|
|
|
17
|
-
**Write it yourself — Never paste the rant.** First line = what went wrong, in at most twelve words (it becomes the title); then what they did, what they expected, what happened, the exact error line. A wish (`--idea`) asks the maintainers to decide something, so it
|
|
17
|
+
**Write it yourself — Never paste the rant.** First line = what went wrong, in at most twelve words (it becomes the title); then what they did, what they expected, what happened, the exact error line. A wish (`--idea`) asks the maintainers to decide something, so open it as an ask (task 1004704): one question of at most twelve words, two or three answers with what each leads to, your pick and why; the rest follows as detail. Write that shape yourself — run no helper first: a project on an older core may not have one, and a failed command before the send is noise (task 1004759). No code, file contents, secrets or personal data.
|
|
18
18
|
|
|
19
19
|
**Send** (stdin, so nothing in the text is mangled by the shell):
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ bongos feedback --message-file - <<'EOF'
|
|
|
24
24
|
EOF
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Add `--idea` for a wish. The command prints one line — relay it, then carry on. Proactive filing: one short sentence, e.g. *"I've sent that to the Bongos team."* If it says
|
|
27
|
+
Add `--idea` for a wish. The command prints one line — relay it, then carry on. Proactive filing: one short sentence, e.g. *"I've sent that to the Bongos team."* If it says "Not sent", relay that in one line and never retry: "runs only on your computer" and "older Bongos" both mean this project cannot send yet, and the second names the file the note is saved in — send it with `--message-file <that file>` after the project is upgraded. Don't send the builder to GitHub or "the Bongos team" elsewhere instead, and don't call it sent.
|
|
28
28
|
|
|
29
29
|
## Files this skill touches
|
|
30
30
|
|
package/bin/bongos.js
CHANGED
|
@@ -195,8 +195,11 @@ function main(argv) {
|
|
|
195
195
|
// of its own, so after a core update lands it resolves the main checkout's older core
|
|
196
196
|
// and the ship fails on a bug the pin already fixed. `ship` installs the pinned core
|
|
197
197
|
// into the checkout and re-runs under it; scripts/gds/core-pin-sync.js.
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// First, both `ship` and `start` (an instance's session start) follow a core move the
|
|
199
|
+
// hub pushed to GitHub: one line saying so, and on a clean main a fast-forward plus
|
|
200
|
+
// install (task 1004732; scripts/gds/core-origin-follow.js).
|
|
201
|
+
if (!COMPILED && (verb === 'ship' || verb === 'start')) {
|
|
202
|
+
const synced = require('../scripts/gds/core-origin-follow.js').ensureCurrentCore({ coreRoot: REPO_ROOT, argv, verb });
|
|
200
203
|
if (synced != null) return synced;
|
|
201
204
|
}
|
|
202
205
|
|
|
@@ -3131,5 +3131,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3131
3131
|
landed since 1.21.145 with no explicit bump. run 39077391288. (task 1002620)
|
|
3132
3132
|
1.21.147 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3133
3133
|
landed since 1.21.146 with no explicit bump. run 39274828902. (task 1002620)
|
|
3134
|
+
1.21.148 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3135
|
+
landed since 1.21.147 with no explicit bump. run 39286989989. (task 1002620)
|
|
3136
|
+
1.21.149 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3137
|
+
landed since 1.21.148 with no explicit bump. run 39329687123. (task 1002620)
|
|
3134
3138
|
---------------------------------------------------------------------------
|
|
3135
3139
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.149",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.149",
|
|
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.21.
|
|
3
|
+
"version": "1.21.149",
|
|
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/release-notes.json
CHANGED
|
@@ -9896,5 +9896,17 @@
|
|
|
9896
9896
|
"id": "1004731",
|
|
9897
9897
|
"text": "After a project's software is updated, its owner now sees a one-time note saying which version it moved to, when, and what changed, and the Software update page lists the recent updates, including any that failed and were undo"
|
|
9898
9898
|
}
|
|
9899
|
+
],
|
|
9900
|
+
"1.21.148": [
|
|
9901
|
+
{
|
|
9902
|
+
"id": "1004732",
|
|
9903
|
+
"text": "When the hub upgrades a project's core, your laptop now notices at the next session start or ship, says so in one line, and on a clean main catches up and reinstalls the new core by itself."
|
|
9904
|
+
}
|
|
9905
|
+
],
|
|
9906
|
+
"1.21.149": [
|
|
9907
|
+
{
|
|
9908
|
+
"id": "1004759",
|
|
9909
|
+
"text": "When a project's site is too old to send Bongos feedback, the command now says 'Not sent' and keeps the note, instead of claiming it was sent."
|
|
9910
|
+
}
|
|
9899
9911
|
]
|
|
9900
9912
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// scripts/gds/core-origin-follow.js — the laptop follows a core move the hub made (task 1004732)
|
|
2
|
+
//
|
|
3
|
+
// WHY. When the hub moves a hosted project's core (provision-core-upgrade.js), it commits
|
|
4
|
+
// the new pin — package.json + lockfile, usually a vendored `file:vendor/bongos-core-X.tgz`
|
|
5
|
+
// rewritten to an exact registry version — on the box and pushes it to the project's
|
|
6
|
+
// GitHub repo. The owner's laptop checkout never hears of it: the next session runs the
|
|
7
|
+
// old core, and the next ship merges against the old pin. Seen on toast (1.21.20 on the
|
|
8
|
+
// laptop while fixes sat in newer releases).
|
|
9
|
+
//
|
|
10
|
+
// THE FIX. Before `bongos start` (an instance's session start: its CLAUDE.md opens every
|
|
11
|
+
// session with it, and a package-installed instance carries no SessionStart hooks) and
|
|
12
|
+
// before `bongos ship`, the dispatcher calls ensureCurrentCore(). It fetches origin/main
|
|
13
|
+
// (bounded; allowed to fail) and compares the checkout's @bongos/core pin with
|
|
14
|
+
// origin/main's. When ORIGIN moved it, the builder gets one plain line saying so. On a
|
|
15
|
+
// clean `main` with no local commits, the checkout is fast-forwarded and the pinned core
|
|
16
|
+
// is installed through core-pin-sync.js (task 1004632) — whose gates (the pin's shape,
|
|
17
|
+
// and the pin having landed on main) still decide what may be installed — then the
|
|
18
|
+
// command re-runs under the new core. A dirty tree, a feature branch or local commits get
|
|
19
|
+
// the line and the command to run, never a forced change. So does a move that is not
|
|
20
|
+
// pin-only (notPinOnly): the fast-forward installs and runs what it brings in, so it is
|
|
21
|
+
// taken only for the hub's commit shape — the pin, the lockfile, a removed tarball — onto
|
|
22
|
+
// a published registry release; anything wider waits for the owner to look.
|
|
23
|
+
//
|
|
24
|
+
// Silent and fail-open: in the core repo itself (the running package is not
|
|
25
|
+
// @bongos/core) it returns before any git spawn; outside git, with no pin, when origin
|
|
26
|
+
// cannot be read, or when the pins already agree it says nothing. A pin the LOCAL side
|
|
27
|
+
// moved (origin's pin is still the one this branch forked from) is not a hub move.
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const { spawnSync } = require('node:child_process');
|
|
34
|
+
const pinSync = require('./core-pin-sync.js');
|
|
35
|
+
|
|
36
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
37
|
+
|
|
38
|
+
function readJson(file, fsImpl = fs) {
|
|
39
|
+
try { return JSON.parse(String(fsImpl.readFileSync(file, 'utf8')).replace(/^/, '')); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function gitOut(args, cwd, run, extra = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const r = run('git', args, { cwd, encoding: 'utf8', ...extra });
|
|
45
|
+
return r && r.status === 0 ? String(r.stdout || '').trim() : null;
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function specOf(pkg) {
|
|
50
|
+
if (!pkg) return null;
|
|
51
|
+
return (pkg.dependencies || {})[pinSync.CORE_PKG] || (pkg.devDependencies || {})[pinSync.CORE_PKG] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The core pin package.json carries at `rev`, or null when that file cannot be read there.
|
|
55
|
+
function specAt(root, rev, run) {
|
|
56
|
+
const body = gitOut(['show', `${rev}:package.json`], root, run, { maxBuffer: 8 * 1024 * 1024 });
|
|
57
|
+
if (body == null) return null;
|
|
58
|
+
try { return specOf(JSON.parse(body.replace(/^/, ''))); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const shown = (spec) => pinSync.versionFromSpec(spec) || spec;
|
|
62
|
+
|
|
63
|
+
// Why the move cannot be applied to this checkout automatically, or null when it can.
|
|
64
|
+
function blockedReason({ branch, dirty, ahead }) {
|
|
65
|
+
if (!branch) return 'this checkout is not on a branch';
|
|
66
|
+
if (branch !== 'main') return `you are on branch ${branch}, which picks the move up when it merges main`;
|
|
67
|
+
if (dirty == null || dirty !== '') return 'this checkout has uncommitted changes';
|
|
68
|
+
if (!Number.isFinite(ahead) || ahead !== 0) return 'main has local commits GitHub does not have';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const REMEDY = 'On a clean main: `git pull --ff-only origin main`, then `npm ci`.';
|
|
73
|
+
const REVIEW_REMEDY = 'Review what GitHub has (`git log HEAD..origin/main`), then `git pull --ff-only origin main` and `npm ci`.';
|
|
74
|
+
const REGISTRY = 'https://registry.npmjs.org/';
|
|
75
|
+
const EXACT_SEMVER = /^\d+\.\d+\.\d+(-[A-Za-z0-9.]+)?$/;
|
|
76
|
+
|
|
77
|
+
function jsonAt(root, rev, file, run) {
|
|
78
|
+
const body = gitOut(['show', `${rev}:${file}`], root, run, { maxBuffer: 64 * 1024 * 1024 });
|
|
79
|
+
if (body == null) return null;
|
|
80
|
+
try { return JSON.parse(body.replace(/^/, '')); } catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The fast-forward installs and then RUNS what it brings in, unattended — so it is taken
|
|
84
|
+
// only when what GitHub has is the hub's pin move and nothing else (the shape
|
|
85
|
+
// provision-core-upgrade.js pushes). Anything wider is reported, never applied. Returns
|
|
86
|
+
// null when the move is pin-only, else the reason it is not.
|
|
87
|
+
// - the only files that differ are package.json, package-lock.json and a removed
|
|
88
|
+
// vendored tarball;
|
|
89
|
+
// - package.json differs ONLY in the @bongos/core spec;
|
|
90
|
+
// - the new spec is an exact version, i.e. a published release (a tarball brought in
|
|
91
|
+
// from GitHub is code nobody released);
|
|
92
|
+
// - the lockfile pins that version from the public registry, and every lockfile
|
|
93
|
+
// source it adds is the public registry too.
|
|
94
|
+
function notPinOnly(root, run) {
|
|
95
|
+
const diff = gitOut(['diff', '--name-status', '--no-renames', 'HEAD', 'origin/main'], root, run, { maxBuffer: 16 * 1024 * 1024 });
|
|
96
|
+
if (diff == null) return 'the change on GitHub could not be read';
|
|
97
|
+
for (const line of diff.split('\n').filter(Boolean)) {
|
|
98
|
+
const [status, file] = line.split('\t');
|
|
99
|
+
const ok = file === 'package.json' || file === 'package-lock.json' ||
|
|
100
|
+
(status === 'D' && /^vendor\/bongos-core-[^/]+\.tgz$/.test(file));
|
|
101
|
+
if (!ok) return 'GitHub has other changes besides the core move';
|
|
102
|
+
}
|
|
103
|
+
const before = jsonAt(root, 'HEAD', 'package.json', run);
|
|
104
|
+
const after = jsonAt(root, 'origin/main', 'package.json', run);
|
|
105
|
+
if (!before || !after) return 'package.json could not be read';
|
|
106
|
+
const spec = specOf(after);
|
|
107
|
+
const strip = (pkg) => {
|
|
108
|
+
const c = JSON.parse(JSON.stringify(pkg));
|
|
109
|
+
for (const k of ['dependencies', 'devDependencies']) if (c[k]) delete c[k][pinSync.CORE_PKG];
|
|
110
|
+
return JSON.stringify(c);
|
|
111
|
+
};
|
|
112
|
+
if (strip(before) !== strip(after)) return 'package.json changed beyond the core pin';
|
|
113
|
+
if (!EXACT_SEMVER.test(String(spec || ''))) return 'the new pin is not a published release';
|
|
114
|
+
const lock = jsonAt(root, 'origin/main', 'package-lock.json', run);
|
|
115
|
+
const entry = lock && lock.packages && lock.packages[`node_modules/${pinSync.CORE_PKG}`];
|
|
116
|
+
if (!entry || entry.version !== spec || !String(entry.resolved || '').startsWith(`${REGISTRY}${pinSync.CORE_PKG}/-/`)) {
|
|
117
|
+
return 'the lockfile does not pin that release from the public registry';
|
|
118
|
+
}
|
|
119
|
+
const prior = new Set(Object.values(((jsonAt(root, 'HEAD', 'package-lock.json', run) || {}).packages) || {}).map((p) => p && p.resolved));
|
|
120
|
+
for (const p of Object.values(lock.packages)) {
|
|
121
|
+
const r = p && p.resolved;
|
|
122
|
+
if (r && !prior.has(r) && !String(r).startsWith(REGISTRY)) return 'the lockfile adds a source outside the public registry';
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Returns { action: 'none' | 'reported' | 'fast-forwarded', reason, from?, to? }.
|
|
128
|
+
function followOriginPin({ coreRoot, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, fetchTimeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
129
|
+
const none = (reason) => ({ action: 'none', reason });
|
|
130
|
+
if (env && (env.BONGOS_SKIP_CORE_SYNC || env[pinSync.SYNCED_ENV])) return none('skipped');
|
|
131
|
+
const running = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
132
|
+
if (!running || running.name !== pinSync.CORE_PKG) return none('core-repo'); // no git spawn
|
|
133
|
+
const top = gitOut(['rev-parse', '--show-toplevel'], cwd, run);
|
|
134
|
+
if (!top) return none('no-git');
|
|
135
|
+
const root = path.resolve(top);
|
|
136
|
+
const local = specOf(readJson(path.join(root, 'package.json'), fsImpl));
|
|
137
|
+
if (!local) return none('no-pin');
|
|
138
|
+
|
|
139
|
+
// Bounded, quiet, never prompts; a failure is fine — the ref below decides.
|
|
140
|
+
gitOut(['fetch', '--quiet', 'origin', 'main'], root, run, {
|
|
141
|
+
timeout: fetchTimeoutMs, env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: '0' },
|
|
142
|
+
});
|
|
143
|
+
const origin = specAt(root, 'origin/main', run);
|
|
144
|
+
if (!origin) return none('origin-unreadable');
|
|
145
|
+
if (origin === local) return none('in-step');
|
|
146
|
+
const forkPoint = gitOut(['merge-base', 'HEAD', 'origin/main'], root, run);
|
|
147
|
+
if (forkPoint && specAt(root, forkPoint, run) === origin) return none('local-change');
|
|
148
|
+
|
|
149
|
+
const say = log || ((s) => process.stderr.write(s + '\n'));
|
|
150
|
+
const move = { from: local, to: origin };
|
|
151
|
+
say(`bongos: this project was upgraded to core ${shown(origin)} on GitHub (this checkout has ${shown(local)}).`);
|
|
152
|
+
const why = blockedReason({
|
|
153
|
+
branch: gitOut(['symbolic-ref', '--short', '-q', 'HEAD'], root, run),
|
|
154
|
+
dirty: gitOut(['status', '--porcelain', '--untracked-files=no'], root, run),
|
|
155
|
+
ahead: Number(gitOut(['rev-list', '--count', 'origin/main..HEAD'], root, run)),
|
|
156
|
+
});
|
|
157
|
+
if (why) {
|
|
158
|
+
say(` Not updated here: ${why}. ${REMEDY}`);
|
|
159
|
+
return { action: 'reported', reason: why, ...move };
|
|
160
|
+
}
|
|
161
|
+
const wider = notPinOnly(root, run);
|
|
162
|
+
if (wider) {
|
|
163
|
+
say(` Not updated automatically: ${wider}. ${REVIEW_REMEDY}`);
|
|
164
|
+
return { action: 'reported', reason: wider, ...move };
|
|
165
|
+
}
|
|
166
|
+
if (gitOut(['merge', '--ff-only', '--quiet', 'origin/main'], root, run) == null) {
|
|
167
|
+
say(` The fast-forward did not go through. ${REMEDY}`);
|
|
168
|
+
return { action: 'reported', reason: 'ff-failed', ...move };
|
|
169
|
+
}
|
|
170
|
+
say(' Fast-forwarded this checkout to it; installing that core next.');
|
|
171
|
+
return { action: 'fast-forwarded', reason: 'origin-moved', ...move };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The dispatcher's one call, for `ship` and `start`. Returns an exit code when it handled
|
|
175
|
+
// the command (installed and re-ran, or refused), or null to run the verb as it is.
|
|
176
|
+
// `ship` always checks the running core against the pin (task 1004632); `start` only
|
|
177
|
+
// after a fast-forward actually moved the pin, so a normal session start pays no more
|
|
178
|
+
// than the fetch.
|
|
179
|
+
function ensureCurrentCore({ coreRoot, argv, verb, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
|
|
180
|
+
const followed = followOriginPin({ coreRoot, cwd, env, run, fsImpl, log });
|
|
181
|
+
if (verb !== 'ship' && followed.action !== 'fast-forwarded') return null;
|
|
182
|
+
return pinSync.ensurePinnedCore({ coreRoot, argv, cwd, env, run, fsImpl, log, ...(npmCli !== undefined ? { npmCli } : {}) });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { followOriginPin, ensureCurrentCore };
|
|
@@ -172,6 +172,28 @@ function unlinkLink(linkPath, { platform = process.platform, fsImpl = fs } = {})
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// A vendored instance keeps an untracked `scripts -> node_modules/@bongos/core/scripts`
|
|
176
|
+
// link in its main checkout (grading needs it; task 1004728). After an install it must
|
|
177
|
+
// still resolve (task 1004732): a link left dangling is re-pointed at the installed core.
|
|
178
|
+
// Only ever a LINK — a real scripts/ directory (the core repo) is never touched.
|
|
179
|
+
// Returns 'absent' | 'not-a-link' | 'ok' | 'repointed' | 'unresolved'.
|
|
180
|
+
function healScriptsLink(root, { fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), platform = process.platform } = {}) {
|
|
181
|
+
const link = path.join(root, 'scripts');
|
|
182
|
+
let st = null;
|
|
183
|
+
try { st = fsImpl.lstatSync(link); } catch { return 'absent'; }
|
|
184
|
+
if (!st.isSymbolicLink()) return 'not-a-link';
|
|
185
|
+
if (fsImpl.existsSync(path.join(link, 'gds'))) return 'ok';
|
|
186
|
+
const target = path.join(root, 'node_modules', '@bongos', 'core', 'scripts');
|
|
187
|
+
const fix = `bongos: the \`scripts\` link in ${root} no longer resolves; point it at ${target}.`;
|
|
188
|
+
if (!fsImpl.existsSync(target)) { log(fix); return 'unresolved'; }
|
|
189
|
+
try {
|
|
190
|
+
unlinkLink(link, { platform, fsImpl });
|
|
191
|
+
fsImpl.symlinkSync(target, link, 'junction');
|
|
192
|
+
} catch { log(fix); return 'unresolved'; }
|
|
193
|
+
log('bongos: re-pointed the `scripts` link at the installed core.');
|
|
194
|
+
return 'repointed';
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
// Install the pinned core into `root` and re-run the same bongos command under it.
|
|
176
198
|
// Returns the exit code to use.
|
|
177
199
|
function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), env = process.env, platform = process.platform, npmCli = npmCliPath(process.execPath, fsImpl) } = {}) {
|
|
@@ -192,10 +214,13 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
192
214
|
// npm's JS entry under this node — no shell. Fallback: `npm`, which on win32 is
|
|
193
215
|
// npm.cmd and needs a shell (Node refuses a .cmd without one); the args are fixed
|
|
194
216
|
// strings, never branch data.
|
|
217
|
+
// npm's output goes to stderr (task 1004732): `bongos start --widget` hands stdout to
|
|
218
|
+
// the renderer verbatim, so an install must never print there.
|
|
195
219
|
const cli = npmCli;
|
|
220
|
+
const stdio = ['ignore', 2, 2];
|
|
196
221
|
const inst = cli
|
|
197
|
-
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio
|
|
198
|
-
: run('npm', installArgs(hasLock), { cwd: root, stdio
|
|
222
|
+
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio, env })
|
|
223
|
+
: run('npm', installArgs(hasLock), { cwd: root, stdio, env, ...(platform === 'win32' ? { shell: true } : {}) });
|
|
199
224
|
if (!inst || inst.error || inst.status !== 0) {
|
|
200
225
|
log(`bongos: installing core ${pinned} failed.`);
|
|
201
226
|
remedy();
|
|
@@ -203,6 +228,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
203
228
|
}
|
|
204
229
|
const bin = path.join(nm, '@bongos', 'core', 'bin', 'bongos.js');
|
|
205
230
|
if (!fsImpl.existsSync(bin)) { remedy(); return 1; }
|
|
231
|
+
healScriptsLink(root, { fsImpl, log, platform });
|
|
206
232
|
const res = run(process.execPath, [bin, ...argv.slice(2)], { stdio: 'inherit', env: { ...env, [SYNCED_ENV]: '1' } });
|
|
207
233
|
if (!res || res.error) return 1;
|
|
208
234
|
return res.status == null ? 1 : res.status;
|
|
@@ -210,7 +236,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
210
236
|
|
|
211
237
|
// The dispatcher's one call. Returns an exit code when it handled the command (synced
|
|
212
238
|
// and re-ran, or refused), or null to run the verb on the current core.
|
|
213
|
-
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log } = {}) {
|
|
239
|
+
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
|
|
214
240
|
if (env && env.BONGOS_SKIP_CORE_SYNC) return null;
|
|
215
241
|
const runningPkg = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
216
242
|
if (!runningPkg || runningPkg.name !== CORE_PKG) return null; // fast path: the core repo itself
|
|
@@ -227,7 +253,7 @@ function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.e
|
|
|
227
253
|
say(remedyLine({ ...plan, hasLock: fsImpl.existsSync(path.join(root, 'package-lock.json')) }));
|
|
228
254
|
return 1;
|
|
229
255
|
}
|
|
230
|
-
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env });
|
|
256
|
+
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env, ...(npmCli !== undefined ? { npmCli } : {}) });
|
|
231
257
|
}
|
|
232
258
|
|
|
233
|
-
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, ensurePinnedCore };
|
|
259
|
+
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, healScriptsLink, ensurePinnedCore };
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
// signed-in builder does nothing extra.
|
|
15
15
|
//
|
|
16
16
|
// Fast and quiet by design (the owner, 2026-10-06): the command prints one line
|
|
17
|
-
// and returns at once; a detached background process
|
|
18
|
-
//
|
|
17
|
+
// and returns at once; a detached background process sends to the hub. The one
|
|
18
|
+
// call made in the foreground is the receipt from the builder's own project, so
|
|
19
|
+
// "Sent" is never printed for a report the project could not vouch for (task
|
|
20
|
+
// 1004759: a project on an older core has no receipt door, and the background
|
|
21
|
+
// 404 was invisible). No task number comes back. Only the typed text
|
|
22
|
+
// (secret-scrubbed) and version stamps travel.
|
|
19
23
|
|
|
20
24
|
'use strict';
|
|
21
25
|
|
|
@@ -47,6 +51,8 @@ const REFUSALS = {
|
|
|
47
51
|
too_long: 'That is too long to send — keep it under 4,000 characters, or split it into two.',
|
|
48
52
|
local_only: 'This project runs only on your computer, so Cloud Bongos cannot confirm who sent the report. Nothing was sent.',
|
|
49
53
|
no_scrubber: 'Feedback was not sent: the secret filter that checks every report is missing from this install.',
|
|
54
|
+
project_too_old: "Not sent: this project's live site runs an older Bongos that cannot send feedback yet. It will work once the project is upgraded.",
|
|
55
|
+
receipt_failed: "Not sent: this project's live site did not confirm who sent it",
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
function stateDir() { return path.join(os.homedir(), '.config', ic.FALLBACK_DIR || 'cloudbongos'); }
|
|
@@ -98,12 +104,15 @@ function repeatKey(message) {
|
|
|
98
104
|
const first = String(message).split('\n').map((l) => l.trim().toLowerCase()).find(Boolean) || '';
|
|
99
105
|
return crypto.createHash('sha256').update(first).digest('hex').slice(0, 16);
|
|
100
106
|
}
|
|
101
|
-
|
|
107
|
+
// `record: false` only looks: the CLI records a report once its project has
|
|
108
|
+
// vouched for it, so a report that never left is not "already sent" (task 1004759).
|
|
109
|
+
function checkAndRecordRepeat(message, { file = path.join(stateDir(), 'feedback-sent.json'), now = Date.now(), record = true } = {}) {
|
|
102
110
|
let seen = {};
|
|
103
111
|
try { seen = JSON.parse(fs.readFileSync(file, 'utf8')) || {}; } catch { /* first report */ }
|
|
104
112
|
for (const [k, t] of Object.entries(seen)) if (!(now - t < REPEAT_WINDOW_MS)) delete seen[k];
|
|
105
113
|
const key = repeatKey(message);
|
|
106
114
|
if (Object.prototype.hasOwnProperty.call(seen, key)) return true;
|
|
115
|
+
if (!record) return false;
|
|
107
116
|
seen[key] = now;
|
|
108
117
|
try {
|
|
109
118
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -112,22 +121,29 @@ function checkAndRecordRepeat(message, { file = path.join(stateDir(), 'feedback-
|
|
|
112
121
|
return false;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
124
|
+
// requestReceipt — the project vouches for this exact text. Foreground: one call
|
|
125
|
+
// to the builder's own project. A 404 means the project's core predates the
|
|
126
|
+
// receipt door (task 1004591), which no retry fixes until it is upgraded.
|
|
127
|
+
async function requestReceipt({ message, kind }, deps = {}) {
|
|
118
128
|
const api = deps.api || await cliClient();
|
|
119
129
|
const base = deps.base || ic.resolveApiBase({ session: loadSessionSync() });
|
|
120
|
-
const doFetch = deps.fetchImpl || fetch;
|
|
121
130
|
const sha256 = crypto.createHash('sha256').update(message, 'utf8').digest('hex');
|
|
122
131
|
const r = await api.request('POST', '/feedback/receipt', { body: { sha256, kind } });
|
|
123
|
-
if (!r.ok) return { ok: false, step: 'receipt', code: errCode(r.data) || `http_${r.status}` };
|
|
132
|
+
if (!r.ok) return { ok: false, step: 'receipt', code: r.status === 404 ? 'project_too_old' : (errCode(r.data) || `http_${r.status}`) };
|
|
124
133
|
const origin = new URL(base).origin;
|
|
125
134
|
const hub = String((r.data && r.data.hub) || origin).replace(/\/+$/, '');
|
|
135
|
+
return { ok: true, receipt: r.data.receipt, origin, hub };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// sendToHub — the vouched report straight to the hub. Returns an outcome
|
|
139
|
+
// object; never prints. Runs in the background child.
|
|
140
|
+
async function sendToHub({ message, kind, receipt, origin, hub }, deps = {}) {
|
|
141
|
+
const doFetch = deps.fetchImpl || fetch;
|
|
126
142
|
const res = await doFetch(`${hub}/api/bongos/sso/feedback/direct`, {
|
|
127
143
|
method: 'POST',
|
|
128
144
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
129
145
|
body: JSON.stringify({
|
|
130
|
-
origin, receipt
|
|
146
|
+
origin, receipt, message, kind,
|
|
131
147
|
context: { cli_version: cliVersion(), os: `${os.platform()} ${os.release()}` },
|
|
132
148
|
}),
|
|
133
149
|
signal: AbortSignal.timeout(15000),
|
|
@@ -138,13 +154,31 @@ async function deliver({ message, kind }, deps = {}) {
|
|
|
138
154
|
return { ok: false, step: 'hub', code: errCode(body) || `http_${res.status}` };
|
|
139
155
|
}
|
|
140
156
|
|
|
157
|
+
// deliver — both halves in one go.
|
|
158
|
+
async function deliver(job, deps = {}) {
|
|
159
|
+
const r = await requestReceipt(job, deps);
|
|
160
|
+
if (!r.ok) return r;
|
|
161
|
+
return sendToHub({ ...job, receipt: r.receipt, origin: r.origin, hub: r.hub }, deps);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A report the project could not vouch for is kept, so it can be sent after the
|
|
165
|
+
// upgrade (`--message-file <it>`) instead of being written again.
|
|
166
|
+
function keepUnsent(message, { dir = stateDir(), now = new Date() } = {}) {
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
169
|
+
const file = path.join(dir, `feedback-unsent-${now.toISOString().replace(/[:.]/g, '-')}.md`);
|
|
170
|
+
fs.writeFileSync(file, message, { mode: 0o600 });
|
|
171
|
+
return file;
|
|
172
|
+
} catch { return null; }
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
// The background child: read the queued report, deliver it, record the outcome
|
|
142
176
|
// where a curious builder (or a maintainer helping them) can look.
|
|
143
177
|
async function runDeliverChild(file) {
|
|
144
178
|
let job;
|
|
145
179
|
try { job = JSON.parse(fs.readFileSync(file, 'utf8')); } finally { fs.rmSync(path.dirname(file), { recursive: true, force: true }); }
|
|
146
180
|
let outcome;
|
|
147
|
-
try { outcome = await deliver(job); } catch (e) { outcome = { ok: false, step: 'network', code: String(e && e.message || e).slice(0, 200) }; }
|
|
181
|
+
try { outcome = job.receipt ? await sendToHub(job) : await deliver(job); } catch (e) { outcome = { ok: false, step: 'network', code: String(e && e.message || e).slice(0, 200) }; }
|
|
148
182
|
try {
|
|
149
183
|
fs.writeFileSync(path.join(stateDir(), 'feedback-last.json'), JSON.stringify({ at: new Date().toISOString(), ...outcome }), { mode: 0o600 });
|
|
150
184
|
} catch { /* nowhere to record — the report itself is what mattered */ }
|
|
@@ -177,16 +211,27 @@ async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
177
211
|
if (isLocalBase(base)) { console.error(REFUSALS.local_only); return 1; }
|
|
178
212
|
|
|
179
213
|
const job = { message: scrubbed.text, kind };
|
|
180
|
-
|
|
214
|
+
const checkRepeat = deps.checkRepeat || checkAndRecordRepeat;
|
|
215
|
+
if (checkRepeat(job.message, { record: false })) {
|
|
181
216
|
console.log('Already sent — the Bongos team has this one.');
|
|
182
217
|
return 0;
|
|
183
218
|
}
|
|
219
|
+
// The receipt in the foreground: "Sent" only once the project has vouched.
|
|
220
|
+
const r = await (deps.requestReceipt || requestReceipt)(job, { base });
|
|
221
|
+
if (!r.ok) {
|
|
222
|
+
const kept = (deps.keepUnsent || keepUnsent)(job.message);
|
|
223
|
+
const why = r.code === 'project_too_old' ? REFUSALS.project_too_old : `${REFUSALS.receipt_failed} (${r.code}).`;
|
|
224
|
+
console.error(kept ? `${why} Your note is saved in ${kept}` : why);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
checkRepeat(job.message);
|
|
228
|
+
const vouched = { ...job, receipt: r.receipt, origin: r.origin, hub: r.hub };
|
|
184
229
|
if (hasFlag('--wait', argv)) {
|
|
185
|
-
const outcome = await (deps.
|
|
230
|
+
const outcome = await (deps.sendToHub || sendToHub)(vouched);
|
|
186
231
|
console.log(outcome.ok ? SENT_LINE : `Not sent (${outcome.step}: ${outcome.code}).`);
|
|
187
232
|
return outcome.ok ? 0 : 1;
|
|
188
233
|
}
|
|
189
|
-
(deps.spawnDelivery || spawnDelivery)(
|
|
234
|
+
(deps.spawnDelivery || spawnDelivery)(vouched);
|
|
190
235
|
console.log(SENT_LINE);
|
|
191
236
|
return 0;
|
|
192
237
|
}
|
|
@@ -201,4 +246,4 @@ if (require.main === module) {
|
|
|
201
246
|
});
|
|
202
247
|
}
|
|
203
248
|
|
|
204
|
-
module.exports = { main, deliver, readMessage, REFUSALS, scrubReport, isLocalBase, repeatKey, checkAndRecordRepeat, SENT_LINE };
|
|
249
|
+
module.exports = { main, deliver, requestReceipt, sendToHub, keepUnsent, readMessage, REFUSALS, scrubReport, isLocalBase, repeatKey, checkAndRecordRepeat, SENT_LINE };
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.149'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// tests/core_origin_follow.mjs — the laptop picks up a core move the hub made (task 1004732)
|
|
2
|
+
//
|
|
3
|
+
// The bug: when the hub moves a hosted project's core it commits the new pin on the box
|
|
4
|
+
// and pushes it to the project's GitHub repo. The owner's laptop checkout never heard of
|
|
5
|
+
// it, so the next session and the next ship ran — and merged against — the old pin.
|
|
6
|
+
//
|
|
7
|
+
// Each case is a REAL temp git setup: a bare "GitHub" origin, a "box" clone that makes
|
|
8
|
+
// the hub's pin commit and pushes it, and a "laptop" clone that is the checkout under
|
|
9
|
+
// test. npm is stubbed (it lays down a fake installed core); git is real.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/core_origin_follow.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const follow = require('../scripts/gds/core-origin-follow.js');
|
|
22
|
+
const pinSync = require('../scripts/gds/core-pin-sync.js');
|
|
23
|
+
|
|
24
|
+
let passed = 0;
|
|
25
|
+
let failed = 0;
|
|
26
|
+
function test(name, fn) {
|
|
27
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
28
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function git(cwd, ...args) {
|
|
32
|
+
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8' });
|
|
33
|
+
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
|
34
|
+
return r.stdout;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const OLD = '1.21.20';
|
|
38
|
+
const NEW = '1.21.115';
|
|
39
|
+
|
|
40
|
+
function writePin(dir, spec, version) {
|
|
41
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'toast', private: true, dependencies: { '@bongos/core': spec } }, null, 2) + '\n');
|
|
42
|
+
fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({
|
|
43
|
+
name: 'toast', lockfileVersion: 3,
|
|
44
|
+
packages: { 'node_modules/@bongos/core': { version, resolved: spec.startsWith('file:') ? spec : `https://registry.npmjs.org/@bongos/core/-/core-${version}.tgz` } },
|
|
45
|
+
}, null, 2) + '\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A fake installed core in `root`: what npm would leave behind.
|
|
49
|
+
function installCore(root, version) {
|
|
50
|
+
const core = path.join(root, 'node_modules', '@bongos', 'core');
|
|
51
|
+
fs.rmSync(path.join(root, 'node_modules'), { recursive: true, force: true });
|
|
52
|
+
fs.mkdirSync(path.join(core, 'bin'), { recursive: true });
|
|
53
|
+
fs.mkdirSync(path.join(core, 'scripts', 'gds'), { recursive: true });
|
|
54
|
+
fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({ name: '@bongos/core', version }));
|
|
55
|
+
fs.writeFileSync(path.join(core, 'bin', 'bongos.js'), '');
|
|
56
|
+
fs.writeFileSync(path.join(core, 'scripts', 'gds', 'fitness.js'), '');
|
|
57
|
+
return core;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// origin (bare) ← box pushes the hub's move; laptop is a clone made BEFORE the move.
|
|
61
|
+
// `move` makes the hub's commit: vendored tgz pin → exact registry pin, tgz removed.
|
|
62
|
+
function setup({ move = true, alsoOnBox = null } = {}) {
|
|
63
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-'));
|
|
64
|
+
const origin = path.join(base, 'origin.git');
|
|
65
|
+
const box = path.join(base, 'box');
|
|
66
|
+
const laptop = path.join(base, 'laptop');
|
|
67
|
+
git(base, 'init', '-q', '--bare', '-b', 'main', origin);
|
|
68
|
+
fs.mkdirSync(box);
|
|
69
|
+
git(box, 'init', '-q', '-b', 'main');
|
|
70
|
+
writePin(box, `file:vendor/bongos-core-${OLD}.tgz`, OLD);
|
|
71
|
+
fs.mkdirSync(path.join(box, 'vendor'));
|
|
72
|
+
fs.writeFileSync(path.join(box, 'vendor', `bongos-core-${OLD}.tgz`), 'old-tarball');
|
|
73
|
+
fs.writeFileSync(path.join(box, '.gitignore'), 'node_modules/\n');
|
|
74
|
+
git(box, 'add', '-A');
|
|
75
|
+
git(box, 'commit', '-qm', 'init: vendored core');
|
|
76
|
+
git(box, 'remote', 'add', 'origin', origin);
|
|
77
|
+
git(box, 'push', '-q', 'origin', 'main');
|
|
78
|
+
git(base, 'clone', '-q', origin, laptop);
|
|
79
|
+
// The laptop has the old core installed and the untracked, excluded scripts link.
|
|
80
|
+
installCore(laptop, OLD);
|
|
81
|
+
fs.symlinkSync(path.join(laptop, 'node_modules', '@bongos', 'core', 'scripts'), path.join(laptop, 'scripts'), 'junction');
|
|
82
|
+
fs.appendFileSync(path.join(laptop, '.git', 'info', 'exclude'), 'scripts\n');
|
|
83
|
+
if (move) {
|
|
84
|
+
writePin(box, NEW, NEW);
|
|
85
|
+
git(box, 'rm', '-q', `vendor/bongos-core-${OLD}.tgz`);
|
|
86
|
+
if (alsoOnBox) alsoOnBox(box);
|
|
87
|
+
git(box, 'add', '-A');
|
|
88
|
+
git(box, 'commit', '-qm', `core: upgrade @bongos/core to ${NEW}`);
|
|
89
|
+
git(box, 'push', '-q', 'origin', 'main');
|
|
90
|
+
}
|
|
91
|
+
// The core the laptop runs (node_modules/@bongos/core) — the dispatcher's REPO_ROOT.
|
|
92
|
+
const coreRoot = path.join(laptop, 'node_modules', '@bongos', 'core');
|
|
93
|
+
return { base, origin, box, laptop, coreRoot };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// git → real; npm → lay down the pinned core; the re-run of bongos → recorded.
|
|
97
|
+
function stubRun(root) {
|
|
98
|
+
const calls = { installs: [], reruns: [], git: 0 };
|
|
99
|
+
const run = (cmd, args, opts) => {
|
|
100
|
+
if (cmd === 'git') { calls.git++; return spawnSync(cmd, args, opts); }
|
|
101
|
+
if (cmd === 'npm' || String(args[0] || '').endsWith('npm-cli.js')) {
|
|
102
|
+
calls.installs.push(cmd === 'npm' ? args : args.slice(1));
|
|
103
|
+
const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
|
|
104
|
+
installCore(root, lock.packages['node_modules/@bongos/core'].version);
|
|
105
|
+
return { status: 0 };
|
|
106
|
+
}
|
|
107
|
+
if (String(args[0] || '').endsWith('bongos.js')) { calls.reruns.push(args.slice(1)); return { status: 0 }; }
|
|
108
|
+
return { status: 1 };
|
|
109
|
+
};
|
|
110
|
+
return { run, calls };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pinOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'];
|
|
114
|
+
const head = (dir) => git(dir, 'rev-parse', 'HEAD').trim();
|
|
115
|
+
|
|
116
|
+
test('clean main: says the project moved, fast-forwards, reinstalls, re-runs, scripts link resolves', () => {
|
|
117
|
+
const { laptop, coreRoot } = setup();
|
|
118
|
+
const { run, calls } = stubRun(laptop);
|
|
119
|
+
const logs = [];
|
|
120
|
+
const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
|
|
121
|
+
assert.equal(code, 0, 'the re-run exit code: ' + logs.join('\n'));
|
|
122
|
+
assert.ok(logs.some((l) => l.includes(`upgraded to core ${NEW} on GitHub`) && l.includes(OLD)), logs.join('\n'));
|
|
123
|
+
assert.equal(pinOf(laptop), NEW, 'fast-forwarded onto the hub commit');
|
|
124
|
+
assert.equal(calls.installs.length, 1, 'npm ran once');
|
|
125
|
+
assert.equal(calls.installs[0][0], 'ci');
|
|
126
|
+
assert.deepEqual(calls.reruns, [['start']], 'start re-ran under the new core');
|
|
127
|
+
assert.ok(fs.existsSync(path.join(laptop, 'scripts', 'gds', 'fitness.js')), 'the scripts link still resolves');
|
|
128
|
+
const installed = JSON.parse(fs.readFileSync(path.join(laptop, 'node_modules', '@bongos', 'core', 'package.json'), 'utf8'));
|
|
129
|
+
assert.equal(installed.version, NEW);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('vendored → registry pin: no tgz left referenced after the follow', () => {
|
|
133
|
+
const { laptop, coreRoot } = setup();
|
|
134
|
+
const { run } = stubRun(laptop);
|
|
135
|
+
follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'ship', '7'], verb: 'ship', cwd: laptop, env: {}, run, log: () => {}, npmCli: null });
|
|
136
|
+
assert.equal(pinOf(laptop), NEW);
|
|
137
|
+
for (const f of ['package.json', 'package-lock.json']) {
|
|
138
|
+
assert.ok(!/bongos-core-[\d.]+\.tgz/.test(fs.readFileSync(path.join(laptop, f), 'utf8')), `${f} still names a tarball`);
|
|
139
|
+
}
|
|
140
|
+
assert.ok(!fs.existsSync(path.join(laptop, 'vendor', `bongos-core-${OLD}.tgz`)), 'the hub removed the old tarball; the follow carried that');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('dirty tree: reports the move only — no fast-forward, no install', () => {
|
|
144
|
+
const { laptop, coreRoot } = setup();
|
|
145
|
+
fs.writeFileSync(path.join(laptop, '.gitignore'), 'node_modules/\nlocal-edit\n');
|
|
146
|
+
const before = head(laptop);
|
|
147
|
+
const { run, calls } = stubRun(laptop);
|
|
148
|
+
const logs = [];
|
|
149
|
+
const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
|
|
150
|
+
assert.equal(code, null, 'carry on with the command');
|
|
151
|
+
assert.ok(logs.some((l) => l.includes(`upgraded to core ${NEW} on GitHub`)), logs.join('\n'));
|
|
152
|
+
assert.ok(logs.some((l) => /uncommitted/.test(l)), 'says why it was not applied: ' + logs.join('\n'));
|
|
153
|
+
assert.equal(head(laptop), before);
|
|
154
|
+
assert.equal(pinOf(laptop), `file:vendor/bongos-core-${OLD}.tgz`);
|
|
155
|
+
assert.equal(calls.installs.length, 0);
|
|
156
|
+
assert.match(fs.readFileSync(path.join(laptop, '.gitignore'), 'utf8'), /local-edit/, 'the edit is untouched');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('feature branch: reports only, never moves the branch', () => {
|
|
160
|
+
const { laptop, coreRoot } = setup();
|
|
161
|
+
git(laptop, 'checkout', '-qb', 'task-49');
|
|
162
|
+
const before = head(laptop);
|
|
163
|
+
const { run, calls } = stubRun(laptop);
|
|
164
|
+
const logs = [];
|
|
165
|
+
const r = follow.followOriginPin({ coreRoot, cwd: laptop, env: {}, run, log: (s) => logs.push(s) });
|
|
166
|
+
assert.equal(r.action, 'reported');
|
|
167
|
+
assert.ok(logs.some((l) => l.includes('task-49')), logs.join('\n'));
|
|
168
|
+
assert.equal(head(laptop), before);
|
|
169
|
+
assert.equal(calls.installs.length, 0);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// The fast-forward installs and RUNS what it brings in, so only the hub's pin-only commit
|
|
173
|
+
// shape onto a published release is applied unattended; anything wider is reported.
|
|
174
|
+
function expectReportedOnly(alsoOnBox, why) {
|
|
175
|
+
const { laptop, coreRoot } = setup({ alsoOnBox });
|
|
176
|
+
const before = head(laptop);
|
|
177
|
+
const { run, calls } = stubRun(laptop);
|
|
178
|
+
const logs = [];
|
|
179
|
+
const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
|
|
180
|
+
assert.equal(code, null);
|
|
181
|
+
assert.ok(logs.some((l) => l.includes('upgraded to core') ), logs.join('\n'));
|
|
182
|
+
assert.ok(logs.some((l) => why.test(l)), `names why (${why}): ` + logs.join('\n'));
|
|
183
|
+
assert.equal(head(laptop), before, 'not fast-forwarded');
|
|
184
|
+
assert.equal(calls.installs.length, 0, 'nothing installed');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
test('a move that carries other code too is reported, never applied', () => {
|
|
188
|
+
expectReportedOnly((box) => fs.writeFileSync(path.join(box, 'postinstall.js'), 'evil()'), /other changes besides the core move/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('a move whose package.json changes more than the core pin is reported, never applied', () => {
|
|
192
|
+
expectReportedOnly((box) => {
|
|
193
|
+
const p = JSON.parse(fs.readFileSync(path.join(box, 'package.json'), 'utf8'));
|
|
194
|
+
p.scripts = { prestart: 'evil' };
|
|
195
|
+
fs.writeFileSync(path.join(box, 'package.json'), JSON.stringify(p, null, 2) + '\n');
|
|
196
|
+
}, /package\.json changed beyond the core pin/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('a move onto a tarball from GitHub (not a published release) is reported, never applied', () => {
|
|
200
|
+
// the pin alone names a tarball (the spec gate) …
|
|
201
|
+
expectReportedOnly((box) => writePin(box, `file:vendor/bongos-core-${NEW}.tgz`, NEW), /not a published release/);
|
|
202
|
+
// … and a tarball file brought in with it is caught by the file gate first
|
|
203
|
+
expectReportedOnly((box) => {
|
|
204
|
+
writePin(box, `file:vendor/bongos-core-${NEW}.tgz`, NEW);
|
|
205
|
+
fs.mkdirSync(path.join(box, 'vendor'), { recursive: true });
|
|
206
|
+
fs.writeFileSync(path.join(box, 'vendor', `bongos-core-${NEW}.tgz`), 'unreleased');
|
|
207
|
+
}, /other changes besides the core move/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('a lockfile that resolves from outside the public registry is reported, never applied', () => {
|
|
211
|
+
expectReportedOnly((box) => {
|
|
212
|
+
const lock = JSON.parse(fs.readFileSync(path.join(box, 'package-lock.json'), 'utf8'));
|
|
213
|
+
lock.packages['node_modules/left-pad'] = { version: '1.0.0', resolved: 'https://evil.example/left-pad.tgz' };
|
|
214
|
+
fs.writeFileSync(path.join(box, 'package-lock.json'), JSON.stringify(lock, null, 2) + '\n');
|
|
215
|
+
}, /source outside the public registry/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('the core repo itself: a no-op, without a single git spawn', () => {
|
|
219
|
+
const { laptop } = setup();
|
|
220
|
+
const coreRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-core-'));
|
|
221
|
+
fs.writeFileSync(path.join(coreRepo, 'package.json'), JSON.stringify({ name: 'cloud-bongos', version: '0.0.2' }));
|
|
222
|
+
const { run, calls } = stubRun(laptop);
|
|
223
|
+
const logs = [];
|
|
224
|
+
assert.equal(follow.ensureCurrentCore({ coreRoot: coreRepo, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
|
|
225
|
+
assert.equal(calls.git, 0);
|
|
226
|
+
assert.deepEqual(logs, []);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('origin unreadable: silent no-op', () => {
|
|
230
|
+
const { laptop, coreRoot, origin } = setup();
|
|
231
|
+
// Drop the remote-tracking ref and point origin at nothing: the fetch fails and there is no origin/main.
|
|
232
|
+
git(laptop, 'update-ref', '-d', 'refs/remotes/origin/main');
|
|
233
|
+
git(laptop, 'remote', 'set-url', 'origin', origin + '-gone');
|
|
234
|
+
const before = head(laptop);
|
|
235
|
+
const { run, calls } = stubRun(laptop);
|
|
236
|
+
const logs = [];
|
|
237
|
+
assert.equal(follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
|
|
238
|
+
assert.deepEqual(logs, [], 'nothing said');
|
|
239
|
+
assert.equal(head(laptop), before);
|
|
240
|
+
assert.equal(calls.installs.length, 0);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('no move on origin: silent, and start pays no install', () => {
|
|
244
|
+
const { laptop, coreRoot } = setup({ move: false });
|
|
245
|
+
const { run, calls } = stubRun(laptop);
|
|
246
|
+
const logs = [];
|
|
247
|
+
assert.equal(follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
|
|
248
|
+
assert.deepEqual(logs, []);
|
|
249
|
+
assert.equal(calls.installs.length, 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('a pin the LAPTOP moved (not pushed yet) is not reported as a hub move', () => {
|
|
253
|
+
const { laptop, coreRoot } = setup({ move: false });
|
|
254
|
+
writePin(laptop, '1.21.99', '1.21.99');
|
|
255
|
+
git(laptop, 'commit', '-qam', 'local pin bump');
|
|
256
|
+
const { run } = stubRun(laptop);
|
|
257
|
+
const logs = [];
|
|
258
|
+
const r = follow.followOriginPin({ coreRoot, cwd: laptop, env: {}, run, log: (s) => logs.push(s) });
|
|
259
|
+
assert.equal(r.action, 'none');
|
|
260
|
+
assert.deepEqual(logs, []);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('a scripts link left dangling after the install is re-pointed at the installed core', () => {
|
|
264
|
+
const { laptop } = setup({ move: false });
|
|
265
|
+
const link = path.join(laptop, 'scripts');
|
|
266
|
+
fs.rmSync(link, { force: true, recursive: false });
|
|
267
|
+
const gone = path.join(laptop, 'old-core-scripts');
|
|
268
|
+
fs.mkdirSync(gone);
|
|
269
|
+
fs.symlinkSync(gone, link, 'junction');
|
|
270
|
+
fs.rmSync(gone, { recursive: true });
|
|
271
|
+
const logs = [];
|
|
272
|
+
assert.equal(pinSync.healScriptsLink(laptop, { log: (s) => logs.push(s) }), 'repointed');
|
|
273
|
+
assert.ok(fs.existsSync(path.join(link, 'gds', 'fitness.js')));
|
|
274
|
+
// a real scripts/ directory (the core repo shape) is never touched
|
|
275
|
+
const real = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-real-'));
|
|
276
|
+
fs.mkdirSync(path.join(real, 'scripts'));
|
|
277
|
+
assert.equal(pinSync.healScriptsLink(real, { log: () => {} }), 'not-a-link');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the dispatcher runs the follow before ship and start', () => {
|
|
281
|
+
const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
|
|
282
|
+
assert.match(src, /verb === 'ship' \|\| verb === 'start'/);
|
|
283
|
+
assert.match(src, /require\('\.\.\/scripts\/gds\/core-origin-follow\.js'\)\.ensureCurrentCore/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
287
|
+
if (failed) process.exit(1);
|
package/tests/core_pin_sync.mjs
CHANGED
|
@@ -228,9 +228,14 @@ test('ensurePinnedCore end to end: deleting the lockfile or adding an .npmrc on
|
|
|
228
228
|
}
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
test('the dispatcher runs the check for ship only', () => {
|
|
231
|
+
test('the dispatcher runs the check for ship (and start only after a followed core move)', () => {
|
|
232
|
+
// Since task 1004732 the dispatcher goes through core-origin-follow.ensureCurrentCore,
|
|
233
|
+
// which calls ensurePinnedCore for `ship` always and for `start` only after it
|
|
234
|
+
// fast-forwarded a hub pin move (tests/core_origin_follow.mjs drives both).
|
|
232
235
|
const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
|
|
233
|
-
assert.match(src, /if \(!COMPILED && verb === 'ship'\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-
|
|
236
|
+
assert.match(src, /if \(!COMPILED && \(verb === 'ship' \|\| verb === 'start'\)\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-origin-follow\.js'\)\.ensureCurrentCore/);
|
|
237
|
+
const follow = fs.readFileSync(new URL('../scripts/gds/core-origin-follow.js', import.meta.url), 'utf8');
|
|
238
|
+
assert.match(follow, /if \(verb !== 'ship' && followed\.action !== 'fast-forwarded'\) return null;\s*return pinSync\.ensurePinnedCore/);
|
|
234
239
|
});
|
|
235
240
|
|
|
236
241
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
@@ -326,6 +326,8 @@ async function runCli(argv, extra = {}) {
|
|
|
326
326
|
requireSession: async () => ({ token: 't' }),
|
|
327
327
|
base: 'https://mercury.cloudbongos.com',
|
|
328
328
|
checkRepeat: () => false,
|
|
329
|
+
requestReceipt: async () => ({ ok: true, receipt: 'R'.repeat(24), origin: 'https://mercury.cloudbongos.com', hub: 'https://cloudbongos.com' }),
|
|
330
|
+
keepUnsent: () => null,
|
|
329
331
|
spawnDelivery: (job) => spawned.push(job),
|
|
330
332
|
...extra,
|
|
331
333
|
});
|
|
@@ -337,7 +339,49 @@ test('the CLI prints one line and hands the network to the background', async ()
|
|
|
337
339
|
const r = await runCli(['claim', 'hung']);
|
|
338
340
|
assert.equal(r.code, 0);
|
|
339
341
|
assert.deepEqual(r.out, ['Sent to the Bongos team.']);
|
|
340
|
-
assert.deepEqual(r.spawned, [{ message: 'claim hung', kind: 'bug' }]);
|
|
342
|
+
assert.deepEqual(r.spawned, [{ message: 'claim hung', kind: 'bug', receipt: 'R'.repeat(24), origin: 'https://mercury.cloudbongos.com', hub: 'https://cloudbongos.com' }]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Task 1004759 (toast, core 1.21.20): the project's live site had no receipt
|
|
346
|
+
// door, the background child got a 404, and the builder had been told "Sent".
|
|
347
|
+
test('a project too old to vouch: "Not sent" in the foreground, the note kept, nothing recorded as sent', async () => {
|
|
348
|
+
const kept = []; const recorded = [];
|
|
349
|
+
const r = await runCli(['--idea', 'one', 'skill', 'for', 'project', 'purpose'], {
|
|
350
|
+
requestReceipt: async () => ({ ok: false, step: 'receipt', code: 'project_too_old' }),
|
|
351
|
+
keepUnsent: (m) => { kept.push(m); return '/home/x/.config/cloudbongos/feedback-unsent-1.md'; },
|
|
352
|
+
checkRepeat: (m, o = {}) => { if (o.record !== false) recorded.push(m); return false; },
|
|
353
|
+
});
|
|
354
|
+
assert.equal(r.code, 1);
|
|
355
|
+
assert.deepEqual(r.out, [], 'never says Sent');
|
|
356
|
+
assert.equal(r.spawned.length, 0);
|
|
357
|
+
assert.match(r.err.join('\n'), /older Bongos/);
|
|
358
|
+
assert.match(r.err.join('\n'), /feedback-unsent-1\.md/, 'says where the note is');
|
|
359
|
+
assert.deepEqual(kept, ['one skill for project purpose']);
|
|
360
|
+
assert.deepEqual(recorded, [], 'a report that never left is not "already sent"');
|
|
361
|
+
const other = await runCli(['x'], { requestReceipt: async () => ({ ok: false, step: 'receipt', code: 'not_signed_in' }) });
|
|
362
|
+
assert.equal(other.code, 1);
|
|
363
|
+
assert.match(other.err.join('\n'), /did not confirm.*not_signed_in/);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('the repeat guard records only after the project vouched', async () => {
|
|
367
|
+
const calls = [];
|
|
368
|
+
await runCli(['x'], { checkRepeat: (m, o = {}) => { calls.push(o.record === false ? 'look' : 'record'); return false; } });
|
|
369
|
+
assert.deepEqual(calls, ['look', 'record']);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('requestReceipt: a 404 from the project reads as project_too_old', async () => {
|
|
373
|
+
const { requestReceipt } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
|
|
374
|
+
const api = { request: async () => ({ ok: false, status: 404, data: { error: 'Not found' } }) };
|
|
375
|
+
const r = await requestReceipt({ message: 'x', kind: 'idea' }, { api, base: 'https://toast.cloudbongos.com' });
|
|
376
|
+
assert.deepEqual(r, { ok: false, step: 'receipt', code: 'project_too_old' });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test('keepUnsent writes the note where the builder can send it again', () => {
|
|
380
|
+
const { keepUnsent } = require_(path.join(ROOT, 'scripts/gds/feedback-send.js'));
|
|
381
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fb-unsent-'));
|
|
382
|
+
const file = keepUnsent('the note', { dir, now: new Date('2026-10-19T10:00:00Z') });
|
|
383
|
+
assert.equal(fs.readFileSync(file, 'utf8'), 'the note');
|
|
384
|
+
assert.match(path.basename(file), /^feedback-unsent-2026-10-19T10-00-00-000Z\.md$/);
|
|
341
385
|
});
|
|
342
386
|
|
|
343
387
|
test('the CLI refuses a project only this computer can reach, and sends nothing', async () => {
|