@bongos/core 1.19.1040 → 1.19.1042

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.
Files changed (75) hide show
  1. package/.bongos-core.json +110 -80
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +6 -1
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +14 -0
  8. package/docs/adr/0339-a-projects-owner-upgrades-it-and-a-hosted-project-hands-off-to-the-hub-door.md +1 -0
  9. package/docs/adr/0340-every-hall-takes-its-update-from-settings-and-deploy-is-the-platform-halls-page.md +64 -0
  10. package/docs/adr/README.md +1 -0
  11. package/docs/api/openapi.json +64 -7
  12. package/docs/api-reference.md +7 -1
  13. package/docs/copy-inventory.md +41 -21
  14. package/docs/copy-registry.json +207 -27
  15. package/docs/file-map.md +1 -1
  16. package/docs/module-api-changelog.md +12 -0
  17. package/modules/grading/grader-subagent.js +7 -0
  18. package/modules/hall-ui/public/atlas.html +1 -1
  19. package/modules/hall-ui/public/blockers.html +1 -1
  20. package/modules/hall-ui/public/board-room.html +1 -1
  21. package/modules/hall-ui/public/collab.html +1 -1
  22. package/modules/hall-ui/public/copy-desk.html +1 -1
  23. package/modules/hall-ui/public/deploy.html +1 -1
  24. package/modules/hall-ui/public/diagrams.html +1 -1
  25. package/modules/hall-ui/public/drachmae.html +1 -1
  26. package/modules/hall-ui/public/fleet.html +1 -1
  27. package/modules/hall-ui/public/gate.html +1 -1
  28. package/modules/hall-ui/public/goals.html +1 -1
  29. package/modules/hall-ui/public/government.html +1 -1
  30. package/modules/hall-ui/public/idea.html +1 -1
  31. package/modules/hall-ui/public/ideas.html +1 -1
  32. package/modules/hall-ui/public/index.html +1 -1
  33. package/modules/hall-ui/public/modules.html +1 -1
  34. package/modules/hall-ui/public/palette.js +1 -0
  35. package/modules/hall-ui/public/people.html +1 -1
  36. package/modules/hall-ui/public/primer.html +1 -1
  37. package/modules/hall-ui/public/profile.html +1 -1
  38. package/modules/hall-ui/public/project-settings.html +1 -1
  39. package/modules/hall-ui/public/ranks.html +1 -1
  40. package/modules/hall-ui/public/roadmap.html +1 -1
  41. package/modules/hall-ui/public/roster.html +1 -1
  42. package/modules/hall-ui/public/sessions.html +1 -1
  43. package/modules/hall-ui/public/settings-software-update.js +194 -0
  44. package/modules/hall-ui/public/settings.css +39 -0
  45. package/modules/hall-ui/public/settings.html +24 -2
  46. package/modules/hall-ui/public/settings.js +5 -0
  47. package/modules/hall-ui/public/settings.states.json +2 -1
  48. package/modules/hall-ui/public/shell.js +25 -39
  49. package/modules/hall-ui/public/studio.html +1 -1
  50. package/modules/hall-ui/public/task.html +1 -1
  51. package/modules/hall-ui/public/watch.html +1 -1
  52. package/modules/hall-ui/public/work.html +1 -1
  53. package/modules/hall-ui/records/panel-family.md +9 -0
  54. package/modules/npm-release/public/work.js +8 -3
  55. package/modules/npm-release/work.js +8 -2
  56. package/package-lock.json +2 -2
  57. package/package.json +1 -1
  58. package/release-notes.json +16 -0
  59. package/scripts/gds/fitness.js +7 -0
  60. package/scripts/gds/main-audit.js +204 -7
  61. package/{modules/npm-release/registry.js → src/bongos/package-registry.js} +14 -7
  62. package/src/bongos/routes/core-update.js +65 -3
  63. package/src/bongos/serve-internal.js +17 -0
  64. package/src/bongos/software-update.js +127 -0
  65. package/src/module-api.js +10 -1
  66. package/tests/core_upgrade_owner_door.mjs +27 -18
  67. package/tests/grade_model_provider.mjs +1 -1
  68. package/tests/grader_reviewer_isolation.mjs +68 -0
  69. package/tests/helpers.mjs +92 -0
  70. package/tests/main_audit_attestation.mjs +238 -0
  71. package/tests/module_api.mjs +1 -0
  72. package/tests/nav_permission_atoms.mjs +1 -1
  73. package/tests/npm_release_page.mjs +11 -1
  74. package/tests/npm_release_work.mjs +9 -94
  75. package/tests/software_update.mjs +449 -0
@@ -0,0 +1,238 @@
1
+ // tests/main_audit_attestation.mjs — the main audit's attestation pass (task 1001433).
2
+ //
3
+ // The pass ties every permission-sensitive commit on main to the pull request that
4
+ // brought it in, and requires that PR's head to carry the server's `gate-author-trust`
5
+ // status, posted by the server's own GitHub identity. The author NAME the older pass
6
+ // reads is typed text; the status creator is not. FLAG-ONLY by the owner's ruling
7
+ // (2026-09-26): findings are 'review', never 'blocker'.
8
+ //
9
+ // Everything here runs on injected git + fetch seams — no repository, no network.
10
+ //
11
+ // Run: node --test tests/main_audit_attestation.mjs
12
+ import { test } from 'node:test';
13
+ import assert from 'node:assert/strict';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const audit = require('../scripts/gds/main-audit.js');
18
+ const {
19
+ ATTEST_CONTEXT, mapIntroducingHeads, isAttested, analyzeAttestation, attestationConfig,
20
+ fetchAttestations, runAttestationPass,
21
+ } = audit;
22
+
23
+ const sha = (c) => c.repeat(40);
24
+ const PROTECTED = 'src/bongos/auth.js'; // floored in the real registry
25
+ const PLAIN = 'docs/notes.md'; // not a protected surface
26
+ const status = (login, state = 'success', context = ATTEST_CONTEXT) => ({ context, state, creator: { login } });
27
+
28
+ // A tiny history. Mainline: R (release) ← M (merges PR head H over base B).
29
+ // The PR carried two commits: A (protected) then H (the head, plain).
30
+ const B = sha('b'); const A = sha('a'); const H = sha('h'); const M = sha('m'); const D = sha('d');
31
+ const commitA = { sha: A, authorName: 'somemetic', parents: [B], files: [PROTECTED] };
32
+ const commitH = { sha: H, authorName: 'somemetic', parents: [A], files: [PLAIN] };
33
+ const mergeM = { sha: M, authorName: 'Lars', parents: [B, H], isMerge: true, files: [] };
34
+ // D: a protected commit pushed straight onto main (no merge brought it in).
35
+ const directD = { sha: D, authorName: 'IAMMASTERQUA', parents: [M], files: [PROTECTED] };
36
+ const revRanges = { [`${B}..${H}`]: [H, A] };
37
+ const revList = (r) => revRanges[r] || [];
38
+
39
+ test('the real registry still floors the fixture paths the way these tests assume', () => {
40
+ const ppc = require('../src/bongos/permission-path-check.js');
41
+ assert.ok(ppc.matchProtected([PROTECTED]).length > 0, `${PROTECTED} must be protected`);
42
+ assert.equal(ppc.matchProtected([PLAIN]).length, 0, `${PLAIN} must not be protected`);
43
+ });
44
+
45
+ // ---- mapIntroducingHeads ----
46
+
47
+ test('a mainline merge vouches for every commit its PR brought in, via its SECOND parent', () => {
48
+ const headOf = mapIntroducingHeads([mergeM], revList);
49
+ assert.equal(headOf.get(A), H);
50
+ assert.equal(headOf.get(H), H, 'a PR head vouches for itself');
51
+ assert.equal(headOf.has(D), false, 'a direct push has no introducing merge');
52
+ });
53
+
54
+ test('a commit keeps the FIRST merge that claims it; single-parent commits are ignored', () => {
55
+ const other = { sha: sha('n'), parents: [sha('x'), sha('y')] };
56
+ const headOf = mapIntroducingHeads([mergeM, other, commitA], (r) => (r === `${sha('x')}..${sha('y')}` ? [A] : revList(r)));
57
+ assert.equal(headOf.get(A), H);
58
+ });
59
+
60
+ // ---- isAttested: the creator is the part that cannot be faked ----
61
+
62
+ test('a success from the trusted account attests; the login match ignores case', () => {
63
+ assert.equal(isAttested([status('owner-acct')], 'Owner-Acct'), true);
64
+ });
65
+
66
+ test('FORGERY: the same context posted by any other account attests nothing', () => {
67
+ assert.equal(isAttested([status('IAMMASTERQUA')], 'owner-acct'), false);
68
+ assert.equal(isAttested([status('github-actions[bot]')], 'owner-acct'), false);
69
+ });
70
+
71
+ test("only the trusted account's NEWEST status for the context decides", () => {
72
+ // newest first, as GitHub returns them
73
+ assert.equal(isAttested([status('owner-acct', 'failure'), status('owner-acct', 'success')], 'owner-acct'), false, 'a later failure withdraws');
74
+ assert.equal(isAttested([status('owner-acct', 'success'), status('owner-acct', 'failure')], 'owner-acct'), true, 'a later success restores');
75
+ // another account can neither attest nor revoke
76
+ assert.equal(isAttested([status('mallory', 'failure'), status('owner-acct', 'success')], 'owner-acct'), true);
77
+ });
78
+
79
+ test('another context, a pending state, or no trusted login attests nothing', () => {
80
+ assert.equal(isAttested([status('owner-acct', 'success', 'unit-report')], 'owner-acct'), false);
81
+ assert.equal(isAttested([status('owner-acct', 'pending')], 'owner-acct'), false);
82
+ assert.equal(isAttested([status('owner-acct')], ''), false);
83
+ assert.equal(isAttested(null, 'owner-acct'), false);
84
+ });
85
+
86
+ // ---- analyzeAttestation ----
87
+
88
+ test('an attested PR head clears every protected commit it brought in', () => {
89
+ const headOf = mapIntroducingHeads([mergeM], revList);
90
+ const r = analyzeAttestation([commitA, commitH], { headOf, attested: new Set([H]), trustedLogin: 'owner-acct' });
91
+ assert.equal(r.checked, 1, 'only the protected commit is checked');
92
+ assert.deepEqual(r.findings, []);
93
+ });
94
+
95
+ test('an unattested PR head is a REVIEW finding naming the head — never a blocker', () => {
96
+ const headOf = mapIntroducingHeads([mergeM], revList);
97
+ const r = analyzeAttestation([commitA], { headOf, attested: new Set(), trustedLogin: 'owner-acct' });
98
+ assert.equal(r.findings.length, 1);
99
+ const f = r.findings[0];
100
+ assert.equal(f.severity, 'review', 'flag-only: the owner ruled this pass must not gate');
101
+ assert.equal(f.kind, 'unattested');
102
+ assert.equal(f.head, H);
103
+ assert.match(f.summary, /pull request that brought it in \(head hhhhhhhhh\)/);
104
+ assert.match(f.summary, /gate-author-trust' status from 'owner-acct'/);
105
+ });
106
+
107
+ test('a direct push to main is flagged as having no pull request at all', () => {
108
+ const r = analyzeAttestation([directD], { headOf: new Map(), attested: new Set(), trustedLogin: 'owner-acct' });
109
+ assert.equal(r.findings.length, 1);
110
+ assert.equal(r.findings[0].severity, 'review');
111
+ assert.equal(r.findings[0].head, D);
112
+ assert.match(r.findings[0].summary, /without passing through a pull request/);
113
+ });
114
+
115
+ test('a ONE-commit pull request (head === the commit) still reads as a pull request', () => {
116
+ const lone = { sha: H, authorName: 'somemetic', parents: [B], files: [PROTECTED] };
117
+ const r = analyzeAttestation([lone], { headOf: new Map([[H, H]]), attested: new Set(), trustedLogin: 'owner-acct' });
118
+ assert.match(r.findings[0].summary, /pull request that brought it in/);
119
+ assert.doesNotMatch(r.findings[0].summary, /without passing through a pull request/);
120
+ });
121
+
122
+ test('a direct push that carries the attestation ITSELF is cleared', () => {
123
+ const r = analyzeAttestation([directD], { headOf: new Map(), attested: new Set([D]), trustedLogin: 'owner-acct' });
124
+ assert.deepEqual(r.findings, []);
125
+ });
126
+
127
+ // ---- attestationConfig ----
128
+
129
+ test('config: Actions env first, GH_TOKEN as the laptop fallback, owner as the default login', () => {
130
+ assert.deepEqual(attestationConfig({ env: { GITHUB_TOKEN: 't1', GITHUB_REPOSITORY: 'owner-acct/core' } }),
131
+ { token: 't1', repo: 'owner-acct/core', trustedLogin: 'owner-acct' });
132
+ assert.equal(attestationConfig({ env: { GH_TOKEN: 't2' } }).token, 't2');
133
+ assert.equal(attestationConfig({ env: {} }).token, null);
134
+ });
135
+
136
+ test('config: the repo is read from the origin remote without ever capturing a credential in it', () => {
137
+ const https = attestationConfig({ env: {}, remoteUrl: 'https://x-access-token:SECRET@github.com/owner/repo.git' });
138
+ assert.equal(https.repo, 'owner/repo');
139
+ assert.ok(!JSON.stringify(https).includes('SECRET'));
140
+ assert.equal(attestationConfig({ env: {}, remoteUrl: 'git@github.com:owner/repo.git' }).repo, 'owner/repo');
141
+ assert.equal(attestationConfig({ env: {}, remoteUrl: 'https://example.com/x/y.git' }).repo, null);
142
+ });
143
+
144
+ test('config: --attest-login overrides the owner default (an App-posting instance)', () => {
145
+ const c = attestationConfig({ env: { GITHUB_REPOSITORY: 'o/r' }, loginArg: 'bongos-app[bot]' });
146
+ assert.equal(c.trustedLogin, 'bongos-app[bot]');
147
+ });
148
+
149
+ // ---- fetchAttestations ----
150
+
151
+ function stubFetch(bySha) {
152
+ const calls = [];
153
+ const impl = async (url, init) => {
154
+ calls.push({ url, init });
155
+ const s = Object.keys(bySha).find((k) => url.includes(`/commits/${k}/`));
156
+ const r = bySha[s];
157
+ if (r instanceof Error) throw r;
158
+ if (typeof r === 'number') return { ok: false, status: r, json: async () => ({}) };
159
+ return { ok: true, status: 200, json: async () => r };
160
+ };
161
+ return { impl, calls };
162
+ }
163
+
164
+ test('fetch: reads each head once, keeps only the trusted attestations, sends the token', async () => {
165
+ const { impl, calls } = stubFetch({ [H]: [status('owner-acct')], [D]: [status('mallory')] });
166
+ const r = await fetchAttestations([H, D, H], { repo: 'o/r', token: 'tok', trustedLogin: 'owner-acct', fetchImpl: impl });
167
+ assert.deepEqual([...r.attested], [H]);
168
+ assert.deepEqual(r.unreadable, []);
169
+ assert.equal(calls.length, 2, 'a head shared by several commits is read once');
170
+ assert.equal(calls[0].url, `https://api.github.com/repos/o/r/commits/${H}/statuses?per_page=100`);
171
+ assert.equal(calls[0].init.headers.Authorization, 'Bearer tok');
172
+ });
173
+
174
+ test('fetch: an unreadable head is reported, never counted as attested or unattested', async () => {
175
+ const { impl } = stubFetch({ [H]: 404, [D]: new Error('network down') });
176
+ const r = await fetchAttestations([H, D], { repo: 'o/r', token: 'tok', trustedLogin: 'owner-acct', fetchImpl: impl });
177
+ assert.equal(r.attested.size, 0);
178
+ assert.deepEqual(r.unreadable.map((u) => u.reason), ['HTTP 404', 'network down']);
179
+ });
180
+
181
+ // ---- runAttestationPass: the whole pass on fake git + fetch ----
182
+
183
+ function fakeGit({ mainline, ranges, remote = 'https://github.com/o/r.git' }) {
184
+ return (args) => {
185
+ if (args === 'remote get-url origin') return `${remote}\n`;
186
+ if (args.startsWith('rev-list --first-parent ')) return mainline.join('\n');
187
+ if (args.startsWith('rev-list ')) return (ranges[args.slice('rev-list '.length)] || []).join('\n');
188
+ throw new Error(`unexpected git ${args}`);
189
+ };
190
+ }
191
+ const history = [directD, mergeM, commitH, commitA];
192
+ const gitImpl = fakeGit({ mainline: [D, M], ranges: revRanges });
193
+
194
+ test('pass: nothing protected in range → it ran, checked nothing, needed no token', async () => {
195
+ const r = await runAttestationPass([commitH], 'x..y', { env: {}, gitImpl: () => { throw new Error('git must not be called'); } });
196
+ assert.deepEqual(r, { ran: true, checked: 0, findings: [] });
197
+ });
198
+
199
+ test('pass: no token → DID NOT RUN with the reason, never a clean pass', async () => {
200
+ const r = await runAttestationPass(history, 'x..y', { env: {}, gitImpl });
201
+ assert.equal(r.ran, false);
202
+ assert.match(r.reason, /no GitHub token/);
203
+ });
204
+
205
+ test('pass: end to end — the attested PR clears, the direct push is flagged', async () => {
206
+ const { impl } = stubFetch({ [H]: [status('o')], [D]: [] });
207
+ const r = await runAttestationPass(history, 'x..y', { env: { GH_TOKEN: 'tok' }, gitImpl, fetchImpl: impl });
208
+ assert.equal(r.ran, true);
209
+ assert.equal(r.checked, 2);
210
+ assert.deepEqual(r.findings.map((f) => f.sha), [D]);
211
+ assert.equal(r.trustedLogin, 'o', 'the owner of the origin remote');
212
+ });
213
+
214
+ test('pass: a FORGED mark on the PR head (right name, wrong account) is flagged', async () => {
215
+ const { impl } = stubFetch({ [H]: [status('IAMMASTERQUA')], [D]: [status('o')] });
216
+ const r = await runAttestationPass(history, 'x..y', { env: { GH_TOKEN: 'tok' }, gitImpl, fetchImpl: impl });
217
+ assert.deepEqual(r.findings.map((f) => f.sha), [A]);
218
+ assert.ok(r.findings.every((f) => f.severity === 'review'));
219
+ });
220
+
221
+ test('pass: a head it could not read makes the whole pass DID NOT RUN', async () => {
222
+ const { impl } = stubFetch({ [H]: 502, [D]: [status('o')] });
223
+ const r = await runAttestationPass(history, 'x..y', { env: { GH_TOKEN: 'tok' }, gitImpl, fetchImpl: impl });
224
+ assert.equal(r.ran, false);
225
+ assert.match(r.reason, /could not read the commit statuses of 1 pull-request head/);
226
+ });
227
+
228
+ test('pass: a merge INSIDE a pull request never vouches — only mainline merges do', async () => {
229
+ // I: a branch's "merge main into me" whose second parent is a main commit P.
230
+ const P = sha('p'); const I = sha('i');
231
+ const inner = { sha: I, authorName: 'x', parents: [A, P], isMerge: true, files: [] };
232
+ const mainP = { sha: P, authorName: 'x', parents: [B], files: [PROTECTED] };
233
+ const g = fakeGit({ mainline: [P], ranges: { [`${A}..${P}`]: [P] } });
234
+ const { impl } = stubFetch({ [P]: [] });
235
+ const r = await runAttestationPass([inner, mainP], 'x..y', { env: { GH_TOKEN: 'tok' }, gitImpl: g, fetchImpl: impl });
236
+ assert.equal(r.findings.length, 1);
237
+ assert.equal(r.findings[0].head, P, 'P answers for itself; the inner merge did not claim it');
238
+ });
@@ -56,6 +56,7 @@ const PUBLISHED_SURFACE = [
56
56
  'buildInfo', 'validateOrRespond', 'LIMITS', 'parseId', 'asyncHandler', 'corsPublicGet', // parseId added 1.8.0 (BV1.R78); asyncHandler + corsPublicGet added 1.12.0 (BV1.R86; lifecycle routes)
57
57
  'parsePagination', 'pageMeta', 'PAGINATION', // R14 (#2001, 1.17.0): shared pagination contract for module list routes (ADR 0119)
58
58
  'responsibilityFor', 'ROLE_RESPONSIBILITIES', // added by task 1003732 (ADR 0275; criterion wa6-written-responsibilities): the ONE source for the sentence each craft is answerable for. It rides the doorway because two modules need the same text — grading judges role-shaped work against it, the hall shows it to the person being judged — and modules never import each other. Declared here deliberately: this list is what makes adding to the doorway a decision rather than a side effect
59
+ 'readPackageRegistry', // added by task 1004296 (ADR 0340 D5): the npm registry reader moved into the core, because Settings → Software update needs it in every hall; the module that publishes a package reads the same answer and cache through here rather than keeping a second copy
59
60
  'logger',
60
61
  ];
61
62
 
@@ -87,7 +87,7 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
87
87
  // the file and left the query alone, so a returning browser kept the cached copy —
88
88
  // a fix that reaches nobody looks identical to a fix that does not work.
89
89
 
90
- const SHELL_SHA = '983282a83fd5aa71';
90
+ const SHELL_SHA = 'ff2017aa9bbbc507';
91
91
 
92
92
  test('shell.js has not changed without its cache-bust being bumped', () => {
93
93
  const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
@@ -22,6 +22,8 @@ const DEPLOY_SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/deplo
22
22
  const FIXTURE = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/hall-preview/fixtures/npm-release__work.json'), 'utf8'));
23
23
 
24
24
  const escapeHtml = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
25
+ // Voice rule V4: quoted data renders with its dashes as commas (docs/voice.md).
26
+ const unDash = (raw) => String(raw == null ? '' : raw).replace(/\s*[—–]\s*/g, ', ').replace(/,\s*,/g, ',').trim();
25
27
 
26
28
  function makeEl() {
27
29
  return { hidden: true, _html: '', set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; } };
@@ -67,7 +69,7 @@ test('every recent task is named, by title, linked to its page', async () => {
67
69
  // Stages longer than the fold show their first rows; the rest sit behind "Show all".
68
70
  for (const t of rows.slice(0, 8)) {
69
71
  assert.ok(html.includes(`href="/task/${t.id}"`), `${stage}: task ${t.id} is linked`);
70
- assert.ok(html.includes(escapeHtml(t.title)), `${stage}: task ${t.id} is named`);
72
+ assert.ok(html.includes(escapeHtml(unDash(t.title))), `${stage}: task ${t.id} is named`);
71
73
  }
72
74
  }
73
75
  assert.match(html, /Show all 15/);
@@ -171,3 +173,11 @@ test('the deploy page loads the module script only for its own view, and only wi
171
173
  assert.deepEqual(bootDeploy({ modules: undefined }), [], 'no module record at all');
172
174
  assert.deepEqual(bootDeploy({ modules: on, search: '?project=mercury' }), [], 'another project\'s view is not this package\'s');
173
175
  });
176
+
177
+ test('a task title and a note line are quoted ledger data: their dashes render as commas (V4)', async () => {
178
+ const stages = { ...FIXTURE.stages, not_merged: [{ ...FIXTURE.stages.not_merged[0], title: 'Deploy page — where the work is – phase 1' }] };
179
+ const b = bootWork(() => ok({ ...FIXTURE, stages }));
180
+ await b.settle();
181
+ assert.ok(b.work().innerHTML.includes('>Deploy page, where the work is, phase 1</a>'));
182
+ assert.doesNotMatch(b.work().innerHTML, /Deploy page [—–]/);
183
+ });
@@ -15,9 +15,8 @@
15
15
  import assert from 'node:assert/strict';
16
16
  import { test } from 'node:test';
17
17
  import { createRequire } from 'node:module';
18
- import { EventEmitter } from 'node:events';
19
- import { Readable } from 'node:stream';
20
- import zlib from 'node:zlib';
18
+
19
+ import { npmTarball, npmPackument, fakeHttpsGet } from './helpers.mjs';
21
20
 
22
21
  const require = createRequire(import.meta.url);
23
22
  const { readWork } = require('../modules/npm-release/work.js');
@@ -26,90 +25,6 @@ const buildRouter = require('../modules/npm-release/routes/work.js');
26
25
  const ORIGIN = 'https://registry.npmjs.org';
27
26
  const PKG = '@bongos/core';
28
27
 
29
- // ---- a real tarball, built by hand ---------------------------------------------------
30
-
31
- function tarHeader(name, size, type = '0') {
32
- const h = Buffer.alloc(512, 0);
33
- h.write(name.slice(0, 100), 0, 'utf8');
34
- h.write('0000644\0', 100, 'ascii');
35
- h.write('0000000\0', 108, 'ascii');
36
- h.write('0000000\0', 116, 'ascii');
37
- h.write(`${size.toString(8).padStart(11, '0')}\0`, 124, 'ascii');
38
- h.write('00000000000\0', 136, 'ascii');
39
- h.write(' ', 148, 'ascii');
40
- h.write(type, 156, 'ascii');
41
- h.write('ustar\0', 257, 'ascii');
42
- h.write('00', 263, 'ascii');
43
- let sum = 0;
44
- for (const b of h) sum += b;
45
- h.write(`${sum.toString(8).padStart(6, '0')}\0 `, 148, 'ascii');
46
- return h;
47
- }
48
- function tarEntry(name, data, type = '0') {
49
- const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
50
- const pad = Buffer.alloc((512 - (body.length % 512)) % 512, 0);
51
- return Buffer.concat([tarHeader(name, body.length, type), body, pad]);
52
- }
53
- function paxEntry(path) {
54
- // A pax record is "<len> <key>=<value>\n", where <len> counts its own digits too.
55
- const rec = (k, v) => {
56
- const body = ` ${k}=${v}\n`;
57
- let n = body.length + 1;
58
- while (String(n).length + body.length !== n) n = String(n).length + body.length;
59
- return `${n}${body}`;
60
- };
61
- return tarEntry('PaxHeader/x', rec('path', path), 'x');
62
- }
63
- /** A gzipped npm-shaped tarball. `notes` null leaves release-notes.json out. */
64
- function tarball({ notes, padBytes = 40000, pax = false } = {}) {
65
- const parts = [
66
- tarEntry('package/package.json', JSON.stringify({ name: PKG })),
67
- // A large file BEFORE the notes, so the reader has to skip across many chunks.
68
- tarEntry('package/big.js', Buffer.alloc(padBytes, 97)),
69
- ];
70
- if (notes) {
71
- const json = JSON.stringify(notes);
72
- if (pax) parts.push(paxEntry('package/release-notes.json'), tarEntry('package/SHORTNAME', json));
73
- else parts.push(tarEntry('package/release-notes.json', json));
74
- }
75
- parts.push(Buffer.alloc(1024, 0));
76
- return zlib.gzipSync(Buffer.concat(parts));
77
- }
78
-
79
- // ---- a fake registry -----------------------------------------------------------------
80
-
81
- function packument({ versions, latest, times }) {
82
- const doc = { name: PKG, 'dist-tags': { latest }, versions: {}, time: {} };
83
- for (const v of versions) {
84
- doc.versions[v] = { version: v, dist: { tarball: `${ORIGIN}/@bongos/core/-/core-${v}.tgz` } };
85
- doc.time[v] = times[v];
86
- }
87
- return doc;
88
- }
89
-
90
- /** `get` answering from `routes` (url → { status, body }), recording every URL asked for. */
91
- function fakeGet(routes) {
92
- const asked = [];
93
- const get = (url, opts, cb) => {
94
- asked.push(url);
95
- const req = new EventEmitter();
96
- req.destroy = () => {};
97
- setImmediate(() => {
98
- const r = routes[url];
99
- if (!r) { req.emit('error', new Error(`no route for ${url}`)); return; }
100
- const body = Buffer.isBuffer(r.body) ? r.body : Buffer.from(typeof r.body === 'string' ? r.body : JSON.stringify(r.body));
101
- // Small chunks, so a tar header lands across a chunk boundary.
102
- const chunks = [];
103
- for (let i = 0; i < body.length; i += 777) chunks.push(body.subarray(i, i + 777));
104
- const res = Readable.from(chunks);
105
- res.statusCode = r.status || 200;
106
- cb(res);
107
- });
108
- return req;
109
- };
110
- return { get, asked };
111
- }
112
-
113
28
  function fakePool(tasks, titles = {}) {
114
29
  const queries = [];
115
30
  return {
@@ -153,13 +68,13 @@ const TASKS = [
153
68
  row('23', 'shipped', '2026-09-24T08:00:00.000Z', { value_summary: 'A hand-landed change nobody numbered.' }),
154
69
  ];
155
70
 
156
- function setup({ latest = '1.0.5', tgz = tarball({ notes: NOTES }), pkStatus = 200, tgzStatus = 200, installed = INSTALLED_TO_103, tasks = TASKS, titles = {} } = {}) {
71
+ function setup({ latest = '1.0.5', tgz = npmTarball({ notes: NOTES }), pkStatus = 200, tgzStatus = 200, installed = INSTALLED_TO_103, tasks = TASKS, titles = {} } = {}) {
157
72
  const versions = Object.keys(TIMES);
158
73
  const routes = {
159
- [`${ORIGIN}/@bongos%2fcore`]: { status: pkStatus, body: pkStatus === 200 ? packument({ versions, latest, times: TIMES }) : 'nope' },
74
+ [`${ORIGIN}/@bongos%2fcore`]: { status: pkStatus, body: pkStatus === 200 ? npmPackument({ versions, latest, times: TIMES }) : 'nope' },
160
75
  [`${ORIGIN}/@bongos/core/-/core-1.0.5.tgz`]: { status: tgzStatus, body: tgz },
161
76
  };
162
- const fg = fakeGet(routes);
77
+ const fg = fakeHttpsGet(routes);
163
78
  const pool = fakePool(tasks, titles);
164
79
  const store = freshStore();
165
80
  const read = (over = {}) => readWork({ pool, pkg: PKG, live: '1.0.3', get: fg.get, store, installedNotes: installed, now: () => Date.parse('2026-09-26T13:00:00Z'), ...over });
@@ -223,7 +138,7 @@ test('notes past the installed copy are read out of the newest package', async (
223
138
  });
224
139
 
225
140
  test('a pax long-name header is followed to the notes file', async () => {
226
- const d = await setup({ tgz: tarball({ notes: NOTES, pax: true }) }).read();
141
+ const d = await setup({ tgz: npmTarball({ notes: NOTES, pax: true }) }).read();
227
142
  assert.equal(d.notes.covers_to, '1.0.5');
228
143
  assert.deepEqual(ids(d.stages.not_live), ['15']);
229
144
  });
@@ -236,7 +151,7 @@ test('when the installed notes already reach the newest version, nothing is down
236
151
  });
237
152
 
238
153
  test('a package without notes is an error, not an empty history', async () => {
239
- const d = await setup({ tgz: tarball({ notes: null }) }).read();
154
+ const d = await setup({ tgz: npmTarball({ notes: null }) }).read();
240
155
  assert.match(d.notes.error, /without release notes/);
241
156
  // The installed copy stands in: the work up to 1.0.3 is still placed.
242
157
  assert.deepEqual(d.notes.source, { from: 'installed', version: '1.0.3' });
@@ -262,9 +177,9 @@ test('a registry that does not answer is reported, and the installed notes still
262
177
 
263
178
  test('the package download is only ever followed on the registry\'s own origin', async () => {
264
179
  const versions = Object.keys(TIMES);
265
- const doc = packument({ versions, latest: '1.0.5', times: TIMES });
180
+ const doc = npmPackument({ versions, latest: '1.0.5', times: TIMES });
266
181
  doc.versions['1.0.5'].dist.tarball = 'https://evil.example/core.tgz';
267
- const fg = fakeGet({ [`${ORIGIN}/@bongos%2fcore`]: { body: doc } });
182
+ const fg = fakeHttpsGet({ [`${ORIGIN}/@bongos%2fcore`]: { body: doc } });
268
183
  const d = await readWork({ pool: fakePool(TASKS), pkg: PKG, live: '1.0.3', get: fg.get, store: freshStore(), installedNotes: INSTALLED_TO_103 });
269
184
  assert.ok(!fg.asked.some((u) => u.includes('evil.example')));
270
185
  assert.match(d.notes.error, /no download/);