@bongos/core 1.21.31 → 1.21.32
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 +37 -27
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +125 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +27 -27
- package/docs/copy-registry.json +34 -34
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +11 -11
- package/modules/government/catalog.js +1 -0
- package/modules/government/migrations/government_024_version_focus_set.sql +19 -0
- package/modules/hall-ui/public/roadmap.css +5 -0
- package/modules/hall-ui/public/roadmap.js +64 -5
- package/modules/lifecycle/routes/versions.js +20 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/project-settings.js +28 -0
- package/src/module-api.js +3 -1
- package/tests/focus_version.mjs +96 -0
|
@@ -147,6 +147,59 @@
|
|
|
147
147
|
return `<a class="rm-gmore" href="${tasksLink(v.id)}" aria-label="${escapeHtml(label)}" title="${escapeHtml(label)}">View tasks ${arrowSvg}</a>`;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// The focus star (task 1004551). The focus version is the project's default — chosen by
|
|
151
|
+
// an Archon here, stored server-side, never inferred. Filled = current focus. Archons
|
|
152
|
+
// get a button that sets/clears it; everyone else gets the same star read-only. The
|
|
153
|
+
// button is a convenience: PUT /versions/focus refuses non-Archons on the server.
|
|
154
|
+
const focus = { id: null, canSet: false };
|
|
155
|
+
const FOCUS_FAIL = 'Could not change the focus version. Reload and try again.';
|
|
156
|
+
function starHtml(v) {
|
|
157
|
+
const on = focus.id === v.id;
|
|
158
|
+
const glyph = on ? '★' : '☆';
|
|
159
|
+
const base = `class="rm-star${on ? ' rm-star--on' : ''}" data-vid="${escapeHtml(v.id)}"`;
|
|
160
|
+
if (focus.canSet) {
|
|
161
|
+
const tip = on ? 'Clear the focus version.' : 'Make this the project’s focus version.';
|
|
162
|
+
return `<button type="button" ${base} aria-pressed="${on}" title="${tip}" aria-label="${tip}">${glyph}</button>`;
|
|
163
|
+
}
|
|
164
|
+
const tip = (on ? 'This is the project’s focus version. ' : 'Not the focus version. ') + 'Only an Archon can set the focus.';
|
|
165
|
+
return `<span ${base} role="img" title="${tip}" aria-label="${tip}">${glyph}</span>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Never throws except to hand a sign-in redirect back to boot(); a failed focus read
|
|
169
|
+
// just means no star is lit.
|
|
170
|
+
async function loadFocus() {
|
|
171
|
+
try {
|
|
172
|
+
const f = await getJSON(`${API}/versions/focus`);
|
|
173
|
+
focus.id = f.focus_version_id || null;
|
|
174
|
+
focus.canSet = f.can_set === true;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err && err.message === 'redirecting to auth') throw err;
|
|
177
|
+
focus.id = null; focus.canSet = false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Flip the stars in place: the PUT already answered with the new focus, so there is
|
|
182
|
+
// nothing to refetch.
|
|
183
|
+
function paintStars() {
|
|
184
|
+
document.querySelectorAll('.rm-star').forEach((el) => {
|
|
185
|
+
const tmp = document.createElement('div');
|
|
186
|
+
tmp.innerHTML = starHtml({ id: el.dataset.vid });
|
|
187
|
+
el.replaceWith(tmp.firstElementChild);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function toggleFocus(vid) {
|
|
192
|
+
const next = focus.id === vid ? null : vid;
|
|
193
|
+
const res = await api.request('PUT', `${API}/versions/focus`, { body: { version_id: next } });
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
const e = res.data && res.data.error;
|
|
196
|
+
toast((e && e.message) || FOCUS_FAIL);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
focus.id = res.data.focus_version_id || null;
|
|
200
|
+
paintStars();
|
|
201
|
+
}
|
|
202
|
+
|
|
150
203
|
// ---- card renderers -------------------------------------------------------
|
|
151
204
|
|
|
152
205
|
function featCard(v) {
|
|
@@ -155,7 +208,7 @@
|
|
|
155
208
|
<div class="rm-featgrid__l">
|
|
156
209
|
<div class="rm-feat-top">
|
|
157
210
|
<div class="rt">
|
|
158
|
-
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span
|
|
211
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span>${starHtml(v)}</div>
|
|
159
212
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
160
213
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
161
214
|
</div>
|
|
@@ -176,7 +229,7 @@
|
|
|
176
229
|
return `<article class="rm-card">
|
|
177
230
|
<div class="rm-feat-top">
|
|
178
231
|
<div class="rt">
|
|
179
|
-
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span
|
|
232
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span>${starHtml(v)}</div>
|
|
180
233
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
181
234
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
182
235
|
</div>
|
|
@@ -218,7 +271,7 @@
|
|
|
218
271
|
return `<article class="rm-card rm-card--staged" style="grid-column:1 / -1">
|
|
219
272
|
<div class="rm-featgrid">
|
|
220
273
|
<div class="rm-featgrid__l">
|
|
221
|
-
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span
|
|
274
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span>${starHtml(v)}</div>
|
|
222
275
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
223
276
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
224
277
|
<div class="rm-staged-state">
|
|
@@ -236,7 +289,7 @@
|
|
|
236
289
|
return `<article class="rm-card rm-card--parked" style="grid-column:1 / -1">
|
|
237
290
|
<div class="rm-feat-top">
|
|
238
291
|
<div class="rt">
|
|
239
|
-
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span
|
|
292
|
+
<div class="rm-row">${phase(v)}<span class="rm-id">${escapeHtml(v.id)}</span>${starHtml(v)}</div>
|
|
240
293
|
<h3 class="rm-name">${escapeHtml(v.name)}</h3>
|
|
241
294
|
<p class="rm-desc rm-desc--clamp">${escapeHtml(v.desc)}</p>
|
|
242
295
|
</div>
|
|
@@ -381,9 +434,10 @@
|
|
|
381
434
|
async function render() {
|
|
382
435
|
const root = $('#rm-root');
|
|
383
436
|
const [progress, versions] = await Promise.all([
|
|
437
|
+
loadFocus(),
|
|
384
438
|
getJSON(`${API}/public/progress`).then((d) => d.progress || []),
|
|
385
439
|
getJSON(`${API}/public/versions`).then((d) => d.versions || []).catch(() => []),
|
|
386
|
-
]);
|
|
440
|
+
]).then(([, p, v]) => [p, v]);
|
|
387
441
|
const all = mergeVersions(progress, versions);
|
|
388
442
|
|
|
389
443
|
// Goal summaries only for versions we actually show goals on (everything but
|
|
@@ -442,5 +496,10 @@
|
|
|
442
496
|
}
|
|
443
497
|
}
|
|
444
498
|
|
|
499
|
+
document.addEventListener('click', (e) => {
|
|
500
|
+
const b = e.target.closest && e.target.closest('button.rm-star');
|
|
501
|
+
if (b) toggleFocus(b.dataset.vid).catch(() => toast(FOCUS_FAIL));
|
|
502
|
+
});
|
|
503
|
+
|
|
445
504
|
boot();
|
|
446
505
|
})();
|
|
@@ -45,6 +45,26 @@ module.exports = function buildVersionsRouter() {
|
|
|
45
45
|
res.json({ versions: all.slice(offset, offset + limit), page: api.pageMeta(limit, offset, all.length) });
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
// The project's focus version (task 1004551) — the one the Roadmap stars. Read: any signed-in builder. Write: Archon only,
|
|
49
|
+
// enforced here on the server, not just by the button hiding. No row = no focus.
|
|
50
|
+
// rank: any-builder — read-only; returns { focus_version_id: string|null, can_set }.
|
|
51
|
+
// can_set tells the page whether to draw a button — a hint only; PUT is the gate.
|
|
52
|
+
router.get('/versions/focus', auth.requireBuilder, asyncHandler('GET /versions/focus', async (req, res) => {
|
|
53
|
+
const rank = String((req.builder && req.builder.rank) || '').toLowerCase();
|
|
54
|
+
res.json({ focus_version_id: await api.projectSettings.getFocusVersionId(), can_set: rank === 'archon' });
|
|
55
|
+
}, { message: false }));
|
|
56
|
+
|
|
57
|
+
// rank: archon (atom version.focus.set, system) — body { version_id } sets the focus; { version_id: null } clears it.
|
|
58
|
+
// An unknown version id is refused rather than stored.
|
|
59
|
+
router.put('/versions/focus', auth.requireBuilder, auth.requirePermission('version.focus.set'), asyncHandler('PUT /versions/focus', async (req, res) => {
|
|
60
|
+
if (validateOrRespond(req, res, { version_id: { type: 'string', maxLength: 64 } })) return;
|
|
61
|
+
const raw = (req.body || {}).version_id;
|
|
62
|
+
if (raw === undefined || (raw !== null && !raw.trim())) return res.fail('bad_version_id', { status: 400, message: 'Pick a version to star, or send null to clear the focus.' });
|
|
63
|
+
if (raw !== null && !(await db.getVersion(raw.trim()))) return res.fail('unknown_version', { status: 404, message: `There is no version "${raw.trim()}" to star.` });
|
|
64
|
+
const focus = await api.projectSettings.setFocusVersionId(raw === null ? null : raw.trim(), req.builder && req.builder.id);
|
|
65
|
+
res.json({ ok: true, focus_version_id: focus });
|
|
66
|
+
}, { message: false }));
|
|
67
|
+
|
|
48
68
|
// rank: public — read-only progress aggregate; mirrors /public/progress.
|
|
49
69
|
router.get('/versions/progress', async (_req, res) => {
|
|
50
70
|
const progress = await db.versionProgress();
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.32",
|
|
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.32",
|
|
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.32",
|
|
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
|
@@ -8942,5 +8942,11 @@
|
|
|
8942
8942
|
"id": "1004550",
|
|
8943
8943
|
"text": "Setting up a new project is quicker: the final check screen now takes one click to accept, with less text and no per-row confirm buttons."
|
|
8944
8944
|
}
|
|
8945
|
+
],
|
|
8946
|
+
"1.21.32": [
|
|
8947
|
+
{
|
|
8948
|
+
"id": "1004551",
|
|
8949
|
+
"text": "An Archon can star a version on the Roadmap as the project's focus; it persists across reloads, and everyone else sees the star read-only (the server refuses non-Archons)."
|
|
8950
|
+
}
|
|
8945
8951
|
]
|
|
8946
8952
|
}
|
|
@@ -48,6 +48,7 @@ const { logger } = require('./logger');
|
|
|
48
48
|
// cannot collide with this one.
|
|
49
49
|
const SETTING_KEYS = {
|
|
50
50
|
ROT_DAYS: 'attention.rot_days',
|
|
51
|
+
FOCUS_VERSION: 'roadmap.focus_version',
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
// The last-resort rot timer, in days. 30 because that is the shortest window in
|
|
@@ -176,6 +177,31 @@ function describeRotTimer(resolved) {
|
|
|
176
177
|
return `${base} — ignoring ${r.invalid.where}="${r.invalid.raw}" (${r.invalid.reason})`;
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
// getFocusVersionId() — the version the project is focused on, or null.
|
|
181
|
+
// NO ROW (or an empty value) MEANS NO FOCUS: the focus is chosen by an Archon in the
|
|
182
|
+
// UI (task 1004551), never inferred in code, so absence is an answer and a default is
|
|
183
|
+
// never seeded. Whether the id still names a real version is the caller's concern —
|
|
184
|
+
// the write path refuses unknown ids, and a reader of a since-deleted version simply
|
|
185
|
+
// finds no card to star.
|
|
186
|
+
async function getFocusVersionId() {
|
|
187
|
+
const row = await getSetting(SETTING_KEYS.FOCUS_VERSION);
|
|
188
|
+
return row && row.value ? String(row.value) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// setFocusVersionId(versionId, builderId) — set the focus, or clear it with null.
|
|
192
|
+
// Clearing deletes the row rather than storing '' so "never set" and "cleared" are the
|
|
193
|
+
// same state, as the null-is-meaningful rule on getSetting requires.
|
|
194
|
+
async function setFocusVersionId(versionId, builderId = null) {
|
|
195
|
+
if (versionId == null || versionId === '') {
|
|
196
|
+
await pool.query('DELETE FROM project_settings WHERE key = $1', [SETTING_KEYS.FOCUS_VERSION]);
|
|
197
|
+
// The row is gone, so updated_by cannot say who cleared it; the log line does.
|
|
198
|
+
logger.info(`project focus version cleared by builder ${builderId == null ? 'unknown' : builderId}`);
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
await setSetting(SETTING_KEYS.FOCUS_VERSION, versionId, builderId);
|
|
202
|
+
return String(versionId);
|
|
203
|
+
}
|
|
204
|
+
|
|
179
205
|
// parseDays is exported for the tests only — the range check and the default are
|
|
180
206
|
// asserted against the SAME constants the resolver reads, because a test that
|
|
181
207
|
// re-typed 30 or 365 would be the second copy of the number this file exists to
|
|
@@ -190,4 +216,6 @@ module.exports = {
|
|
|
190
216
|
setSetting,
|
|
191
217
|
resolveRotDays,
|
|
192
218
|
describeRotTimer,
|
|
219
|
+
getFocusVersionId,
|
|
220
|
+
setFocusVersionId,
|
|
193
221
|
};
|
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.32'; // 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');
|
|
@@ -444,6 +444,8 @@ module.exports = {
|
|
|
444
444
|
get KEYS() { return require('./bongos/project-settings').SETTING_KEYS; },
|
|
445
445
|
get get() { return require('./bongos/project-settings').getSetting; },
|
|
446
446
|
get set() { return require('./bongos/project-settings').setSetting; },
|
|
447
|
+
get getFocusVersionId() { return require('./bongos/project-settings').getFocusVersionId; },
|
|
448
|
+
get setFocusVersionId() { return require('./bongos/project-settings').setFocusVersionId; },
|
|
447
449
|
get describeRotTimer() { return require('./bongos/project-settings').describeRotTimer; },
|
|
448
450
|
// The RANGE CHECK, shared rather than re-typed. The settings page validates a
|
|
449
451
|
// submitted timer with the very parser the resolver reads it back through, so the
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// tests/focus_version.mjs — the roadmap focus star (task 1004551): the settings accessor,
|
|
2
|
+
// the Archon-only write route and the any-builder read, run against a fake pool and fake
|
|
3
|
+
// requests. No DB needed.
|
|
4
|
+
//
|
|
5
|
+
// Run: node --test tests/focus_version.mjs
|
|
6
|
+
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { test } from 'node:test';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
|
|
12
|
+
process.env.NODE_ENV = 'test';
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
// A one-table fake pool, installed before project-settings loads.
|
|
16
|
+
const store = new Map();
|
|
17
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
18
|
+
require(poolPath);
|
|
19
|
+
require.cache[poolPath].exports.pool = {
|
|
20
|
+
async query(sql, p) {
|
|
21
|
+
if (/^\s*SELECT/i.test(sql)) { const v = store.get(p[0]); return { rows: v == null ? [] : [{ key: p[0], value: v }] }; }
|
|
22
|
+
if (/^\s*INSERT/i.test(sql)) { store.set(p[0], p[1]); return { rows: [{ key: p[0], value: p[1] }] }; }
|
|
23
|
+
if (/^\s*DELETE/i.test(sql)) { store.delete(p[0]); return { rows: [] }; }
|
|
24
|
+
throw new Error(`unexpected sql: ${sql}`);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const ps = require('../src/bongos/project-settings.js');
|
|
28
|
+
const db = require('../modules/lifecycle/db.js');
|
|
29
|
+
const router = require('../modules/lifecycle/routes/versions.js')();
|
|
30
|
+
|
|
31
|
+
const src = (rel) => readFileSync(new URL('../' + rel, import.meta.url), 'utf8');
|
|
32
|
+
db.getVersion = async (id) => (id === 'BONGOS-V2' ? { id } : null);
|
|
33
|
+
|
|
34
|
+
// The route's own handler is the LAST layer on its stack (after the auth middleware).
|
|
35
|
+
function handler(method, path) {
|
|
36
|
+
const layer = router.stack.find((l) => l.route && l.route.path === path && l.route.methods[method]);
|
|
37
|
+
assert.ok(layer, `${method} ${path} is registered`);
|
|
38
|
+
return { chain: layer.route.stack, last: layer.route.stack[layer.route.stack.length - 1].handle };
|
|
39
|
+
}
|
|
40
|
+
async function call(method, path, req) {
|
|
41
|
+
const out = { status: 200, body: null };
|
|
42
|
+
const res = {
|
|
43
|
+
status(n) { out.status = n; return this; },
|
|
44
|
+
json(b) { out.body = b; return this; },
|
|
45
|
+
fail(code, o) { const a = typeof o === 'number' ? { status: o } : (o || {}); out.status = a.status; out.body = { error: { code, message: a.message || code } }; return this; },
|
|
46
|
+
};
|
|
47
|
+
await handler(method, path).last({ body: {}, builder: { id: 9, rank: 'archon' }, ...req }, res, (e) => { throw e; });
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test('no row means no focus', async () => {
|
|
52
|
+
store.clear();
|
|
53
|
+
assert.equal(await ps.getFocusVersionId(), null);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('PUT stores a known version and GET returns it', async () => {
|
|
57
|
+
store.clear();
|
|
58
|
+
const put = await call('put', '/versions/focus', { body: { version_id: 'BONGOS-V2' } });
|
|
59
|
+
assert.equal(put.body.focus_version_id, 'BONGOS-V2');
|
|
60
|
+
assert.equal(store.get(ps.SETTING_KEYS.FOCUS_VERSION), 'BONGOS-V2');
|
|
61
|
+
const got = await call('get', '/versions/focus', {});
|
|
62
|
+
assert.deepEqual(got.body, { focus_version_id: 'BONGOS-V2', can_set: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('PUT with null clears the focus (the row is gone, not blank)', async () => {
|
|
66
|
+
store.set(ps.SETTING_KEYS.FOCUS_VERSION, 'BONGOS-V2');
|
|
67
|
+
const put = await call('put', '/versions/focus', { body: { version_id: null } });
|
|
68
|
+
assert.equal(put.body.focus_version_id, null);
|
|
69
|
+
assert.equal(store.has(ps.SETTING_KEYS.FOCUS_VERSION), false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('an unknown version is refused with a readable message and nothing is stored', async () => {
|
|
73
|
+
store.clear();
|
|
74
|
+
const put = await call('put', '/versions/focus', { body: { version_id: 'NOPE' } });
|
|
75
|
+
assert.equal(put.status, 404);
|
|
76
|
+
assert.match(put.body.error.message, /NOPE/);
|
|
77
|
+
assert.equal(store.size, 0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('a missing or blank version_id is refused, not treated as a clear', async () => {
|
|
81
|
+
for (const body of [{}, { version_id: ' ' }]) {
|
|
82
|
+
const put = await call('put', '/versions/focus', { body });
|
|
83
|
+
assert.equal(put.status, 400);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a non-Archon reads the focus but is told it cannot set it', async () => {
|
|
88
|
+
store.set(ps.SETTING_KEYS.FOCUS_VERSION, 'BONGOS-V2');
|
|
89
|
+
const got = await call('get', '/versions/focus', { builder: { id: 4, rank: 'metic' } });
|
|
90
|
+
assert.deepEqual(got.body, { focus_version_id: 'BONGOS-V2', can_set: false });
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('the write route carries the Archon-only permission gate; the read route does not', () => {
|
|
94
|
+
assert.match(src('modules/lifecycle/routes/versions.js'), /router\.put\('\/versions\/focus',\s*auth\.requireBuilder,\s*auth\.requirePermission\('version\.focus\.set'\)/);
|
|
95
|
+
assert.doesNotMatch(src('modules/lifecycle/routes/versions.js').match(/router\.get\('\/versions\/focus',[^\n]*/)[0], /requireRank/);
|
|
96
|
+
});
|