@bongos/core 1.19.279 → 1.19.281

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.
@@ -14,7 +14,7 @@ import { strict as assert } from 'node:assert';
14
14
  import { createRequire } from 'node:module';
15
15
 
16
16
  const require = createRequire(import.meta.url);
17
- const { formatClaimFailure } = require('../scripts/gds/claim.js');
17
+ const { formatClaimFailure, claimErrorInfo, goalMembershipOfferPlan } = require('../scripts/gds/claim.js');
18
18
 
19
19
  let passed = 0;
20
20
  let failed = 0;
@@ -36,7 +36,7 @@ t('never dumps [object Object] for any known code', () => {
36
36
  'session_already_has_bound_claim',
37
37
  'newcomer_must_ship_first', 'ALREADY_CLAIMED', 'MODULE_CONFLICT',
38
38
  'TOUCHES_CONFLICT', 'TASK_NOT_READY', 'TASK_NOT_FOUND', 'DEPS_NOT_SHIPPED',
39
- 'OPEN_BLOCKER',
39
+ 'OPEN_BLOCKER', 'GOAL_MEMBERSHIP_REQUIRED',
40
40
  'some_unmapped_future_code',
41
41
  ];
42
42
  for (const c of codes) {
@@ -138,5 +138,58 @@ t('unknown code: fallback carries code + message, exit 1', () => {
138
138
  assert.match(text(r), /boom/);
139
139
  });
140
140
 
141
+ // ---- task 1003138: the goal-membership refusal is resolvable in-session ----
142
+ // The task-1003137 gate returns 403 GOAL_MEMBERSHIP_REQUIRED with { goal_id,
143
+ // visibility, action: join|apply, goal_title? }. claim.js offers the join (or
144
+ // files the application) inline; these pin the pure halves — the offer plan and
145
+ // the non-interactive fallthrough rendering.
146
+
147
+ t('GOAL_MEMBERSHIP_REQUIRED fallthrough: join names the one-command path, apply names the approval wait', () => {
148
+ const join = text(formatClaimFailure(
149
+ envelope('GOAL_MEMBERSHIP_REQUIRED', { goal_id: 1000045, visibility: 'public', action: 'join', goal_title: 'Public builder profiles' }, 'not a member', 403), 1002784));
150
+ assert.match(join, /goal 1000045/);
151
+ assert.match(join, /Public builder profiles/);
152
+ assert.match(join, /--join/, 'join path advertises the one-command re-run');
153
+ const apply = text(formatClaimFailure(
154
+ envelope('GOAL_MEMBERSHIP_REQUIRED', { goal_id: 1000099, visibility: 'private', action: 'apply' }, 'not a member', 403), 5));
155
+ assert.match(apply, /goal 1000099/);
156
+ assert.match(apply, /approv/i, 'apply path says approval gates the claim');
157
+ });
158
+
159
+ t('goalMembershipOfferPlan: join offer matches the owner-approved shape (goal named, title shown, [Y/n])', () => {
160
+ const p = goalMembershipOfferPlan({ goal_id: 1000045, visibility: 'public', action: 'join', goal_title: 'Public builder profiles' }, 1002784);
161
+ assert.equal(p.action, 'join');
162
+ assert.match(p.headerLines[0], /Task 1002784 lives in goal 1000045/);
163
+ assert.match(p.headerLines[1], /"Public builder profiles" \(public\)/);
164
+ assert.match(p.headerLines[2], /not a member/i);
165
+ assert.equal(p.question, 'Join goal 1000045 and claim? [Y/n] ');
166
+ });
167
+
168
+ t('goalMembershipOfferPlan: private goal offers apply and shows no title (there is none to leak)', () => {
169
+ const p = goalMembershipOfferPlan({ goal_id: 1000099, visibility: 'private', action: 'apply' }, 5);
170
+ assert.equal(p.action, 'apply');
171
+ assert.deepEqual(p.headerLines, ['✗ Goal 1000099 is private.']);
172
+ assert.equal(p.question, 'Apply to join? [Y/n] ');
173
+ });
174
+
175
+ t('goalMembershipOfferPlan: public protected-scope goal offers apply and says why', () => {
176
+ const p = goalMembershipOfferPlan({ goal_id: 7, visibility: 'public', action: 'apply', goal_title: 'Core wall' }, 5);
177
+ assert.equal(p.action, 'apply');
178
+ assert.match(p.headerLines[0], /Archon-mediated/i);
179
+ });
180
+
181
+ t('goalMembershipOfferPlan: a malformed body plans nothing (falls back to formatClaimFailure)', () => {
182
+ assert.equal(goalMembershipOfferPlan(null, 5), null);
183
+ assert.equal(goalMembershipOfferPlan({ action: 'join' }, 5), null, 'no goal_id → no plan');
184
+ assert.equal(goalMembershipOfferPlan({ goal_id: 7, action: 'teleport' }, 5), null, 'unknown action → no plan');
185
+ });
186
+
187
+ t('claimErrorInfo: reads envelope and flat shapes identically', () => {
188
+ const env = claimErrorInfo(envelope('X', { a: 1 }, 'msg'));
189
+ assert.equal(env.code, 'X'); assert.equal(env.message, 'msg'); assert.equal(env.d.a, 1);
190
+ const flat = claimErrorInfo({ status: 409, data: { error: 'X', a: 1 } });
191
+ assert.equal(flat.code, 'X'); assert.equal(flat.d.a, 1);
192
+ });
193
+
141
194
  console.log(`\nclaim_error_surface: ${passed} passed, ${failed} failed`);
142
195
  process.exit(failed === 0 ? 0 : 1);
@@ -37,6 +37,9 @@ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-page.j
37
37
  // the REAL file in the same context rather than stubbing it — stubbing would let
38
38
  // the extraction rot without this, the only test that executes either board.
39
39
  const TASK_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/task-lib.js'), 'utf8');
40
+ // task 1003135: the page's pure presentational helpers moved to goals-lib.js
41
+ // (window.OTBGoalsLib) — run the REAL file too, same reasoning as task-lib above.
42
+ const GOALS_LIB_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-lib.js'), 'utf8');
40
43
 
41
44
  const { test, summary } = makeRunner();
42
45
 
@@ -132,6 +135,7 @@ async function render({ rank = 'metic', goalStatus = 'open', criteria = Object.v
132
135
  sandbox.globalThis = sandbox;
133
136
  vm.createContext(sandbox);
134
137
  vm.runInContext(TASK_LIB_SRC, sandbox, { filename: 'task-lib.js' });
138
+ vm.runInContext(GOALS_LIB_SRC, sandbox, { filename: 'goals-lib.js' });
135
139
  vm.runInContext(SRC, sandbox, { filename: 'goals-page.js' });
136
140
 
137
141
  // Let boot()'s promise chain settle (me → goals → details → route → render).
@@ -0,0 +1,70 @@
1
+ // tests/goals_lib.mjs — the Goals page's extracted pure helpers (goals-lib.js,
2
+ // task 1003135). Pins (a) that the lib loads standalone in a bare window — it
3
+ // must not silently grow a dependency on the page's IIFE state, which is the
4
+ // whole reason it could move out of the oversized goals-page.js — and (b) the
5
+ // behaviour of the helpers the page destructures.
6
+ //
7
+ // Run: node tests/goals_lib.mjs
8
+
9
+ import { strict as assert } from 'node:assert';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import vm from 'node:vm';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { makeRunner } from './helpers.mjs';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals-lib.js'), 'utf8');
18
+ const { test, summary } = makeRunner();
19
+
20
+ const windowObj = {};
21
+ const sandbox = { window: windowObj, Set, Map, Math, Number, String, Array, Object, Date, JSON };
22
+ sandbox.globalThis = sandbox;
23
+ vm.createContext(sandbox);
24
+ vm.runInContext(SRC, sandbox, { filename: 'goals-lib.js' });
25
+ const lib = windowObj.OTBGoalsLib;
26
+
27
+ await test('loads standalone in a bare window (no page-state dependency) and exports the full surface', () => {
28
+ assert.ok(lib, 'window.OTBGoalsLib must be set');
29
+ for (const k of ['DONE_STATUSES', 'CUT_STATUSES', 'isTodoStatus', 'ring', 'memberName',
30
+ 'readinessOf', 'EMPTY_ROLLUP', 'goalRollLine', 'matchesTodoSearch', 'msOf', 'taskActivityMs']) {
31
+ assert.ok(k in lib, `missing export: ${k}`);
32
+ }
33
+ });
34
+
35
+ await test('readinessOf buckets match the task-1002439 rules', () => {
36
+ assert.equal(lib.readinessOf({ status: 'shipped' }), 'done');
37
+ assert.equal(lib.readinessOf({ status: 'abandoned' }), 'cut');
38
+ assert.equal(lib.readinessOf({ status: 'active' }), 'active');
39
+ assert.equal(lib.readinessOf({ status: 'ready' }), 'ready');
40
+ assert.equal(lib.readinessOf({ status: 'backlog', blocked_by: [{ satisfied: false }] }), 'blocked');
41
+ assert.equal(lib.readinessOf({ status: 'backlog' }), 'backlog');
42
+ });
43
+
44
+ await test('matchesTodoSearch: bare number, #N and title fragments all match (task 1003135 search semantics)', () => {
45
+ const t = { id: 1002163, title: 'Show the goal id on the row' };
46
+ assert.ok(lib.matchesTodoSearch(t, ''));
47
+ assert.ok(lib.matchesTodoSearch(t, '1002163'));
48
+ assert.ok(lib.matchesTodoSearch(t, '#1002163'));
49
+ assert.ok(lib.matchesTodoSearch(t, '2163'), 'substring id match');
50
+ assert.ok(lib.matchesTodoSearch(t, 'goal id'));
51
+ assert.ok(!lib.matchesTodoSearch(t, 'zzz'));
52
+ assert.ok(lib.matchesTodoSearch(t, '#'), 'a lone # narrows nothing');
53
+ });
54
+
55
+ await test('goalRollLine renders only non-zero buckets; ring is well-formed SVG', () => {
56
+ const line = lib.goalRollLine({ ready: 2, active: 0, blocked: 1, backlog: 0 });
57
+ assert.match(line, /<b>2<\/b> ready/);
58
+ assert.match(line, /<b>1<\/b> blocked/);
59
+ assert.ok(!line.includes('in progress'), 'zero buckets are omitted');
60
+ assert.equal(lib.goalRollLine({ ready: 0, active: 0, blocked: 0, backlog: 0 }), '');
61
+ assert.match(lib.ring(2, 5, false), /aria-label="2 of 5 criteria satisfied"/);
62
+ });
63
+
64
+ await test('memberName label idiom: display name, then @login, then builder #id', () => {
65
+ assert.equal(lib.memberName({ display_name: 'A', github_login: 'a', builder_id: 1 }), 'A');
66
+ assert.equal(lib.memberName({ github_login: 'a', builder_id: 1 }), '@a');
67
+ assert.equal(lib.memberName({ builder_id: 1 }), 'builder #1');
68
+ });
69
+
70
+ summary();