@bongos/core 1.25.29 → 1.25.31
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 +35 -30
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -6
- package/docs/copy-inventory.md +17 -17
- package/docs/copy-registry.json +17 -17
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +358 -385
- package/modules/government/docket.js +37 -6
- package/modules/hall-ui/public/city.js +1 -1
- package/modules/hall-ui/public/craft-mode.js +4 -0
- package/modules/hall-ui/public/gov-feeds.js +2 -2
- package/modules/hall-ui/public/home-city.js +9 -1
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/craft_mode_home.mjs +12 -1
- package/tests/docket_load_speed.mjs +99 -0
- package/tests/hall_city_living.mjs +1 -1
- package/tests/hall_gov_counts.mjs +1 -1
- package/tests/hall_gov_real_site.mjs +1 -1
- package/tests/hall_street_deck.mjs +1 -1
- package/tests/hall_street_depth.mjs +2 -2
- package/tests/home_city_widget.mjs +12 -1
|
@@ -631,6 +631,12 @@ async function attachAsks(matters, readAsks, timeoutMs) {
|
|
|
631
631
|
// task 1004705) and the docket says `routed: true`. null routes nothing.
|
|
632
632
|
async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null, readAsks = null, readFlags = null, seats = null } = {}) {
|
|
633
633
|
const held = new Set(permissions || []);
|
|
634
|
+
// `decisions`, `government` and `seats` may arrive as promises (docketFor starts
|
|
635
|
+
// those reads alongside the sources, task 1005310); they are awaited once the
|
|
636
|
+
// sources have answered. The handler is attached here, before anything waits, so
|
|
637
|
+
// a failure is never an unhandled rejection while the sources are reading.
|
|
638
|
+
const governmentReads = Promise.all([decisions, government, seats]);
|
|
639
|
+
governmentReads.catch(() => {});
|
|
634
640
|
const ctx = Object.freeze({
|
|
635
641
|
builderId: String(viewer.id),
|
|
636
642
|
rank: viewer.rank || null,
|
|
@@ -681,8 +687,11 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
681
687
|
const order = declared.map((d) => d.source);
|
|
682
688
|
sourceStatus.sort((a, b) => order.indexOf(a.source) - order.indexOf(b.source));
|
|
683
689
|
matters.sort(compareMatters);
|
|
684
|
-
|
|
685
|
-
|
|
690
|
+
// Asks and reword flags are two independent reads on the same matters, each
|
|
691
|
+
// writing its own field: read together (task 1005310), alongside the
|
|
692
|
+
// government reads started above.
|
|
693
|
+
await Promise.all([attachAsks(matters, readAsks, timeoutMs), attachFlags(matters, readFlags, timeoutMs)]);
|
|
694
|
+
[decisions, government, seats] = await governmentReads;
|
|
686
695
|
// THE DIRECTION DESK (task 1004705): who each matter waits on, from real seats.
|
|
687
696
|
const yours = seats ? routing.routeAll(matters, ctx.builderId, seats, now) : null;
|
|
688
697
|
|
|
@@ -872,16 +881,38 @@ async function findMatterFor(builder, matterId) {
|
|
|
872
881
|
|
|
873
882
|
// The route's entry point: resolve the viewer's permissions server-side (fresh,
|
|
874
883
|
// uncached — ADR 0016), then build.
|
|
875
|
-
|
|
884
|
+
//
|
|
885
|
+
// ONE BUILD PER VIEWER AT A TIME (task 1005310). The street asks for the docket
|
|
886
|
+
// and for GET /government/counts in the same breath, and counts is built from
|
|
887
|
+
// this same docket, so every street load built it twice, side by side, and each
|
|
888
|
+
// slowed the other. A second ask for the SAME viewer that arrives while a build
|
|
889
|
+
// is running shares that build; the moment it finishes nothing is kept, so the
|
|
890
|
+
// next ask builds afresh. Nothing is cached: no answer is older than the request
|
|
891
|
+
// that receives it, and permissions are still resolved inside every build.
|
|
892
|
+
const building = new Map();
|
|
893
|
+
function docketFor(builder) {
|
|
894
|
+
const key = String(builder && builder.id);
|
|
895
|
+
const running = building.get(key);
|
|
896
|
+
if (running) return running;
|
|
897
|
+
const p = buildDocketFor(builder).finally(() => { building.delete(key); });
|
|
898
|
+
building.set(key, p);
|
|
899
|
+
return p;
|
|
900
|
+
}
|
|
901
|
+
async function buildDocketFor(builder) {
|
|
876
902
|
const permissions = await resolver.resolveBuilderPermissions(builder.id);
|
|
877
903
|
// The table is read per request, uncached (ADR 0016). A failed read leaves
|
|
878
904
|
// every building's decision null; it never fails the city.
|
|
879
|
-
|
|
905
|
+
// The government reads (decision rules → the government → its seats) run
|
|
906
|
+
// ALONGSIDE the sources (task 1005310): buildDocket waits for them only once
|
|
907
|
+
// its sources have answered. Every step below catches its own failure.
|
|
908
|
+
const decisions = board.decisionRulesView().catch((err) => {
|
|
880
909
|
log.warn({ err: err && err.message }, 'governor docket: decision rules unreadable');
|
|
881
910
|
return null;
|
|
882
911
|
});
|
|
883
|
-
const government = permissions.includes(GOVERNMENT_GATE.permission)
|
|
884
|
-
|
|
912
|
+
const government = permissions.includes(GOVERNMENT_GATE.permission)
|
|
913
|
+
? decisions.then((d) => loadGovernment({ decisions: d }))
|
|
914
|
+
: Promise.resolve(null);
|
|
915
|
+
const seats = government.then((g) => (g ? loadSeats(g) : null));
|
|
885
916
|
return buildDocket({ viewer: builder, permissions, decisions, government, readAsks: asksDb.asksFor, readFlags: asksDb.unclearFor, seats });
|
|
886
917
|
}
|
|
887
918
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
leads: '/api/bongos/government/leads',
|
|
57
57
|
// the pots each goal draws from, the reserve, the spend cap (task 1004712)
|
|
58
58
|
treasury: '/api/bongos/government/treasury',
|
|
59
|
-
claims: '/api/bongos/tasks/claimable?
|
|
59
|
+
claims: '/api/bongos/tasks/claimable?view=claims',
|
|
60
60
|
tasks: '/api/bongos/tasks?status=active&limit=200',
|
|
61
61
|
cost: '/api/bongos/public/cost-summary',
|
|
62
62
|
roster: '/api/bongos/builders/roster',
|
|
@@ -265,6 +265,10 @@
|
|
|
265
265
|
return false;
|
|
266
266
|
}
|
|
267
267
|
function homeHref(mode) {
|
|
268
|
+
// In game mode the street is not a place (shell-game.js would send it back
|
|
269
|
+
// home), so the patron's home there is Home itself (task 1005307).
|
|
270
|
+
var g = window.OTBShellGame;
|
|
271
|
+
if (g && typeof g.on === 'function' && g.on() === true) return '/';
|
|
268
272
|
var street = mode === 'patron' && window.GovCity && window.GovCity.STREET_HREF;
|
|
269
273
|
return street || '/';
|
|
270
274
|
}
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
presets: '/government/presets',
|
|
43
43
|
goals: '/goals',
|
|
44
44
|
rollups: '/goals/rollups',
|
|
45
|
-
// the
|
|
46
|
-
claims: '/tasks/claimable?
|
|
45
|
+
// the open claims alone (?view=claims, task 1005310): the claimable feed is not built
|
|
46
|
+
claims: '/tasks/claimable?view=claims',
|
|
47
47
|
tasks: '/tasks?status=active&limit=200',
|
|
48
48
|
cost: '/public/cost-summary',
|
|
49
49
|
roster: '/builders/roster',
|
|
@@ -355,9 +355,17 @@
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
// ---- the governor's Home is the street; tests swap the hop for a recorder ----
|
|
358
|
+
// Classic view only (task 1005307): in game mode the street is not a place, so
|
|
359
|
+
// shell-game.js sends it straight back here — hopping from Home made an endless
|
|
360
|
+
// reload loop. The game's patron stays on the city and walks on from the visor.
|
|
361
|
+
// shell-game.js is the one answer: a page without it is never in game mode.
|
|
362
|
+
function gameOn() {
|
|
363
|
+
const g = window.OTBShellGame;
|
|
364
|
+
return !!(g && typeof g.on === 'function' && g.on() === true);
|
|
365
|
+
}
|
|
358
366
|
function streetHop() {
|
|
359
367
|
const href = window.GovCity && window.GovCity.STREET_HREF;
|
|
360
|
-
if (document.documentElement.dataset.craft !== 'patron' || !href) return false;
|
|
368
|
+
if (document.documentElement.dataset.craft !== 'patron' || !href || gameOn()) return false;
|
|
361
369
|
(window.OTBHomeStreetHop || ((h) => window.location.assign(h)))(href);
|
|
362
370
|
return true;
|
|
363
371
|
}
|
|
@@ -219,7 +219,7 @@ module.exports = function buildTasksRouter() {
|
|
|
219
219
|
// RETURNED feed, never the query — lifecycle/CLAUDE.md has the why.
|
|
220
220
|
const claimNeeds = parseNeedsFilter(req.query.needs); if (claimNeeds && claimNeeds.bad) return badNeedsFilter(res); // task 1004950
|
|
221
221
|
const claimLimit = posInt(req.query.limit, 'bad_limit', 1000);
|
|
222
|
-
if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' });
|
|
222
|
+
if (claimLimit && claimLimit.bad) return res.fail('bad_limit', { status: 400, message: 'limit must be a positive integer (max 1000).' }); if (req.query.view === 'claims') return res.json({ active_claims: await db.listActiveClaims() }); // open claims only, no claimable feed: the street needs just these (task 1005310)
|
|
223
223
|
const [feed, active] = await Promise.all([
|
|
224
224
|
db.listClaimableTasks({
|
|
225
225
|
versionId: version || null, builderId: req.builder.id,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.31",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.31",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.31",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -11949,5 +11949,17 @@
|
|
|
11949
11949
|
"id": "1005299",
|
|
11950
11950
|
"text": "The city's sky clock no longer says \"hour 24\" in the last three minutes before midnight, and its check no longer fails at random late at night."
|
|
11951
11951
|
}
|
|
11952
|
+
],
|
|
11953
|
+
"1.25.30": [
|
|
11954
|
+
{
|
|
11955
|
+
"id": "1005307",
|
|
11956
|
+
"text": "Choosing Patron in game mode no longer reloads the hall endlessly; it settles on the city."
|
|
11957
|
+
}
|
|
11958
|
+
],
|
|
11959
|
+
"1.25.31": [
|
|
11960
|
+
{
|
|
11961
|
+
"id": "1005310",
|
|
11962
|
+
"text": "The governor's street asks the server for less and gets its answers sooner."
|
|
11963
|
+
}
|
|
11952
11964
|
]
|
|
11953
11965
|
}
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.31'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -31,7 +31,7 @@ function el() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// One page: `page` is body[data-page], `active` the nav item the shell marked.
|
|
34
|
-
function boot({ page, active = null, govCity = false }) {
|
|
34
|
+
function boot({ page, active = null, govCity = false, game = false }) {
|
|
35
35
|
const hops = [];
|
|
36
36
|
const body = el();
|
|
37
37
|
body.setAttribute('data-page', page);
|
|
@@ -55,6 +55,7 @@ function boot({ page, active = null, govCity = false }) {
|
|
|
55
55
|
location: { search: '' },
|
|
56
56
|
};
|
|
57
57
|
if (govCity) window.GovCity = { STREET_HREF: '/builders/street' };
|
|
58
|
+
if (game) window.OTBShellGame = { on: () => true };
|
|
58
59
|
window.window = window;
|
|
59
60
|
vm.runInNewContext(CRAFT, { window, document, CustomEvent: function () {}, Promise, console });
|
|
60
61
|
return { CM: window.OTBCraftMode, hops };
|
|
@@ -83,6 +84,16 @@ test('choosing Patron on another craft\'s page lands on the street', () => {
|
|
|
83
84
|
assert.equal(boot({ page: 'studio' }).CM.homeHref('patron'), '/');
|
|
84
85
|
});
|
|
85
86
|
|
|
87
|
+
// task 1005307: in game mode the street is not a place (shell-game.js sends it back
|
|
88
|
+
// Home, and Home sent a patron to the street again: an endless reload loop), so a
|
|
89
|
+
// stranded page goes straight Home.
|
|
90
|
+
test('in game mode, choosing Patron on another craft\'s page goes Home, never the street', () => {
|
|
91
|
+
const { CM, hops } = boot({ page: 'studio', active: 'studio', govCity: true, game: true });
|
|
92
|
+
assert.equal(CM.homeHref('patron'), '/');
|
|
93
|
+
CM.choose('patron');
|
|
94
|
+
assert.deepEqual(hops, ['/']);
|
|
95
|
+
});
|
|
96
|
+
|
|
86
97
|
test('a page the chosen craft also shows stays put', () => {
|
|
87
98
|
// goals is in Artisan and Artist alike
|
|
88
99
|
const { CM, hops } = boot({ page: 'goals', active: 'goals' });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// tests/docket_load_speed.mjs — the governor's street loads faster (task 1005310).
|
|
2
|
+
//
|
|
3
|
+
// Measured on the live hub: the street asked for GET /government/docket and
|
|
4
|
+
// GET /government/counts together, counts rebuilt the same docket, and each build
|
|
5
|
+
// waited for the government reads before it even started its sources. Three
|
|
6
|
+
// properties now hold, and each is driven here against the real docket.js:
|
|
7
|
+
// 1. two asks for the SAME viewer while a build runs share that one build, and
|
|
8
|
+
// nothing is kept once it finishes (no cache: the next ask builds afresh);
|
|
9
|
+
// 2. the sources start reading before the government reads have answered;
|
|
10
|
+
// 3. a government read that fails is never an unhandled rejection while the
|
|
11
|
+
// sources are still reading, and the docket still answers or fails as before.
|
|
12
|
+
// And the street's claims read (GET /tasks/claimable?view=claims) answers only the
|
|
13
|
+
// open claims, without building the claimable feed.
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import { test, after } from 'node:test';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
24
|
+
const docket = require(path.join(ROOT, 'modules', 'government', 'docket.js'));
|
|
25
|
+
const board = require(path.join(ROOT, 'modules', 'government', 'board.js'));
|
|
26
|
+
const resolver = require(path.join(ROOT, 'modules', 'government', 'resolver.js'));
|
|
27
|
+
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
|
|
28
|
+
|
|
29
|
+
const REAL = { perms: resolver.resolveBuilderPermissions, rules: board.decisionRulesView };
|
|
30
|
+
after(() => { resolver.resolveBuilderPermissions = REAL.perms; board.decisionRulesView = REAL.rules; });
|
|
31
|
+
|
|
32
|
+
const order = [];
|
|
33
|
+
let permReads = 0;
|
|
34
|
+
// The docket's own atom only: no government gate, so no government tables are read.
|
|
35
|
+
resolver.resolveBuilderPermissions = async () => { permReads += 1; return ['board.vote.cast']; };
|
|
36
|
+
board.decisionRulesView = () => new Promise((r) => setTimeout(() => { order.push('decisions'); r(null); }, 40));
|
|
37
|
+
api.contribute(docket.CONTRIBUTION_POINT, {
|
|
38
|
+
source: 'blockers', building: 'planning', gate: { anyBuilder: true },
|
|
39
|
+
read: async () => { order.push('source'); return []; },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('two asks for one viewer while a build runs share it; the next ask builds afresh', async () => {
|
|
43
|
+
permReads = 0;
|
|
44
|
+
const viewer = { id: 7001, rank: 'metic' };
|
|
45
|
+
const [a, b] = await Promise.all([docket.docketFor(viewer), docket.docketFor({ ...viewer })]);
|
|
46
|
+
assert.equal(permReads, 1, 'one build for both asks');
|
|
47
|
+
assert.equal(a, b, 'both asks receive the same answer');
|
|
48
|
+
await docket.docketFor(viewer);
|
|
49
|
+
assert.equal(permReads, 2, 'a finished build is not kept: the next ask builds again');
|
|
50
|
+
await Promise.all([docket.docketFor({ id: 7002 }), docket.docketFor({ id: 7003 })]);
|
|
51
|
+
assert.equal(permReads, 4, 'different viewers never share a build');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the sources start reading before the government reads answer', async () => {
|
|
55
|
+
order.length = 0;
|
|
56
|
+
await docket.docketFor({ id: 7004, rank: 'metic' });
|
|
57
|
+
assert.deepEqual(order, ['source', 'decisions']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const VIEWER = { id: 42, rank: 'metic' };
|
|
61
|
+
const NOW = Date.parse('2026-11-14T12:00:00Z');
|
|
62
|
+
const slowSource = (ms) => ({
|
|
63
|
+
sources: new Map([['blockers', { source: 'blockers', building: 'planning', gate: { anyBuilder: true },
|
|
64
|
+
read: () => new Promise((r) => setTimeout(() => r([{ id: 1, title: 'Pick a host', state: 'needs', rule: 'decide' }]), ms)) }]]),
|
|
65
|
+
readings: new Map(), floors: new Map(), histories: new Map(), desks: new Map(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a government read handed in as a promise answers exactly as its value does', async () => {
|
|
69
|
+
const plain = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(1), now: NOW, decisions: [], government: null, seats: null });
|
|
70
|
+
const later = await docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(1), now: NOW,
|
|
71
|
+
decisions: Promise.resolve([]), government: Promise.resolve(null), seats: Promise.resolve(null) });
|
|
72
|
+
assert.deepEqual(later, plain);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a failed government read is never an unhandled rejection while the sources read', async () => {
|
|
76
|
+
const unhandled = [];
|
|
77
|
+
const onUnhandled = (e) => unhandled.push(e);
|
|
78
|
+
process.on('unhandledRejection', onUnhandled);
|
|
79
|
+
try {
|
|
80
|
+
const boom = new Error('government unreadable');
|
|
81
|
+
await assert.rejects(docket.buildDocket({ viewer: VIEWER, permissions: ['board.vote.cast'], contributions: slowSource(30), now: NOW,
|
|
82
|
+
decisions: Promise.resolve(null), government: Promise.reject(boom), seats: Promise.resolve(null) }), /government unreadable/);
|
|
83
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
84
|
+
assert.deepEqual(unhandled, []);
|
|
85
|
+
} finally {
|
|
86
|
+
process.off('unhandledRejection', onUnhandled);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('the claims read answers the open claims without building the claimable feed', () => {
|
|
91
|
+
const src = read('modules/lifecycle/routes/tasks.js');
|
|
92
|
+
const at = src.indexOf("req.query.view === 'claims'");
|
|
93
|
+
assert.ok(at > 0, 'GET /tasks/claimable has a claims-only view');
|
|
94
|
+
assert.ok(at < src.indexOf('db.listClaimableTasks({', at - 2000), 'it answers before the claimable feed is built');
|
|
95
|
+
assert.match(src.slice(at, at + 200), /active_claims: await db\.listActiveClaims\(\)/);
|
|
96
|
+
for (const f of ['modules/hall-ui/public/gov-feeds.js', 'modules/hall-ui/public/city.js']) {
|
|
97
|
+
assert.match(read(f), /tasks\/claimable\?view=claims/, `${f} asks for the claims alone`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -488,7 +488,7 @@ async function bootLiving({ asks = false, answer = null } = {}) {
|
|
|
488
488
|
const feeds = {
|
|
489
489
|
'/api/bongos/government/docket': asks ? withAsks(docketRouted()) : docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
|
|
490
490
|
'/api/bongos/government/leads': leadsRead(), '/api/bongos/government/treasury': fx('government__treasury'),
|
|
491
|
-
'/api/bongos/government/constitution': fx('government__constitution'), '/api/bongos/tasks/claimable?
|
|
491
|
+
'/api/bongos/government/constitution': fx('government__constitution'), '/api/bongos/tasks/claimable?view=claims': fx('tasks__claimable'),
|
|
492
492
|
'/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/public/cost-summary': fx('public__cost-summary'),
|
|
493
493
|
'/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'), '/api/bongos/blockers?limit=100': fx('blockers'),
|
|
494
494
|
'/api/bongos/autonomy/fence': fx('autonomy__fence'), '/api/bongos/moving': fx('moving'),
|
|
@@ -172,7 +172,7 @@ test('in the harness COUNT_FEEDS, with the docket the page already read, give th
|
|
|
172
172
|
assert.equal(L.countsOf(m, s).on_others, L.countsOf(m).on_others - 1);
|
|
173
173
|
// and nothing the shell reads is a task list
|
|
174
174
|
assert.ok(!F.COUNT_FEEDS.includes('tasks') && !F.COUNT_FEEDS.includes('goals'), 'the tally reads no task or goal list');
|
|
175
|
-
assert.match(F.FEEDS.claims, /
|
|
175
|
+
assert.match(F.FEEDS.claims, /[?&]view=claims\b/, 'the claims ride the claims-only view of the claimable route (task 1005310)');
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
// ── 3. every surface prints the same numbers ────────────────────────────────
|
|
@@ -197,7 +197,7 @@ const FEED = {
|
|
|
197
197
|
'/api/bongos/government/docket': 'government__docket', '/api/bongos/government/counts': 'government__counts', '/api/bongos/goals': 'goals', '/api/bongos/goals/rollups': 'goals__rollups',
|
|
198
198
|
'/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster', '/api/bongos/blockers': 'blockers',
|
|
199
199
|
'/api/bongos/public/cost-summary': 'public__cost-summary', '/api/bongos/tasks/claimable': 'tasks__claimable',
|
|
200
|
-
'/api/bongos/tasks/claimable?
|
|
200
|
+
'/api/bongos/tasks/claimable?view=claims': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
|
|
201
201
|
'/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
|
|
202
202
|
'/api/bongos/government/constitution': 'government__constitution',
|
|
203
203
|
// the hub's law book, as the route answers it (no law passed yet)
|
|
@@ -411,7 +411,7 @@ const FEED = {
|
|
|
411
411
|
'/api/bongos/me': 'me', '/api/bongos/builders/roster': 'builders__roster',
|
|
412
412
|
'/api/bongos/blockers': 'blockers', '/api/bongos/public/cost-summary': 'public__cost-summary',
|
|
413
413
|
// the living model's reads, as gov-feeds.js names them (one read each per page)
|
|
414
|
-
'/api/bongos/tasks/claimable?
|
|
414
|
+
'/api/bongos/tasks/claimable?view=claims': 'tasks__claimable', '/api/bongos/tasks?status=active&limit=200': 'tasks__status-active',
|
|
415
415
|
'/api/bongos/blockers?limit=100': 'blockers', '/api/bongos/moving': 'moving', '/api/bongos/autonomy/fence': 'autonomy__fence',
|
|
416
416
|
'/api/bongos/government/constitution': 'government__constitution', '/api/bongos/government/leads': 'government__leads',
|
|
417
417
|
};
|
|
@@ -471,7 +471,7 @@ async function bootPage({ withLiving = true } = {}) {
|
|
|
471
471
|
const F = {
|
|
472
472
|
'/api/bongos/government/docket': docketRouted(), '/api/bongos/goals': fx('goals'), '/api/bongos/goals/rollups': fx('goals__rollups'),
|
|
473
473
|
'/api/bongos/government/leads': leadsRead(),
|
|
474
|
-
'/api/bongos/tasks/claimable?
|
|
474
|
+
'/api/bongos/tasks/claimable?view=claims': fx('tasks__claimable'), '/api/bongos/tasks?status=active': fx('tasks__status-active'),
|
|
475
475
|
'/api/bongos/public/cost-summary': fx('public__cost-summary'), '/api/bongos/builders/roster': fx('builders__roster'), '/api/bongos/me': fx('me'),
|
|
476
476
|
// the living model's reads, as gov-feeds.js names them
|
|
477
477
|
'/api/bongos/tasks?status=active&limit=200': fx('tasks__status-active'), '/api/bongos/blockers?limit=100': fx('blockers'),
|
|
@@ -493,7 +493,7 @@ test('the page draws the living street: the skyline, a pod per claim, the plates
|
|
|
493
493
|
const p = await bootPage();
|
|
494
494
|
const all = (id) => walk(p.byId.get(id));
|
|
495
495
|
const cls = (id, c) => all(id).filter((e) => (e.className || '').split(' ').includes(c));
|
|
496
|
-
for (const u of ['/api/bongos/tasks/claimable?
|
|
496
|
+
for (const u of ['/api/bongos/tasks/claimable?view=claims', '/api/bongos/government/leads']) assert.ok(p.calls.includes(u), u);
|
|
497
497
|
assert.ok(!p.calls.some((u) => /governor-overlay/.test(u)), 'no overlay is read');
|
|
498
498
|
assert.equal(p.calls.filter((u) => u === '/api/bongos/me').length, 1, 'the living feeds are read once');
|
|
499
499
|
assert.equal(cls('streetDepthNear', 'street-depth-sil__face').length, 1);
|
|
@@ -48,12 +48,13 @@ const stubs = (status, craft) => `
|
|
|
48
48
|
window.OTBCraftMode = { iconHtml: (k, cls) => '<svg class="' + cls + '" data-icon="' + k + '"></svg>', choose: (m) => window.__chosen.push(m), current: () => window.__craft };
|
|
49
49
|
window.OTB = { contributeHallWidget(w) { window.__widget = w; } };`;
|
|
50
50
|
|
|
51
|
-
async function boot({ status = 200, width = 1280, still = true, craft = null } = {}) {
|
|
51
|
+
async function boot({ status = 200, width = 1280, still = true, craft = null, game = false } = {}) {
|
|
52
52
|
const ctx = await browser.newContext({ viewport: { width, height: 900 }, reducedMotion: still ? 'reduce' : 'no-preference' });
|
|
53
53
|
const page = await ctx.newPage();
|
|
54
54
|
await page.clock.install();
|
|
55
55
|
await page.setContent(`<!doctype html><html><head><style>${CSS}</style></head><body><section id="home-city" class="scroll home-city" hidden></section></body></html>`);
|
|
56
56
|
await page.addScriptTag({ content: stubs(status, craft) });
|
|
57
|
+
if (game) await page.addScriptTag({ content: 'window.OTBShellGame = { on: () => true };' });
|
|
57
58
|
await page.addScriptTag({ content: GOV });
|
|
58
59
|
await page.addScriptTag({ content: DRAW });
|
|
59
60
|
await page.addScriptTag({ content: WIDGET });
|
|
@@ -184,6 +185,16 @@ test('the governor\'s Home is the street: Patron on Home goes there, opened or c
|
|
|
184
185
|
await ctx.close();
|
|
185
186
|
});
|
|
186
187
|
|
|
188
|
+
// task 1005307: in game mode the street is not a place — shell-game.js sends it back to
|
|
189
|
+
// Home — so hopping there from Home was an endless reload loop. The game's patron stays.
|
|
190
|
+
test('in game mode the patron stays on Home: no hop to the street, opened or chosen', { skip }, async () => {
|
|
191
|
+
const { ctx, page } = await boot({ craft: 'patron', game: true });
|
|
192
|
+
await page.waitForFunction(() => document.querySelectorAll('.hc-rung').length > 0);
|
|
193
|
+
const flipped = await page.evaluate(() => { document.dispatchEvent(new CustomEvent('otb:craft', { detail: { mode: 'patron' } })); return window.__hops.slice(); });
|
|
194
|
+
await ctx.close();
|
|
195
|
+
assert.deepEqual(flipped, [], 'Home with Patron in game mode builds the city and never leaves for the street');
|
|
196
|
+
});
|
|
197
|
+
|
|
187
198
|
test('a refused read leaves the section hidden: never an empty plate', { skip }, async () => {
|
|
188
199
|
const { ctx, page } = await boot({ status: 401 });
|
|
189
200
|
await page.evaluate(() => new Promise((r) => setTimeout(r, 0)));
|