@bongos/core 1.20.66 → 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.
@@ -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, ''));