@bongos/core 1.19.1042 → 1.19.1044
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.
- package/.bongos-core.json +64 -39
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +129 -3
- package/docs/api-reference.md +5 -3
- package/docs/copy-inventory.md +38 -38
- package/docs/copy-registry.json +41 -41
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +4 -1
- package/migrations/core_256_blocker_unlink_releases_task.sql +84 -0
- package/modules/hall-ui/public/style.css +4 -0
- package/modules/hall-ui/public/task.css +6 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/task.js +17 -0
- package/modules/hall-ui/records/record-family.md +2 -0
- package/modules/ideas/blockers.js +60 -0
- package/modules/ideas/routes/blockers.js +41 -0
- package/modules/npm-release/module.json +8 -2
- package/modules/npm-release/public/task-widget.js +79 -0
- package/modules/npm-release/routes/task-where.js +48 -0
- package/modules/npm-release/work.js +70 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/exec-path-guard.js +1 -1
- package/scripts/gds/seam-wiring-guard.js +4 -2
- package/scripts/hall-preview/server.js +14 -1
- package/src/bongos/serve-internal.js +18 -4
- package/src/module-api.js +1 -1
- package/src/module-loader/loader.js +13 -3
- package/tests/blocker_unlink.mjs +251 -0
- package/tests/hall_record_world.mjs +23 -1
- package/tests/module_loader.mjs +27 -0
- package/tests/npm_release_where.mjs +323 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
// tests/npm_release_where.mjs — a task's own page says where its code is (task 1004301):
|
|
2
|
+
// the npm-release module's one-task reading (readTaskWhere), its route, the browser widget
|
|
3
|
+
// that draws the line, and the core seam that puts the widget on the task page without the
|
|
4
|
+
// page naming the module.
|
|
5
|
+
//
|
|
6
|
+
// WHAT IS AT RISK. The line is read by the person who shipped the task, and the case the
|
|
7
|
+
// task exists for is the ship that never landed: "done, but not merged" must be said for a
|
|
8
|
+
// completed/confirmed task, never a version it is not in. Every other stage must be the
|
|
9
|
+
// deploy page's own answer, the widget must write only into the slot the page gives it and
|
|
10
|
+
// only for the task still on screen, and a failure must draw nothing rather than something
|
|
11
|
+
// wrong.
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/npm_release_where.mjs
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import vm from 'node:vm';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { npmTarball, npmPackument, fakeHttpsGet } from './helpers.mjs';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const { readTaskWhere } = require('../modules/npm-release/work.js');
|
|
26
|
+
const buildRouter = require('../modules/npm-release/routes/task-where.js');
|
|
27
|
+
|
|
28
|
+
const ORIGIN = 'https://registry.npmjs.org';
|
|
29
|
+
const PKG = '@bongos/core';
|
|
30
|
+
const TIMES = {
|
|
31
|
+
'1.0.1': '2026-09-22T10:00:00.000Z',
|
|
32
|
+
'1.0.2': '2026-09-23T10:00:00.000Z',
|
|
33
|
+
'1.0.3': '2026-09-24T10:00:00.000Z',
|
|
34
|
+
'1.0.4': '2026-09-25T10:00:00.000Z',
|
|
35
|
+
};
|
|
36
|
+
const NOTES = {
|
|
37
|
+
'1.0.1': [{ id: '11', text: 'Released long ago.' }],
|
|
38
|
+
'1.0.2': [{ id: '12', text: 'Running here, not released.' }],
|
|
39
|
+
'1.0.3': [{ id: '13', text: 'Published, not running.' }, { text: 'A hand-landed change nobody numbered.' }],
|
|
40
|
+
'1.0.4': [],
|
|
41
|
+
};
|
|
42
|
+
const TASKS = {
|
|
43
|
+
11: { status: 'shipped', value_summary: 'x', at: '2026-09-21T12:00:00Z' },
|
|
44
|
+
12: { status: 'shipped', value_summary: 'x', at: '2026-09-22T12:00:00Z' },
|
|
45
|
+
13: { status: 'shipped', value_summary: 'x', at: '2026-09-23T12:00:00Z' },
|
|
46
|
+
14: { status: 'shipped', value_summary: 'A hand-landed change nobody numbered.', at: '2026-09-23T13:00:00Z' },
|
|
47
|
+
15: { status: 'shipped', value_summary: 'Merged after the last version.', at: '2026-09-25T12:00:00Z' },
|
|
48
|
+
16: { status: 'shipped', value_summary: 'A scope edit.', at: '2026-09-20T12:00:00Z' },
|
|
49
|
+
20: { status: 'confirmed', value_summary: 'Stranded.', at: '2026-09-25T12:00:00Z' },
|
|
50
|
+
21: { status: 'completed', value_summary: null, at: '2026-09-25T13:00:00Z' },
|
|
51
|
+
30: { status: 'active', value_summary: null, at: '2026-09-25T13:00:00Z' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function fakePool() {
|
|
55
|
+
const queries = [];
|
|
56
|
+
return {
|
|
57
|
+
queries,
|
|
58
|
+
async query(sql, params) {
|
|
59
|
+
queries.push({ sql, params });
|
|
60
|
+
if (/FROM tasks WHERE id = \$1::bigint/.test(sql)) {
|
|
61
|
+
const t = TASKS[params[0]];
|
|
62
|
+
return { rows: t ? [{ id: params[0], ...t }] : [] };
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`unexpected query: ${sql}`);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const freshStore = () => ({ packument: { at: 0, value: null, inflight: null }, notes: { version: null, at: 0, value: null, inflight: null } });
|
|
69
|
+
|
|
70
|
+
function setup({ latest = '1.0.1', pkStatus = 200 } = {}) {
|
|
71
|
+
const versions = Object.keys(TIMES);
|
|
72
|
+
const newest = versions[versions.length - 1];
|
|
73
|
+
const fg = fakeHttpsGet({
|
|
74
|
+
[`${ORIGIN}/@bongos%2fcore`]: { status: pkStatus, body: pkStatus === 200 ? npmPackument({ versions, latest, times: TIMES }) : 'nope' },
|
|
75
|
+
[`${ORIGIN}/@bongos/core/-/core-${newest}.tgz`]: { body: npmTarball({ notes: NOTES }) },
|
|
76
|
+
});
|
|
77
|
+
const pool = fakePool();
|
|
78
|
+
const store = freshStore();
|
|
79
|
+
const read = (id, over = {}) => readTaskWhere({ pool, id, pkg: PKG, live: '1.0.2', get: fg.get, store, installedNotes: () => null, now: () => Date.parse('2026-09-26T13:00:00Z'), ...over });
|
|
80
|
+
return { read, pool, asked: fg.asked };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- the reading ---------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
test('each shipped task gets the deploy page\'s own stage and the version that carries it', async () => {
|
|
86
|
+
const { read } = setup();
|
|
87
|
+
const pick = async (id) => { const w = await read(id); return [w.stage, w.version]; };
|
|
88
|
+
assert.deepEqual(await pick(11), ['released', '1.0.1']);
|
|
89
|
+
assert.deepEqual(await pick(12), ['live_unreleased', '1.0.2']);
|
|
90
|
+
assert.deepEqual(await pick(13), ['not_live', '1.0.3']);
|
|
91
|
+
assert.deepEqual(await pick(14), ['not_live', '1.0.3'], 'a hand-landed merge is matched on the ship summary');
|
|
92
|
+
assert.deepEqual(await pick(15), ['unversioned', null], 'merged after the newest version went out');
|
|
93
|
+
assert.deepEqual(await pick(16), ['no_version', null]);
|
|
94
|
+
const w = await read(12);
|
|
95
|
+
assert.equal(w.live, '1.0.2');
|
|
96
|
+
assert.equal(w.released, '1.0.1');
|
|
97
|
+
assert.equal(w.partial, false);
|
|
98
|
+
assert.equal(w.task_id, '12');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a task marked done whose code never merged says so — the case this line exists for', async () => {
|
|
102
|
+
const { read } = setup();
|
|
103
|
+
for (const id of [20, 21]) {
|
|
104
|
+
const w = await read(id);
|
|
105
|
+
assert.equal(w.stage, 'not_merged', String(id));
|
|
106
|
+
assert.equal(w.version, null);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('a task not yet done has nothing to place, and costs no registry read', async () => {
|
|
111
|
+
const { read, asked } = setup();
|
|
112
|
+
const w = await read(30);
|
|
113
|
+
assert.deepEqual(w, { task_id: '30', package: PKG, status: 'active', stage: null });
|
|
114
|
+
assert.deepEqual(asked, [], 'no packument, no tarball');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('an id that is not a task answers null; one that is not a number is never queried', async () => {
|
|
118
|
+
const { read, pool } = setup();
|
|
119
|
+
assert.equal(await read(999), null);
|
|
120
|
+
for (const bad of ['1; DROP', '', null, '12a', '1234567890123']) assert.equal(await read(bad), null, String(bad));
|
|
121
|
+
assert.equal(pool.queries.length, 1, 'only the real id reached the database');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('a registry that does not answer is a partial reading, said so', async () => {
|
|
125
|
+
const { read } = setup({ pkStatus: 500 });
|
|
126
|
+
const w = await read(12);
|
|
127
|
+
assert.equal(w.partial, true);
|
|
128
|
+
assert.equal(w.stage, 'no_version', 'with no notes nothing can be placed in a version');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('the offered flag follows the deploy page: published before this hall ran it, and already released', async () => {
|
|
132
|
+
const { read } = setup({ latest: '1.0.4' });
|
|
133
|
+
const w = await read(13);
|
|
134
|
+
assert.equal(w.stage, 'not_live');
|
|
135
|
+
assert.equal(w.offered, true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ---- the route -----------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
function layers(read) {
|
|
141
|
+
const router = buildRouter({ read });
|
|
142
|
+
const layer = router.stack.find((l) => l.route && l.route.path === '/npm-release/task/:id');
|
|
143
|
+
return layer.route.stack.map((s) => s.handle);
|
|
144
|
+
}
|
|
145
|
+
function call(handler, id) {
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const res = { code: 200, status(c) { this.code = c; return this; }, json(v) { resolve({ status: this.code, body: v }); } };
|
|
148
|
+
Promise.resolve(handler({ params: { id } }, res)).catch((e) => resolve({ status: 'threw', body: e }));
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test('the route asks only for a signed-in builder — nothing on the line is not already public', () => {
|
|
153
|
+
const handles = layers(async () => ({}));
|
|
154
|
+
assert.equal(handles.length, 2, 'requireBuilder, then the handler');
|
|
155
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/npm-release/routes/task-where.js'), 'utf8');
|
|
156
|
+
assert.match(src, /'\/npm-release\/task\/:id',\s*\n\s*api\.requireBuilder,\s*\n\s*async/);
|
|
157
|
+
assert.doesNotMatch(src, /requirePermission|api\.requireRank\(/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('the route: 400 for a non-number, 404 for no task, 500 plainly on a failed read', async () => {
|
|
161
|
+
const h = (read) => layers(read)[1];
|
|
162
|
+
assert.deepEqual((await call(h(async () => ({ stage: 'released' })), '12')).body, { stage: 'released' });
|
|
163
|
+
assert.equal((await call(h(async () => { throw new Error('no'); }), 'x')).status, 400);
|
|
164
|
+
assert.equal((await call(h(async () => null), '12')).status, 404);
|
|
165
|
+
const failed = await call(h(async () => { throw new Error('pool gone'); }), '12');
|
|
166
|
+
assert.equal(failed.status, 500);
|
|
167
|
+
assert.equal(failed.body.error.code, 'task_where_failed');
|
|
168
|
+
assert.doesNotMatch(JSON.stringify(failed.body), /pool gone/, 'the internal message stays in the log');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ---- the widget ----------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const WIDGET_SRC = fs.readFileSync(path.join(ROOT, 'modules/npm-release/public/task-widget.js'), 'utf8');
|
|
174
|
+
|
|
175
|
+
function bootWidget(answer, { shown = null } = {}) {
|
|
176
|
+
const slot = { _html: '', set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; } };
|
|
177
|
+
const listeners = {};
|
|
178
|
+
const asked = [];
|
|
179
|
+
const windowObj = {
|
|
180
|
+
OTBTaskShown: shown,
|
|
181
|
+
BongosClient: { createClient: () => ({ request: async (m, url) => { asked.push(url); return answer(url); } }) },
|
|
182
|
+
};
|
|
183
|
+
const documentObj = {
|
|
184
|
+
getElementById: (id) => (id === 'task-contrib' ? slot : null),
|
|
185
|
+
addEventListener: (ev, fn) => { (listeners[ev] ||= []).push(fn); },
|
|
186
|
+
};
|
|
187
|
+
const sandbox = { window: windowObj, document: documentObj, console, Map, Promise, encodeURIComponent, String };
|
|
188
|
+
vm.createContext(sandbox);
|
|
189
|
+
vm.runInContext(WIDGET_SRC, sandbox, { filename: 'task-widget.js' });
|
|
190
|
+
const announce = (detail) => { windowObj.OTBTaskShown = detail; for (const fn of listeners['otb:task-shown'] || []) fn({ detail }); };
|
|
191
|
+
const settle = async () => { for (let i = 0; i < 6; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
192
|
+
return { slot, asked, announce, settle, win: windowObj };
|
|
193
|
+
}
|
|
194
|
+
const ok = (data) => ({ status: 200, ok: true, data });
|
|
195
|
+
|
|
196
|
+
test('each stage reads as one plain sentence under the rail', async () => {
|
|
197
|
+
const cases = [
|
|
198
|
+
[{ stage: 'released', version: '1.19.1036', live: '1.19.1040' }, /In <b>1\.19\.1036<\/b> · running here · released to other projects\./],
|
|
199
|
+
[{ stage: 'live_unreleased', version: '1.19.1040', live: '1.19.1040' }, /In <b>1\.19\.1040<\/b> · running here · not released to other projects yet\./],
|
|
200
|
+
[{ stage: 'not_live', version: '1.19.1041' }, /In <b>1\.19\.1041<\/b> · published, not running here yet\./],
|
|
201
|
+
[{ stage: 'not_live', version: '1.19.1041', offered: true }, /not running here yet · already offered to other projects\./],
|
|
202
|
+
[{ stage: 'unversioned' }, /Merged, waiting for the next published version\./],
|
|
203
|
+
[{ stage: 'no_version' }, /Shipped, though no published version lists it — usually a change that needed no code/],
|
|
204
|
+
[{ stage: 'not_merged' }, /<b>Done, but not merged\.<\/b> The ledger marks this task done, and its code is not on main yet/],
|
|
205
|
+
];
|
|
206
|
+
for (const [w, re] of cases) {
|
|
207
|
+
const b = bootWidget(() => ok(w));
|
|
208
|
+
b.announce({ id: '12', status: w.stage === 'not_merged' ? 'confirmed' : 'shipped' });
|
|
209
|
+
await b.settle();
|
|
210
|
+
assert.match(b.slot.innerHTML, re, w.stage);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('a partial reading is said; a version is escaped; an unknown stage draws nothing', async () => {
|
|
215
|
+
const partial = bootWidget(() => ok({ stage: 'no_version', partial: true }));
|
|
216
|
+
partial.announce({ id: '12', status: 'shipped' });
|
|
217
|
+
await partial.settle();
|
|
218
|
+
assert.match(partial.slot.innerHTML, /\(a partial reading: the registry or the release notes could not be read\)/);
|
|
219
|
+
|
|
220
|
+
const evil = bootWidget(() => ok({ stage: 'released', version: '<img src=x>' }));
|
|
221
|
+
evil.announce({ id: '12', status: 'shipped' });
|
|
222
|
+
await evil.settle();
|
|
223
|
+
assert.match(evil.slot.innerHTML, /In <b><img src=x><\/b>/);
|
|
224
|
+
|
|
225
|
+
const odd = bootWidget(() => ok({ stage: 'somewhere-else' }));
|
|
226
|
+
odd.announce({ id: '12', status: 'shipped' });
|
|
227
|
+
await odd.settle();
|
|
228
|
+
assert.equal(odd.slot.innerHTML, '');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('work not yet done asks nothing; a failure draws nothing rather than something wrong', async () => {
|
|
232
|
+
const idle = bootWidget(() => ok({ stage: 'released' }));
|
|
233
|
+
idle.announce({ id: '12', status: 'active' });
|
|
234
|
+
await idle.settle();
|
|
235
|
+
assert.deepEqual(idle.asked, []);
|
|
236
|
+
assert.equal(idle.slot.innerHTML, '');
|
|
237
|
+
|
|
238
|
+
for (const answer of [() => ({ status: 403, ok: false, data: null }), () => ({ status: 404, ok: false, data: null }), () => { throw new Error('offline'); }]) {
|
|
239
|
+
const b = bootWidget(answer);
|
|
240
|
+
b.announce({ id: '12', status: 'shipped' });
|
|
241
|
+
await b.settle();
|
|
242
|
+
assert.equal(b.slot.innerHTML, '');
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('one read per task, however often the page re-renders — and never for a task no longer shown', async () => {
|
|
247
|
+
const b = bootWidget(() => ok({ stage: 'released', version: '1.0.1' }));
|
|
248
|
+
b.announce({ id: '12', status: 'shipped' });
|
|
249
|
+
b.announce({ id: '12', status: 'shipped' });
|
|
250
|
+
await b.settle();
|
|
251
|
+
assert.deepEqual(b.asked, ['/api/bongos/npm-release/task/12']);
|
|
252
|
+
|
|
253
|
+
// The page moved on to another task while the first read was in flight.
|
|
254
|
+
let release;
|
|
255
|
+
const slow = bootWidget(() => new Promise((r) => { release = () => r(ok({ stage: 'released', version: '9.9.9' })); }));
|
|
256
|
+
slow.announce({ id: '12', status: 'shipped' });
|
|
257
|
+
slow.win.OTBTaskShown = { id: '13', status: 'active' };
|
|
258
|
+
release();
|
|
259
|
+
await slow.settle();
|
|
260
|
+
assert.equal(slow.slot.innerHTML, '', 'task 12\'s line is not written onto task 13\'s page');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('a widget that loads after the first announcement still draws the line', async () => {
|
|
264
|
+
const b = bootWidget(() => ok({ stage: 'released', version: '1.0.1' }), { shown: { id: '12', status: 'shipped' } });
|
|
265
|
+
await b.settle();
|
|
266
|
+
assert.match(b.slot.innerHTML, /In <b>1\.0\.1<\/b>/);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ---- the seam that puts it on the page -----------------------------------------------
|
|
270
|
+
|
|
271
|
+
test('the module declares the task page section and ships its widget; the seam guard counts it wired', () => {
|
|
272
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/npm-release/module.json'), 'utf8'));
|
|
273
|
+
assert.deepEqual(manifest.contributes.uiSections, ['task-where']);
|
|
274
|
+
assert.ok(manifest.contributes.routes.includes('task-where'));
|
|
275
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'modules/npm-release/public/task-widget.js')));
|
|
276
|
+
const guard = fs.readFileSync(path.join(ROOT, 'scripts/gds/seam-wiring-guard.js'), 'utf8');
|
|
277
|
+
assert.match(guard, /alternates: \(dir\) => \[path\.join\(dir, 'public', 'task-widget\.js'\)\]/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the server injects task widgets just before task.js, and names no module doing it', () => {
|
|
281
|
+
const { injectModuleHallWidgets } = require('../src/bongos/serve-internal.js');
|
|
282
|
+
const page = '<body><script src="/builders/md-reader.js" defer></script><script src="/builders/task.js?v=abc" defer></script></body>';
|
|
283
|
+
const out = injectModuleHallWidgets(page, [{ key: 'npm-release', src: '/modules/npm-release/public/task-widget.js' }], 'task');
|
|
284
|
+
assert.ok(out.indexOf('/modules/npm-release/public/task-widget.js') < out.indexOf('/builders/task.js'), 'before the consumer, so its listener exists first');
|
|
285
|
+
assert.ok(out.indexOf('/builders/md-reader.js') < out.indexOf('task-widget.js'));
|
|
286
|
+
assert.match(out, /<script src="\/modules\/npm-release\/public\/task-widget\.js" defer><\/script>/);
|
|
287
|
+
assert.equal(injectModuleHallWidgets(page, [], 'task'), page, 'no module, no change');
|
|
288
|
+
const index = '<script src="/builders/builders.js?v=1"></script>';
|
|
289
|
+
assert.match(injectModuleHallWidgets(index, [{ key: 'x', src: '/m.js' }]), /^<script src="\/m\.js" defer><\/script><script src="\/builders\/builders\.js/, 'the index injection is unchanged');
|
|
290
|
+
const si = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8');
|
|
291
|
+
assert.match(si, /hallWidgetScripts\(\{ isEnabled: isModuleEnabled, file: 'task-widget\.js' \}\),\s*\n\s*'task',/);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// ---- the fixes the first grade asked for ---------------------------------------------
|
|
295
|
+
|
|
296
|
+
test('the installed release notes are read from disk once per process, not once per view', async () => {
|
|
297
|
+
const realRead = fs.readFileSync;
|
|
298
|
+
let reads = 0;
|
|
299
|
+
fs.readFileSync = function (p, ...rest) {
|
|
300
|
+
if (String(p).endsWith('release-notes.json')) reads++;
|
|
301
|
+
return realRead.call(this, p, ...rest);
|
|
302
|
+
};
|
|
303
|
+
try {
|
|
304
|
+
const { pool } = setup();
|
|
305
|
+
const fg = fakeHttpsGet({ [`${ORIGIN}/@bongos%2fcore`]: { status: 500, body: 'down' } });
|
|
306
|
+
for (let i = 0; i < 3; i++) {
|
|
307
|
+
// No installedNotes: the module's own default, which is the path every task view takes.
|
|
308
|
+
await readTaskWhere({ pool, id: 12, pkg: PKG, live: '1.0.2', get: fg.get, store: freshStore() });
|
|
309
|
+
}
|
|
310
|
+
} finally { fs.readFileSync = realRead; }
|
|
311
|
+
assert.equal(reads, 1, 'three task views, one disk read');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test('the widget keeps only the last few readings', async () => {
|
|
315
|
+
const b = bootWidget((url) => ok({ stage: 'released', version: url.split('/').pop() }));
|
|
316
|
+
for (let i = 1; i <= 12; i++) b.announce({ id: String(i), status: 'shipped' });
|
|
317
|
+
await b.settle();
|
|
318
|
+
b.announce({ id: '12', status: 'shipped' });
|
|
319
|
+
b.announce({ id: '1', status: 'shipped' });
|
|
320
|
+
await b.settle();
|
|
321
|
+
assert.equal(b.asked.filter((u) => u.endsWith('/12')).length, 1, 'a recent task is served from the cache');
|
|
322
|
+
assert.equal(b.asked.filter((u) => u.endsWith('/1')).length, 2, 'an old one was evicted and is read again');
|
|
323
|
+
});
|