@bongos/core 1.20.65 → 1.20.67

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.
@@ -171,9 +171,14 @@ await test('the SQL rollup buckets MIRROR goals-lib readinessOf exactly', () =>
171
171
  assert.match(lib, /DONE_STATUSES = new Set\(\['completed', 'confirmed', 'shipped'\]\)/);
172
172
  assert.match(vocab, /const CUT_STATUSES = \['abandoned', 'dropped'\]/);
173
173
  assert.match(lib, /CUT_STATUSES = new Set\(\['abandoned', 'dropped'\]\)/);
174
- // blocked = backlog with >=1 unshipped dependency, both sides
175
- assert.match(rollup, /WHEN s\.status = 'backlog' AND s\.has_unshipped_dep THEN 'blocked'/);
176
- assert.match(lib, /s === 'backlog' && \(t\.blocked_by \|\| \[\]\)\.some\(\(d\) => !d\.satisfied\)/);
174
+ // blocked = >=1 unshipped dependency on an unclaimed task, both sides — and
175
+ // on BOTH sides the arm sits AHEAD of 'ready' (task 1004487), because a dep
176
+ // added after promotion leaves the row at 'ready' and a status-first
177
+ // bucketing would then count un-claimable work as ready. The ORDER is the
178
+ // assertion, not just the presence of the clause.
179
+ assert.match(rollup, /WHEN s\.has_unshipped_dep\s+THEN 'blocked'\s*\n\s*WHEN s\.status = 'ready'\s+THEN 'ready'/);
180
+ assert.match(lib, /const hasUnmetDeps = \(t\) => \(\(t && t\.blocked_by\) \|\| \[\]\)\.some\(\(d\) => !d\.satisfied\);/);
181
+ assert.match(lib, /if \(hasUnmetDeps\(t\)\) return 'blocked';\s*\n\s*if \(s === 'ready'\) return 'ready';/);
177
182
  // cut is excluded from the total on both sides (migration 017's rule)
178
183
  assert.match(rollup, /FILTER \(WHERE b\.bucket <> 'cut'\)::int\s+AS total/);
179
184
  assert.match(read('modules/hall-ui/public/goals-page.js'), /total: row\.total \|\| 0/);
@@ -25,7 +25,16 @@ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-lib.js
25
25
  const TASK_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/task-lib.js'), 'utf8');
26
26
  const { test, summary } = makeRunner();
27
27
 
28
- const windowObj = {};
28
+ // window.OTB.escapeHtml is the ONE sibling the lib reaches for that is not a
29
+ // loadable file (it lives in the hall's shared dom-utils). The real one, copied
30
+ // to its behaviour, so a tag that forgets to escape a dependency title fails
31
+ // here rather than passing against a pass-through fake.
32
+ const windowObj = {
33
+ OTB: {
34
+ escapeHtml: (s) => String(s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;')
35
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'),
36
+ },
37
+ };
29
38
  const sandbox = { window: windowObj, Set, Map, Math, Number, String, Array, Object, Date, JSON };
30
39
  sandbox.globalThis = sandbox;
31
40
  vm.createContext(sandbox);
@@ -39,7 +48,8 @@ await test('loads standalone in a bare window (no page-state dependency) and exp
39
48
  assert.ok(lib, 'window.OTBGoalsLib must be set');
40
49
  assert.equal('state' in sandbox, false, 'the lib must not reach for page state');
41
50
  for (const k of ['DONE_STATUSES', 'CUT_STATUSES', 'isTodoStatus', 'ring', 'memberName',
42
- 'readinessOf', 'EMPTY_ROLLUP', 'goalRollLine', 'matchesTodoSearch', 'msOf', 'taskActivityMs']) {
51
+ 'hasUnmetDeps', 'taskStatusTagHtml', 'readinessOf', 'EMPTY_ROLLUP', 'goalRollLine',
52
+ 'matchesTodoSearch', 'msOf', 'taskActivityMs']) {
43
53
  assert.ok(k in lib, `missing export: ${k}`);
44
54
  }
45
55
  });
@@ -53,6 +63,52 @@ await test('readinessOf buckets match the task-1002439 rules', () => {
53
63
  assert.equal(lib.readinessOf({ status: 'backlog' }), 'backlog');
54
64
  });
55
65
 
66
+ // Task 1004487 — the case the backlog-only condition missed. ADR 0015 rules
67
+ // that a dependency added AFTER promotion leaves the row at `ready` and that
68
+ // the readers are what change (core_264 did it for the claimable feed), so a
69
+ // `ready` row with an unshipped dependency must bucket as blocked here too —
70
+ // otherwise the Goals page counts work POST /claims refuses as claimable.
71
+ await test('a READY task with an unshipped dependency is blocked, not ready', () => {
72
+ assert.equal(lib.readinessOf({ status: 'ready', blocked_by: [{ satisfied: false }] }), 'blocked');
73
+ // …and a satisfied edge does not demote a genuinely claimable row.
74
+ assert.equal(lib.readinessOf({ status: 'ready', blocked_by: [{ satisfied: true }] }), 'ready');
75
+ assert.equal(lib.readinessOf({ status: 'ready', blocked_by: [] }), 'ready');
76
+ // A live claim still outranks the dependency question: the task is being
77
+ // worked, so "in progress" is the true answer whatever its edges say.
78
+ assert.equal(lib.readinessOf({ status: 'active', blocked_by: [{ satisfied: false }] }), 'active');
79
+ // Settled work is never re-bucketed by a stale edge either.
80
+ assert.equal(lib.readinessOf({ status: 'shipped', blocked_by: [{ satisfied: false }] }), 'done');
81
+ assert.equal(lib.readinessOf({ status: 'abandoned', blocked_by: [{ satisfied: false }] }), 'cut');
82
+ });
83
+
84
+ await test('hasUnmetDeps mirrors the DEPS_NOT_SHIPPED rule and tolerates a missing array', () => {
85
+ assert.equal(lib.hasUnmetDeps({ blocked_by: [{ satisfied: true }, { satisfied: false }] }), true);
86
+ assert.equal(lib.hasUnmetDeps({ blocked_by: [{ satisfied: true }] }), false);
87
+ // A caller that never asked for ?include=deps gets no array at all; the page
88
+ // must render rather than throw, and must not invent a block it cannot see.
89
+ assert.equal(lib.hasUnmetDeps({ status: 'ready' }), false);
90
+ assert.equal(lib.hasUnmetDeps(null), false);
91
+ });
92
+
93
+ await test("taskStatusTagHtml says blocked on a dep-blocked ready row, and the real status otherwise", () => {
94
+ const blocked = lib.taskStatusTagHtml({
95
+ status: 'ready',
96
+ blocked_by: [{ id: 640, title: 'Palette overhaul', satisfied: false },
97
+ { id: 587, title: 'Already landed', satisfied: true }],
98
+ });
99
+ assert.match(blocked, />blocked<\/span>$/, 'the word the claim gate would use, not the status column');
100
+ assert.match(blocked, /title="Waiting on #640 Palette overhaul"/, 'it names the unmet edge');
101
+ assert.doesNotMatch(blocked, /587/, 'and not the one that already shipped');
102
+ // Every other row is untouched — this must not become a second status vocabulary.
103
+ assert.equal(lib.taskStatusTagHtml({ status: 'active' }),
104
+ '<span class="task__tag" title="Task status">active</span>');
105
+ assert.equal(lib.taskStatusTagHtml({ status: 'ready', blocked_by: [{ satisfied: true }] }),
106
+ '<span class="task__tag" title="Task status">ready</span>');
107
+ // A dependency title is ledger prose on a title attribute — escape it.
108
+ const nasty = lib.taskStatusTagHtml({ status: 'ready', blocked_by: [{ id: 1, title: '"><b>x', satisfied: false }] });
109
+ assert.doesNotMatch(nasty, /<b>/);
110
+ });
111
+
56
112
  await test('matchesTodoSearch: bare number, #N and title fragments all match (task 1003135 search semantics)', () => {
57
113
  const t = { id: 1002163, title: 'Show the goal id on the row' };
58
114
  assert.ok(lib.matchesTodoSearch(t, ''));