@bongos/core 1.23.3 → 1.23.4
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 +19 -14
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/bongos/module.js +91 -1
- package/src/module-api.js +1 -1
- package/tests/module_store_cli.mjs +184 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/bongos/package-core.js",
|
|
5
|
-
"core_version": "1.23.
|
|
6
|
-
"core_contract": "1.23.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.23.4",
|
|
6
|
+
"core_contract": "1.23.4",
|
|
7
|
+
"source_commit": "b78c52677b921b7b2a6f25834c77b7d516810aa9",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-26T21:
|
|
9
|
+
"built_at": "2026-10-26T21:43:52.539Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 616,
|
|
13
13
|
"agent_docs_stubbed": 29,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3552,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 4198,
|
|
20
|
+
"tree_sha256": "e5ed2ca130fbbb98e21c941d9c51e72760f74f72e2d48d6d91a4e789bdd6ce10",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -2937,12 +2937,12 @@
|
|
|
2937
2937
|
{
|
|
2938
2938
|
"path": "docs/module-api-changelog.md",
|
|
2939
2939
|
"mode": "0000644",
|
|
2940
|
-
"sha256": "
|
|
2940
|
+
"sha256": "3907ee1e7036c99afa1790df344933c6652b4d9ffe972715551ad9bdb2bb12d9"
|
|
2941
2941
|
},
|
|
2942
2942
|
{
|
|
2943
2943
|
"path": "docs/modules-contract.md",
|
|
2944
2944
|
"mode": "0000644",
|
|
2945
|
-
"sha256": "
|
|
2945
|
+
"sha256": "ed7ab0d4799a935bb296457ebdba667c38dcfea5ebaec72e61968cd3ad91a6eb"
|
|
2946
2946
|
},
|
|
2947
2947
|
{
|
|
2948
2948
|
"path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
|
|
@@ -10832,12 +10832,12 @@
|
|
|
10832
10832
|
{
|
|
10833
10833
|
"path": "package-lock.json",
|
|
10834
10834
|
"mode": "0000644",
|
|
10835
|
-
"sha256": "
|
|
10835
|
+
"sha256": "793a3d85e023eafb3f0eb63e7a1c1b10f07a66e331adfb63d06a05921ad2f451"
|
|
10836
10836
|
},
|
|
10837
10837
|
{
|
|
10838
10838
|
"path": "package.json",
|
|
10839
10839
|
"mode": "0000644",
|
|
10840
|
-
"sha256": "
|
|
10840
|
+
"sha256": "db95c78d828b9fd2117b5a7b4e12d741fb21856022a6830b6cd286017a065f6f"
|
|
10841
10841
|
},
|
|
10842
10842
|
{
|
|
10843
10843
|
"path": "public-docs/index.html",
|
|
@@ -10857,7 +10857,7 @@
|
|
|
10857
10857
|
{
|
|
10858
10858
|
"path": "release-notes.json",
|
|
10859
10859
|
"mode": "0000644",
|
|
10860
|
-
"sha256": "
|
|
10860
|
+
"sha256": "a491785c22b68e414bdf9668ca809f671a8927d06560aabfbc04ce3d56dfc131"
|
|
10861
10861
|
},
|
|
10862
10862
|
{
|
|
10863
10863
|
"path": "scripts/bongos/CLAUDE.md",
|
|
@@ -11747,7 +11747,7 @@
|
|
|
11747
11747
|
{
|
|
11748
11748
|
"path": "scripts/bongos/module.js",
|
|
11749
11749
|
"mode": "0000644",
|
|
11750
|
-
"sha256": "
|
|
11750
|
+
"sha256": "6fc19f18daa947837ee09d9d06010380144022ee6f637f3fd444f56cf558bfab"
|
|
11751
11751
|
},
|
|
11752
11752
|
{
|
|
11753
11753
|
"path": "scripts/bongos/move-escalation.js",
|
|
@@ -14712,7 +14712,7 @@
|
|
|
14712
14712
|
{
|
|
14713
14713
|
"path": "src/module-api.js",
|
|
14714
14714
|
"mode": "0000644",
|
|
14715
|
-
"sha256": "
|
|
14715
|
+
"sha256": "e9850d0157caab4215194a5e69e04420b0d9e3b97a8d1867f8f84e9087b88930"
|
|
14716
14716
|
},
|
|
14717
14717
|
{
|
|
14718
14718
|
"path": "src/module-loader/catalog.js",
|
|
@@ -18484,6 +18484,11 @@
|
|
|
18484
18484
|
"mode": "0000644",
|
|
18485
18485
|
"sha256": "ba4233ed072c87df9dbc5c75af10655ad8eaaf9d8ccbe69df146052467a1279e"
|
|
18486
18486
|
},
|
|
18487
|
+
{
|
|
18488
|
+
"path": "tests/module_store_cli.mjs",
|
|
18489
|
+
"mode": "0000644",
|
|
18490
|
+
"sha256": "cf4df707f62fa142f106822c44404603f8ec32213d61b6707818f5649c443275"
|
|
18491
|
+
},
|
|
18487
18492
|
{
|
|
18488
18493
|
"path": "tests/module_store_delist.mjs",
|
|
18489
18494
|
"mode": "0000644",
|
|
@@ -3223,5 +3223,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3223
3223
|
landed since 1.23.1 with no explicit bump. run 40399986777. (task 1002620)
|
|
3224
3224
|
1.23.3 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3225
3225
|
landed since 1.23.2 with no explicit bump. run 40402796050. (task 1002620)
|
|
3226
|
+
1.23.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3227
|
+
landed since 1.23.3 with no explicit bump. run 40406624494. (task 1002620)
|
|
3226
3228
|
---------------------------------------------------------------------------
|
|
3227
3229
|
```
|
package/docs/modules-contract.md
CHANGED
|
@@ -176,6 +176,8 @@ closed on every unknown, including a ledger read it cannot complete.
|
|
|
176
176
|
|
|
177
177
|
**Attribution (`author`/`origin`/`license`/`maintainer`) is portable metadata, not a cross-Bongos link** — each repo runs its own Bongos with its own builder identities ([ADR 0100](adr/0100-two-repo-instance-model.md) §3), so credit travels *with the module* rather than through a foreign key ([ADR 0107](adr/0107-module-upstreaming-contribution-to-core-catalog.md) §2). `bongos module new` stamps all four as TODO placeholders (license defaults to `AGPL-3.0`) so a module carries its authorship from the day it's scaffolded, not bolted on later at upstream-submit time.
|
|
178
178
|
|
|
179
|
+
**`bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]`** (task 1003811) is the **store** from the terminal: what the catalog lists, in its order — listed modules ranked, then New ones, unranked — with each module's score, price and place (parent, and sub-category once approved). It asks `GET /store/modules`, the same read the hall's "In the store" group makes, and filters, ranks and caps nothing itself: the quality floor, the ranking and the cap of ten per sub-category live only in `src/bongos/module-listing.js`, so the hall and the terminal cannot disagree about which modules are the best. `tests/module_store_cli.mjs` sends the same queries through the API and the CLI and requires the same modules in the same order.
|
|
180
|
+
|
|
179
181
|
**`bongos module list [--catalog] [--search <text>] [--json]`** ([ADR 0107](adr/0107-module-upstreaming-contribution-to-core-catalog.md) §2/§3, [ADR 0167](adr/0167-module-catalog-source-vs-provenance.md)) is the browsable catalog from the terminal: every module on disk with its enabled state, its author + origin credit, and its upkeep posture. `--catalog` narrows to the **core-shipped** modules — the shared catalog an instance opts into (§6, default-off). Two axes travel on each row and they are *not* the same question: **`source`** (which tree ships it — the core package or this instance's own repo, read structurally from the loader's discovery root, [ADR 0108](adr/0108-instance-composition-configurable-root.md) §1) and **`provenance`** (who built it, derived from the declared `origin`). A core module that declares no attribution is credited to the platform and the credit is **marked inferred** (`*`) rather than left blank or silently invented. The hall's Modules tab renders the same model (`src/module-loader/catalog.js`) via `GET /modules`, so the two surfaces cannot drift.
|
|
180
182
|
|
|
181
183
|
**`bongos module check <key> [--sign-off "Name <email>"]`** ([ADR 0107](adr/0107-module-upstreaming-contribution-to-core-catalog.md) §4) is the submit-time pre-check a module must clear before it can be upstreamed. It reuses (never re-types) the public-mirror publish denylist (`scripts/bongos/publish-manifest.js`'s `matchesDeny`) to reject content that cannot ship under AGPL (game content, pixel art, copyrighted reference material) — deliberately the DENY half only, since a brand-new module is never on the publish ALLOWLIST until it's accepted into core (§6). It also requires `module.json`'s `license` to be AGPL-3.0 (inbound = outbound) and a declared `maintenance` posture — acceptance is a commitment to upkeep, so a module with no `maintenance.status`, or one already `deprecated`/`orphaned`, is refused ([ADR 0166](adr/0166-module-maintenance-and-deprecation-policy.md); see below). On a pass, `--sign-off` records a lightweight DCO-style affirmation (own work, licensed AGPL-3.0 — never a copyright-assignment CLA) to `modules/<key>/.upstream-signoff.json`.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.4",
|
|
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.23.
|
|
3
|
+
"version": "1.23.4",
|
|
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
|
@@ -10401,5 +10401,11 @@
|
|
|
10401
10401
|
"id": "1003809",
|
|
10402
10402
|
"text": "The module store now shows at most ten modules in each sub-category, and a module that lands eleventh tells its author why."
|
|
10403
10403
|
}
|
|
10404
|
+
],
|
|
10405
|
+
"1.23.4": [
|
|
10406
|
+
{
|
|
10407
|
+
"id": "1003811",
|
|
10408
|
+
"text": "Builders can now browse the module store from the terminal with bongos module store, and it always shows the same modules in the same order as the hall."
|
|
10409
|
+
}
|
|
10404
10410
|
]
|
|
10405
10411
|
}
|
package/scripts/bongos/module.js
CHANGED
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
// model as the hall's Modules tab (src/module-loader/catalog.js), so the two
|
|
32
32
|
// surfaces cannot disagree about who built what.
|
|
33
33
|
//
|
|
34
|
+
// bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]
|
|
35
|
+
// Task 1003811 (criterion wa5-curated-listing) — the STORE from the terminal:
|
|
36
|
+
// what the catalog lists, in its order. It asks GET /store/modules, the same
|
|
37
|
+
// read the hall's "In the store" group makes, and never filters, ranks or caps
|
|
38
|
+
// anything itself — the quality floor, the ranking and the cap of ten per
|
|
39
|
+
// sub-category live in one place (src/bongos/module-listing.js), so the hall
|
|
40
|
+
// and the terminal cannot disagree about which modules are the best.
|
|
41
|
+
//
|
|
34
42
|
// bongos module check <key>
|
|
35
43
|
// ADR 0107 §4 — the submit-time pre-check a module must clear before it can
|
|
36
44
|
// be upstreamed into the shared core catalog. REUSES (never re-types) the
|
|
@@ -58,6 +66,7 @@
|
|
|
58
66
|
// bongos module new <key> scaffold modules/<key>/
|
|
59
67
|
// bongos module new <key> --force overwrite an existing modules/<key>/
|
|
60
68
|
// bongos module list [--catalog] browse the module catalog (author + origin credit)
|
|
69
|
+
// bongos module store [--sort <part>] [--max-cents N] [--max-credits N] what the store lists, best first
|
|
61
70
|
// bongos upgrade [--no-store] pre-check enabled modules vs the core version + list store updates
|
|
62
71
|
// bongos module check <key> [--sign-off "Name <email>"] AGPL/publish pre-check + DCO sign-off
|
|
63
72
|
// bongos module install <key> [--version X.Y.Z] acquire from the store, verify, place (not enabled)
|
|
@@ -1060,6 +1069,84 @@ function storeError(res) {
|
|
|
1060
1069
|
return (err && (err.message || err.code)) || `HTTP ${res.status}`;
|
|
1061
1070
|
}
|
|
1062
1071
|
|
|
1072
|
+
// ---------------------------------------------------------------------------
|
|
1073
|
+
// bongos module store — what the store lists (task 1003811)
|
|
1074
|
+
// ---------------------------------------------------------------------------
|
|
1075
|
+
|
|
1076
|
+
const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]';
|
|
1077
|
+
|
|
1078
|
+
// The query GET /store/modules takes, from argv. Returns { query } or { error }.
|
|
1079
|
+
// The server checks the values too; this only refuses what is not a value at all.
|
|
1080
|
+
function storeQuery(args) {
|
|
1081
|
+
const params = [];
|
|
1082
|
+
for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents'], ['--max-credits', 'max_credits']]) {
|
|
1083
|
+
const i = args.indexOf(flag);
|
|
1084
|
+
if (i === -1) continue;
|
|
1085
|
+
const v = args[i + 1];
|
|
1086
|
+
if (!v || v.startsWith('--')) return { error: `${flag} needs a value` };
|
|
1087
|
+
params.push(`${param}=${encodeURIComponent(v)}`);
|
|
1088
|
+
}
|
|
1089
|
+
return { query: params.length ? `?${params.join('&')}` : '' };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function formatPrice(price) {
|
|
1093
|
+
if (!price || price.free) return 'free';
|
|
1094
|
+
const parts = [];
|
|
1095
|
+
if (price.cents != null) parts.push(`$${(price.cents / 100).toFixed(2)}`);
|
|
1096
|
+
if (price.credits != null) parts.push(`${price.credits} credits`);
|
|
1097
|
+
return parts.join(' or ') || 'free';
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function formatPlace(category) {
|
|
1101
|
+
if (!category) return '';
|
|
1102
|
+
return category.label ? `${category.parent_title} › ${category.label}` : category.parent_title;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Pure renderer over the GET /store/modules body. It keeps the server's order and
|
|
1106
|
+
// adds nothing to it: listed in rank, then New, never ranked.
|
|
1107
|
+
function formatStore(body) {
|
|
1108
|
+
const out = [];
|
|
1109
|
+
out.push('bongos module store — what the store lists, best first');
|
|
1110
|
+
out.push(` floor ${body.floor} out of 100 · at most ${body.cap} per sub-category`);
|
|
1111
|
+
out.push(` ${body.ranking}`);
|
|
1112
|
+
if (body.sort) {
|
|
1113
|
+
const part = (body.sort_parts || []).find((x) => x.part === body.sort);
|
|
1114
|
+
out.push(` sorted by ${part ? part.title : body.sort}`);
|
|
1115
|
+
}
|
|
1116
|
+
const max = body.max_price;
|
|
1117
|
+
if (max) out.push(` at most ${[max.cents != null ? `$${(max.cents / 100).toFixed(2)}` : null, max.credits != null ? `${max.credits} credits` : null].filter(Boolean).join(' or ')}`);
|
|
1118
|
+
out.push('');
|
|
1119
|
+
const listed = body.listed || [];
|
|
1120
|
+
if (!listed.length) out.push(' Nothing is listed yet.');
|
|
1121
|
+
listed.forEach((m, i) => {
|
|
1122
|
+
out.push(` ${String(i + 1).padStart(2)}. ${m.key}@${m.version} ${m.overall}/100 ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
1123
|
+
});
|
|
1124
|
+
const fresh = body.new || [];
|
|
1125
|
+
if (fresh.length) {
|
|
1126
|
+
out.push('');
|
|
1127
|
+
out.push(' New — not ranked until projects have installed and used them:');
|
|
1128
|
+
for (const m of fresh) out.push(` ${m.key}@${m.version} ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
1129
|
+
}
|
|
1130
|
+
out.push('');
|
|
1131
|
+
out.push(' Why a module is not here: GET /store/modules/<key>/listing');
|
|
1132
|
+
return out.join('\n');
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
async function cmdStore(args, { log = console.log, errlog = console.error, request } = {}) {
|
|
1136
|
+
const q = storeQuery(args);
|
|
1137
|
+
if (q.error) { errlog(`bongos module store: ${q.error}`); errlog(STORE_USAGE); return 2; }
|
|
1138
|
+
let call;
|
|
1139
|
+
try { call = request || storeRequest(); }
|
|
1140
|
+
catch (e) { errlog(`bongos module store: ${e.message}`); return 1; }
|
|
1141
|
+
let res;
|
|
1142
|
+
try { res = await call('GET', `/store/modules${q.query}`); }
|
|
1143
|
+
catch (e) { errlog(`bongos module store: the store could not be reached — ${e.message}`); return 1; }
|
|
1144
|
+
if (!res.ok) { errlog(`bongos module store: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
|
|
1145
|
+
if (args.includes('--json')) { log(JSON.stringify(res.data, null, 2)); return 0; }
|
|
1146
|
+
log(formatStore(res.data || {}));
|
|
1147
|
+
return 0;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1063
1150
|
// Where install places a module: the INSTANCE's own modules/ root (ADR 0108 §1),
|
|
1064
1151
|
// which the loader discovers. In a single checkout that is this repo's modules/.
|
|
1065
1152
|
function instanceModulesDir() {
|
|
@@ -1319,6 +1406,8 @@ function printHelp(out = console.log) {
|
|
|
1319
1406
|
out(' bongos module new <key> Scaffold modules/<key>/ (manifest + skeleton + sample route + HOWTO.md + CLAUDE.md)');
|
|
1320
1407
|
out(' bongos module list [--catalog] [--search <text>] [--json]');
|
|
1321
1408
|
out(' Browse the module catalog — what you can enable, with author + origin credit');
|
|
1409
|
+
out(' bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]');
|
|
1410
|
+
out(' What the store lists, best first: above the quality floor, ranked, ten per sub-category');
|
|
1322
1411
|
out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
|
|
1323
1412
|
out(' then list newer store versions of installed modules (never blocks)');
|
|
1324
1413
|
out(' bongos module check <key> [--sign-off "Name <email>"]');
|
|
@@ -1338,6 +1427,7 @@ async function main(argv) {
|
|
|
1338
1427
|
const sub = args[0];
|
|
1339
1428
|
if (sub === 'new') return cmdNew(args.slice(1));
|
|
1340
1429
|
if (sub === 'list' || sub === 'ls') return cmdList(args.slice(1));
|
|
1430
|
+
if (sub === 'store') return cmdStore(args.slice(1));
|
|
1341
1431
|
if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
|
|
1342
1432
|
if (sub === 'check') return cmdCheck(args.slice(1));
|
|
1343
1433
|
if (sub === 'submit') return cmdSubmit(args.slice(1));
|
|
@@ -1354,7 +1444,7 @@ async function main(argv) {
|
|
|
1354
1444
|
}
|
|
1355
1445
|
|
|
1356
1446
|
module.exports = {
|
|
1357
|
-
main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
|
|
1447
|
+
main, cmdNew, cmdList, cmdStore, formatStore, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
|
|
1358
1448
|
storeRequest,
|
|
1359
1449
|
gatherCatalog, formatCatalog, ownProjectName,
|
|
1360
1450
|
scaffoldModule, gatherUpgrade, evaluateUpgrade, gatherUpdates, evaluateUpdates, buildManifest,
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.23.
|
|
84
|
+
const CORE_VERSION = '1.23.4'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// tests/module_store_cli.mjs — `bongos module store` and the hall read ONE catalog
|
|
2
|
+
// (task 1003811, criterion wa5-curated-listing). The terminal command asks the
|
|
3
|
+
// real GET /store/modules handler here — off the real router, with pool.js replaced
|
|
4
|
+
// in the require cache (the module_store_listing.mjs harness) — so the test pins
|
|
5
|
+
// what the task asks: the same query through the API and through the CLI returns
|
|
6
|
+
// the same modules in the same order, capped the same way. Two surfaces that drift
|
|
7
|
+
// on which modules are "the best" is worse than either being wrong alone.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/module_store_cli.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
|
|
17
|
+
// ---- the fake registry the route reads ----
|
|
18
|
+
const state = { modules: {}, versions: [], scores: [], prices: [], categories: {} };
|
|
19
|
+
const newestBy = (rows, keyOf, newer) => {
|
|
20
|
+
const out = new Map();
|
|
21
|
+
for (const r of rows) {
|
|
22
|
+
const k = keyOf(r);
|
|
23
|
+
if (!out.has(k) || newer(r, out.get(k))) out.set(k, r);
|
|
24
|
+
}
|
|
25
|
+
return [...out.values()];
|
|
26
|
+
};
|
|
27
|
+
const pool = {
|
|
28
|
+
async query(sql, params = []) {
|
|
29
|
+
if (/FROM store_module_categories c\b/.test(sql)) {
|
|
30
|
+
return { rows: params[0].filter((k) => state.categories[k]).map((k) => ({ module_key: k, ...state.categories[k] })) };
|
|
31
|
+
}
|
|
32
|
+
if (/FROM store_modules m JOIN store_module_versions v/.test(sql)) {
|
|
33
|
+
return {
|
|
34
|
+
rows: state.versions
|
|
35
|
+
.filter((v) => state.modules[v.module_key] && state.modules[v.module_key].status === 'listed')
|
|
36
|
+
.filter((v) => !params[0] || params[0].includes(v.module_key))
|
|
37
|
+
.map((v) => ({ ...v, version_id: v.id, title: state.modules[v.module_key].title })),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (/FROM store_module_prices/.test(sql)) {
|
|
41
|
+
return { rows: newestBy(state.prices.filter((p) => params[0].includes(p.module_key)), (p) => p.module_key, (a, b) => a.set_at > b.set_at) };
|
|
42
|
+
}
|
|
43
|
+
if (/FROM module_assessment_scores/.test(sql)) {
|
|
44
|
+
const ids = params[0].map(String);
|
|
45
|
+
return { rows: newestBy(state.scores.filter((s) => ids.includes(String(s.version_id))), (s) => s.version_id, (a, b) => a.computed_at > b.computed_at) };
|
|
46
|
+
}
|
|
47
|
+
return { rows: [] };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
51
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
|
|
52
|
+
|
|
53
|
+
const buildModulesRouter = require('../src/bongos/routes/modules.js');
|
|
54
|
+
const mod = require('../scripts/bongos/module.js');
|
|
55
|
+
|
|
56
|
+
function storeHandler() {
|
|
57
|
+
const layer = buildModulesRouter().stack.find((l) => l.route && l.route.path === '/store/modules' && l.route.methods.get);
|
|
58
|
+
const stack = layer.route.stack;
|
|
59
|
+
return stack[stack.length - 1].handle;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The API, as a browser or the hall reads it.
|
|
63
|
+
async function apiGet(query) {
|
|
64
|
+
const res = {
|
|
65
|
+
statusCode: 200, body: undefined,
|
|
66
|
+
json(b) { this.body = b; return this; },
|
|
67
|
+
status(c) { this.statusCode = c; return this; },
|
|
68
|
+
set() { return this; },
|
|
69
|
+
fail(code, opts) { this.statusCode = opts.status; this.body = { error: { code, message: opts.message } }; return this; },
|
|
70
|
+
};
|
|
71
|
+
let nextErr = null;
|
|
72
|
+
await storeHandler()({ builder: { id: 0 }, query }, res, (err) => { nextErr = err; });
|
|
73
|
+
if (nextErr) throw nextErr;
|
|
74
|
+
return res;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The CLI's request function, answered by the same handler: the URL the CLI
|
|
78
|
+
// builds is parsed back into the query the route reads.
|
|
79
|
+
const request = async (method, urlPath) => {
|
|
80
|
+
assert.equal(method, 'GET');
|
|
81
|
+
const url = new URL(urlPath, 'http://store.test');
|
|
82
|
+
assert.equal(url.pathname, '/store/modules');
|
|
83
|
+
const res = await apiGet(Object.fromEntries(url.searchParams));
|
|
84
|
+
return { ok: res.statusCode < 400, status: res.statusCode, data: res.body };
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function cliJson(args) {
|
|
88
|
+
const lines = [];
|
|
89
|
+
const errs = [];
|
|
90
|
+
const code = await mod.cmdStore([...args, '--json'], { log: (l) => lines.push(l), errlog: (l) => errs.push(l), request });
|
|
91
|
+
return { code, body: lines.length ? JSON.parse(lines.join('\n')) : null, errs };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const keys = (b) => b.listed.map((m) => m.key);
|
|
95
|
+
|
|
96
|
+
// Fifteen Stripe modules above 94 (the owner's example), one module in a proposed
|
|
97
|
+
// sub-category, one with no category, one New, and one under the floor.
|
|
98
|
+
function seed() {
|
|
99
|
+
state.modules = {}; state.versions = []; state.scores = []; state.prices = []; state.categories = {};
|
|
100
|
+
const add = (key, s, cat) => {
|
|
101
|
+
const id = state.versions.length + 1;
|
|
102
|
+
state.modules[key] = { status: 'listed', title: key };
|
|
103
|
+
state.versions.push({ id, module_key: key, version: '1.0.0', published_at: 't' });
|
|
104
|
+
state.scores.push({ id, version_id: id, computed_at: 1, overall: 70, security_passed: true, is_new: false, ...s });
|
|
105
|
+
if (cat) state.categories[key] = cat;
|
|
106
|
+
};
|
|
107
|
+
const stripe = { parent: 'money', sub_key: 'stripe-payments', label: 'Stripe payments', status: 'approved' };
|
|
108
|
+
for (let i = 0; i < 15; i++) add(`stripe-${String(i).padStart(2, '0')}`, { overall: 99 - i }, stripe);
|
|
109
|
+
add('waiting', { overall: 60 }, { parent: 'money', sub_key: 'stripe-billing', label: 'Stripe billing', status: 'proposed' });
|
|
110
|
+
add('loose', { overall: 88 });
|
|
111
|
+
add('fresh', { is_new: true, overall: null });
|
|
112
|
+
add('weak', { overall: 20 });
|
|
113
|
+
state.prices = [
|
|
114
|
+
{ module_key: 'stripe-01', price_cents: 900, price_credits: null, set_at: 1 },
|
|
115
|
+
{ module_key: 'loose', price_cents: null, price_credits: 40, set_at: 1 },
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test('the terminal shows what the API lists, in its order, capped the same way', async () => {
|
|
120
|
+
seed();
|
|
121
|
+
const api = (await apiGet({})).body;
|
|
122
|
+
const cli = await cliJson([]);
|
|
123
|
+
assert.equal(cli.code, 0);
|
|
124
|
+
assert.deepEqual(cli.body, api, 'the CLI hands back the catalog exactly as the API answers it');
|
|
125
|
+
assert.equal(keys(api).filter((k) => k.startsWith('stripe-')).length, 10, 'the cap of ten holds');
|
|
126
|
+
assert.ok(!keys(api).includes('stripe-10') && !keys(api).includes('weak'), 'the eleventh and the one under the floor are not listed');
|
|
127
|
+
assert.ok(keys(api).includes('waiting') && keys(api).includes('loose'), 'a proposed sub-category and no category are not capped');
|
|
128
|
+
assert.deepEqual(api.new.map((m) => m.key), ['fresh']);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the same query through both: a sort and a maximum price give the same modules in the same order', async () => {
|
|
132
|
+
seed();
|
|
133
|
+
for (const [args, query] of [
|
|
134
|
+
[['--sort', 'tests'], { sort: 'tests' }],
|
|
135
|
+
[['--max-cents', '500'], { max_cents: '500' }],
|
|
136
|
+
[['--max-credits', '0'], { max_credits: '0' }],
|
|
137
|
+
[['--sort', 'docs', '--max-cents', '1000', '--max-credits', '100'], { sort: 'docs', max_cents: '1000', max_credits: '100' }],
|
|
138
|
+
]) {
|
|
139
|
+
const api = (await apiGet(query)).body;
|
|
140
|
+
const cli = await cliJson(args);
|
|
141
|
+
assert.equal(cli.code, 0, args.join(' '));
|
|
142
|
+
assert.deepEqual(keys(cli.body), keys(api), args.join(' '));
|
|
143
|
+
assert.deepEqual(cli.body.new.map((m) => m.key), api.new.map((m) => m.key), args.join(' '));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the text view keeps the API order: listed ranked, then New, and says the floor and the cap', async () => {
|
|
148
|
+
seed();
|
|
149
|
+
const api = (await apiGet({})).body;
|
|
150
|
+
const lines = [];
|
|
151
|
+
assert.equal(await mod.cmdStore([], { log: (l) => lines.push(l), errlog: () => {}, request }), 0);
|
|
152
|
+
const text = lines.join('\n');
|
|
153
|
+
assert.match(text, /floor 50 out of 100 · at most 10 per sub-category/);
|
|
154
|
+
const at = (k) => text.indexOf(`${k}@`);
|
|
155
|
+
const order = [...keys(api), ...api.new.map((m) => m.key)].map(at);
|
|
156
|
+
assert.ok(order.every((i) => i !== -1), 'every listed and New module is shown');
|
|
157
|
+
assert.deepEqual(order, [...order].sort((a, b) => a - b), 'in the order the API gave');
|
|
158
|
+
assert.equal(at('stripe-10'), -1, 'the eleventh is not shown');
|
|
159
|
+
assert.match(text, /stripe-00@1\.0\.0 {2}99\/100 {2}free {2}Money › Stripe payments/);
|
|
160
|
+
assert.match(text, /stripe-01@1\.0\.0 {2}98\/100 {2}\$9\.00/);
|
|
161
|
+
assert.match(text, /loose@1\.0\.0 {2}88\/100 {2}40 credits$/m, 'no category, no place');
|
|
162
|
+
assert.match(text, /waiting@1\.0\.0 {2}60\/100 {2}free {2}Money$/m, 'a proposed sub-category shows its parent only');
|
|
163
|
+
assert.match(text, /New — not ranked[\s\S]*fresh@1\.0\.0/);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('a value the server refuses, a flag with no value, and no session are reported, not swallowed', async () => {
|
|
167
|
+
seed();
|
|
168
|
+
const bad = await cliJson(['--sort', 'security']);
|
|
169
|
+
assert.equal(bad.code, 2);
|
|
170
|
+
assert.match(bad.errs.join('\n'), /the store answered/);
|
|
171
|
+
const missing = await cliJson(['--max-cents']);
|
|
172
|
+
assert.equal(missing.code, 2);
|
|
173
|
+
assert.match(missing.errs.join('\n'), /--max-cents needs a value/);
|
|
174
|
+
const errs = [];
|
|
175
|
+
const offline = await mod.cmdStore([], { log: () => {}, errlog: (l) => errs.push(l), request: async () => { throw new Error('ECONNREFUSED'); } });
|
|
176
|
+
assert.equal(offline, 1);
|
|
177
|
+
assert.match(errs.join('\n'), /could not be reached — ECONNREFUSED/);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('`bongos module store` is dispatched and documented in the help', async () => {
|
|
181
|
+
const out = [];
|
|
182
|
+
mod.printHelp((l) => out.push(l));
|
|
183
|
+
assert.match(out.join('\n'), /bongos module store \[--sort <part>\]/);
|
|
184
|
+
});
|