@bongos/core 1.19.972 → 1.19.973
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 +74 -64
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0325-the-managed-fleet-control-plane-is-the-hub-we-already-run.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +107 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +6 -6
- package/docs/branding-contract.md +1 -1
- package/docs/canonical-permissions.md +24 -24
- package/docs/copy-inventory.md +45 -38
- package/docs/copy-registry.json +130 -67
- package/docs/design/projects-hub-direction-v2.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +2 -2
- package/docs/file-map.md +7 -7
- package/docs/handoff-template.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +2 -2
- package/docs/packs/engineer.md +5 -5
- package/docs/packs/ideator.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/gds-db-backup.md +3 -3
- package/docs/recipes/managed-settings-remote-control.md +9 -9
- package/docs/recipes/memory-sync-conflicts.md +2 -2
- package/docs/recipes/ops-gotchas.md +24 -5
- package/docs/recipes/overnight-builder.md +3 -3
- package/docs/recipes/parallel-goal-waves.md +3 -3
- package/docs/recipes/reranking-a-goals-board.md +2 -2
- package/docs/recipes/search-index-ingestion.md +1 -1
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/windows-builders.md +1 -1
- package/modules/hall-ui/public/deploy.html +10 -5
- package/modules/hall-ui/public/deploy.js +199 -110
- package/modules/hall-ui/public/project-settings.css +36 -0
- package/modules/provisioning/core-upgrade.js +61 -2
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/index.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +1 -1
- package/modules/public-landing/public/terms.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/api-alias-caller-scan.js +47 -9
- package/scripts/gds/cloudbongos-token-isolation.conf +33 -0
- package/scripts/gds/provision-core-upgrade.js +132 -1
- package/scripts/gds/run-unit-tests.js +14 -9
- package/scripts/hall-preview/server.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/api_alias_caller_ratchet.mjs +68 -2
- package/tests/core_upgrade_door.mjs +81 -0
- package/tests/core_upgrade_runner.mjs +132 -0
- package/tests/deploy_page_projects.mjs +234 -79
- package/tests/idea_spark_hall.mjs +101 -12
- package/tests/known_failing_ratchet.mjs +19 -9
- package/tests/landing_legal_pages.mjs +5 -1
- package/tests/projects_hub_pre_uat.mjs +12 -6
- package/tests/served_paths_shipped.mjs +36 -2
|
@@ -65,6 +65,15 @@ function boot({
|
|
|
65
65
|
// `{ outcome: null }`, which is the server saying "nothing to state yet"; the
|
|
66
66
|
// line must go quiet for both, and there is a case below for each.
|
|
67
67
|
landing = undefined,
|
|
68
|
+
// OUT-OF-ORDER READS (task 1004108). Names of fakes whose answers are PARKED
|
|
69
|
+
// instead of returned: the call is recorded and its response computed at once,
|
|
70
|
+
// but the caller's `await` does not resolve until the test releases it. That is
|
|
71
|
+
// the only way two reads of the same endpoint are ever in flight together here
|
|
72
|
+
// — every other fake is an `async` function that settles before the next call
|
|
73
|
+
// is made, so a monotonic-sequence guard sits on an unreachable branch and
|
|
74
|
+
// deleting it reds nothing. Pass e.g. `defer: ['preview']` and drive
|
|
75
|
+
// `r.deferred` in whatever order the race being pinned requires.
|
|
76
|
+
defer = [],
|
|
68
77
|
} = {}) {
|
|
69
78
|
const els = new Map();
|
|
70
79
|
const makeEl = () => ({
|
|
@@ -88,6 +97,21 @@ function boot({
|
|
|
88
97
|
};
|
|
89
98
|
|
|
90
99
|
const calls = [];
|
|
100
|
+
|
|
101
|
+
// Parked calls, in REQUEST order. Each carries the name it was made under, the
|
|
102
|
+
// arguments it was made with, and `release()` — which hands the caller the
|
|
103
|
+
// answer the fake had already computed. Releasing out of order is the point.
|
|
104
|
+
const deferred = [];
|
|
105
|
+
const deferSet = new Set(defer);
|
|
106
|
+
const gate = (name, fn) => (...args) => {
|
|
107
|
+
const answer = fn(...args); // record the call and compute the reply NOW...
|
|
108
|
+
if (!deferSet.has(name)) return answer;
|
|
109
|
+
let release;
|
|
110
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
111
|
+
deferred.push({ name, args, release: () => release(answer) }); // ...hand it over LATER
|
|
112
|
+
return held;
|
|
113
|
+
};
|
|
114
|
+
|
|
91
115
|
const client = {
|
|
92
116
|
inbox: {
|
|
93
117
|
getInboxTemplate: async () => ({ status: 200, ok: true, data: { template: TEMPLATE } }),
|
|
@@ -117,7 +141,7 @@ function boot({
|
|
|
117
141
|
// real matrix for the fixtures this file already files with: a goal named
|
|
118
142
|
// ⇒ it lands there, no goal ⇒ it floats. A test that cares about a
|
|
119
143
|
// particular verdict passes `landing` and overrides the lot.
|
|
120
|
-
postInboxPreviewLanding: async (a) => {
|
|
144
|
+
postInboxPreviewLanding: gate('preview', async (a) => {
|
|
121
145
|
calls.push(['preview', a]);
|
|
122
146
|
if (landing === null) return { status: 500, ok: false, data: null };
|
|
123
147
|
if (landing !== undefined) return { status: 200, ok: true, data: landing };
|
|
@@ -127,7 +151,7 @@ function boot({
|
|
|
127
151
|
return { status: 200, ok: true, data: { outcome: 'routed', reason: 'bug_trusted_filer', status: 'ready', goal: { id: String(gid), title: g ? g.title : `goal ${gid}` }, version_id: 'BV1', refusal: null } };
|
|
128
152
|
}
|
|
129
153
|
return { status: 200, ok: true, data: { outcome: 'floats', reason: 'no_goal', status: null, goal: null, version_id: null, refusal: null } };
|
|
130
|
-
},
|
|
154
|
+
}),
|
|
131
155
|
},
|
|
132
156
|
// task 1003069 (R08) — the quick-filing goal picker's reads: the building
|
|
133
157
|
// versions (to scope the suggester) and the open goal list (the picker's
|
|
@@ -137,7 +161,7 @@ function boot({
|
|
|
137
161
|
},
|
|
138
162
|
goals: {
|
|
139
163
|
getGoals: async () => { calls.push(['goals', null]); return { status: 200, ok: true, data: { goals } }; },
|
|
140
|
-
getGoalsSuggestions: async (a) => { calls.push(['suggest', a]); return { status: 200, ok: true, data: { suggestions } }; },
|
|
164
|
+
getGoalsSuggestions: gate('suggest', async (a) => { calls.push(['suggest', a]); return { status: 200, ok: true, data: { suggestions } }; }),
|
|
141
165
|
},
|
|
142
166
|
};
|
|
143
167
|
|
|
@@ -173,7 +197,7 @@ function boot({
|
|
|
173
197
|
|
|
174
198
|
const el = (id) => documentObj.getElementById(id);
|
|
175
199
|
return {
|
|
176
|
-
calls, toasts, store, el,
|
|
200
|
+
calls, toasts, store, el, deferred,
|
|
177
201
|
queueHtml: () => el('fi-queue').innerHTML,
|
|
178
202
|
fill: () => { for (const f of TEMPLATE.fields) el(`fi-f-${f.key}`).value = `an answer for ${f.key} long enough to count`; },
|
|
179
203
|
submit: () => el('fi-form')._handlers.submit({ preventDefault() {} }),
|
|
@@ -678,11 +702,67 @@ await test('filing clears the line — gravity describes a draft, and the draft
|
|
|
678
702
|
assert.equal(r.el('fq-receipts').hidden, false, 'the RECEIPT is what survives instead (task 1004082)');
|
|
679
703
|
});
|
|
680
704
|
|
|
705
|
+
// --- out-of-order reads (task 1004108) -----------------------------------
|
|
706
|
+
//
|
|
707
|
+
// Everything above this line drives fakes that settle before the next call is
|
|
708
|
+
// made, so `refreshGravity`'s monotonic `gravitySeq` guard sat on a branch no
|
|
709
|
+
// case could reach: deleting it redded NOTHING, which the closing note below
|
|
710
|
+
// used to record as an honest gap. `boot({ defer: [...] })` parks a named fake's
|
|
711
|
+
// answer until the test hands it back, so two reads are genuinely in flight at
|
|
712
|
+
// once and the slower one can land last.
|
|
713
|
+
|
|
714
|
+
await test('two previews are genuinely in flight at once — the harness can interleave them', async () => {
|
|
715
|
+
// The non-vacuity guard for the case beneath it. If `defer` ever stopped
|
|
716
|
+
// parking, the race case below would still pass — it would just be testing two
|
|
717
|
+
// sequential reads again, which is the exact hole this pair exists to close.
|
|
718
|
+
const r = boot({ defer: ['preview'] });
|
|
719
|
+
await settle(); await settle();
|
|
720
|
+
r.el('fq-title').value = 'a loose thought';
|
|
721
|
+
r.el('fq-goal')._handlers.change();
|
|
722
|
+
await settle(); await settle();
|
|
723
|
+
assert.equal(r.deferred.length, 1, 'the first read is parked, not answered');
|
|
724
|
+
assert.equal(r.el('fq-gravity').textContent, '', 'and nothing is painted while it is in flight');
|
|
725
|
+
r.el('fq-goal').value = '77';
|
|
726
|
+
r.el('fq-goal')._handlers.change();
|
|
727
|
+
await settle(); await settle();
|
|
728
|
+
assert.equal(r.deferred.length, 2, 'the second read starts while the first is still unanswered');
|
|
729
|
+
assert.deepEqual(r.deferred.map((d) => d.name), ['preview', 'preview']);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
await test('a slow preview answering after a faster one never repaints the stale verdict', async () => {
|
|
733
|
+
// The race a builder hits by picking a goal and then changing their mind
|
|
734
|
+
// faster than the server answers. The FIRST request (no goal ⇒ "floats") comes
|
|
735
|
+
// back LAST; the line must keep the SECOND request's verdict.
|
|
736
|
+
const r = boot({ defer: ['preview'] });
|
|
737
|
+
await settle(); await settle();
|
|
738
|
+
r.el('fq-title').value = 'the widget is broken';
|
|
739
|
+
r.el('fq-goal').value = '';
|
|
740
|
+
r.el('fq-goal')._handlers.change();
|
|
741
|
+
await settle();
|
|
742
|
+
r.el('fq-goal').value = '77';
|
|
743
|
+
r.el('fq-goal')._handlers.change();
|
|
744
|
+
await settle();
|
|
745
|
+
assert.equal(r.deferred.length, 2, 'both reads are in flight');
|
|
746
|
+
assert.equal(r.deferred[0].args[0].body.goal_id, undefined, 'the first asked with no goal');
|
|
747
|
+
assert.equal(r.deferred[1].args[0].body.goal_id, 77, 'the second asked with the goal picked');
|
|
748
|
+
|
|
749
|
+
r.deferred[1].release(); // the NEWER answer lands first
|
|
750
|
+
await settle(); await settle();
|
|
751
|
+
assert.match(r.el('fq-gravity').textContent, /Goal Seventy Seven/, 'the current verdict paints');
|
|
752
|
+
|
|
753
|
+
r.deferred[0].release(); // and now the older, slower one arrives
|
|
754
|
+
await settle(); await settle();
|
|
755
|
+
assert.match(r.el('fq-gravity').textContent, /Goal Seventy Seven/, 'the stale answer does not repaint the line');
|
|
756
|
+
assert.doesNotMatch(r.el('fq-gravity').textContent, /floats in the inbox/, 'the superseded verdict never reaches the builder');
|
|
757
|
+
assert.equal(r.el('fq-gravity').dataset.tone, 'lands');
|
|
758
|
+
});
|
|
759
|
+
|
|
681
760
|
// Every guard above was reverted and each redded its named case (RUN, and these
|
|
682
761
|
// are the measured counts, not the predicted ones):
|
|
683
762
|
// drop renderGravity from the goal-change handler -> 2 red (named-goal + filing-clears)
|
|
684
763
|
// infer from the select instead of the verdict -> 4 red (both lies, + backlog + refusal)
|
|
685
|
-
// drop the seq guard and paint every answer ->
|
|
764
|
+
// drop the seq guard and paint every answer -> 1 red (the race case; task 1004108)
|
|
765
|
+
// make boot's `defer` never park (gate returns at once) -> 2 red (both race cases)
|
|
686
766
|
// rename the id in ideas.html -> 1 red (the markup pin ONLY)
|
|
687
767
|
//
|
|
688
768
|
// THE FIRST LINE COST A TEST FIX, and it is the reason to run these rather than
|
|
@@ -692,13 +772,22 @@ await test('filing clears the line — gravity describes a draft, and the draft
|
|
|
692
772
|
// rendered at all. It asserts on the TEXT now. A stub's defaults can agree with
|
|
693
773
|
// a broken page, which is the same lesson the markup pin below teaches twice.
|
|
694
774
|
//
|
|
695
|
-
// THE THIRD LINE
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
700
|
-
//
|
|
701
|
-
//
|
|
775
|
+
// THE THIRD LINE WAS AN HONEST GAP UNTIL TASK 1004108, and the shape of the fix
|
|
776
|
+
// is worth keeping: the guard was not wrong, it was UNREACHABLE. Every fake here
|
|
777
|
+
// was an `async` function that settled before the next call was made, so two
|
|
778
|
+
// previews could never overlap and deleting `if (seq !== gravitySeq) return;`
|
|
779
|
+
// redded nothing — a green suite over a branch no case could enter. `boot({
|
|
780
|
+
// defer: ['preview'] })` parks a named fake's answer until the test hands it
|
|
781
|
+
// back, which makes the overlap real and the guard a tested property. The FOURTH
|
|
782
|
+
// line above is the guard on the guard: if `defer` ever stops parking, the race
|
|
783
|
+
// case would quietly go back to testing two sequential reads, so a case asserts
|
|
784
|
+
// that both calls are in flight before the ordering is asserted at all.
|
|
785
|
+
//
|
|
786
|
+
// `getGoalsSuggestions` is gated the same way and for the same reason, but has
|
|
787
|
+
// NO case yet: `refreshGoalSuggestions` carries no sequence guard — only
|
|
788
|
+
// `goalTouchedByUser` — so a test of its ordering would red against the current
|
|
789
|
+
// page rather than pin it. That is a production bug to file, not a test to
|
|
790
|
+
// write, and the harness is now able to reach it the day it is fixed.
|
|
702
791
|
|
|
703
792
|
console.log(`\nidea_spark_hall: ${passed} passed, ${failed} failed`);
|
|
704
793
|
if (failed > 0) process.exit(1);
|
|
@@ -38,16 +38,26 @@ const TESTS_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
38
38
|
// right all along. The apex flip removed the redirect, so the test passes and
|
|
39
39
|
// the entry had to go in the same change — this file fails the suite the moment
|
|
40
40
|
// a parked test starts passing.
|
|
41
|
-
|
|
41
|
+
//
|
|
42
|
+
// Ratcheted down 1 → 0 by task 1002617, which unparked served_paths_shipped — the last
|
|
43
|
+
// entry. Its own note here had PREDICTED the resolution ("whoever lands that claim should
|
|
44
|
+
// re-check this assertion and remove this entry"); the claim landed (task 1002200, the
|
|
45
|
+
// self-healing /docs/openapi.json endpoint) and the entry outlived it by months. That is
|
|
46
|
+
// the graveyard this file exists to prevent, and it took this ratchet going red to close
|
|
47
|
+
// the loop — which is the ratchet working, just later than it should have.
|
|
48
|
+
//
|
|
49
|
+
// AT ZERO, this file now asserts something stronger than a cap: that NOTHING is parked.
|
|
50
|
+
// Parking the next failure means raising this number in the same diff, which is exactly
|
|
51
|
+
// the deliberate, reviewable act it is meant to be.
|
|
52
|
+
const MAX_KNOWN_FAILING = 0;
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
54
|
+
// EMPTY — nothing is parked, and that is the intended steady state.
|
|
55
|
+
//
|
|
56
|
+
// An entry here is invisible coverage: the test still runs, but its failure is expected,
|
|
57
|
+
// so nobody looks at it. The last one sat for months while the very change it was waiting
|
|
58
|
+
// on had already landed. If you add one, give it an owner and a concrete condition for
|
|
59
|
+
// removal, and raise MAX_KNOWN_FAILING in the same diff.
|
|
60
|
+
const KNOWN_FAILING = {};
|
|
51
61
|
|
|
52
62
|
function runsRed(name) {
|
|
53
63
|
try {
|
|
@@ -103,7 +103,11 @@ test('the landing page and the hub footers link privacy, terms and contact', ()
|
|
|
103
103
|
for (const href of ['/privacy', '/terms', '/contact']) {
|
|
104
104
|
assert.match(foot, new RegExp(`<a href="${href}">`), `${file}: footer links ${href}`);
|
|
105
105
|
}
|
|
106
|
-
|
|
106
|
+
// task 1003920 split one link into two: the WORDS now open the published code and the licence
|
|
107
|
+
// sits behind its own link beside it, so the claim can be checked by whoever clicks it. Both
|
|
108
|
+
// halves are asserted — a bare /Open source/ would pass again the moment the licence link went.
|
|
109
|
+
assert.match(foot, /<a href="https:\/\/www\.npmjs\.com\/package\/@bongos\/core"[^>]*>Open source<\/a>/, `${file}: the words open the actual code`);
|
|
110
|
+
assert.match(foot, /<a href="https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0\.html"[^>]*>AGPL-3\.0<\/a>/, `${file}: the licence link stays`);
|
|
107
111
|
assert.match(foot, /Built by agents/, `${file}: the attribution half stays`);
|
|
108
112
|
}
|
|
109
113
|
});
|
|
@@ -474,16 +474,22 @@ test('decision 9 — only mouse-driven focus on a wizard heading goes ringless',
|
|
|
474
474
|
});
|
|
475
475
|
|
|
476
476
|
test('decision 10 — both footers carry the open-source (AGPL-3.0) link, in link ink', () => {
|
|
477
|
-
|
|
477
|
+
// task 1003920 split this into TWO links: the words "Open source" open the published package and
|
|
478
|
+
// the licence sits behind its own link beside it. Both are asserted, and CODE is anchored first
|
|
479
|
+
// because it is the half 1003920 added and the half nothing else in the repo pins.
|
|
480
|
+
const CODE = /<a href="https:\/\/www\.npmjs\.com\/package\/@bongos\/core" target="_blank" rel="noopener">Open source<\/a>/;
|
|
481
|
+
const AGPL = /\(<a href="https:\/\/www\.gnu\.org\/licenses\/agpl-3\.0\.html" target="_blank" rel="noopener">AGPL-3\.0<\/a>\)\./;
|
|
478
482
|
const hubFoot = (HUB.match(/<footer class="foot">[\s\S]*?<\/footer>/) || [''])[0];
|
|
479
483
|
// task 1003298: the chrome-world landing's footer is a real <footer>; the
|
|
480
484
|
// pre-world page used <div class="foot">. Either element is the footer.
|
|
481
485
|
const indexFoot = (INDEX.match(/<footer(?:\s[^>]*)?>[\s\S]*?<\/footer>|<div class="foot">[\s\S]*?<\/div>/) || [''])[0];
|
|
482
|
-
assert.match(hubFoot,
|
|
483
|
-
assert.match(indexFoot,
|
|
484
|
-
|
|
485
|
-
assert.
|
|
486
|
-
|
|
486
|
+
assert.match(hubFoot, CODE, 'projects.html: the words open the actual code');
|
|
487
|
+
assert.match(indexFoot, CODE, 'index.html: the words open the actual code');
|
|
488
|
+
assert.match(hubFoot, AGPL, 'projects.html: the licence sits beside it');
|
|
489
|
+
assert.match(indexFoot, AGPL, 'index.html: the licence sits beside it');
|
|
490
|
+
// both links sit OUTSIDE the apex-only span, so a child instance keeps them too
|
|
491
|
+
assert.ok(hubFoot.indexOf('<!--apex-only:end-->') < hubFoot.search(CODE), 'projects.html: after the apex-only block');
|
|
492
|
+
assert.ok(indexFoot.indexOf('<!--apex-only:end-->') < indexFoot.search(CODE), 'index.html: after the apex-only block');
|
|
487
493
|
assert.match(COSMOS, /\.foot a\{color:var\(--link-ink\);font-weight:700;text-decoration:none;\}/, 'never the browser’s blue');
|
|
488
494
|
});
|
|
489
495
|
|
|
@@ -47,14 +47,39 @@ const INTENTIONALLY_UNSHIPPED = {
|
|
|
47
47
|
// module is individually allowlisted), so the base alone is not a shippable-asset claim.
|
|
48
48
|
const NOT_SERVED_BASE = new Set([
|
|
49
49
|
'modules', // path.join(ROOT,'modules') → MODULES_ROOT; real serves are path.join(MODULES_ROOT, key, 'public', rel)
|
|
50
|
+
// Surfaced by widening the scan to root ladders (task 1002617). tryLoadInstanceConfig
|
|
51
|
+
// does path.join(root, 'config', name) with a DYNAMIC name, and the result is READ as
|
|
52
|
+
// instance configuration — never sendFile()d to a client, so it is not a served asset
|
|
53
|
+
// and this test's premise (a served path must ship or the box 404s) does not apply.
|
|
54
|
+
// Its exclusion is also deliberate rather than an oversight: serve-internal.js:59-62
|
|
55
|
+
// records that config/ is host content under publish-manifest INSTANCE_EXCLUDE, which
|
|
56
|
+
// is exactly WHY that function walks an instance-root-first ladder. Shipping it would
|
|
57
|
+
// be the bug.
|
|
58
|
+
'config',
|
|
50
59
|
]);
|
|
51
60
|
|
|
52
61
|
// Extract the leading contiguous string-literal prefix of every path.join(ROOT, ...)
|
|
53
62
|
// call. A trailing non-literal arg (a variable) means "some file under this dir", so the
|
|
54
63
|
// literal prefix is the directory that must ship.
|
|
64
|
+
// ROOT-LADDER FORMS (task 1002617). The scan originally matched `path.join(ROOT, …)`
|
|
65
|
+
// only. Task 1002200 made the OpenAPI spec self-healing and multi-rooted:
|
|
66
|
+
//
|
|
67
|
+
// const roots = [...new Set([resolveDocsRoot(), ROOT])];
|
|
68
|
+
// candidates: roots.map((r) => path.join(r, 'docs/api/openapi.json'))
|
|
69
|
+
//
|
|
70
|
+
// so the served path stopped matching and the scan silently stopped seeing it. The
|
|
71
|
+
// existence guard below is what caught that — it is the only reason this test was parked
|
|
72
|
+
// rather than quietly asserting nothing, which is precisely what a self-guard is for.
|
|
73
|
+
//
|
|
74
|
+
// The fix is to match any single-identifier first argument, not to special-case `r`.
|
|
75
|
+
// A root ladder is still a root: whichever entry wins, the file must be IN the package or
|
|
76
|
+
// the standalone box 404s. Accepting any identifier does pull in non-asset bases
|
|
77
|
+
// (`root + 'config'`, `ROOT + 'infra'`, `ROOT + 'modules'`); those are handled by the
|
|
78
|
+
// existing NOT_SERVED_BASE / INTENTIONALLY_UNSHIPPED lists, which is what they are for,
|
|
79
|
+
// and each is individually asserted by the stale-exception guard.
|
|
55
80
|
function extractRootJoinPrefixes(src) {
|
|
56
81
|
const out = new Set();
|
|
57
|
-
const re = /path\.join\(\s*
|
|
82
|
+
const re = /path\.join\(\s*[A-Za-z_$][\w$]*\s*,/g;
|
|
58
83
|
let m;
|
|
59
84
|
while ((m = re.exec(src))) {
|
|
60
85
|
// Arg span up to the matching close paren. These calls contain no nested parens
|
|
@@ -84,10 +109,19 @@ function probe(prefix) {
|
|
|
84
109
|
|
|
85
110
|
const prefixes = extractRootJoinPrefixes(fs.readFileSync(SERVE_INTERNAL, 'utf8'));
|
|
86
111
|
|
|
112
|
+
// THE SELF-GUARD, and it has now earned its keep once (task 1002617). A static source
|
|
113
|
+
// scan degrades to silence when the code it scans changes shape: the regex stops
|
|
114
|
+
// matching, `prefixes` empties, and every downstream assertion passes over nothing. That
|
|
115
|
+
// is strictly worse than a red test, because it reads as coverage. This test failed for
|
|
116
|
+
// months for exactly the right reason — the OpenAPI serve moved to a root ladder — and
|
|
117
|
+
// it was parked in KNOWN_FAILING rather than diagnosed. Keep it, and keep it specific:
|
|
118
|
+
// a bare `size > 0` would have stayed green through the very change that blinded it.
|
|
87
119
|
test('extraction found the known served assets (guards against a silent no-match)', () => {
|
|
88
120
|
assert.ok(prefixes.size >= 5, `expected several ROOT-served paths, found ${prefixes.size}`);
|
|
89
121
|
assert.ok(prefixes.has('clients/bongos-client/bongos-client.global.js'), 'must see the client bundle read');
|
|
90
|
-
assert.ok(prefixes.has('docs/api/openapi.json'),
|
|
122
|
+
assert.ok(prefixes.has('docs/api/openapi.json'),
|
|
123
|
+
'must see the openapi spec read — it is served from a ROOT LADDER (roots.map(r => path.join(r, …))), '
|
|
124
|
+
+ 'so a scan matching only `path.join(ROOT, …)` goes blind here');
|
|
91
125
|
});
|
|
92
126
|
|
|
93
127
|
test('every ROOT-served asset ships in the core package (isPublishable)', () => {
|