@bongos/core 1.19.989 → 1.19.991

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,91 @@
1
+ // tests/text_fit.mjs — measured-width text fitting, EXECUTED (task 1004200; moved
2
+ // out of goal-forest-lib.js into its own file by task 1004204 so the goal map
3
+ // and the goal forest share one copy).
4
+ //
5
+ // text-fit.js is pure — the caller hands it `measure` — so these run the real
6
+ // code against a proportional fake where no fixed character count can fit.
7
+ //
8
+ // Run: node tests/text_fit.mjs
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { createRequire } from 'node:module';
14
+ import { makeRunner } from './helpers.mjs';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+ const FILE = path.join(ROOT, 'modules/hall-ui/public/text-fit.js');
19
+ const L = require(FILE);
20
+
21
+ const { test, summary } = makeRunner();
22
+
23
+ // ---- fitting a line to its box (task 1004200) ---------------------------------------
24
+
25
+ // A proportional fake: wide capitals cost three times a narrow letter, so any
26
+ // character budget that fits one title overflows the other.
27
+ const WIDTHS = { W: 12, M: 11, i: 3, '…': 8, '🚀': 14 };
28
+ const measure = (s) => Array.from(s).reduce((w, c) => w + (WIDTHS[c] ?? 6), 0);
29
+
30
+ await test('fitText leaves a title that fits untouched', () => {
31
+ assert.equal(L.fitText('#7 Fix', 136, measure), '#7 Fix');
32
+ });
33
+
34
+ await test('fitText cuts to the MEASURED width, so wide glyphs keep fewer characters than narrow ones', () => {
35
+ const wide = L.fitText('W'.repeat(40), 136, measure);
36
+ const narrow = L.fitText('i'.repeat(80), 136, measure);
37
+ for (const s of [wide, narrow]) {
38
+ assert.ok(measure(s) <= 136, `${s} measures ${measure(s)}, past the box`);
39
+ assert.ok(s.endsWith('…'), 'a cut title says it was cut');
40
+ }
41
+ assert.ok(Array.from(narrow).length > 3 * Array.from(wide).length, 'no fixed character count could produce both');
42
+ assert.equal(wide, 'W'.repeat(10) + '…', 'the LONGEST prefix that fits: 10 W + … = 128px, 11 would be 140px');
43
+ });
44
+
45
+ await test('fitText never ends on a dangling space and never splits an emoji', () => {
46
+ const spaced = L.fitText('ab cd ef gh ij kl mn op qr', 62, measure);
47
+ assert.doesNotMatch(spaced, /\s…$/);
48
+ const emoji = L.fitText('🚀'.repeat(30), 60, measure);
49
+ assert.equal(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(emoji), false, 'no lone high surrogate');
50
+ });
51
+
52
+ await test('fitText returns a bare ellipsis when not even one character fits', () => {
53
+ assert.equal(L.fitText('WWW', 10, measure), '…');
54
+ });
55
+
56
+ await test('fitLines keeps a short title on one line, whole', () => {
57
+ assert.deepEqual(L.fitLines('#7 Fix', 136, 2, measure), ['#7 Fix']);
58
+ });
59
+
60
+ await test('fitLines wraps at a space, and a second line that fits is left whole', () => {
61
+ assert.deepEqual(L.fitLines('aaaa bbbb cccc dddd', 60, 2, measure), ['aaaa bbbb', 'cccc dddd']);
62
+ assert.deepEqual(L.fitLines('a b\n c', 60, 2, measure), ['a b c'], 'runs of whitespace read as one space');
63
+ });
64
+
65
+ await test('fitLines ellipsises the LAST line when the title outruns every line, and no line overflows', () => {
66
+ const lines = L.fitLines('BV1.R26 — Real governance: the constitution becomes enforceable at last', 136, 2, measure);
67
+ assert.equal(lines.length, 2);
68
+ for (const l of lines) assert.ok(measure(l) <= 136, `"${l}" measures ${measure(l)}`);
69
+ assert.ok(lines[1].endsWith('…'));
70
+ assert.ok(!lines[0].endsWith('…'), 'only the last line is cut');
71
+ });
72
+
73
+ await test('fitLines breaks a word wider than a whole line instead of overflowing, and always finishes', () => {
74
+ const lines = L.fitLines('W'.repeat(40), 136, 2, measure);
75
+ assert.deepEqual(lines, ['W'.repeat(11), 'W'.repeat(10) + '…']);
76
+ assert.ok(L.fitLines('WWW', 5, 2, measure).length <= 2, 'a box narrower than one glyph still terminates');
77
+ });
78
+
79
+ await test('fitLines with one line is fitText', () => {
80
+ const t = 'Move the remaining hall pages onto the kit';
81
+ assert.deepEqual(L.fitLines(t, 90, 1, measure), [L.fitText(t, 90, measure)]);
82
+ });
83
+
84
+ // ---- purity -----------------------------------------------------------------------
85
+
86
+ await test('the helper is pure: no DOM, no fetch — the caller measures', () => {
87
+ const src = fs.readFileSync(FILE, 'utf8').replace(/^\s*\/\/.*$/gm, '');
88
+ assert.equal(/\bdocument\b|\bfetch\b|\bwindow\b|getComputedTextLength/.test(src.replace(/root\.OTBTextFit/, '')), false);
89
+ });
90
+
91
+ summary();
@@ -0,0 +1,144 @@
1
+ // tests/upgrade_artist_gate.mjs — the artist gate holds the pin on the UPGRADE
2
+ // path too (task 1004205; ADR 0241).
3
+ //
4
+ // go-live.js consulted the gate; the vendored `upgrade.js --from` this instance
5
+ // actually moves its pin with did not, so an open review held nothing. These
6
+ // pin the gate reader against a fake instance, and runUpgrade refusing BEFORE
7
+ // anything moves — with a downgrade (rolling back) never held.
8
+ //
9
+ // Run: node tests/upgrade_artist_gate.mjs
10
+ import assert from 'node:assert/strict';
11
+ import { createRequire } from 'node:module';
12
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
13
+ import { tmpdir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ import { makeRunner } from './helpers.mjs';
16
+ import { checkArtistGate } from '../scripts/gds/upgrade-artist-gate.js';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const u = require('../scripts/gds/upgrade.js');
20
+ const { test, summary } = makeRunner();
21
+
22
+ // A fake instance: answers GET <origin>/api/bongos/artist-gate with `status` + `body`.
23
+ function instance(status, body) {
24
+ const asked = [];
25
+ const fetchImpl = async (url, init) => {
26
+ asked.push({ url, method: init && init.method });
27
+ return { status, ok: status >= 200 && status < 300, json: async () => { if (body === undefined) throw new Error('not json'); return body; } };
28
+ };
29
+ return { asked, fetchImpl };
30
+ }
31
+ const HOLD = {
32
+ ok: true, mode: 'strict', holds: true, reviews: [{ id: '1004199' }],
33
+ halt: { code: 'ARTIST_REVIEW_UNRESOLVED', message: '1 unresolved artist review hold this deploy', detail: 'Held by task 1004199. Two ways out.' },
34
+ };
35
+
36
+ // ---- the reader ---------------------------------------------------------------
37
+
38
+ await test('it asks the instance being upgraded, at the ORIGIN of --health-url', async () => {
39
+ const i = instance(200, { ok: true, mode: 'strict', holds: false, reviews: [], halt: null });
40
+ const r = await checkArtistGate({ healthUrl: 'http://127.0.0.1:3000/api/bongos/healthz' }, { fetchImpl: i.fetchImpl });
41
+ assert.deepEqual(i.asked, [{ url: 'http://127.0.0.1:3000/api/bongos/artist-gate', method: 'GET' }]);
42
+ assert.deepEqual(r, { halts: [], notes: [] }, 'strict with nothing open holds nothing');
43
+ });
44
+
45
+ await test('an open review on a strict project HALTS, naming the review and the way out', async () => {
46
+ const r = await checkArtistGate({ healthUrl: 'https://cloudbongos.com/api/bongos/healthz' }, { fetchImpl: instance(200, HOLD).fetchImpl });
47
+ assert.equal(r.halts.length, 1);
48
+ assert.equal(r.halts[0].code, 'ARTIST_REVIEW_UNRESOLVED');
49
+ assert.match(r.halts[0].detail, /1004199/);
50
+ });
51
+
52
+ await test('advisory and off hold nothing', async () => {
53
+ for (const mode of ['advisory', 'off']) {
54
+ const r = await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: instance(200, { mode, holds: false, reviews: [], halt: null }).fetchImpl });
55
+ assert.deepEqual(r.halts, [], mode);
56
+ }
57
+ });
58
+
59
+ await test('a gate that is THERE and broken halts; one that cannot be read at all is a note', async () => {
60
+ assert.equal((await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: instance(500, {}).fetchImpl })).halts[0].code, 'ARTIST_GATE_UNREADABLE');
61
+ for (const [status, body] of [[404, {}], [200, undefined], [200, { hello: 'an SPA page' }]]) {
62
+ const r = await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: instance(status, body).fetchImpl });
63
+ assert.deepEqual(r.halts, [], `${status} → no halt`);
64
+ assert.equal(r.notes[0].code, 'ARTIST_GATE_UNREAD', `${status} → said out loud, never read as "nothing holds" silently`);
65
+ }
66
+ const down = await checkArtistGate({ healthUrl: 'http://x/h' }, { fetchImpl: async () => { throw new Error('ECONNREFUSED'); } });
67
+ assert.equal(down.notes[0].code, 'ARTIST_GATE_UNREAD');
68
+ });
69
+
70
+ await test('with no --health-url there is nothing to ask — a note, never a silent pass', async () => {
71
+ for (const healthUrl of [undefined, 'not a url']) {
72
+ const r = await checkArtistGate({ healthUrl }, { fetchImpl: async () => { throw new Error('must not be called'); } });
73
+ assert.deepEqual(r.halts, []);
74
+ assert.equal(r.notes[0].code, 'ARTIST_GATE_UNREAD');
75
+ }
76
+ });
77
+
78
+ // ---- runUpgrade -----------------------------------------------------------------
79
+
80
+ function consumer(version = '1.14.1') {
81
+ const dir = mkdtempSync(join(tmpdir(), 'otb-upgrade-gate-'));
82
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({
83
+ name: 'c', version: '0.1.0', dependencies: { '@bongos/core': `file:vendor/bongos-core-${version}.tgz` }, scripts: { migrate: 'true', start: 'true' },
84
+ }, null, 2) + '\n');
85
+ mkdirSync(join(dir, 'node_modules', '@bongos', 'core'), { recursive: true });
86
+ writeFileSync(join(dir, 'node_modules', '@bongos', 'core', 'package.json'), JSON.stringify({ name: '@bongos/core', version }) + '\n');
87
+ return dir;
88
+ }
89
+ function tarball(version) {
90
+ const src = mkdtempSync(join(tmpdir(), 'src-'));
91
+ writeFileSync(join(src, `bongos-core-${version}.tgz`), 'TGZ');
92
+ return join(src, `bongos-core-${version}.tgz`);
93
+ }
94
+ const run = (calls) => (cmd, args) => { calls.push(`${cmd} ${(args || []).join(' ')}`); return cmd === 'git' ? { status: 0, stdout: '' } : { status: 0 }; };
95
+ const holding = async () => ({ halts: [{ code: 'ARTIST_REVIEW_UNRESOLVED', message: '1 unresolved artist review hold this deploy', detail: 'Held by task 1004199.' }], notes: [] });
96
+
97
+ await test('runUpgrade REFUSES a held pin before anything moves — --force included', async () => {
98
+ for (const force of [false, true]) {
99
+ const dir = consumer();
100
+ const calls = [];
101
+ const res = await u.runUpgrade({ to: '1.15.0', from: tarball('1.15.0'), instance: dir, force, skipMigrate: true },
102
+ { log: () => {}, err: () => {}, run: run(calls), checkArtistGate: holding });
103
+ assert.equal(res.ok, false);
104
+ assert.equal(res.artistGate, true);
105
+ assert.match(res.error, /unresolved artist review/);
106
+ assert.match(res.error, /1004199/, 'names the review');
107
+ assert.equal(u.readPinnedCoreVersion(dir), '1.14.1', 'the pin never moved');
108
+ assert.ok(!calls.some((c) => c.startsWith('npm')), 'nothing installed');
109
+ }
110
+ });
111
+
112
+ await test('a dry run reports the hold too — that is what a dry run is for', async () => {
113
+ const res = await u.runUpgrade({ to: '1.15.0', from: tarball('1.15.0'), instance: consumer(), dryRun: true, skipMigrate: true },
114
+ { log: () => {}, err: () => {}, run: run([]), checkArtistGate: holding });
115
+ assert.equal(res.ok, false);
116
+ assert.equal(res.artistGate, true);
117
+ });
118
+
119
+ await test('nothing open: the upgrade proceeds exactly as before, and a note is printed, not swallowed', async () => {
120
+ const errs = [];
121
+ const res = await u.runUpgrade({ to: '1.15.0', from: tarball('1.15.0'), instance: consumer(), dryRun: true, skipMigrate: true },
122
+ { log: () => {}, err: (m) => errs.push(m), run: run([]), checkArtistGate: async () => ({ halts: [], notes: [{ code: 'ARTIST_GATE_UNREAD', message: 'could not read this instance\'s artist gate (x)' }] }) });
123
+ assert.equal(res.ok, true);
124
+ assert.ok(errs.some((m) => /could not read this instance's artist gate/.test(m)));
125
+ });
126
+
127
+ await test('a DOWNGRADE is never held — rolling back must not wait on a review', async () => {
128
+ let asked = 0;
129
+ const res = await u.runUpgrade({ to: '1.14.0', from: tarball('1.14.0'), instance: consumer('1.14.1'), allowDowngrade: true, dryRun: true, skipMigrate: true },
130
+ { log: () => {}, err: () => {}, run: run([]), checkArtistGate: async () => { asked++; return holding(); } });
131
+ assert.equal(asked, 0);
132
+ assert.equal(res.ok, true);
133
+ });
134
+
135
+ await test('the gate runs from the pre-flight, before the pin step, and the reader is go-live\'s own', () => {
136
+ const whole = readFileSync(new URL('../scripts/gds/upgrade.js', import.meta.url), 'utf8');
137
+ const src = whole.slice(whole.indexOf('async function runUpgrade('));
138
+ const gateAt = src.indexOf('checkArtistGate)(opts, deps)');
139
+ assert.ok(gateAt > 0 && gateAt < src.indexOf('writePin(instanceDir'), 'checked before the pin is written');
140
+ const gateSrc = readFileSync(new URL('../scripts/gds/upgrade-artist-gate.js', import.meta.url), 'utf8');
141
+ assert.match(gateSrc, /require\('\.\/go-live'\)/, 'one reader and one judgement for both deploy paths');
142
+ });
143
+
144
+ summary();