@bongos/core 1.21.167 → 1.21.169
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 +157 -72
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +13 -0
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +308 -5
- package/docs/api-reference.md +7 -3
- package/docs/architecture.md +2 -0
- package/docs/copy-inventory.md +21 -20
- package/docs/copy-registry.json +34 -25
- package/docs/module-api-changelog.md +5 -1
- package/docs/modules-contract.md +2 -1
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +430 -430
- package/modules/autonomy/governor-docket.js +10 -5
- package/modules/builder-settings/builder-needs.js +29 -3
- package/modules/copy-desk/guarded-lines.js +89 -0
- package/modules/copy-desk/guarded-lines.json +14 -0
- package/modules/copy-desk/module.json +2 -2
- package/modules/copy-desk/page-status.js +77 -7
- package/modules/copy-desk/pages.js +4 -0
- package/modules/copy-desk/pollers/stuck-page-digest.js +34 -0
- package/modules/copy-desk/routes/copy-desk.js +8 -0
- package/modules/copy-desk/stuck-tweaks.js +79 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +6 -1
- package/modules/copy-desk/tests/copy_page_status.mjs +2 -2
- package/modules/government/asks-db.js +38 -2
- package/modules/government/catalog.js +1 -1
- package/modules/government/docket.js +20 -2
- package/modules/government/migrations/government_032_ask_flags.sql +35 -0
- package/modules/government/routes/government.js +43 -1
- package/modules/hall-ui/public/approval-queue-lib.js +4 -0
- package/modules/hall-ui/public/approval-queue.js +1 -0
- package/modules/hall-ui/public/city-decide.js +642 -0
- package/modules/hall-ui/public/city-draw.js +14 -3
- package/modules/hall-ui/public/city-switchboard.js +141 -0
- package/modules/hall-ui/public/city.css +176 -10
- package/modules/hall-ui/public/city.html +8 -3
- package/modules/hall-ui/public/city.js +185 -38
- package/modules/hall-ui/public/tweak-editor-lib.js +21 -2
- package/modules/hall-ui/public/tweak-editor.css +2 -0
- package/modules/hall-ui/public/tweak-editor.js +3 -1
- package/modules/lifecycle/db-claims.js +20 -5
- package/modules/lifecycle/rebase-gate.js +78 -0
- package/modules/lifecycle/routes/claims.js +10 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/gds/ask.js +68 -1
- package/scripts/gds/claim.js +6 -1
- package/scripts/gds/copy-apply.js +2 -0
- package/src/bongos/module-promotion.js +219 -0
- package/src/bongos/module-store.js +47 -9
- package/src/bongos/route-rank-check.js +7 -0
- package/src/bongos/routes/me.js +11 -0
- package/src/bongos/routes/modules.js +72 -10
- package/src/module-api.js +1 -1
- package/src/platform-server.js +5 -0
- package/tests/claim_gate_ci_unblock.mjs +4 -0
- package/tests/claim_gate_rebase.mjs +56 -3
- package/tests/copy_desk_page_editor.mjs +1 -1
- package/tests/copy_desk_page_submit.mjs +42 -0
- package/tests/copy_guarded_lines.mjs +54 -0
- package/tests/copy_stuck_page_tweaks.mjs +118 -0
- package/tests/government_ask_flags.mjs +80 -0
- package/tests/government_seed.mjs +1 -0
- package/tests/government_treasury.mjs +1 -0
- package/tests/governor_docket.mjs +1 -1
- package/tests/hall_approval_queue.mjs +15 -0
- package/tests/hall_city.mjs +5 -4
- package/tests/hall_city_living.mjs +118 -12
- package/tests/hall_city_rewording.mjs +51 -0
- package/tests/hall_city_switchboard.mjs +63 -0
- package/tests/hall_gov_real_site.mjs +2 -2
- package/tests/hall_street_deck.mjs +6 -4
- package/tests/hall_street_depth.mjs +1 -1
- package/tests/hall_street_desk.mjs +5 -3
- package/tests/hall_tweak_editor.mjs +26 -0
- package/tests/module_channel_install.mjs +112 -0
- package/tests/module_promotion.mjs +290 -0
- package/tests/module_upgrade_updates.mjs +2 -2
- package/tests/rebase_gate_set_aside.mjs +73 -0
|
@@ -181,7 +181,7 @@ test('salmon only for what waits on the viewer: on the floor whose seat it waits
|
|
|
181
181
|
// a stranger: nothing waits on them, so nothing is salmon and the door is plain
|
|
182
182
|
const s = city({ me: STRANGER });
|
|
183
183
|
assert.equal(allPolys(s).filter((x) => /city-win--you/.test(x.p.cls || '')).length, 0);
|
|
184
|
-
assert.ok(s.pins.every((p) => p.state === 'moving' || p.state === 'settled'), 'no salmon pin');
|
|
184
|
+
assert.ok(s.pins.every((p) => p.state === 'moving' || p.state === 'settled' || p.state === 'switch'), 'no salmon pin (the switchboard marker is ink)');
|
|
185
185
|
assert.deepEqual(s.cta, City.ctaFor(0));
|
|
186
186
|
assert.equal(s.cta.label, 'start governing');
|
|
187
187
|
});
|
|
@@ -249,7 +249,7 @@ test('quiet buildings sit darker; busy ones keep the prototype\'s recipe exactly
|
|
|
249
249
|
if (b.quiet) assert.notDeepEqual(recipe, ref.map((p) => p.f), `${s[0]} is quiet: its faces are mixed toward the void`);
|
|
250
250
|
else assert.deepEqual(recipe, ref.map((p) => p.f), `${s[0]} is busy: the recipe is untouched`);
|
|
251
251
|
}
|
|
252
|
-
assert.deepEqual(m.civic.filter((c) => c.quiet).map((c) => c.key).sort(), ['people', 'rules'], 'no agent and no ask: Rules and People');
|
|
252
|
+
assert.deepEqual(m.civic.filter((c) => c.quiet).map((c) => c.key).sort(), ['automation', 'people', 'rules'], 'no agent and no ask: Automation (its switch is a standing state, not an ask), Rules and People');
|
|
253
253
|
assert.equal(City.mixToward('#808080', '#000000', 0.5), '#404040');
|
|
254
254
|
assert.equal(City.mixToward('rgba(255,255,255,0.4)', '#000000', 0.5), 'rgba(255,255,255,0.2)', 'a line thins out');
|
|
255
255
|
});
|
|
@@ -323,7 +323,7 @@ test('the card: who holds it, one fact, its count, two doors (its first ask is t
|
|
|
323
323
|
assert.equal(p.enter_href, '/builders/street#building=planning');
|
|
324
324
|
for (const c of m.civic) assert.deepEqual(Object.keys(c.card).sort(), ['agents', 'deck_href', 'enter_href', 'fact', 'key', 'lead', 'name', 'on_others', 'on_you', 'open', 'plate'], c.key + ': every answer is the deck\'s');
|
|
325
325
|
assert.equal(civicOf(m, 'charter').card.plate, 'owner', 'an unseated building is the owner\'s');
|
|
326
|
-
assert.equal(civicOf(m, 'automation').card.fact.text, '
|
|
326
|
+
assert.equal(civicOf(m, 'automation').card.fact.text, 'AI building: off', 'the real fence');
|
|
327
327
|
assert.equal(civicOf(m, 'release').card.fact.text, '2 agents working');
|
|
328
328
|
});
|
|
329
329
|
|
|
@@ -341,7 +341,7 @@ test('the door is "decide N in a row" to the deck; the line counts you, others a
|
|
|
341
341
|
const m = city();
|
|
342
342
|
const c = m.counts;
|
|
343
343
|
assert.ok(c.on_you > 0 && c.on_others > 0);
|
|
344
|
-
assert.deepEqual([m.cta.label, m.cta.href], ['decide ' + c.on_you + ' in a row', '
|
|
344
|
+
assert.deepEqual([m.cta.label, m.cta.href], ['decide ' + c.on_you + ' in a row', '#decide']);
|
|
345
345
|
assert.equal(m.line, c.on_you + ' to do · ' + c.on_others + ' wait on others · 5 agents working');
|
|
346
346
|
assert.equal(City.livingLine(1, 0, 1), '1 to do · 1 agent working');
|
|
347
347
|
assert.equal(City.livingLine(0, 3, 0), 'nothing to do · 3 wait on others');
|
|
@@ -390,7 +390,7 @@ test('motion is CSS, long, inside the budget, with a still frame on every moving
|
|
|
390
390
|
|
|
391
391
|
test('salmon is spent only on what waits on you (and the door, the focus ring)', () => {
|
|
392
392
|
const css = read('city.css').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
393
|
-
const ALLOWED = /city-win--you|city-pin--(urgent|needs)|city-pin__ring|city-pin__age|city-start|city-card__go--you|city-ask--you|city-plaque__tile\[data-key="you"\]|city-card__count--you|focus-visible|body\[data-page="city"\]\s*$/;
|
|
393
|
+
const ALLOWED = /city-win--you|city-pin--(urgent|needs)|city-pin__ring|city-pin__age|city-start|city-card__go--you|city-ask--you|city-plaque__tile\[data-key="you"\]|city-card__count--you|city-card--you|ask-crab|decide__yours|decide-ask--you|decide__tr--you|focus-visible|body\[data-page="city"\]\s*$/;
|
|
394
394
|
let checked = 0;
|
|
395
395
|
for (const r of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
396
396
|
if (!/var\(--accent(-deep|-lift)?\)/.test(r[2])) continue;
|
|
@@ -507,7 +507,7 @@ async function bootLiving({ asks = false, answer = null } = {}) {
|
|
|
507
507
|
vm.runInContext(read('city-draw.js'), ctx);
|
|
508
508
|
assert.ok(window.GovLiving && window.GovCity);
|
|
509
509
|
// the deck's card and sender, as city.html loads them (task 1004702)
|
|
510
|
-
for (const f of ['street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street-deck.js']) vm.runInContext(read(f), ctx);
|
|
510
|
+
for (const f of ['street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'street-deck.js', 'city-decide.js', 'city-switchboard.js']) vm.runInContext(read(f), ctx);
|
|
511
511
|
assert.ok(window.GovDeck && window.GovDesk, 'the deck\'s scripts are on the page');
|
|
512
512
|
vm.runInContext(read('city.js'), ctx);
|
|
513
513
|
await window.GovCityPage.ready;
|
|
@@ -529,7 +529,7 @@ test('the page reads the living feeds and draws the sky, the plates, the plaque
|
|
|
529
529
|
const m = city();
|
|
530
530
|
const start = p.byId.get('cityStart');
|
|
531
531
|
assert.equal(start.textContent, m.cta.label);
|
|
532
|
-
assert.equal(start.attrs.href, '
|
|
532
|
+
assert.equal(start.attrs.href, '#decide');
|
|
533
533
|
assert.equal(p.byId.get('cityLine').textContent, m.line);
|
|
534
534
|
for (const id of ['cityBlds', 'citySky', 'cityPins', 'cityPlaque']) assert.equal(findAll(p.byId.get(id), (n) => 'data-proto' in n.attrs).length, 0, id + ': nothing marked prototype');
|
|
535
535
|
assert.doesNotMatch(read('city.html'), /cityKey|cityToast|data-proto/, 'the page has no prototype key and no in-place answer toast');
|
|
@@ -543,17 +543,38 @@ test('a building opens its card in place: who holds it, one fact, what waits, it
|
|
|
543
543
|
assert.equal(card.attrs.hidden, undefined);
|
|
544
544
|
assert.equal(card.attrs['data-building'], 'planning');
|
|
545
545
|
assert.match(card.textContent, /Planning/);
|
|
546
|
-
// the viewer (builder 2012) leads Planning: "Your seat."
|
|
547
|
-
|
|
546
|
+
// the viewer (builder 2012) leads Planning: "Your seat." rides the name as a hint, not a line
|
|
547
|
+
const nameEl = findAll(card, (x) => /\bcity-card__name\b/.test(String(x.className)))[0];
|
|
548
|
+
assert.equal(nameEl.attrs.title, 'Your seat.');
|
|
549
|
+
assert.doesNotMatch(card.textContent, /Your seat\./, 'no subtitle line');
|
|
548
550
|
const n = civicOf(city(), 'planning').on_you;
|
|
549
|
-
|
|
551
|
+
const total = civicOf(city(), 'planning').open;
|
|
552
|
+
assert.match(card.textContent, new RegExp('Total ' + total + ' / Yours ' + n));
|
|
553
|
+
assert.ok(/\bcity-card--you\b/.test(card.className), 'a building on you wears the project colour');
|
|
554
|
+
assert.doesNotMatch(card.textContent, /Only you can answer/, 'the glow says it, not a line');
|
|
550
555
|
assert.ok(/\bgov-glass\b/.test(card.className), 'the card is glass');
|
|
551
|
-
|
|
552
|
-
|
|
556
|
+
assert.equal(findAll(card, (x) => x.tagName === 'a' && /city-card__enter/.test(String(x.className))).length, 0, 'no door to the street');
|
|
557
|
+
const go = findAll(card, (x) => /\bcity-card__go\b/.test(String(x.className)));
|
|
558
|
+
assert.deepEqual(go.map((b) => b.textContent), ['decide all ' + n], 'one decide button, in place');
|
|
559
|
+
assert.equal(findAll(card, (x) => /\bcity-card__arrow\b/.test(String(x.className))).length, 2, 'arrows to flip between buildings');
|
|
560
|
+
card.fire('keydown', { key: 'ArrowRight', preventDefault() {} });
|
|
561
|
+
assert.notEqual(p.byId.get('cityCard').attrs['data-building'], 'planning', 'the right arrow flips to the next building');
|
|
562
|
+
card.fire('keydown', { key: 'ArrowLeft', preventDefault() {} });
|
|
563
|
+
assert.equal(p.byId.get('cityCard').attrs['data-building'], 'planning', 'and the left arrow flips back');
|
|
553
564
|
// the building's first ask stands in place, as the deck draws it
|
|
554
565
|
const ask = findAll(card, (x) => /\bcity-ask\b/.test(String(x.className)));
|
|
555
566
|
assert.equal(ask.length, 1, 'its first ask, in place');
|
|
556
567
|
assert.ok(/\bask--here\b/.test(ask[0].className));
|
|
568
|
+
// answered in the card: the desk row's own acts are its answers, never "answer it on its desk"
|
|
569
|
+
assert.ok(findAll(card, (x) => /\bask__opt\b/.test(String(x.className))).length > 0, 'the card carries its answers');
|
|
570
|
+
assert.doesNotMatch(card.textContent, /Answer it on its desk/);
|
|
571
|
+
// Tab steps to the building's next item, Shift+Tab back
|
|
572
|
+
const stepN = () => findAll(p.byId.get('cityCard'), (x) => /\bcity-card__stepn\b/.test(String(x.className)))[0].textContent;
|
|
573
|
+
assert.equal(stepN(), '1 of ' + n);
|
|
574
|
+
p.byId.get('cityCard').fire('keydown', { key: 'Tab', preventDefault() {} });
|
|
575
|
+
assert.equal(stepN(), '2 of ' + n);
|
|
576
|
+
p.byId.get('cityCard').fire('keydown', { key: 'Tab', shiftKey: true, preventDefault() {} });
|
|
577
|
+
assert.equal(stepN(), '1 of ' + n);
|
|
557
578
|
card.fire('keydown', { key: 'Escape', preventDefault() {} });
|
|
558
579
|
assert.equal(card.attrs.hidden, '', 'Escape closes it');
|
|
559
580
|
const again = p.byId.get('cityHits').children.find((b) => b.attrs['data-building'] === 'planning');
|
|
@@ -574,6 +595,10 @@ test('the city card answers its building\'s first ask in place, through the same
|
|
|
574
595
|
const Kd = p.window.GovDeck;
|
|
575
596
|
const op = opts[1];
|
|
576
597
|
const key = op.attrs['data-key'];
|
|
598
|
+
// the first press picks that answer and sends nothing; the same number again confirms it
|
|
599
|
+
card.fire('keydown', { key: op.attrs['data-n'], preventDefault() {} });
|
|
600
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
601
|
+
assert.equal(p.sent.length, 0, 'one press only picks');
|
|
577
602
|
card.fire('keydown', { key: op.attrs['data-n'], preventDefault() {} });
|
|
578
603
|
await new Promise((r) => setTimeout(r, 20));
|
|
579
604
|
assert.equal(p.sent.length, 1, 'one request');
|
|
@@ -615,3 +640,84 @@ test('Planning stands behind nothing taller, so its lit floor shows', () => {
|
|
|
615
640
|
for (const o of front) assert.ok(o.roof < planning.roof, 'planning stands behind the taller ' + o.key);
|
|
616
641
|
assert.deepEqual(City.CIVIC_SLOTS.find((s) => s[0] === 'charter').slice(1), [5, 5], 'the Charter rises from the back corner');
|
|
617
642
|
});
|
|
643
|
+
|
|
644
|
+
// ── the decide view and the switchboard, through the real page (task 1004784) ──
|
|
645
|
+
|
|
646
|
+
test('the decide view: one by one with Tab inside a building and arrows across, number keys pick then confirm', async () => {
|
|
647
|
+
const p = await bootLiving({ asks: true });
|
|
648
|
+
const d = p.window.GovCityPage.openDecide(null);
|
|
649
|
+
const view = p.byId.get('cityDecide');
|
|
650
|
+
assert.ok(view && view.attrs.hidden === undefined, 'the view opens over the city');
|
|
651
|
+
const pos = () => findAll(view, (x) => /\bdecide__pos-n\b/.test(String(x.className)))[0].textContent;
|
|
652
|
+
const first = pos();
|
|
653
|
+
const building = first.replace(/ \d+ of \d+$/, '');
|
|
654
|
+
view.fire('keydown', { key: 'ArrowRight', preventDefault() {}, stopPropagation() {} });
|
|
655
|
+
assert.notEqual(pos().replace(/ \d+ of \d+$/, ''), building, 'right goes to the next building');
|
|
656
|
+
view.fire('keydown', { key: 'ArrowLeft', preventDefault() {}, stopPropagation() {} });
|
|
657
|
+
assert.equal(pos(), first, 'left comes back to the first item of the first building');
|
|
658
|
+
const n = Number(/of (\d+)$/.exec(first)[1]);
|
|
659
|
+
if (n > 1) {
|
|
660
|
+
view.fire('keydown', { key: 'Tab', preventDefault() {}, stopPropagation() {} });
|
|
661
|
+
assert.equal(pos(), building + ' 2 of ' + n, 'Tab is the next item in the same building');
|
|
662
|
+
view.fire('keydown', { key: 'Tab', shiftKey: true, preventDefault() {}, stopPropagation() {} });
|
|
663
|
+
}
|
|
664
|
+
// walk to a card with answers, then pick and confirm
|
|
665
|
+
const live = () => { const o = findAll(view, (x) => /\bask__opt\b/.test(String(x.className))); return o.length && !/ask__opt--off/.test(o[0].className); };
|
|
666
|
+
const k = (key) => view.fire('keydown', { key, preventDefault() {}, stopPropagation() {} });
|
|
667
|
+
for (let b = 0; b < 12 && !live(); b++) { for (let i = 0; i < 30 && !live(); i++) k('Tab'); if (!live()) k('ArrowRight'); }
|
|
668
|
+
assert.ok(live(), 'some card answers in place');
|
|
669
|
+
const askId = () => (findAll(view, (x) => x.attrs && x.attrs['data-ask'])[0] || { attrs: {} }).attrs['data-ask'];
|
|
670
|
+
const was = askId();
|
|
671
|
+
const before = p.sent.length;
|
|
672
|
+
view.fire('keydown', { key: '1', preventDefault() {}, stopPropagation() {} });
|
|
673
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
674
|
+
assert.equal(p.sent.length, before, 'the first press only picks');
|
|
675
|
+
assert.equal(askId(), was, 'and stays on the same card');
|
|
676
|
+
assert.deepEqual(JSON.parse(JSON.stringify(view.__armed)), { id: was, n: 1 }, 'the pick is held, waiting for its confirm');
|
|
677
|
+
view.fire('keydown', { key: '1', preventDefault() {}, stopPropagation() {} });
|
|
678
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
679
|
+
assert.notEqual(askId(), was, 'the second press of the same number answers it, and the next card comes up');
|
|
680
|
+
assert.ok(d && d.isOpen());
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test('the decide table: one row per open ask, and the bulk bar answers every ticked row through its own route', async () => {
|
|
684
|
+
const p = await bootLiving({ asks: true });
|
|
685
|
+
p.window.GovCityPage.openDecide(null, 'table');
|
|
686
|
+
const view = p.byId.get('cityDecide');
|
|
687
|
+
const rows = () => findAll(view, (x) => x.tagName === 'tr' && x.parentNode && x.parentNode.tagName === 'tbody');
|
|
688
|
+
const all = rows();
|
|
689
|
+
assert.ok(all.length > 1, 'rows are drawn');
|
|
690
|
+
const answerable = all.filter((r) => findAll(r, (x) => /\bdecide__opt\b/.test(String(x.className)) && !('disabled' in x.attrs)).length);
|
|
691
|
+
assert.ok(answerable.length >= 1, 'a row answers in place');
|
|
692
|
+
const box = findAll(answerable[0], (x) => /\bdecide__row-box\b/.test(String(x.className)))[0];
|
|
693
|
+
box.checked = true;
|
|
694
|
+
box.fire('change');
|
|
695
|
+
const bulk = findAll(view, (x) => /\bdecide__bulk-btn\b/.test(String(x.className)));
|
|
696
|
+
assert.ok(bulk.length >= 1, 'a ticked row offers its answers in the bulk bar');
|
|
697
|
+
const before = p.sent.length;
|
|
698
|
+
bulk[0].fire('click');
|
|
699
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
700
|
+
assert.equal(p.sent.length, before + 1, 'one request per ticked row');
|
|
701
|
+
assert.match(p.sent[p.sent.length - 1].url, /^\/api\/bongos\//);
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('Automation opens as a switchboard through the page, and a flip sends the switch\'s own request', async () => {
|
|
705
|
+
const p = await bootLiving();
|
|
706
|
+
p.window.GovCityPage.openCard('automation');
|
|
707
|
+
const card = p.byId.get('cityCard');
|
|
708
|
+
assert.ok(/\bcity-card--board\b/.test(card.className), 'the card is a switchboard');
|
|
709
|
+
assert.equal(findAll(card, (x) => String(x.className).split(' ').includes('ask')).length, 0, 'no ask is drawn on it');
|
|
710
|
+
const toggles = findAll(card, (x) => /\bswb__toggle\b/.test(String(x.className)));
|
|
711
|
+
const none = findAll(card, (x) => /\bswb__none\b/.test(String(x.className)));
|
|
712
|
+
assert.ok(toggles.length || none.length, 'it draws the viewer\'s switches, or says they hold none');
|
|
713
|
+
if (toggles.length) {
|
|
714
|
+
const plain = toggles.find((t) => !findAll(t.parentNode.parentNode, (x) => /\bswb__why\b/.test(String(x.className))).length) || toggles[0];
|
|
715
|
+
const before = p.sent.length;
|
|
716
|
+
plain.fire('click');
|
|
717
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
718
|
+
const sentNow = p.sent.length - before;
|
|
719
|
+
const opened = findAll(card, (x) => /\bswb__why\b/.test(String(x.className)) && !('hidden' in x.attrs)).length;
|
|
720
|
+
assert.ok(sentNow === 1 || opened === 1, 'a flip sends one request, or asks why first');
|
|
721
|
+
if (sentNow) assert.match(p.sent[p.sent.length - 1].url, /\/api\/bongos\/autonomy\//);
|
|
722
|
+
}
|
|
723
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// tests/hall_city_rewording.mjs — a matter sent for a rewrite (the "?", owner
|
|
2
|
+
// 2026-10-23) leaves the to-do until its new words land, and is counted apart
|
|
3
|
+
// as "Rewording", building by building.
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import vm from 'node:vm';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
13
|
+
|
|
14
|
+
function boot() {
|
|
15
|
+
const ctx = { console, document: {} };
|
|
16
|
+
ctx.window = ctx;
|
|
17
|
+
vm.createContext(ctx);
|
|
18
|
+
vm.runInContext(fs.readFileSync(path.join(PUB, 'city-decide.js'), 'utf8'), ctx);
|
|
19
|
+
return ctx.window.GovCityDecide;
|
|
20
|
+
}
|
|
21
|
+
const DOCKET = { matters: [
|
|
22
|
+
{ id: 'blockers:1', building: 'planning', unclear: { times: 1 } },
|
|
23
|
+
{ id: 'blockers:2', building: 'planning' },
|
|
24
|
+
{ id: 'redteam_reports:3', building: 'disputes', unclear: { times: 2 } },
|
|
25
|
+
] };
|
|
26
|
+
|
|
27
|
+
test('a flagged matter is left out of the queue as if answered, and the answered stay out too', () => {
|
|
28
|
+
const G = boot();
|
|
29
|
+
const done = JSON.parse(JSON.stringify(G.withoutReworded({ 'blockers:9': true }, DOCKET)));
|
|
30
|
+
assert.deepEqual(Object.keys(done).sort(), ['blockers:1', 'blockers:9', 'redteam_reports:3']);
|
|
31
|
+
assert.equal(done['blockers:2'], undefined, 'an unflagged matter stays in the queue');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('Rewording counts per building, and everywhere', () => {
|
|
35
|
+
const G = boot();
|
|
36
|
+
assert.equal(G.rewordingCount(DOCKET, 'planning'), 1);
|
|
37
|
+
assert.equal(G.rewordingCount(DOCKET, 'disputes'), 1);
|
|
38
|
+
assert.equal(G.rewordingCount(DOCKET, 'treasury'), 0);
|
|
39
|
+
assert.equal(G.rewordingCount(DOCKET, null), 2);
|
|
40
|
+
assert.equal(G.rewordingCount({ matters: [] }, null), 0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('the "?" asks before it sends: the send lives behind a confirm button', () => {
|
|
44
|
+
const src = fs.readFileSync(path.join(PUB, 'city-decide.js'), 'utf8');
|
|
45
|
+
const fn = src.split('function unclearButton')[1].split('\n }\n')[0];
|
|
46
|
+
const send = fn.indexOf("on.send({ method: 'POST'");
|
|
47
|
+
const confirm = fn.indexOf("go.addEventListener('click'");
|
|
48
|
+
assert.ok(send > 0 && confirm > 0 && send > confirm, 'the request is sent only from the confirm button');
|
|
49
|
+
assert.match(fn, /Send this to Claude to rewrite\?/);
|
|
50
|
+
assert.match(fn, /Nothing is decided or changed/);
|
|
51
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// tests/hall_city_switchboard.mjs — Automation as a switchboard (owner, 2026-10-23).
|
|
2
|
+
// The board draws only the switches the docket hands THIS viewer, every flip
|
|
3
|
+
// sends the desk row's own request, and the fence is never a to-do.
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import vm from 'node:vm';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const PUB = path.join(ROOT, 'modules/hall-ui/public');
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const autoDocket = require(path.join(ROOT, 'modules/autonomy/governor-docket.js'));
|
|
16
|
+
const FIXTURE = JSON.parse(fs.readFileSync(path.join(ROOT, 'scripts/hall-preview/fixtures/government__docket.json'), 'utf8'));
|
|
17
|
+
const DOCKET = FIXTURE.body || FIXTURE;
|
|
18
|
+
|
|
19
|
+
function boot() {
|
|
20
|
+
const ctx = { console };
|
|
21
|
+
ctx.window = ctx;
|
|
22
|
+
vm.createContext(ctx);
|
|
23
|
+
for (const f of ['street-draw.js', 'street-card.js', 'street-inside.js', 'street-desk.js', 'city-switchboard.js']) vm.runInContext(fs.readFileSync(path.join(PUB, f), 'utf8'), ctx);
|
|
24
|
+
return ctx;
|
|
25
|
+
}
|
|
26
|
+
const j = (v) => JSON.parse(JSON.stringify(v));
|
|
27
|
+
const GOALS = [{ id: '1000089', title: 'Working area 3', status: 'open' }];
|
|
28
|
+
|
|
29
|
+
test('the fence is a standing state, never a to-do', () => {
|
|
30
|
+
for (const mayManage of [true, false]) {
|
|
31
|
+
for (const p of [{ enabled: false, goals: [] }, { enabled: true, goals: [] }]) {
|
|
32
|
+
assert.equal(autoDocket.fenceDocketMatters(p, mayManage)[0].state, 'settled');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
assert.deepEqual(autoDocket.switchDeskRows({ enabled: false, goals: [] })[0].acts, [{ key: 'switch_on' }], 'off: the master row switches it on');
|
|
36
|
+
assert.equal(autoDocket.switchDeskRows({ enabled: true, goals: [] })[0].acts[0].key, 'switch_off', 'on, even with no goal: the master row switches it off');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('the board draws the viewer\'s switches, and each flip is the desk row\'s request', () => {
|
|
40
|
+
const w = boot();
|
|
41
|
+
const m = j(w.GovCitySwitchboard.model(DOCKET, { now: Date.parse('2026-10-09T15:00:00Z'), goals: GOALS }));
|
|
42
|
+
assert.deepEqual(m.groups.map((g) => g.title), ['AI builds on its own (Autobongos)', 'Goals the AI may work on', 'Computers doing the building']);
|
|
43
|
+
const master = m.groups[0].switches[0];
|
|
44
|
+
assert.equal(master.on, false);
|
|
45
|
+
const live = w.GovCitySwitchboard.model(DOCKET, { now: Date.parse('2026-10-09T15:00:00Z'), goals: GOALS });
|
|
46
|
+
const flip = live.groups[0].switches[0].flip;
|
|
47
|
+
assert.deepEqual(j(w.GovMatterCard.request(flip.row.card, flip.key, flip.input)), { method: 'POST', url: '/api/bongos/autonomy/fence', body: { enabled: true } });
|
|
48
|
+
const goal = live.groups[1].switches[0];
|
|
49
|
+
assert.equal(goal.on, false, 'an open goal not yet allowed is an off switch');
|
|
50
|
+
assert.deepEqual(j(w.GovMatterCard.request(goal.flip.row.card, goal.flip.key, goal.flip.input)).body, { goal_id: 1000089 });
|
|
51
|
+
const runner = live.groups[2].switches[0];
|
|
52
|
+
assert.equal(runner.on, true);
|
|
53
|
+
assert.ok(runner.flip.reasons.length > 0, 'pausing a runner asks why, in one tap');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('a viewer handed no Automation desks gets no switches', () => {
|
|
57
|
+
const w = boot();
|
|
58
|
+
const bare = j(DOCKET);
|
|
59
|
+
bare.desks = bare.desks.filter((d) => !/^automation\./.test(d.key));
|
|
60
|
+
const m = j(w.GovCitySwitchboard.model(bare, { goals: GOALS }));
|
|
61
|
+
assert.equal(m.any, false);
|
|
62
|
+
assert.deepEqual(m.groups, []);
|
|
63
|
+
});
|
|
@@ -555,9 +555,9 @@ test('a Lead the government appointed stands on their building on a real site; e
|
|
|
555
555
|
assert.match(hit('planning').attrs['aria-label'], new RegExp('led by ' + A_REAL_LEAD));
|
|
556
556
|
assert.match(hit('release').attrs['aria-label'], /held by the owner/);
|
|
557
557
|
hit('planning').fire('click');
|
|
558
|
-
assert.match(city.byId.get('cityCard').
|
|
558
|
+
assert.match(city.byId.get('cityCard').children[0].children[0].attrs.title, new RegExp('Led by ' + A_REAL_LEAD + '\\.'));
|
|
559
559
|
hit('treasury').fire('click');
|
|
560
|
-
assert.match(city.byId.get('cityCard').
|
|
560
|
+
assert.match(city.byId.get('cityCard').children[0].children[0].attrs.title, /hold(s)? it until a Lead is seated\./);
|
|
561
561
|
noPrototype(city, 'city card with real Leads', { seatsRead: true });
|
|
562
562
|
const street = await bootStreet('', false);
|
|
563
563
|
assert.ok(street.calls.includes('/api/bongos/government/leads'), 'the street reads it too');
|
|
@@ -141,7 +141,6 @@ test('a real answer is the desk row\'s own request, to the route the desk uses',
|
|
|
141
141
|
'redteam_reports:confirm': 'POST /api/bongos/security/reports/N/confirm',
|
|
142
142
|
'redteam_reports:dispute': 'POST /api/bongos/security/reports/N/dispute',
|
|
143
143
|
'override_requests:release': 'POST /api/bongos/override-requests/N/decide',
|
|
144
|
-
'autobongos_fence:switch_on': 'POST /api/bongos/autonomy/fence',
|
|
145
144
|
'treasury_spending:answer': 'POST /api/bongos/government/treasury/borrows/N/answer',
|
|
146
145
|
});
|
|
147
146
|
// a blocker's option is its own chip, so its words are the resolution note
|
|
@@ -151,8 +150,11 @@ test('a real answer is the desk row\'s own request, to the route the desk uses',
|
|
|
151
150
|
const cr = K.cardView(lv.city.asks.find((x) => x.id === 'criteria_uat:1000184'), c);
|
|
152
151
|
assert.deepEqual(cr.options.map((o) => o.plan.mode), ['record', 'send', 'note']);
|
|
153
152
|
assert.equal(cr.options[0].plan.id, '1000184');
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
// the fence is a standing state on the Automation switchboard, never a to-do
|
|
154
|
+
// (task 1004784): its matter is settled and carries no act, so the switch is
|
|
155
|
+
// flipped from the switchboard's desk row, not from a card
|
|
156
|
+
const fence = lv.city.asks.find((x) => x.id === 'autobongos_fence:fence');
|
|
157
|
+
assert.ok(!fence || fence.state !== 'needs', 'the fence never waits on anyone');
|
|
156
158
|
});
|
|
157
159
|
|
|
158
160
|
test('a viewer the server hands no acts gets answers marked not from here, never a send', () => {
|
|
@@ -492,7 +494,7 @@ test('the page: #deck draws the top card over the street; a key answers it throu
|
|
|
492
494
|
assert.equal(next.getAttribute('data-ask'), 'blockers:1000143');
|
|
493
495
|
assert.match(next.className, /\bask--rise\b/, 'the next one rises');
|
|
494
496
|
assert.equal(p.cls(p.deck(), 'deck-dock__of')[0].textContent, ' of 8 to do');
|
|
495
|
-
assert.equal(p.byId.get('hudNeeds').textContent, '
|
|
497
|
+
assert.equal(p.byId.get('hudNeeds').textContent, '36', 'the street\'s header moves with the deck');
|
|
496
498
|
});
|
|
497
499
|
|
|
498
500
|
test('the page: a refusal keeps the card and counts nothing; S skips to the back', async () => {
|
|
@@ -276,7 +276,7 @@ test('what needs you lights salmon windows on the floor that holds it, with a ri
|
|
|
276
276
|
assert.ok(d.windows.some((w) => w.building === 'planning'), 'the viewer leads Planning');
|
|
277
277
|
assert.ok(!d.windows.some((w) => ['review', 'release', 'treasury'].includes(w.building)), 'what waits on another Lead lights nothing');
|
|
278
278
|
// the hub as it is (no Lead read, everything on the owner seat): every docket need is the viewer's own
|
|
279
|
-
assert.deepEqual(street({ leads: null, docket: fx('government__docket') }).depth.windows.map((w) => w.key), ['needs:
|
|
279
|
+
assert.deepEqual(street({ leads: null, docket: fx('government__docket') }).depth.windows.map((w) => w.key), ['needs:disputes:0', 'needs:planning:2', 'needs:release:0', 'needs:review:1', 'needs:treasury:4']);
|
|
280
280
|
// moving and settled light nothing; nor does a source no floor holds
|
|
281
281
|
const doc2 = clone(doc);
|
|
282
282
|
doc2.matters.forEach((m) => { m.state = 'moving'; });
|
|
@@ -404,7 +404,8 @@ function actedDocket({ longWords = false, force = true } = {}) {
|
|
|
404
404
|
desk('people.builders', 'people', buildersRoute.builderDeskRows([{ id: 7, display_name: t('Ana Ruiz'), github_login: 'ana', rank: 'thetes' }, { id: 95, display_name: 'Me', rank: 'archon' }], { viewerId: '95', mayRank: true, mayStop: true, ranks: LADDER })),
|
|
405
405
|
desk('people.ranks', 'people', govFloors.rankDeskRows([{ rank_key: 'tester', label: t('Tester'), is_system: false }, { rank_key: 'archon', label: 'Archon', is_system: true }], [{ builder_id: 7, display_name: 'Ana Ruiz', rank_key: 'tester' }, { builder_id: 8, github_login: 'kai', rank_key: 'metic' }])),
|
|
406
406
|
desk('disputes.confirmed', 'disputes', [reportsRoute.confirmedDeskRow({ id: 3, title: t('token leak'), severity: 'low', confirmed_at: '2026-09-28T10:00:00.000Z' })]),
|
|
407
|
-
|
|
407
|
+
// the switch is a standing row in either position (the fence matter is settled, never a to-do)
|
|
408
|
+
desk('automation.switch', 'automation', autoDocket.switchDeskRows({ enabled: false, paused_reason: 'payments touched', goals: [] }).map((r) => ({ ...r, id: 'switch-off' })).concat(autoDocket.switchDeskRows(project))),
|
|
408
409
|
desk('automation.goals', 'automation', autoDocket.goalDeskRows(project)),
|
|
409
410
|
desk('automation.runners', 'automation', autoDocket.runnerDeskRows([{ builder_id: 7, display_name: t('Ana Ruiz'), runners: [{ working_task_id: 12 }] }, { builder_id: 8, owner_paused: true, owner_paused_reason: 'spending too much', runners: [] }], NOW, healthy)),
|
|
410
411
|
];
|
|
@@ -469,10 +470,11 @@ test('Disputes: a report is confirmed or disputed, a confirmed one marked fixed,
|
|
|
469
470
|
test('Automation: switch on and off, allow a goal from the open goals, first in line, disallow, pause and resume a runner', () => {
|
|
470
471
|
const dk = actedDocket();
|
|
471
472
|
const sw = actedRows(dk, 'automation', 0);
|
|
472
|
-
|
|
473
|
+
assert.ok(!sw.some((r) => r.source === 'autobongos_fence'), 'the switch is not a to-do: its matter is settled');
|
|
474
|
+
const off = sw.find((r) => r.actions.some((a) => a.key === 'switch_on'));
|
|
473
475
|
assert.deepEqual(off.actions.map((a) => a.label), ['switch on']);
|
|
474
476
|
assert.deepEqual(j(C.request(off.card, 'switch_on')), { method: 'POST', url: '/api/bongos/autonomy/fence', body: { enabled: true } });
|
|
475
|
-
const on = sw.find((r) => r.
|
|
477
|
+
const on = sw.find((r) => r.actions.some((a) => a.key === 'switch_off'));
|
|
476
478
|
assert.deepEqual(j(C.request(on.card, 'switch_off', on.choices.switch_off.options[0].input)).body, { enabled: false, reason: 'pause for now' });
|
|
477
479
|
const goals = actedRows(dk, 'automation', 1);
|
|
478
480
|
const first = goals.find((r) => r.name === 'Governor city');
|
|
@@ -805,6 +805,32 @@ try {
|
|
|
805
805
|
assert.equal(b.el('tw-frame').hidden, true);
|
|
806
806
|
});
|
|
807
807
|
|
|
808
|
+
// task 1004776: a guarded line shows what it must still say under it, and the
|
|
809
|
+
// note turns to a warning while the artist types words that drop it.
|
|
810
|
+
await test('a guarded line shows its must-still-say note, and warns as the artist types it away', async () => {
|
|
811
|
+
const guarded = require('../modules/copy-desk/guarded-lines.js');
|
|
812
|
+
guarded._setList({ lines: [{ page_id: 'landing:index', file: 'modules/public-landing/public/index.html', must_contain: ['ship the code'], must_still_say: 'Agents ship the code.' }] });
|
|
813
|
+
try {
|
|
814
|
+
const ed = editorRead({ draftLines: null });
|
|
815
|
+
const b = bootEditor({ answers: { 'GET /copy-desk/pages/landing:index/draft': ok(ed) } });
|
|
816
|
+
await b.settle();
|
|
817
|
+
assert.equal(b.el('tw-L0004-note').textContent, 'must still say: Agents ship the code.');
|
|
818
|
+
assert.match(b.el('tw-L0004-note').className, /tw-note--guarded/);
|
|
819
|
+
b.type('L0004', 'Agents claim the tasks.');
|
|
820
|
+
assert.equal(b.el('tw-L0004-note').textContent, 'this no longer says what it must · must still say: Agents ship the code.');
|
|
821
|
+
b.type('L0004', 'Our agents ship the code.');
|
|
822
|
+
assert.equal(b.el('tw-L0004-note').textContent, 'must still say: Agents ship the code.');
|
|
823
|
+
} finally {
|
|
824
|
+
guarded._setList(null);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
await test('the submit warning is plain words the editor shows after sending', () => {
|
|
829
|
+
assert.deepEqual(lib.submitWarningHtml([{ key: 'L0004', must_still_say: 'Agents ship the code.' }]),
|
|
830
|
+
['A line you changed no longer says what it must: Agents ship the code. It was sent anyway; the person approving it will see this.']);
|
|
831
|
+
assert.deepEqual(lib.submitWarningHtml(undefined), []);
|
|
832
|
+
});
|
|
833
|
+
|
|
808
834
|
await test('the page writes only the copy-desk routes it names', () => {
|
|
809
835
|
const js = read('tweak-editor.js');
|
|
810
836
|
const writes = [...js.matchAll(/api\.request\('(POST|PUT|PATCH|DELETE)', ([^,]+),/g)].map((m) => `${m[1]} ${m[2]}`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// tests/module_channel_install.mjs — install and update honour the channel a
|
|
2
|
+
// project is on (task 1003805, ADR 0344 D1/D2, ADR 0338 D2).
|
|
3
|
+
//
|
|
4
|
+
// WHY. The channels exist (core_276) and new versions land on tester (task
|
|
5
|
+
// 1003803), but until acquire reads them, every project got the newest version
|
|
6
|
+
// whatever its channel. These pin the rule: a tester project gets the newest on
|
|
7
|
+
// tester or general, everyone else the newest on general, nobody a withdrawn one,
|
|
8
|
+
// and `bongos upgrade` reports the same version update would hand over. Run
|
|
9
|
+
// against a fake pg client; the routes are pinned in their source.
|
|
10
|
+
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const { servedChannels, resolveAcquirable, latestVersions } = require('../src/bongos/module-store.js');
|
|
21
|
+
const { testsVersionsOf } = require('../src/bongos/module-testing.js');
|
|
22
|
+
|
|
23
|
+
const row = (version, channel) => ({ id: 1, module_key: 'weather', version, channel, core_version: '^1.0.0', tarball_sha256: 'a'.repeat(64), tree_sha256: 'b'.repeat(64), tarball_bytes: 10, artifact_path: `weather/weather-${version}.tgz` });
|
|
24
|
+
|
|
25
|
+
function fakeDb(versions) {
|
|
26
|
+
return {
|
|
27
|
+
async query(sql) {
|
|
28
|
+
if (sql.includes('FROM store_modules ')) return { rows: [{ module_key: 'weather', status: 'listed' }] };
|
|
29
|
+
if (sql.includes('FROM store_module_versions')) return { rows: versions };
|
|
30
|
+
if (sql.includes('FROM store_module_prices')) return { rows: [] };
|
|
31
|
+
throw new Error(`unhandled: ${sql}`);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const SHELF = [row('1.0.0', 'general'), row('1.1.0', 'general'), row('1.2.0', 'tester'), row('2.0.0', 'withdrawn')];
|
|
36
|
+
const GENERAL = servedChannels(false);
|
|
37
|
+
const TESTER = servedChannels(true);
|
|
38
|
+
|
|
39
|
+
test('a tester project takes tester and general versions, everyone else general, nobody withdrawn', () => {
|
|
40
|
+
assert.deepEqual(TESTER, ['tester', 'general']);
|
|
41
|
+
assert.deepEqual(GENERAL, ['general']);
|
|
42
|
+
for (const c of [TESTER, GENERAL]) assert.ok(!c.includes('withdrawn'));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('"general only" keeps a tester project on general for that one module', () => {
|
|
46
|
+
const testing = { tester: true, general_only: ['weather'] };
|
|
47
|
+
assert.deepEqual(servedChannels(testsVersionsOf(testing, 'weather')), GENERAL);
|
|
48
|
+
assert.deepEqual(servedChannels(testsVersionsOf(testing, 'clock')), TESTER);
|
|
49
|
+
assert.deepEqual(servedChannels(testsVersionsOf(null, 'clock')), GENERAL, 'an unreadable choice is not a tester');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('with no version asked for, each project gets the newest on its own channels', async () => {
|
|
53
|
+
const general = await resolveAcquirable('weather', { channels: GENERAL, db: fakeDb(SHELF) });
|
|
54
|
+
assert.equal(general.version.version, '1.1.0');
|
|
55
|
+
assert.equal(general.version.channel, 'general');
|
|
56
|
+
const tester = await resolveAcquirable('weather', { channels: TESTER, db: fakeDb(SHELF) });
|
|
57
|
+
assert.equal(tester.version.version, '1.2.0', 'the newer withdrawn 2.0.0 is never handed out');
|
|
58
|
+
assert.equal(tester.version.channel, 'tester');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('an exact version off the project\'s channels is refused, never handed over', async () => {
|
|
62
|
+
const early = await resolveAcquirable('weather', { version: '1.2.0', channels: GENERAL, db: fakeDb(SHELF) });
|
|
63
|
+
assert.equal(early.ok, false);
|
|
64
|
+
assert.equal(early.code, 'version_not_on_channel');
|
|
65
|
+
assert.match(early.message, /testers/);
|
|
66
|
+
const gone = await resolveAcquirable('weather', { version: '2.0.0', channels: TESTER, db: fakeDb(SHELF) });
|
|
67
|
+
assert.equal(gone.code, 'version_not_on_channel');
|
|
68
|
+
assert.match(gone.message, /withdrawn/);
|
|
69
|
+
assert.equal((await resolveAcquirable('weather', { version: '1.0.0', channels: GENERAL, db: fakeDb(SHELF) })).ok, true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('a module whose only version is still with testers has nothing for a general project yet', async () => {
|
|
73
|
+
const r = await resolveAcquirable('weather', { channels: GENERAL, db: fakeDb([row('1.0.0', 'tester')]) });
|
|
74
|
+
assert.equal(r.code, 'no_versions_on_channel');
|
|
75
|
+
assert.equal(r.status, 404);
|
|
76
|
+
assert.match(r.message, /testers/);
|
|
77
|
+
const gone = await resolveAcquirable('weather', { channels: TESTER, db: fakeDb([row('1.0.0', 'withdrawn')]) });
|
|
78
|
+
assert.equal(gone.code, 'no_versions_on_channel');
|
|
79
|
+
assert.match(gone.message, /withdrawn/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('no channels means no channel check — recording a version already placed', async () => {
|
|
83
|
+
const r = await resolveAcquirable('weather', { version: '2.0.0', db: fakeDb(SHELF) });
|
|
84
|
+
assert.equal(r.ok, true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('upgrade reports the newest version on the channels this project takes', async () => {
|
|
88
|
+
const db = { async query() { return { rows: SHELF.map((r) => ({ ...r, status: 'listed' })) }; } };
|
|
89
|
+
const general = await latestVersions(['weather'], { db, channelsFor: () => GENERAL });
|
|
90
|
+
assert.equal(general.weather.version, '1.1.0');
|
|
91
|
+
const tester = await latestVersions(['weather'], { db, channelsFor: () => TESTER });
|
|
92
|
+
assert.equal(tester.weather.version, '1.2.0');
|
|
93
|
+
assert.equal(tester.weather.channel, 'tester');
|
|
94
|
+
const none = await latestVersions(['weather'], { db, channelsFor: () => ['nothing'] });
|
|
95
|
+
assert.deepEqual(none, {});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('the acquire, download and latest routes ask the project\'s channels; recording does not', () => {
|
|
99
|
+
const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/modules.js'), 'utf8');
|
|
100
|
+
const route = (decl) => {
|
|
101
|
+
const start = src.indexOf(decl);
|
|
102
|
+
assert.ok(start !== -1, decl);
|
|
103
|
+
return src.slice(start, src.indexOf('}));', start));
|
|
104
|
+
};
|
|
105
|
+
const acquire = route("router.post('/store/modules/:key/acquire'");
|
|
106
|
+
assert.match(acquire, /resolveAcquirable\(key, \{ version: wanted \|\| null, channels: \(await projectChannels\(\)\)\(key\) \}\)/);
|
|
107
|
+
assert.match(acquire, /channel: v\.channel,/);
|
|
108
|
+
assert.doesNotMatch(acquire, /channel: 'general'/);
|
|
109
|
+
assert.match(route("router.get('/store/modules/:key/versions/:version/tarball'"), /channels: \(await projectChannels\(\)\)\(key\)/);
|
|
110
|
+
assert.match(route("router.get('/store/modules/latest'"), /latestVersions\(keys, \{ channelsFor: await projectChannels\(\) \}\)/);
|
|
111
|
+
assert.doesNotMatch(route("router.post('/store/modules/:key/acquired'"), /channels/);
|
|
112
|
+
});
|