@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.
@@ -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
+ });
@@ -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
- assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work', 'board-room', 'ideas', 'copy-desk', 'studio'],
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
+ });