@bongos/core 1.21.152 → 1.21.153
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 +42 -27
- package/docs/copy-inventory.md +313 -307
- package/docs/copy-registry.json +413 -359
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-readings.json +222 -216
- package/modules/hall-ui/public/modules.html +13 -0
- package/modules/hall-ui/public/modules.js +85 -0
- package/modules/hall-ui/public/modules.states.json +2 -1
- package/modules/hall-ui/records/catalog.md +2 -0
- package/modules/lifecycle/github-failure.js +50 -0
- package/modules/lifecycle/routes/tasks.js +2 -2
- package/modules/provisioning/github-setup.js +51 -1
- package/modules/provisioning/routes/provisioning.js +7 -2
- package/modules/public-landing/public/projects.html +33 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +14 -0
- package/src/bongos/module-listing.js +76 -2
- package/src/module-api.js +1 -1
- package/tests/github_failure.mjs +79 -0
- package/tests/github_install_nudge.mjs +219 -0
- package/tests/hall_catalog_world.mjs +19 -2
- package/tests/module_store_listing.mjs +78 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// tests/github_install_nudge.mjs — a project whose GitHub App is not installed keeps showing
|
|
2
|
+
// its owner the install link (task 1004763).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Toast (instance 52) was made before the wizard asked anyone to install the app (task
|
|
5
|
+
// 1004649), so nobody did, and every ship ended land-not-proven: the platform could not read
|
|
6
|
+
// the repo to see the merge. The install link lived only inside the creation wizard's
|
|
7
|
+
// checklist. Now the owner's two reads carry `github_install`, and the project list card and
|
|
8
|
+
// the project's own page say "Install the GitHub App" until the runner has seen the install.
|
|
9
|
+
//
|
|
10
|
+
// Three layers, DB-free and network-free:
|
|
11
|
+
// 1. modules/provisioning/github-setup.js — installNudge (pure) and installNudgesFor (two reads for a whole list).
|
|
12
|
+
// 2. the REAL list and instance routes over a mounted router: the field rides both.
|
|
13
|
+
// 3. projects.html's ghInstallRow, groupFor and the click handler, run in a vm: the link on
|
|
14
|
+
// the card and the manage page, "Needs you", and the click asking for a re-check.
|
|
15
|
+
//
|
|
16
|
+
// Run: node --test tests/github_install_nudge.mjs
|
|
17
|
+
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import { test, after } from 'node:test';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import vm from 'node:vm';
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const GS = require(path.join(ROOT, 'modules', 'provisioning', 'github-setup.js'));
|
|
29
|
+
const HUB = fs.readFileSync(path.join(ROOT, 'modules', 'public-landing', 'public', 'projects.html'), 'utf8').replace(/\r\n/g, '\n');
|
|
30
|
+
|
|
31
|
+
const INSTALL = 'https://github.com/apps/uattest-app/installations/new';
|
|
32
|
+
const APP_ROW = { id: 9, instance_id: 56, status: 'done', app_id: 5, app_slug: 'uattest-app', state: 'SECRETSTATE', attempts: 1,
|
|
33
|
+
created_at: '2026-10-01T00:00:00Z', updated_at: '2026-10-01T00:05:00Z', resolved_at: '2026-10-01T00:05:00Z' };
|
|
34
|
+
const INST = { id: 56, slug: 'uattest', status: 'active', target_ref: 'acme/uattest' };
|
|
35
|
+
|
|
36
|
+
// ── 1. the helper ───────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
test('installNudge: the link and repo while the install is unconfirmed, null otherwise', () => {
|
|
39
|
+
const setup = (install, url = INSTALL) => ({ checklist: { install }, install_url: url, repo: 'acme/uattest' });
|
|
40
|
+
assert.deepEqual(GS.installNudge(setup('now')), { install_url: INSTALL, repo: 'acme/uattest' });
|
|
41
|
+
assert.equal(GS.installNudge(setup('done')), null, 'GitHub confirmed it: nothing to say');
|
|
42
|
+
assert.equal(GS.installNudge(setup('todo', null)), null, 'no app of ours: nothing to install');
|
|
43
|
+
assert.equal(GS.installNudge(null), null);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// A fake pool answering the two batched reads from plain tables, counting the queries.
|
|
47
|
+
function fakeDb({ attempts = [], installedEvents = [], fail = false } = {}) {
|
|
48
|
+
const db = { queries: 0 };
|
|
49
|
+
db.query = async (sql, params) => {
|
|
50
|
+
db.queries += 1;
|
|
51
|
+
if (fail) throw new Error('db down');
|
|
52
|
+
if (/FROM provisioning_oauth_manifests/.test(sql)) {
|
|
53
|
+
const ids = params[0].map(String);
|
|
54
|
+
return { rows: attempts.filter((r) => ids.includes(String(r.instance_id))) };
|
|
55
|
+
}
|
|
56
|
+
if (/FROM provisioning_events/.test(sql)) {
|
|
57
|
+
assert.equal(params[2], GS.SETUP_EVENTS.installed, 'only the verified install counts, never a report');
|
|
58
|
+
const since = new Map(params[0].map((id, i) => [String(id), Date.parse(params[1][i])]));
|
|
59
|
+
return { rows: installedEvents.filter((e) => since.has(String(e.instance_id)) && Date.parse(e.created_at) >= since.get(String(e.instance_id))) };
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`unexpected query: ${sql}`);
|
|
62
|
+
};
|
|
63
|
+
return db;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
test('installNudgesFor: two reads for the whole list; silent with no app, once installed, switched off, or on a failed read', async () => {
|
|
67
|
+
const insts = [
|
|
68
|
+
INST,
|
|
69
|
+
{ id: 52, slug: 'toast', status: 'active', target_ref: 'acme/toast' },
|
|
70
|
+
{ id: 13, slug: 'charter', status: 'active', target_ref: 'acme/charter' },
|
|
71
|
+
{ id: 70, slug: 'gone', status: 'torn_down', target_ref: 'acme/gone' },
|
|
72
|
+
{ id: 71, slug: 'old', status: 'active', target_ref: 'acme/old' },
|
|
73
|
+
];
|
|
74
|
+
const db = fakeDb({
|
|
75
|
+
attempts: [
|
|
76
|
+
{ instance_id: '56', app_slug: 'uattest-app', created_at: '2026-10-01T00:00:00Z' },
|
|
77
|
+
{ instance_id: '52', app_slug: 'toast-app', created_at: '2026-10-01T00:00:00Z' },
|
|
78
|
+
{ instance_id: '13', app_slug: null, created_at: '2026-10-01T00:00:00Z' },
|
|
79
|
+
{ instance_id: '70', app_slug: 'gone-app', created_at: '2026-10-01T00:00:00Z' },
|
|
80
|
+
{ instance_id: '71', app_slug: 'old-app', created_at: '2026-10-05T00:00:00Z' },
|
|
81
|
+
],
|
|
82
|
+
installedEvents: [
|
|
83
|
+
{ instance_id: '52', created_at: '2026-10-20T10:00:00Z' },
|
|
84
|
+
{ instance_id: '71', created_at: '2026-10-02T00:00:00Z' },
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const n = await GS.installNudgesFor(db, insts);
|
|
88
|
+
assert.equal(db.queries, 2, 'two reads, however many projects');
|
|
89
|
+
assert.deepEqual(n.get('56'), { install_url: 'https://github.com/apps/uattest-app/installations/new', repo: 'acme/uattest' }, 'the stateless link: never the state token');
|
|
90
|
+
assert.equal(n.get('52'), null, 'the runner saw the install');
|
|
91
|
+
assert.equal(n.get('13'), null, 'no app was made: nothing to install');
|
|
92
|
+
assert.equal(n.get('70'), null, 'a switched-off project is silent');
|
|
93
|
+
assert.deepEqual(n.get('71'), { install_url: 'https://github.com/apps/old-app/installations/new', repo: 'acme/old' }, 'an install seen before the latest attempt began does not count for it');
|
|
94
|
+
const empty = fakeDb();
|
|
95
|
+
assert.equal((await GS.installNudgesFor(empty, [{ ...INST, status: 'torn_down' }])).get('56'), null);
|
|
96
|
+
assert.equal(empty.queries, 0, 'nothing live: no read at all');
|
|
97
|
+
const broken = await GS.installNudgesFor(fakeDb({ fail: true }), insts);
|
|
98
|
+
assert.ok([...broken.values()].every((v) => v === null), 'a failed read is no nudge, never a failed list');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('the instance read: the nudge comes from the same checklist the wizard reads', () => {
|
|
102
|
+
const read = (install, url) => ({ checklist: { install }, install_url: url, repo: 'acme/uattest' });
|
|
103
|
+
assert.equal(GS.installNudge(read('now', INSTALL)).install_url, INSTALL);
|
|
104
|
+
assert.equal(GS.installNudge(read('todo', null)), null, 'no app: githubSetupRead gives no link, so no nudge');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── 2. the owner's two reads ────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
test('GET /provisioning/instances and /provisioning/instances/:id carry github_install', async () => {
|
|
110
|
+
const provisioning = require(path.join(ROOT, 'modules', 'provisioning', 'provisioning.js'));
|
|
111
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
112
|
+
const catalogBridge = require(path.join(ROOT, 'modules', 'provisioning', 'catalog-bridge.js'));
|
|
113
|
+
const saved = {
|
|
114
|
+
list: provisioning.listInstancesForOwner, byId: provisioning.getInstanceById, open: provisioning.getOpenIntent,
|
|
115
|
+
latest: provisioning.getLatestOAuthManifestForInstance, rb: api.requireBuilder, heal: catalogBridge.healInstanceCatalog, facts: GS.githubSetupFacts,
|
|
116
|
+
};
|
|
117
|
+
const owned = [{ ...INST, owner_builder_id: 7 }, { id: 52, slug: 'toast', status: 'active', target_ref: 'acme/toast', owner_builder_id: 7 }];
|
|
118
|
+
provisioning.listInstancesForOwner = async () => owned;
|
|
119
|
+
provisioning.getInstanceById = async (_db, id) => owned.find((i) => Number(i.id) === Number(id)) || null;
|
|
120
|
+
provisioning.getOpenIntent = async () => null;
|
|
121
|
+
provisioning.getLatestOAuthManifestForInstance = async (_db, id) => ({ ...APP_ROW, instance_id: id, app_slug: `app-${id}` });
|
|
122
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, rank: 'metic' }; next(); };
|
|
123
|
+
catalogBridge.healInstanceCatalog = async () => {};
|
|
124
|
+
GS.githubSetupFacts = async () => ({});
|
|
125
|
+
const realBatch = GS.installNudgesFor;
|
|
126
|
+
const listDb = fakeDb({
|
|
127
|
+
attempts: owned.map((i) => ({ instance_id: String(i.id), app_slug: `app-${i.id}`, created_at: '2026-10-01T00:00:00Z' })),
|
|
128
|
+
installedEvents: [{ instance_id: '52', created_at: '2026-10-20T10:00:00Z' }],
|
|
129
|
+
});
|
|
130
|
+
GS.installNudgesFor = (_pool, rows) => realBatch(listDb, rows);
|
|
131
|
+
|
|
132
|
+
const express = require('express');
|
|
133
|
+
const app = express();
|
|
134
|
+
app.use((_req, res, next) => { res.fail = (code, opts) => res.status(typeof opts === 'number' ? opts : (opts && opts.status) || 400).json({ error: { code } }); next(); });
|
|
135
|
+
app.use('/api/bongos', require(path.join(ROOT, 'modules', 'provisioning', 'routes', 'provisioning.js'))());
|
|
136
|
+
const server = app.listen(0);
|
|
137
|
+
await new Promise((r) => server.once('listening', r));
|
|
138
|
+
after(() => {
|
|
139
|
+
server.close();
|
|
140
|
+
Object.assign(provisioning, { listInstancesForOwner: saved.list, getInstanceById: saved.byId, getOpenIntent: saved.open, getLatestOAuthManifestForInstance: saved.latest });
|
|
141
|
+
api.requireBuilder = saved.rb; catalogBridge.healInstanceCatalog = saved.heal; GS.githubSetupFacts = saved.facts; GS.installNudgesFor = realBatch;
|
|
142
|
+
});
|
|
143
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
144
|
+
|
|
145
|
+
const list = await (await fetch(`${base}/provisioning/instances`)).json();
|
|
146
|
+
const by = Object.fromEntries(list.instances.map((i) => [i.slug, i]));
|
|
147
|
+
assert.deepEqual(by.uattest.github_install, { install_url: 'https://github.com/apps/app-56/installations/new', repo: 'acme/uattest' });
|
|
148
|
+
assert.equal(by.toast.github_install, null, 'installed: no nudge');
|
|
149
|
+
assert.equal(listDb.queries, 2, 'the list reads the nudges in two queries');
|
|
150
|
+
|
|
151
|
+
const one = await (await fetch(`${base}/provisioning/instances/56`)).json();
|
|
152
|
+
assert.deepEqual(one.instance.github_install, { install_url: 'https://github.com/apps/app-56/installations/new', repo: 'acme/uattest' });
|
|
153
|
+
assert.ok(!/SECRETSTATE/.test(JSON.stringify(one)) && !/SECRETSTATE/.test(JSON.stringify(list)), 'the state token never rides an owner read');
|
|
154
|
+
assert.equal(one.github_setup.checklist.install, 'now', 'the wizard checklist still rides the same read');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ── 3. the page ─────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function grab(head) {
|
|
160
|
+
const at = HUB.indexOf(`\n ${head}`);
|
|
161
|
+
assert.ok(at >= 0, `${head} must exist`);
|
|
162
|
+
const end = HUB.indexOf('\n }', at + 1);
|
|
163
|
+
const close = HUB.slice(end, end + 5) === '\n };' ? end + 5 : end + 4;
|
|
164
|
+
return HUB.slice(at, close);
|
|
165
|
+
}
|
|
166
|
+
function page() {
|
|
167
|
+
const ctx = { STATE_GROUP: { active: 'live', error: 'attention', torn_down: 'gone' } };
|
|
168
|
+
vm.createContext(ctx);
|
|
169
|
+
vm.runInContext(['function esc(', 'function ghInstallRow(', 'function groupFor('].map(grab).join('\n'), ctx);
|
|
170
|
+
return ctx;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test('ghInstallRow: the install link on its own, for an unconfirmed install only', () => {
|
|
174
|
+
const p = page();
|
|
175
|
+
const html = p.ghInstallRow({ id: 56, status: 'active', github_install: { install_url: INSTALL, repo: 'acme/uattest' } });
|
|
176
|
+
assert.match(html, /Install the GitHub App ↗/);
|
|
177
|
+
assert.match(html, new RegExp(`href="${INSTALL.replace(/[.?/]/g, '\\$&')}" target="_blank" rel="noopener" data-gh-install="56"`));
|
|
178
|
+
assert.match(html, /<code>acme\/uattest<\/code>/);
|
|
179
|
+
assert.equal(p.ghInstallRow({ id: 52, status: 'active', github_install: null }), '');
|
|
180
|
+
assert.equal(p.ghInstallRow({ id: 56, status: 'torn_down', github_install: { install_url: INSTALL } }), '', 'switched off: silent');
|
|
181
|
+
assert.equal(p.ghInstallRow({ id: 56, status: 'active', github_install: { install_url: 'https://evil.example/x' } }), '', 'only GitHub\'s own app pages are linked');
|
|
182
|
+
assert.match(p.ghInstallRow({ id: 56, status: 'active', github_install: { install_url: INSTALL, repo: '<b>x</b>' } }), /<b>/, 'the repo is escaped');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('groupFor: a project waiting on the install counts under Needs you', () => {
|
|
186
|
+
const p = page();
|
|
187
|
+
assert.equal(p.groupFor({ status: 'active', last_verified_up: true, github_install: { install_url: INSTALL } }), 'attention');
|
|
188
|
+
assert.equal(p.groupFor({ status: 'active', last_verified_up: true, github_install: null }), 'live');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('the card and the project\'s own page both draw it', () => {
|
|
192
|
+
assert.match(grab('function renderCard('), /var gh = ghInstallRow\(inst\);\n if \(gh\) body \+= '<div class="projMsg">' \+ gh \+ '<\/div>';/);
|
|
193
|
+
assert.match(grab('function renderManage('), /var gh = ghInstallRow\(inst\);\n \$\('mGh'\)\.innerHTML = gh;\n \$\('mGh'\)\.hidden = !gh;/);
|
|
194
|
+
assert.match(HUB, /<div class="projMsg" id="mGh" hidden><\/div>/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('the click opens GitHub and asks the platform to look again', async () => {
|
|
198
|
+
const at = HUB.indexOf("\n document.addEventListener('click', function (ev) {\n var a = ev.target.closest && ev.target.closest('a[data-gh-install]');");
|
|
199
|
+
assert.ok(at >= 0, 'the install click handler must exist');
|
|
200
|
+
const src = HUB.slice(at, HUB.indexOf('\n });', at) + 6);
|
|
201
|
+
let listener = null;
|
|
202
|
+
const calls = [];
|
|
203
|
+
const ctx = {
|
|
204
|
+
API: '/api/bongos',
|
|
205
|
+
document: { addEventListener: (type, fn) => { if (type === 'click') listener = fn; } },
|
|
206
|
+
fetch: (url, opts) => { calls.push({ url, opts }); return Promise.resolve({ ok: true }); },
|
|
207
|
+
};
|
|
208
|
+
vm.createContext(ctx);
|
|
209
|
+
vm.runInContext(src, ctx);
|
|
210
|
+
let prevented = false;
|
|
211
|
+
const link = { getAttribute: (k) => (k === 'data-gh-install' ? '56' : null) };
|
|
212
|
+
listener({ target: { closest: (sel) => (sel === 'a[data-gh-install]' ? link : null) }, preventDefault: () => { prevented = true; } });
|
|
213
|
+
assert.equal(prevented, false, 'the link still opens GitHub');
|
|
214
|
+
assert.equal(calls.length, 1);
|
|
215
|
+
assert.equal(calls[0].url, '/api/bongos/provisioning/instances/56/github-app/pick-up');
|
|
216
|
+
assert.equal(calls[0].opts.method, 'POST');
|
|
217
|
+
listener({ target: { closest: () => null }, preventDefault() {} });
|
|
218
|
+
assert.equal(calls.length, 1, 'any other click asks nothing');
|
|
219
|
+
});
|
|
@@ -39,7 +39,7 @@ const noAlias = (rawCss, where) => {
|
|
|
39
39
|
|
|
40
40
|
// ---- modules ----------------------------------------------------------------
|
|
41
41
|
|
|
42
|
-
test('modules: ONE panel of family rows — the filter bar, the figure band and two groups on one .scroll', () => {
|
|
42
|
+
test('modules: ONE panel of family rows — the filter bar, the figure band and two groups on one .scroll (a third, the store, since task 1003799)', () => {
|
|
43
43
|
const html = read('modules.html');
|
|
44
44
|
assert.match(html, /<link rel="stylesheet" href="\/builders\/oversight\.css">\s*<link rel="stylesheet" href="\/builders\/modules\.css">/, 'the family sheet loads before the page sheet');
|
|
45
45
|
assert.equal((html.match(/class="scroll[ "]/g) || []).length, 1, 'one panel, not three');
|
|
@@ -47,7 +47,7 @@ test('modules: ONE panel of family rows — the filter bar, the figure band and
|
|
|
47
47
|
assert.match(html, /class="ov-figs" id="mod-figs"/, 'the figure band is the family\'s');
|
|
48
48
|
assert.match(html, /<ul class="ov-rows" id="core-grid">/, 'the core catalog is a run of rows');
|
|
49
49
|
assert.match(html, /<ul class="ov-rows" id="own-grid">/, 'the project\'s own modules are a run of rows');
|
|
50
|
-
assert.equal((html.match(/class="ov-group__head"/g) || []).length,
|
|
50
|
+
assert.equal((html.match(/class="ov-group__head"/g) || []).length, 3, 'three group heads: the core, this project, the store');
|
|
51
51
|
assert.doesNotMatch(decomment(html), /mod-grid|mod-card|mod-browse|submit-scroll|class="settings-status"/, 'the card grid, the boxed search and the third panel are gone');
|
|
52
52
|
assert.doesNotMatch(html, /\d+ module\(s\)/, 'the page head no longer says the numbers as prose');
|
|
53
53
|
});
|
|
@@ -89,6 +89,23 @@ test('modules: upkeep colours the exception — undeclared is the warn pill, mai
|
|
|
89
89
|
assert.match(js, /cat\.parentTitle, cat\.sub\]/, 'the hall search covers the category, as searchCatalog does');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
test('modules: the store group reads strengths and weaknesses off the server row (task 1003799)', () => {
|
|
93
|
+
const js = read('modules.js');
|
|
94
|
+
const html = read('modules.html');
|
|
95
|
+
assert.match(html, /<div class="ov-group" id="store-group" hidden>/, 'the store group is hidden until the store answers');
|
|
96
|
+
assert.match(html, /<ul class="ov-rows" id="store-grid"><\/ul>/);
|
|
97
|
+
assert.match(js, /api\.request\('GET', `\$\{API\}\/store\/modules`\)/, 'it reads GET /store/modules');
|
|
98
|
+
assert.match(js, /<span class="ov-fact">strong at \$\{partsList\(strong\)\}<\/span>/, 'a strength is a plain fact');
|
|
99
|
+
assert.match(js, /<span class="fact-pill fact-pill--warn">weak at \$\{partsList\(weak\)\}<\/span>/, 'a weakness is the warn pill');
|
|
100
|
+
assert.match(js, /not measured yet/, 'a part with no data is named, never counted as weak');
|
|
101
|
+
assert.match(js, /<details class="mod-more"[^`]*Strengths and weaknesses of/, 'the detail sentence is one click away');
|
|
102
|
+
assert.doesNotMatch(decomment(js), /score\s*>=\s*80|STRONG_AT/, 'the page does not re-derive the reading — the server owns the thresholds');
|
|
103
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts', 'hall-preview', 'fixtures', 'store__modules.json'), 'utf8'));
|
|
104
|
+
const two = fixture.listed.filter((m) => m.overall === 74);
|
|
105
|
+
assert.equal(two.length, 2, 'the harness shows the criterion: two modules on the same score');
|
|
106
|
+
assert.notDeepEqual(two[0].strengths.map((s) => s.part), two[1].strengths.map((s) => s.part), '…that are strong at different things');
|
|
107
|
+
});
|
|
108
|
+
|
|
92
109
|
test('modules: the kit filter bar carries the search and a state lens, and the empty state is the kit\'s', () => {
|
|
93
110
|
const js = read('modules.js');
|
|
94
111
|
assert.match(js, /kit\.makeFilterBar\(\{/, 'the bar is the kit\'s');
|
|
@@ -15,7 +15,7 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
16
16
|
|
|
17
17
|
// ---- the fake registry the route reads ----
|
|
18
|
-
const state = { modules: {}, versions: [], scores: [] };
|
|
18
|
+
const state = { modules: {}, versions: [], scores: [], signals: [] };
|
|
19
19
|
const pool = {
|
|
20
20
|
async query(sql, params = []) {
|
|
21
21
|
if (/FROM store_modules m JOIN store_module_versions v/.test(sql)) {
|
|
@@ -35,6 +35,17 @@ const pool = {
|
|
|
35
35
|
}
|
|
36
36
|
return { rows: [...newest.values()] };
|
|
37
37
|
}
|
|
38
|
+
if (/FROM module_assessment_signals/.test(sql)) {
|
|
39
|
+
const ids = params[0].map(String);
|
|
40
|
+
const newest = new Map();
|
|
41
|
+
for (const s of state.signals) {
|
|
42
|
+
if (!ids.includes(String(s.version_id))) continue;
|
|
43
|
+
const k = `${s.version_id}:${s.part}`;
|
|
44
|
+
const have = newest.get(k);
|
|
45
|
+
if (!have || s.measured_at > have.measured_at || (s.measured_at === have.measured_at && s.id > have.id)) newest.set(k, s);
|
|
46
|
+
}
|
|
47
|
+
return { rows: [...newest.values()] };
|
|
48
|
+
}
|
|
38
49
|
return { rows: [] };
|
|
39
50
|
},
|
|
40
51
|
};
|
|
@@ -154,3 +165,69 @@ test('GET /store/modules: an empty store answers empty lists and the floor', asy
|
|
|
154
165
|
const res = await invoke(handle, {});
|
|
155
166
|
assert.deepEqual(res.body, { floor: 50, listed: [], new: [] });
|
|
156
167
|
});
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// strengths and weaknesses (task 1003799, ADR 0343 D4)
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const { readParts, STRONG_AT } = require('../src/bongos/module-listing.js');
|
|
174
|
+
const sig = (part, score, extra = {}) => ({ part, outcome: 'scored', score, sample_size: null, ...extra });
|
|
175
|
+
|
|
176
|
+
test('readParts: a part at or above STRONG_AT is a strength, below the floor a weakness, between is neither', () => {
|
|
177
|
+
assert.equal(STRONG_AT, 80);
|
|
178
|
+
const r = readParts([sig('tests', 80), sig('install', 79), sig('reliability', 49), sig('docs', 50)]);
|
|
179
|
+
assert.deepEqual(r.strengths.map((s) => s.part), ['tests']);
|
|
180
|
+
assert.deepEqual(r.weaknesses.map((s) => s.part), ['reliability']);
|
|
181
|
+
assert.deepEqual(r.unmeasured.map((s) => s.part), ['tester_feedback'], 'a part with no row is unmeasured');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('readParts: an unmeasured part is never a weakness, and Security is never listed', () => {
|
|
185
|
+
const r = readParts([
|
|
186
|
+
{ part: 'security', outcome: 'passed', score: null },
|
|
187
|
+
{ part: 'install', outcome: 'no_data', score: null },
|
|
188
|
+
{ part: 'reliability', outcome: 'pending', score: null },
|
|
189
|
+
{ part: 'tester_feedback', outcome: 'not_scored', score: null },
|
|
190
|
+
sig('tests', 10),
|
|
191
|
+
]);
|
|
192
|
+
assert.deepEqual(r.weaknesses.map((s) => s.part), ['tests']);
|
|
193
|
+
assert.deepEqual(r.unmeasured.map((s) => s.part), ['install', 'reliability', 'tester_feedback', 'docs']);
|
|
194
|
+
for (const list of [r.strengths, r.weaknesses, r.unmeasured]) assert.ok(!list.some((s) => s.part === 'security'));
|
|
195
|
+
assert.ok(!('security' in r.parts), 'the gate is not one of the parts a buyer compares');
|
|
196
|
+
assert.deepEqual(r.parts.install, { outcome: 'no_data', score: null, sample_size: null });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('readParts: strongest first, weakest first, and the detail says what the number rests on', () => {
|
|
200
|
+
const r = readParts([sig('tests', 85, { sample_size: 40 }), sig('docs', 95), sig('install', 30, { sample_size: 1 }), sig('reliability', 45, { sample_size: 12 })]);
|
|
201
|
+
assert.deepEqual(r.strengths.map((s) => [s.part, s.score]), [['docs', 95], ['tests', 85]]);
|
|
202
|
+
assert.deepEqual(r.weaknesses.map((s) => [s.part, s.score]), [['install', 30], ['reliability', 45]]);
|
|
203
|
+
assert.equal(r.strengths[1].detail, 'Tests scored 85 out of 100, from 40 test cases.');
|
|
204
|
+
assert.equal(r.weaknesses[0].detail, 'Clean installs scored 30 out of 100, from 1 install.');
|
|
205
|
+
assert.equal(r.strengths[0].detail, 'How-to scored 95 out of 100.');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('the criterion: two modules with the same overall score read differently', async () => {
|
|
209
|
+
state.modules = { steady: { status: 'listed', title: 'Steady' }, tested: { status: 'listed', title: 'Tested' } };
|
|
210
|
+
state.versions = [
|
|
211
|
+
{ id: 11, module_key: 'steady', version: '1.0.0', published_at: 't' },
|
|
212
|
+
{ id: 12, module_key: 'tested', version: '1.0.0', published_at: 't' },
|
|
213
|
+
];
|
|
214
|
+
state.scores = [
|
|
215
|
+
{ id: 11, version_id: 11, computed_at: 1, ...score({ overall: 74 }) },
|
|
216
|
+
{ id: 12, version_id: 12, computed_at: 1, ...score({ overall: 74 }) },
|
|
217
|
+
];
|
|
218
|
+
state.signals = [
|
|
219
|
+
{ id: 1, version_id: 11, measured_at: 1, ...sig('tests', 58) },
|
|
220
|
+
{ id: 2, version_id: 11, measured_at: 1, ...sig('reliability', 40) }, // superseded below
|
|
221
|
+
{ id: 3, version_id: 11, measured_at: 2, ...sig('reliability', 90) },
|
|
222
|
+
{ id: 4, version_id: 12, measured_at: 1, ...sig('tests', 96) },
|
|
223
|
+
{ id: 5, version_id: 12, measured_at: 1, ...sig('reliability', 52) },
|
|
224
|
+
];
|
|
225
|
+
const { handle } = handlerFor(buildModulesRouter(), 'get', '/store/modules');
|
|
226
|
+
const res = await invoke(handle, {});
|
|
227
|
+
const by = Object.fromEntries(res.body.listed.map((m) => [m.key, m]));
|
|
228
|
+
assert.equal(by.steady.overall, by.tested.overall);
|
|
229
|
+
assert.deepEqual(by.steady.strengths.map((s) => s.part), ['reliability'], 'the newest reliability row wins');
|
|
230
|
+
assert.deepEqual(by.tested.strengths.map((s) => s.part), ['tests']);
|
|
231
|
+
assert.equal(by.steady.parts.reliability.score, 90);
|
|
232
|
+
state.signals = [];
|
|
233
|
+
});
|