@bongos/core 1.19.827 → 1.19.829
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 +50 -20
- package/docs/copy-inventory.md +20 -14
- package/docs/copy-registry.json +83 -19
- package/docs/module-api-changelog.md +4 -0
- package/docs/recipes/windows-builders.md +20 -28
- package/modules/hall-ui/public/blockers-lib.js +110 -0
- package/modules/hall-ui/public/blockers.html +65 -0
- package/modules/hall-ui/public/blockers.js +88 -0
- package/modules/hall-ui/public/goal-inbox.js +74 -0
- package/modules/hall-ui/public/index.html +4 -0
- package/modules/hall-ui/public/shell.js +9 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/run-unit-tests.js +29 -2
- package/src/module-api.js +1 -1
- package/tests/blockers_hall.mjs +107 -0
- package/tests/blockers_lib.mjs +102 -0
- package/tests/hall_nav.mjs +4 -1
- package/tests/hall_page_gate_map.mjs +8 -0
- package/tests/unit_runner_teardown_abort.mjs +67 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// tests/blockers_lib.mjs — the shared blocker-row renderer (blockers-lib.js,
|
|
2
|
+
// task 1003455, goal 1000089).
|
|
3
|
+
//
|
|
4
|
+
// Pins that the lib is genuinely pure (no window.OTB, no DOM) so it loads
|
|
5
|
+
// standalone via require() the way goals-lib.js loads standalone in a vm — and
|
|
6
|
+
// that its output is what both callers (goal-inbox.js's attention-inbox slice,
|
|
7
|
+
// blockers.js's full-queue page) actually depend on: a title link into the
|
|
8
|
+
// EXISTING #/blocker/N overlay (task-detail.js), a quoted note, and a `·`-joined
|
|
9
|
+
// facts line naming which tasks a blocker holds up.
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/blockers_lib.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const lib = require('../modules/hall-ui/public/blockers-lib.js');
|
|
19
|
+
|
|
20
|
+
test('loads standalone via require() — no window, no DOM (Node-require-safe like hall-kit.js)', () => {
|
|
21
|
+
assert.ok(lib, 'module.exports must be set');
|
|
22
|
+
for (const k of ['esc', 'fmtDate', 'blockingLine', 'blockerFacts']) {
|
|
23
|
+
assert.ok(k in lib, `missing export: ${k}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('blockerFacts: title links into the SAME #/blocker/N overlay task-detail.js renders', () => {
|
|
28
|
+
const b = { id: 42, title: 'Need the owner’s AWS card on file', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [] };
|
|
29
|
+
const { titleHtml } = lib.blockerFacts(b, { blockerHref: (id) => `#/blocker/${id}` });
|
|
30
|
+
assert.match(titleHtml, /href="#\/blocker\/42"/);
|
|
31
|
+
assert.match(titleHtml, /Need the owner/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('blockerFacts: with no blockerHref, the title renders as plain text, not a dead link', () => {
|
|
35
|
+
const b = { id: 42, title: 'Something stuck', created_at: null, blocking_task_ids: [] };
|
|
36
|
+
const { titleHtml } = lib.blockerFacts(b);
|
|
37
|
+
assert.doesNotMatch(titleHtml, /<a /);
|
|
38
|
+
assert.match(titleHtml, /<span/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('blockerFacts: the caller-supplied classes land on the right fragments', () => {
|
|
42
|
+
const b = { id: 1, title: 'x', body_md: 'a reason', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [] };
|
|
43
|
+
const { titleHtml, noteHtml, factsHtml } = lib.blockerFacts(b, {
|
|
44
|
+
titleClass: 'ginbox__goal', noteClass: 'ginbox__note', whenClass: 'ginbox__sub',
|
|
45
|
+
});
|
|
46
|
+
assert.match(titleHtml, /class="ginbox__goal"/);
|
|
47
|
+
assert.match(noteHtml, /class="ginbox__note"/);
|
|
48
|
+
assert.match(factsHtml, /class="ginbox__sub"/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('blockingLine: no linked tasks reads honestly, not as a false "0 tasks"', () => {
|
|
52
|
+
assert.equal(lib.blockingLine({ blocking_task_ids: [] }, null), 'Blocking nothing yet');
|
|
53
|
+
assert.equal(lib.blockingLine({}, null), 'Blocking nothing yet');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('blockingLine: singular/plural noun agreement, and every id becomes a link when taskHref is given', () => {
|
|
57
|
+
const one = lib.blockingLine({ blocking_task_ids: [5] }, (id) => `/task/${id}`);
|
|
58
|
+
assert.equal(one, 'Blocking 1 task: <a href="/task/5">#5</a>');
|
|
59
|
+
const many = lib.blockingLine({ blocking_task_ids: [5, 6] }, (id) => `/task/${id}`);
|
|
60
|
+
assert.equal(many, 'Blocking 2 tasks: <a href="/task/5">#5</a>, <a href="/task/6">#6</a>');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('blockingLine: with no taskHref the ids still render, as plain text — never a dead link', () => {
|
|
64
|
+
const line = lib.blockingLine({ blocking_task_ids: [5] }, null);
|
|
65
|
+
assert.equal(line, 'Blocking 1 task: #5');
|
|
66
|
+
assert.doesNotMatch(line, /<a /);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('blockerFacts: the facts line is ONE `·`-joined span, not several ungapped siblings', () => {
|
|
70
|
+
// The bug this shape avoids: two adjacent .ginbox__sub spans have no flex gap
|
|
71
|
+
// between them (unlike .ov-row__facts, which is a flex container) and would
|
|
72
|
+
// render Filed-date and Blocking-line running together with no separator.
|
|
73
|
+
const b = { id: 1, title: 'x', created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [7] };
|
|
74
|
+
const { factsHtml } = lib.blockerFacts(b, { taskHref: (id) => `/task/${id}` });
|
|
75
|
+
assert.equal((factsHtml.match(/<span/g) || []).length, 1, 'exactly one wrapping span');
|
|
76
|
+
assert.match(factsHtml, /Filed .* · Blocking 1 task/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('blockerFacts: body_md becomes a quoted note; an empty one renders nothing', () => {
|
|
80
|
+
const withNote = lib.blockerFacts({ id: 1, title: 'x', body_md: 'reason', blocking_task_ids: [] });
|
|
81
|
+
assert.match(withNote.noteHtml, /<p class="ov-row__sub">“reason”<\/p>/);
|
|
82
|
+
const noNote = lib.blockerFacts({ id: 1, title: 'x', body_md: ' ', blocking_task_ids: [] });
|
|
83
|
+
assert.equal(noNote.noteHtml, '');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('blockerFacts: a very long body_md is clipped, so one enormous blocker cannot blow up the row', () => {
|
|
87
|
+
const long = 'x'.repeat(500);
|
|
88
|
+
const { noteHtml } = lib.blockerFacts({ id: 1, title: 't', body_md: long, blocking_task_ids: [] });
|
|
89
|
+
assert.ok(noteHtml.length < 500, 'clipped well below the source length');
|
|
90
|
+
assert.match(noteHtml, /…”<\/p>$/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('blockerFacts: HTML in a title/body/task id is escaped, not injected', () => {
|
|
94
|
+
const b = {
|
|
95
|
+
id: 1, title: '<script>alert(1)</script>', body_md: '<img onerror=alert(1)>',
|
|
96
|
+
created_at: '2026-08-30T00:00:00.000Z', blocking_task_ids: [1],
|
|
97
|
+
};
|
|
98
|
+
const { titleHtml, noteHtml, factsHtml } = lib.blockerFacts(b, { taskHref: (id) => `/task/"><script>${id}` });
|
|
99
|
+
assert.doesNotMatch(titleHtml, /<script>/);
|
|
100
|
+
assert.doesNotMatch(noteHtml, /<img /);
|
|
101
|
+
assert.doesNotMatch(factsHtml, /<script>/);
|
|
102
|
+
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -255,7 +255,10 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
255
255
|
// (2026-09-09) was to move it: a sitting is work the project does, beside Goals
|
|
256
256
|
// and Tasks, not one of Government's monitoring surfaces. The item is invisible
|
|
257
257
|
// to anyone without the atom, so most builders never see it here.
|
|
258
|
-
|
|
258
|
+
// task 1003455 (goal 1000089): 'blockers' joins the same group, after Tasks —
|
|
259
|
+
// its subject is the project (what's stuck on the owner), not a person or a
|
|
260
|
+
// craft, and its one data source (GET /blockers) is public like Roadmap's.
|
|
261
|
+
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'blockers', 'board-room', 'ideas', 'copy-desk', 'studio'],
|
|
259
262
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
260
263
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
261
264
|
// People (task 1002577): the /scouting directory graduated into the hall.
|
|
@@ -129,6 +129,14 @@ const ORACLE = {
|
|
|
129
129
|
'/builders/drachmae': null,
|
|
130
130
|
'/builders/profile': null,
|
|
131
131
|
|
|
132
|
+
// task 1003455: the blocker queue. GET /blockers is deliberately public (the
|
|
133
|
+
// status dashboard is meant to read it too — "anyone landing on /builders or
|
|
134
|
+
// the status dashboard can see what's stuck on the owner", the route's own
|
|
135
|
+
// handler comment), so the page must not be gated tighter than the API it
|
|
136
|
+
// fronts — the same rule that puts /roster and /drachmae here. The nav link
|
|
137
|
+
// is still cosmetically 'signed', same as those two.
|
|
138
|
+
'/builders/blockers': null,
|
|
139
|
+
|
|
132
140
|
// The bare detail slugs, gated by task 1003629 — the decision idea 1000907
|
|
133
141
|
// (task 1003448) asked for. They resolve to the same task.html / idea.html
|
|
134
142
|
// shells their /:id forms do, so they now sit on the same tier: the id-less
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// tests/unit_runner_teardown_abort.mjs
|
|
2
|
+
//
|
|
3
|
+
// The unit runner tells a libuv TEARDOWN abort apart from a failing suite (task 1003845).
|
|
4
|
+
//
|
|
5
|
+
// On Windows + Node 24 a suite that hard-exits after serving HTTP can print a clean
|
|
6
|
+
// verdict and THEN die natively:
|
|
7
|
+
//
|
|
8
|
+
// Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76
|
|
9
|
+
//
|
|
10
|
+
// exit 127 or 0xC0000409, nondeterministically. tests/provisioning_github_app_routes.mjs
|
|
11
|
+
// did it 5 runs of 6 on origin/main, and 51 suites share that shape. Counted as FAILED,
|
|
12
|
+
// these swap in and out of a Windows baseline with timing, so a genuinely new failure
|
|
13
|
+
// can take a vanished one's place without the count moving.
|
|
14
|
+
//
|
|
15
|
+
// The runner recognises the class by its stderr signature AND the suite's own clean
|
|
16
|
+
// verdict, never by a curated list of names. The cases below are the ways that pairing
|
|
17
|
+
// must refuse: a real failure is never allowed to read as teardown noise.
|
|
18
|
+
//
|
|
19
|
+
// Run: node --test tests/unit_runner_teardown_abort.mjs
|
|
20
|
+
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { test } from 'node:test';
|
|
23
|
+
import { isCleanTeardownAbort } from '../scripts/gds/run-unit-tests.js';
|
|
24
|
+
|
|
25
|
+
// Captured from a real aborted run of tests/provisioning_github_app_routes.mjs.
|
|
26
|
+
const ABORT_76 = 'Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\\win\\async.c, line 76\r\n';
|
|
27
|
+
const ABORT_94 = 'Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\\win\\async.c, line 94\r\n';
|
|
28
|
+
const CLEAN = ' PASS non-numeric :id → 400 bad_id (never a 500)\n\nprovisioning github-app routes: 23 passed, 0 failed\n';
|
|
29
|
+
|
|
30
|
+
test('a clean verdict followed by the libuv abort is teardown noise', () => {
|
|
31
|
+
assert.equal(isCleanTeardownAbort(CLEAN, ABORT_76), true);
|
|
32
|
+
assert.equal(isCleanTeardownAbort(' PASS a\n\n9 passed, 0 failed\n', ABORT_94), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('the abort does not excuse a verdict that reports failures', () => {
|
|
36
|
+
assert.equal(isCleanTeardownAbort(' PASS a\n FAIL b\n\n22 passed, 1 failed\n', ABORT_76), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('a FAIL line outranks a summary that claims zero failures', () => {
|
|
40
|
+
assert.equal(isCleanTeardownAbort(' FAIL b\n\n23 passed, 0 failed\n', ABORT_76), false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('FAIL inside a passing case title is not a FAIL line', () => {
|
|
44
|
+
// tests/search_isolation.mjs prints exactly this and passes.
|
|
45
|
+
const stdout = ' ok rank helper is FAIL-CLOSED: unknown/missing rank → least privilege only\n\n5 passed, 0 failed\n';
|
|
46
|
+
assert.equal(isCleanTeardownAbort(stdout, ABORT_76), true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('an ordinary non-zero exit with no abort signature stays a failure', () => {
|
|
50
|
+
assert.equal(isCleanTeardownAbort(CLEAN, ''), false);
|
|
51
|
+
assert.equal(isCleanTeardownAbort(CLEAN, 'Error: listen EADDRINUSE\n'), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('an abort with no verdict at all is a crash, not noise', () => {
|
|
55
|
+
assert.equal(isCleanTeardownAbort(' PASS a\n', ABORT_76), false);
|
|
56
|
+
assert.equal(isCleanTeardownAbort('', ABORT_76), false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the verdict must be the LAST thing the suite printed', () => {
|
|
60
|
+
// A mid-file section summary does not vouch for whatever ran after it.
|
|
61
|
+
const midFile = 'section one: 4 passed, 0 failed\n PASS c\nnext section starting\n';
|
|
62
|
+
assert.equal(isCleanTeardownAbort(midFile, ABORT_76), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('a suite that asserted nothing is not vouched for', () => {
|
|
66
|
+
assert.equal(isCleanTeardownAbort('\n0 passed, 0 failed\n', ABORT_76), false);
|
|
67
|
+
});
|