@bongos/core 1.19.864 → 1.19.866
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 +43 -28
- package/docs/copy-inventory.md +131 -104
- package/docs/copy-registry.json +410 -122
- package/docs/module-api-changelog.md +4 -0
- package/modules/hall-ui/public/blockers.html +4 -1
- package/modules/hall-ui/public/blockers.js +283 -18
- package/modules/hall-ui/public/builders.js +2 -2
- package/modules/hall-ui/public/goal-inbox.js +6 -0
- package/modules/hall-ui/public/hall-render.js +50 -4
- package/modules/hall-ui/public/people.css +9 -0
- package/modules/hall-ui/public/roster.js +11 -1
- package/modules/hall-ui/public/style.css +28 -0
- package/modules/hall-ui/public/task-detail.js +92 -3
- package/modules/lifecycle/routes/tasks.js +33 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/acclaim_feed_evidence_db.mjs +186 -0
- package/tests/blockers_hall.mjs +92 -0
- package/tests/blockers_page_ui.mjs +325 -0
- package/tests/hall_builders_page_pass.mjs +324 -0
- package/tests/helpers.mjs +13 -0
- package/tests/joinability_invite_only.mjs +11 -1
- package/tests/task_detail_ui.mjs +79 -1
|
@@ -153,9 +153,97 @@
|
|
|
153
153
|
if (kind === 'idea' && r.triage_note) {
|
|
154
154
|
nodes.push(h('p', 'deeplink-card__note', 'Triage note: ' + r.triage_note));
|
|
155
155
|
}
|
|
156
|
+
|
|
157
|
+
// task 1003456 (goal 1000089): the write half of the blocker queue. The
|
|
158
|
+
// overlay is the SAME summary→detail relationship the idea triage panel
|
|
159
|
+
// above already has with the /idea page — a compact card, not the full
|
|
160
|
+
// queue — so it carries only Resolve (the one-tap "this is done" verb);
|
|
161
|
+
// Link a task lives on the fuller /blockers page, which has room for the
|
|
162
|
+
// task-id field and the queue to confirm against. Rendered only for
|
|
163
|
+
// metic/archon, the COSMETIC mirror of the real blocker.review floor —
|
|
164
|
+
// "AUTHORITY IS UNCHANGED... the hall must add none of its own" — the
|
|
165
|
+
// server enforces the same gate regardless, exactly the idea panel's rule.
|
|
166
|
+
if (kind === 'blocker' && r.status === 'open' && (rank === 'metic' || rank === 'archon')) {
|
|
167
|
+
nodes.push(blockerPanel(r));
|
|
168
|
+
}
|
|
169
|
+
// A resolved blocker says what closed it — otherwise the status word has
|
|
170
|
+
// no story behind it (the idea triage_note precedent, two blocks up).
|
|
171
|
+
if (kind === 'blocker' && r.status === 'resolved') {
|
|
172
|
+
nodes.push(h('p', 'deeplink-card__note', 'Resolved' + (r.resolution_note ? ': ' + r.resolution_note : '.')));
|
|
173
|
+
}
|
|
156
174
|
return nodes;
|
|
157
175
|
}
|
|
158
176
|
|
|
177
|
+
// ---- the blocker resolve panel (task 1003456) ------------------------------
|
|
178
|
+
//
|
|
179
|
+
// One verb, one form — the renderDiscard shape below, at blocker scale.
|
|
180
|
+
function blockerPanel(blocker) {
|
|
181
|
+
var box = h('div', 'td-triage');
|
|
182
|
+
var mode = 'idle';
|
|
183
|
+
|
|
184
|
+
function note(text) {
|
|
185
|
+
var p = box.querySelector('[data-err]');
|
|
186
|
+
if (p) p.textContent = text || '';
|
|
187
|
+
}
|
|
188
|
+
function busy(on, submitLabel) {
|
|
189
|
+
box.querySelectorAll('button, input').forEach(function (el) { el.disabled = on; });
|
|
190
|
+
var submit = box.querySelector('[data-submit]');
|
|
191
|
+
if (submit) submit.textContent = on ? '…' : submitLabel;
|
|
192
|
+
}
|
|
193
|
+
function go(next) { mode = next; render(); }
|
|
194
|
+
|
|
195
|
+
function render() {
|
|
196
|
+
if (mode === 'idle') return renderIdle();
|
|
197
|
+
return renderResolve();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderIdle() {
|
|
201
|
+
box.innerHTML = '<div class="td-actions"><button type="button" class="td-btn td-btn--primary" data-go="resolve">Resolve</button></div>';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderResolve() {
|
|
205
|
+
box.innerHTML =
|
|
206
|
+
'<form class="td-form" data-form="resolve">'
|
|
207
|
+
+ '<label class="td-field td-field--wide"><span>Resolution note (optional)</span>'
|
|
208
|
+
+ '<input type="text" name="note" maxlength="1000" placeholder="what got un-stuck"></label>'
|
|
209
|
+
+ '<div class="td-actions">'
|
|
210
|
+
+ '<button type="submit" class="td-btn td-btn--primary" data-submit>Resolve</button>'
|
|
211
|
+
+ '<button type="button" class="td-btn" data-go="idle">Cancel</button>'
|
|
212
|
+
+ '</div><p class="td-action-note" data-err></p></form>';
|
|
213
|
+
box.querySelector('[data-form="resolve"]').addEventListener('submit', function (ev) {
|
|
214
|
+
ev.preventDefault();
|
|
215
|
+
submitResolve(ev.target);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function submitResolve(form) {
|
|
220
|
+
note('');
|
|
221
|
+
busy(true, 'Resolve');
|
|
222
|
+
try {
|
|
223
|
+
await postJSON(API + '/blockers/' + encodeURIComponent(blocker.id) + '/resolve', {
|
|
224
|
+
resolution_note: String(form.note.value || '').trim() || null,
|
|
225
|
+
});
|
|
226
|
+
toast('Blocker #' + blocker.id + ' resolved.');
|
|
227
|
+
try {
|
|
228
|
+
window.dispatchEvent(new CustomEvent('otb:blocker-resolved', { detail: { id: blocker.id } }));
|
|
229
|
+
} catch (_) { /* CustomEvent unsupported — the refresh is a nicety */ }
|
|
230
|
+
clearLink();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
// Honest copy, never a silent no-op — the done-when, verbatim.
|
|
233
|
+
var msg = (err && err.status === 403) ? 'You need Metic+ access to resolve blockers.' : (errMessage(err) || 'Could not resolve that blocker. Try again.');
|
|
234
|
+
busy(false, 'Resolve');
|
|
235
|
+
note(msg);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
box.addEventListener('click', function (e) {
|
|
240
|
+
var btn = e.target.closest('[data-go]');
|
|
241
|
+
if (btn) go(btn.dataset.go);
|
|
242
|
+
});
|
|
243
|
+
render();
|
|
244
|
+
return box;
|
|
245
|
+
}
|
|
246
|
+
|
|
159
247
|
// ---- the triage panel (task 1002447) ---------------------------------------
|
|
160
248
|
//
|
|
161
249
|
// Promote → POST /tasks, then PATCH the idea promoted + linked to it.
|
|
@@ -633,10 +721,11 @@
|
|
|
633
721
|
render({ loading: true, kind: kind, id: id });
|
|
634
722
|
try {
|
|
635
723
|
var ref = await fetchRef(kind, id);
|
|
636
|
-
// task 759: probe the viewer's rank
|
|
637
|
-
//
|
|
724
|
+
// task 759: probe the viewer's rank for an idea OR a blocker (task
|
|
725
|
+
// 1003456 added the latter) so the overlay can offer Metic+ triage /
|
|
726
|
+
// resolve. Best-effort — a failure just hides the button.
|
|
638
727
|
var viewer = null;
|
|
639
|
-
if (kind === 'idea' && ref) {
|
|
728
|
+
if ((kind === 'idea' || kind === 'blocker') && ref) {
|
|
640
729
|
try {
|
|
641
730
|
var me = await getJSON(API + '/me', { softAuth: true });
|
|
642
731
|
viewer = (me && me.builder) ? me.builder : null;
|
|
@@ -29,6 +29,10 @@ const api = require('../../../src/module-api');
|
|
|
29
29
|
// depRefusalRow: the ONE shape a dependency refusal carries, shared with the claim
|
|
30
30
|
// path so promote and claim describe a blocked task identically (task 1003553).
|
|
31
31
|
const { depRefusalRow } = require('../dead-deps.js');
|
|
32
|
+
// gradeQualityWhere: the ONE definition of a task_grades row that is an actual
|
|
33
|
+
// quality verdict rather than an outage or an errored worker (task 1002652).
|
|
34
|
+
// The acclaim feed reads it so a grader outage never reaches a voter as 0.0.
|
|
35
|
+
const { gradeQualityWhere } = require('../db-shared');
|
|
32
36
|
const auth = api;
|
|
33
37
|
const db = require('../db');
|
|
34
38
|
const hierarchy = require('../hierarchy');
|
|
@@ -771,9 +775,28 @@ module.exports = function buildTasksRouter() {
|
|
|
771
775
|
t.shipped_at, t.shipped_by,
|
|
772
776
|
b.github_login, b.display_name,
|
|
773
777
|
COALESCE(pv.net, 0)::int AS net_votes,
|
|
774
|
-
mine.vote AS my_vote
|
|
778
|
+
mine.vote AS my_vote,
|
|
779
|
+
-- task 1002835: a karma vote is uninformed without the two facts
|
|
780
|
+
-- the ship already recorded. BOTH stay NULL when absent — an
|
|
781
|
+
-- ungraded ship and a zero-scoring one are different claims, and
|
|
782
|
+
-- so are an unlogged cost and a free one, so neither is
|
|
783
|
+
-- COALESCEd to a number the client would print as a measurement.
|
|
784
|
+
--
|
|
785
|
+
-- BOTH JOINS ARE INDEXED LOOKUPS, not scans: task_grades.task_id
|
|
786
|
+
-- is declared UNIQUE (migrations/016_pms_task_grades.sql:14, which
|
|
787
|
+
-- gives it its own index) and cost_log.task_id carries
|
|
788
|
+
-- idx_cost_log_task (migrations/003_pms.sql:152). Neither needs a
|
|
789
|
+
-- migration here, and neither degrades as cost_log grows.
|
|
790
|
+
CASE WHEN ${gradeQualityWhere('g')} THEN g.score END AS grade_score,
|
|
791
|
+
CASE WHEN ${gradeQualityWhere('g')} THEN g.passed END AS grade_passed,
|
|
792
|
+
cost.total_usd AS cost_usd
|
|
775
793
|
FROM tasks t
|
|
776
794
|
LEFT JOIN builders b ON b.id = t.shipped_by
|
|
795
|
+
LEFT JOIN task_grades g ON g.task_id = t.id
|
|
796
|
+
LEFT JOIN LATERAL (
|
|
797
|
+
SELECT SUM(amount_usd) AS total_usd
|
|
798
|
+
FROM cost_log WHERE task_id = t.id
|
|
799
|
+
) cost ON true
|
|
777
800
|
LEFT JOIN LATERAL (
|
|
778
801
|
SELECT SUM(vote) AS net
|
|
779
802
|
FROM peer_votes WHERE target_task_id = t.id
|
|
@@ -788,7 +811,15 @@ module.exports = function buildTasksRouter() {
|
|
|
788
811
|
LIMIT $2`,
|
|
789
812
|
[req.builder.id, limit]
|
|
790
813
|
);
|
|
791
|
-
|
|
814
|
+
const canVote = ['metic', 'archon'].includes(req.builder.rank);
|
|
815
|
+
// The cost is INTERNAL SPEND, and it is on this feed for one reason: to
|
|
816
|
+
// inform a vote. So it reaches only the ranks that can cast one. The grade
|
|
817
|
+
// is a quality verdict rather than spend and stays readable by any builder,
|
|
818
|
+
// as it already is on the grades table. Note the field is OMITTED, not
|
|
819
|
+
// nulled: a null here means "nothing was logged", and a reader who is not
|
|
820
|
+
// shown a figure must not be told a figure is missing.
|
|
821
|
+
const tasks = canVote ? rows : rows.map(({ cost_usd, ...rest }) => rest);
|
|
822
|
+
res.json({ tasks, can_vote: canVote });
|
|
792
823
|
} catch (err) {
|
|
793
824
|
// Log the detail server-side; don't return err.message — a raw Postgres
|
|
794
825
|
// error can leak schema internals (table/column/constraint names).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.866",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.866",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.866",
|
|
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",
|
|
@@ -71,6 +71,11 @@ const SUPPORT_MODULES = new Set(['helpers']);
|
|
|
71
71
|
|
|
72
72
|
// Need a live DB / server / network — run in the integration-smoke gate instead.
|
|
73
73
|
const INTEGRATION = new Set([
|
|
74
|
+
// task 1004004: the acclaim feed proves a claim about SQL semantics — that a
|
|
75
|
+
// grader outage nulls rather than scoring 0.0, and that a summed cost over no
|
|
76
|
+
// rows is NULL rather than a free ship. Only a transaction engine settles
|
|
77
|
+
// those, so this runs here and the DB-free sibling covers the renderer.
|
|
78
|
+
'acclaim_feed_evidence_db',
|
|
74
79
|
'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
|
|
75
80
|
'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
|
|
76
81
|
// consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
|
package/src/module-api.js
CHANGED
|
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
71
71
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
72
72
|
// the entry to that file. Look for a version's history there, not here.
|
|
73
73
|
// ---------------------------------------------------------------------------
|
|
74
|
-
const CORE_VERSION = '1.19.
|
|
74
|
+
const CORE_VERSION = '1.19.866'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
75
75
|
|
|
76
76
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
77
77
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// tests/acclaim_feed_evidence_db.mjs — the peer-acclaim feed's grade + cost
|
|
2
|
+
// evidence, against a REAL Postgres (task 1004004, from task 1002835).
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS AND ITS DB-FREE SIBLING IS NOT ENOUGH.
|
|
5
|
+
// tests/hall_builders_page_pass.mjs runs the RENDERER over every shape the row
|
|
6
|
+
// can take, and asserts the route's SQL by reading its source. That proves the
|
|
7
|
+
// client is honest about a NULL — it cannot prove the query ever PRODUCES one.
|
|
8
|
+
// The whole argument of part C is a claim about SQL semantics that only a
|
|
9
|
+
// transaction engine can settle:
|
|
10
|
+
//
|
|
11
|
+
// - a grader OUTAGE row carries score 0 / passed=false in task_grades, and
|
|
12
|
+
// gradeQualityWhere must turn that into NULL rather than let a voter read
|
|
13
|
+
// it as a 0.0 verdict;
|
|
14
|
+
// - SUM(amount_usd) over ZERO cost_log rows is NULL, not 0, which is the
|
|
15
|
+
// difference between "no cost logged" and "this ship was free";
|
|
16
|
+
// - a ship with several cost rows must sum them, not pick one;
|
|
17
|
+
// - the two LATERALs and the grades join must not multiply the row out.
|
|
18
|
+
//
|
|
19
|
+
// Runs the route's own query text — sliced out of the shipped source, never
|
|
20
|
+
// retyped — so a future edit to the route is tested rather than a copy of it.
|
|
21
|
+
// Self-skips without a reachable Postgres; in the INTEGRATION set, so the
|
|
22
|
+
// DB-free unit gate never runs it and .github/workflows/integration.yml does.
|
|
23
|
+
//
|
|
24
|
+
// Run: DATABASE_URL=... node tests/acclaim_feed_evidence_db.mjs
|
|
25
|
+
import { createRequire } from 'node:module';
|
|
26
|
+
import { strict as assert } from 'node:assert';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { fileURLToPath } from 'node:url';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const { Pool } = require('pg');
|
|
33
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
34
|
+
|
|
35
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
36
|
+
try {
|
|
37
|
+
await pool.query('SELECT 1');
|
|
38
|
+
} catch (e) {
|
|
39
|
+
console.log(`acclaim_feed_evidence_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
40
|
+
await pool.end().catch(() => {});
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The query under test, taken from the shipped route. gradeQualityWhere is
|
|
45
|
+
// interpolated by the route at call time, so interpolate it the same way.
|
|
46
|
+
const { gradeQualityWhere } = require('../modules/lifecycle/db-shared.js');
|
|
47
|
+
const routeSrc = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
48
|
+
const start = routeSrc.indexOf('`SELECT t.id, t.title, t.value_summary, t.version_id, t.status,');
|
|
49
|
+
assert.ok(start > 0, 'the peer-votes feed query is still where this test slices it from');
|
|
50
|
+
const end = routeSrc.indexOf('LIMIT $2`', start);
|
|
51
|
+
assert.ok(end > start, 'the feed query still ends at its LIMIT');
|
|
52
|
+
const SQL = routeSrc
|
|
53
|
+
.slice(start + 1, end + 'LIMIT $2'.length)
|
|
54
|
+
.replace(/\$\{gradeQualityWhere\('g'\)\}/g, gradeQualityWhere('g'));
|
|
55
|
+
assert.ok(!SQL.includes('${'), 'every interpolation in the sliced query was resolved');
|
|
56
|
+
|
|
57
|
+
const VOTER = 90000801;
|
|
58
|
+
const SHIPPER = 90000802;
|
|
59
|
+
const VER = 'ZZZ-ACC';
|
|
60
|
+
// graded+costed, outage, ungraded, costed-twice, zero-cost
|
|
61
|
+
const T = { GOOD: 90000811, OUTAGE: 90000812, UNGRADED: 90000813, TWICE: 90000814, FREE: 90000815 };
|
|
62
|
+
const IDS = Object.values(T);
|
|
63
|
+
|
|
64
|
+
const q = (sql, params) => pool.query(sql, params).then((r) => r.rows);
|
|
65
|
+
|
|
66
|
+
async function cleanup() {
|
|
67
|
+
await q('DELETE FROM peer_votes WHERE target_task_id = ANY($1)', [IDS]);
|
|
68
|
+
await q('DELETE FROM cost_log WHERE task_id = ANY($1)', [IDS]);
|
|
69
|
+
await q('DELETE FROM task_grades WHERE task_id = ANY($1)', [IDS]);
|
|
70
|
+
await q('DELETE FROM tasks WHERE id = ANY($1)', [IDS]);
|
|
71
|
+
await q('DELETE FROM builders WHERE id = ANY($1)', [[VOTER, SHIPPER]]);
|
|
72
|
+
await q('DELETE FROM versions WHERE id = $1', [VER]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let failures = 0;
|
|
76
|
+
const check = (name, fn) => {
|
|
77
|
+
try { fn(); console.log(` PASS ${name}`); } catch (e) {
|
|
78
|
+
failures += 1; console.log(` FAIL ${name}\n ${e.message}`);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
await cleanup();
|
|
84
|
+
|
|
85
|
+
await q("INSERT INTO versions (id, name, status) VALUES ($1, 'acclaim evidence fixture', 'planning')", [VER]);
|
|
86
|
+
for (const [id, login] of [[VOTER, 'zz-acc-voter'], [SHIPPER, 'zz-acc-shipper']]) {
|
|
87
|
+
await q("INSERT INTO builders (id, github_login, display_name, rank) VALUES ($1, $2, $2, 'metic')", [id, login]);
|
|
88
|
+
}
|
|
89
|
+
for (const [id, title] of [
|
|
90
|
+
[T.GOOD, '__acc__ graded and costed'],
|
|
91
|
+
[T.OUTAGE, '__acc__ grader outage'],
|
|
92
|
+
[T.UNGRADED, '__acc__ never graded'],
|
|
93
|
+
[T.TWICE, '__acc__ two cost rows'],
|
|
94
|
+
[T.FREE, '__acc__ a real zero'],
|
|
95
|
+
]) {
|
|
96
|
+
await q(
|
|
97
|
+
`INSERT INTO tasks (id, version_id, title, status, shipped_by, shipped_at, discipline, kind)
|
|
98
|
+
VALUES ($1, $2, $3, 'shipped', $4, now(), 'engineer', 'feature')`,
|
|
99
|
+
[id, VER, title, SHIPPER],
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const grade = (taskId, score, passed, rubric, signals) => q(
|
|
104
|
+
`INSERT INTO task_grades (task_id, score, passed, threshold, grader_kind, grader_version, rubric_scores, signals)
|
|
105
|
+
VALUES ($1, $2, $3, 7.0, 'subagent', 'test', $4::jsonb, $5::jsonb)`,
|
|
106
|
+
[taskId, score, passed, JSON.stringify(rubric), JSON.stringify(signals)],
|
|
107
|
+
);
|
|
108
|
+
const FULL = { functional_fidelity: 8, code_quality: 8 };
|
|
109
|
+
await grade(T.GOOD, 8.5, true, FULL, {});
|
|
110
|
+
// The shape that matters: an outage parks score 0 / passed=false with
|
|
111
|
+
// json-null rubric axes. A voter must never read that as a 0.0 verdict.
|
|
112
|
+
await grade(T.OUTAGE, 0, false, { functional_fidelity: null, code_quality: null }, { panel_outcome: 'unavailable' });
|
|
113
|
+
await grade(T.TWICE, 6.0, false, FULL, {});
|
|
114
|
+
await grade(T.FREE, 0, false, FULL, {});
|
|
115
|
+
|
|
116
|
+
const cost = (taskId, amount) => q(
|
|
117
|
+
"INSERT INTO cost_log (task_id, amount_usd, category, source) VALUES ($1, $2, 'api', 'test')",
|
|
118
|
+
[taskId, amount],
|
|
119
|
+
);
|
|
120
|
+
await cost(T.GOOD, '12.3456');
|
|
121
|
+
await cost(T.TWICE, '1.50');
|
|
122
|
+
await cost(T.TWICE, '2.25');
|
|
123
|
+
await cost(T.FREE, '0');
|
|
124
|
+
// T.OUTAGE and T.UNGRADED get no cost row at all.
|
|
125
|
+
|
|
126
|
+
const rows = await q(SQL, [VOTER, 100]);
|
|
127
|
+
const byId = new Map(rows.map((r) => [String(r.id), r]));
|
|
128
|
+
const got = (id) => {
|
|
129
|
+
const row = byId.get(String(id));
|
|
130
|
+
assert.ok(row, `task ${id} is on the feed`);
|
|
131
|
+
return row;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
check('every fixture ship reaches the feed exactly once (the joins do not multiply rows)', () => {
|
|
135
|
+
for (const id of IDS) got(id);
|
|
136
|
+
for (const id of IDS) {
|
|
137
|
+
const n = rows.filter((r) => String(r.id) === String(id)).length;
|
|
138
|
+
assert.equal(n, 1, `task ${id} appears once, not ${n}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
check('a graded, costed ship carries both figures', () => {
|
|
143
|
+
const r = got(T.GOOD);
|
|
144
|
+
assert.equal(Number(r.grade_score), 8.5);
|
|
145
|
+
assert.equal(r.grade_passed, true);
|
|
146
|
+
assert.equal(Number(r.cost_usd), 12.3456);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
check('a GRADER OUTAGE arrives as NULL, never as its placeholder 0', () => {
|
|
150
|
+
const r = got(T.OUTAGE);
|
|
151
|
+
assert.equal(r.grade_score, null, 'the outage score is nulled by gradeQualityWhere');
|
|
152
|
+
assert.equal(r.grade_passed, null, 'and so is its passed flag — no verdict was reached');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
check('a ship that was never graded has no grade row and so no score', () => {
|
|
156
|
+
assert.equal(got(T.UNGRADED).grade_score, null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
check('an unlogged cost is NULL, not 0 — the sum is never COALESCEd', () => {
|
|
160
|
+
assert.equal(got(T.UNGRADED).cost_usd, null, 'no cost_log row means no figure, not a free ship');
|
|
161
|
+
assert.equal(got(T.OUTAGE).cost_usd, null);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
check('several cost rows sum rather than one winning', () => {
|
|
165
|
+
assert.equal(Number(got(T.TWICE).cost_usd), 3.75);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
check('a genuine zero survives, because it IS a measurement', () => {
|
|
169
|
+
const r = got(T.FREE);
|
|
170
|
+
assert.equal(Number(r.cost_usd), 0, 'a logged 0 is a logged 0, distinct from NULL');
|
|
171
|
+
assert.notEqual(r.cost_usd, null);
|
|
172
|
+
assert.equal(Number(r.grade_score), 0, 'a real 0.0 verdict is not an outage and is not nulled');
|
|
173
|
+
assert.equal(r.grade_passed, false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
check('the caller never sees their own ships, so nobody votes on themselves', () => {
|
|
177
|
+
assert.ok(!rows.some((r) => String(r.shipped_by) === String(VOTER)));
|
|
178
|
+
});
|
|
179
|
+
} finally {
|
|
180
|
+
await cleanup().catch(() => {});
|
|
181
|
+
await pool.end().catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const passed = 8 - failures;
|
|
185
|
+
console.log(`\nacclaim_feed_evidence_db: ${passed} passed, ${failures} failed`);
|
|
186
|
+
process.exit(failures ? 1 : 0);
|
package/tests/blockers_hall.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const GOAL_INBOX_SRC = readFileSync(join(HALL, 'goal-inbox.js'), 'utf8');
|
|
|
27
27
|
const BLOCKERS_JS_SRC = readFileSync(join(HALL, 'blockers.js'), 'utf8');
|
|
28
28
|
const BLOCKERS_HTML_SRC = readFileSync(join(HALL, 'blockers.html'), 'utf8');
|
|
29
29
|
const INDEX_HTML_SRC = readFileSync(join(HALL, 'index.html'), 'utf8');
|
|
30
|
+
const TASK_DETAIL_SRC = readFileSync(join(HALL, 'task-detail.js'), 'utf8');
|
|
30
31
|
|
|
31
32
|
// ---- the attention-inbox slice (goal-inbox.js) -----------------------------
|
|
32
33
|
|
|
@@ -105,3 +106,94 @@ test('every title link opens the SAME #/blocker/N overlay task-detail.js already
|
|
|
105
106
|
assert.match(GOAL_INBOX_SRC, /`#\/blocker\/\$\{encodeURIComponent\(id\)\}`/);
|
|
106
107
|
assert.match(BLOCKERS_JS_SRC, /'#\/blocker\/' \+ encodeURIComponent\(id\)/);
|
|
107
108
|
});
|
|
109
|
+
|
|
110
|
+
// ---- task 1003456: the write half — file, resolve, link -------------------
|
|
111
|
+
//
|
|
112
|
+
// POST /blockers, POST /blockers/:id/resolve and POST /blockers/:id/link
|
|
113
|
+
// already carry their own server-side rank floors (any-builder / blocker.review
|
|
114
|
+
// respectively), proven by the fitness route-rank-check gate — not re-proven
|
|
115
|
+
// here. What this pins is the CLIENT side of the done-when: filing/resolving/
|
|
116
|
+
// linking are actually wired to those routes, resolve/link mirror the real
|
|
117
|
+
// floor cosmetically (never invent a stricter or looser one), filing itself
|
|
118
|
+
// carries no such gate, and a refusal renders as honest copy rather than a
|
|
119
|
+
// silent no-op.
|
|
120
|
+
|
|
121
|
+
test('blockers.js files a blocker against the real POST /blockers route', () => {
|
|
122
|
+
assert.match(BLOCKERS_JS_SRC, /writeJSON\('POST', API \+ '\/blockers', \{ title: title, body_md: note \}\)/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('blockers.js resolves against the real POST /blockers/:id/resolve route', () => {
|
|
126
|
+
assert.match(BLOCKERS_JS_SRC, /API \+ '\/blockers\/' \+ encodeURIComponent\(id\) \+ '\/resolve'/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('blockers.js links against the real POST /blockers/:id/link route, from both the file form and a row', () => {
|
|
130
|
+
const matches = BLOCKERS_JS_SRC.match(/\/blockers\/'\s*\+\s*encodeURIComponent\([^)]+\)\s*\+\s*'\/link'/g) || [];
|
|
131
|
+
assert.ok(matches.length >= 2, `expected the file-form link attempt AND the per-row link form, found ${matches.length}`);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('filing has no rank gate — any signed-in builder sees the File button, mirroring the any-builder route', () => {
|
|
135
|
+
assert.match(BLOCKERS_JS_SRC, /fileArea\.innerHTML = fileFormHtml\(\); wireFileForm\(\);/,
|
|
136
|
+
'the file form is mounted unconditionally, not behind an isReviewer()/rank check');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('resolve/link controls mirror the SAME metic-or-archon floor the idea triage panel already uses — no floor of its own', () => {
|
|
140
|
+
assert.match(BLOCKERS_JS_SRC, /isReviewer\s*=\s*function\s*\(\)\s*\{\s*return viewer\.rank === 'metic' \|\| viewer\.rank === 'archon';\s*\}/);
|
|
141
|
+
assert.match(TASK_DETAIL_SRC, /kind === 'blocker' && r\.status === 'open' && \(rank === 'metic' \|\| rank === 'archon'\)/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a 403 on resolve/link/file-link renders HONEST copy, not a silent no-op (the done-when, verbatim)', () => {
|
|
145
|
+
assert.match(BLOCKERS_JS_SRC, /err && err\.status === 403.*You need Metic\+ access to do that\./);
|
|
146
|
+
// The file-then-link flow specifically: a filer without blocker.review still
|
|
147
|
+
// gets told the filing succeeded AND that linking needs a Metic+ — never a
|
|
148
|
+
// swallowed catch that leaves them guessing whether anything happened.
|
|
149
|
+
assert.match(BLOCKERS_JS_SRC, /catch \(linkErr\) \{[\s\S]*?reviewerRefusalMessage\(linkErr,/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('the reviewer-only Metic+ message is scoped to resolve/link, never applied to filing itself (which has no rank floor)', () => {
|
|
153
|
+
// Two distinct helpers: a bare 403 on POST /blockers (filing) is NOT a
|
|
154
|
+
// blocker.review refusal and must not be read as one.
|
|
155
|
+
assert.match(BLOCKERS_JS_SRC, /function refusalMessage\(err, fallback\) \{\s*return errMessage\(err\) \|\| fallback;\s*\}/);
|
|
156
|
+
assert.match(BLOCKERS_JS_SRC, /function reviewerRefusalMessage\(err, fallback\)/);
|
|
157
|
+
assert.match(BLOCKERS_JS_SRC, /errEl\.textContent = refusalMessage\(err, 'Could not file that blocker\. Try again\.'\);/,
|
|
158
|
+
'the FILE form\'s own catch uses the generic helper, not the reviewer-specific one');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('a deduped file with a task # typed says the link was NOT attempted, rather than a silent no-op', () => {
|
|
162
|
+
assert.match(BLOCKERS_JS_SRC, /taskIdRaw\s*\n\s*\?\s*'That blocker was already filed\. It was NOT linked/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a 401 on any blockers.js write redirects to sign-in, same as every other hall write', () => {
|
|
166
|
+
assert.match(BLOCKERS_JS_SRC, /res\.status === 401.*window\.location\.assign\(API \+ '\/auth\/web\/start'\)/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('task-detail.js\'s blocker resolve panel posts to the real route and reports a refusal honestly', () => {
|
|
170
|
+
assert.match(TASK_DETAIL_SRC, /function blockerPanel\(blocker\)/);
|
|
171
|
+
assert.match(TASK_DETAIL_SRC, /postJSON\(API \+ '\/blockers\/' \+ encodeURIComponent\(blocker\.id\) \+ '\/resolve'/);
|
|
172
|
+
assert.match(TASK_DETAIL_SRC, /function submitResolve[\s\S]*?err && err\.status === 403[\s\S]*?You need Metic\+ access to resolve blockers\./);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('resolving through the overlay tells the two OTHER blocker surfaces to refresh, so a resolved row does not sit there stale', () => {
|
|
176
|
+
assert.match(TASK_DETAIL_SRC, /new CustomEvent\('otb:blocker-resolved', \{ detail: \{ id: blocker\.id \} \}\)/);
|
|
177
|
+
assert.match(GOAL_INBOX_SRC, /window\.addEventListener\('otb:blocker-resolved', loadBlockers\)/);
|
|
178
|
+
assert.match(BLOCKERS_JS_SRC, /window\.addEventListener\('otb:blocker-resolved', reload\)/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('blockers.html carries the file-form mount, above the list', () => {
|
|
182
|
+
const fileAt = BLOCKERS_HTML_SRC.indexOf('id="blockers-file-area"');
|
|
183
|
+
const bodyAt = BLOCKERS_HTML_SRC.indexOf('id="blockers-body"');
|
|
184
|
+
assert.ok(fileAt > -1 && bodyAt > -1);
|
|
185
|
+
assert.ok(fileAt < bodyAt, 'filing sits above the queue it adds to');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('a row never carries .ov-row--lead — this row has no lead/face column, and oversight.css\'s --lead grid would misplace the actions column on a phone', () => {
|
|
189
|
+
assert.doesNotMatch(BLOCKERS_JS_SRC, /ov-row--lead/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('viewer carries no unread field — dead state is not left lying around', () => {
|
|
193
|
+
assert.doesNotMatch(BLOCKERS_JS_SRC, /\bauthed\b/, 'viewer.authed was written and never read');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('a resolved blocker on the overlay shows what closed it, the idea triage_note precedent', () => {
|
|
197
|
+
assert.match(TASK_DETAIL_SRC, /kind === 'blocker' && r\.status === 'resolved'/);
|
|
198
|
+
assert.match(TASK_DETAIL_SRC, /'Resolved' \+ \(r\.resolution_note \? ': ' \+ r\.resolution_note : '\.'\)/);
|
|
199
|
+
});
|