@bongos/core 1.21.140 → 1.21.142
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 +69 -49
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0351-a-criterion-closes-on-a-uat.md +1 -1
- package/docs/api/openapi.json +3 -4
- package/docs/copy-inventory.md +107 -104
- package/docs/copy-registry.json +156 -129
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +1 -0
- package/docs/page-readings.json +417 -417
- package/modules/copy-desk/page-status.js +51 -2
- package/modules/copy-desk/tests/copy_page_status.mjs +37 -0
- package/modules/government/db.js +6 -0
- package/modules/government/decision-board.js +2 -1
- package/modules/hall-ui/public/city-draw.js +4 -1
- package/modules/hall-ui/public/city.css +1 -1
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/goals.js +31 -8
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/my-agents.js +35 -10
- package/modules/hall-ui/public/my-agents.states.json +1 -1
- package/modules/hall-ui/public/settings-interaction.js +7 -2
- package/modules/hall-ui/public/settings-law-locks.js +53 -0
- package/modules/hall-ui/public/settings.html +3 -2
- package/modules/hall-ui/public/settings.js +9 -4
- package/modules/hall-ui/public/street-deck.css +4 -0
- package/modules/hall-ui/public/street-office.js +73 -1
- package/modules/hall-ui/public/street.html +4 -4
- package/modules/hall-ui/public/street.js +4 -1
- package/modules/hall-ui/public/studio-lib.js +21 -1
- package/modules/hall-ui/public/studio.css +11 -0
- package/modules/hall-ui/public/studio.js +8 -1
- package/modules/hall-ui/records/living-city.md +3 -1
- package/modules/ideas/governor-docket.js +4 -1
- package/modules/lifecycle/claimable-counts.js +19 -0
- package/modules/lifecycle/criterion-uat-db.js +18 -1
- package/modules/lifecycle/criterion-uat.js +12 -3
- package/modules/lifecycle/routes/project-settings.js +2 -2
- package/modules/lifecycle/routes/tasks.js +10 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/src/bongos/project-settings.js +5 -4
- package/src/module-api.js +1 -1
- package/tests/criterion_uat.mjs +42 -0
- package/tests/government_tidy_up.mjs +223 -0
- package/tests/hall_goals_open_first.mjs +120 -0
- package/tests/hall_studio_home.mjs +20 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// tests/government_tidy_up.mjs — the government tidy-up after the goal 1000111
|
|
2
|
+
// chain (task 1004724). One case per item that changed behaviour; DB-free (every
|
|
3
|
+
// read is stubbed at the module seam it goes through).
|
|
4
|
+
//
|
|
5
|
+
// 1 the spend-cap refusals are plain sentences, sent as-is
|
|
6
|
+
// 2 "no Lead to remove" says what to do next
|
|
7
|
+
// 3 a setting a law already holds is named by its row name, not its key
|
|
8
|
+
// 5 /settings shows the law locks its reads report (settings-law-locks.js)
|
|
9
|
+
// 7 in every city column the taller building stands at the back
|
|
10
|
+
// 8 the city plaque's words are in the body face in both modes
|
|
11
|
+
// 9 the docket reads every open blocker, so its counts are complete
|
|
12
|
+
// 10 the office offers "who should lead", filed as the appoint_lead People matter
|
|
13
|
+
// 11 a habit already up for a vote reads "proposed" after a reload
|
|
14
|
+
// 12 two appointments racing to one unseated building: a clean refusal
|
|
15
|
+
// 4 the treasury's dry-pot ask shows once: the book's card steps aside for the desk row
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const HALL = path.join(ROOT, 'modules', 'hall-ui', 'public');
|
|
27
|
+
const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), 'utf8');
|
|
28
|
+
|
|
29
|
+
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
30
|
+
const seams = require(path.join(ROOT, 'src', 'module-seams.js'));
|
|
31
|
+
const projectSettings = require(path.join(ROOT, 'src', 'bongos', 'project-settings.js'));
|
|
32
|
+
const govDb = require(path.join(ROOT, 'modules', 'government', 'db.js'));
|
|
33
|
+
const board = require(path.join(ROOT, 'modules', 'government', 'board.js'));
|
|
34
|
+
const decisionBoard = require(path.join(ROOT, 'modules', 'government', 'decision-board.js'));
|
|
35
|
+
const ideasBlockers = require(path.join(ROOT, 'modules', 'ideas', 'blockers.js'));
|
|
36
|
+
const ideasDocket = require(path.join(ROOT, 'modules', 'ideas', 'governor-docket.js'));
|
|
37
|
+
const City = require(path.join(HALL, 'city-draw.js'));
|
|
38
|
+
const MyAgents = require(path.join(HALL, 'my-agents.js'));
|
|
39
|
+
const Laws = require(path.join(HALL, 'settings-law-locks.js'));
|
|
40
|
+
const Office = require(path.join(HALL, 'street-office.js'));
|
|
41
|
+
|
|
42
|
+
async function withStubs(target, stubs, fn) {
|
|
43
|
+
const originals = {};
|
|
44
|
+
for (const [k, v] of Object.entries(stubs)) { originals[k] = target[k]; target[k] = v; }
|
|
45
|
+
try { return await fn(); } finally { for (const [k, v] of Object.entries(originals)) target[k] = v; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test('1: every spend-cap refusal is a plain sentence, and the route sends it as-is', () => {
|
|
49
|
+
for (const raw of ['', 'lots', 0, -5, projectSettings.MAX_SPEND_CAP_USD + 1]) {
|
|
50
|
+
const { reason } = projectSettings.parseSpendCap(raw);
|
|
51
|
+
assert.ok(reason, 'refused: ' + raw);
|
|
52
|
+
assert.match(reason, /^[A-Z].*\.$/, 'a sentence: ' + reason);
|
|
53
|
+
assert.doesNotMatch(reason, /usd|\.\.|^empty$|out of range/i, 'no code words: ' + reason);
|
|
54
|
+
}
|
|
55
|
+
assert.equal(projectSettings.parseSpendCap(200).usd, 200);
|
|
56
|
+
const route = read('modules', 'lifecycle', 'routes', 'project-settings.js');
|
|
57
|
+
assert.match(route, /res\.fail\('bad_spend_cap', \{ status: 400, message: parsed\.reason \}\)/);
|
|
58
|
+
assert.doesNotMatch(route, /message: `usd: /);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('2: removing a Lead from a building with none says what to do next', async () => {
|
|
62
|
+
await withStubs(govDb, {
|
|
63
|
+
listDecisionRules: async () => [], listLeads: async () => [],
|
|
64
|
+
createMatter: async () => { throw new Error('nothing is filed'); },
|
|
65
|
+
openBoardItem: async () => { throw new Error('nothing is opened'); },
|
|
66
|
+
}, async () => {
|
|
67
|
+
const out = await board.openMatter({ area: 'people', title: 'No Lead', action: { type: 'remove_lead', building: 'review' }, proposedBy: '42' });
|
|
68
|
+
assert.equal(out.reason, 'no_lead');
|
|
69
|
+
assert.match(out.what_this_means, /has no Lead to remove/);
|
|
70
|
+
assert.match(out.what_this_means, /propose a Lead/, 'the next step is named');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('3: a setting a law already holds is named by its row name in the toast', () => {
|
|
75
|
+
const src = read('modules', 'hall-ui', 'public', 'my-agents.js');
|
|
76
|
+
assert.match(src, /nameFor\(str\(err\.details\.setting\)\)/);
|
|
77
|
+
assert.doesNotMatch(src, /already holds ' \+ str\(err\.details/, 'never the raw key');
|
|
78
|
+
assert.equal(MyAgents.SETTING.verbosity.name, 'Reply length');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('5: a law lock on /settings disables what it rules out and names the law', () => {
|
|
82
|
+
const data = { law_locks: [
|
|
83
|
+
{ setting: 'verbosity', value: 'concise', law_no: 2, law_text: 'Agents keep replies short', enforced: true, allowed: ['concise'] },
|
|
84
|
+
{ setting: 'wandering', max: 'focused', law_no: 3, law_text: 'Agents stay on task', enforced: true, allowed: ['locked', 'focused'] },
|
|
85
|
+
{ setting: 'machinery', value: 'nonsense', law_no: 4, enforced: false, reason: 'value_not_allowed' },
|
|
86
|
+
] };
|
|
87
|
+
const fixed = Laws.lockFor(data, 'verbosity');
|
|
88
|
+
assert.equal(Laws.allows(fixed, 'concise'), true);
|
|
89
|
+
assert.equal(Laws.allows(fixed, 'detailed'), false);
|
|
90
|
+
assert.equal(Laws.lawLine(fixed), 'Law 2 sets this for every agent in the project: "Agents keep replies short". Only a vote can change it.');
|
|
91
|
+
const cap = Laws.lockFor(data, 'wandering');
|
|
92
|
+
assert.deepEqual(['locked', 'focused', 'balanced', 'exploratory'].map((v) => Laws.allows(cap, v)), [true, true, false, false]);
|
|
93
|
+
assert.match(Laws.lawLine(cap), /^Law 3 caps this/);
|
|
94
|
+
assert.equal(Laws.tagFor(cap), 'set by law 3');
|
|
95
|
+
assert.equal(Laws.lockFor(data, 'machinery'), null, 'a lock that enforces nothing locks nothing');
|
|
96
|
+
assert.equal(Laws.lockFor({}, 'verbosity'), null);
|
|
97
|
+
assert.equal(Laws.allows(null, 'anything'), true);
|
|
98
|
+
// both panels read it, and the page loads it before either
|
|
99
|
+
assert.match(read('modules', 'hall-ui', 'public', 'settings-interaction.js'), /Laws\.lockFor\(data, f\.field\)/);
|
|
100
|
+
assert.match(read('modules', 'hall-ui', 'public', 'settings.js'), /Laws\.lockFor\(data, 'wandering'\)/);
|
|
101
|
+
const html = read('modules', 'hall-ui', 'public', 'settings.html');
|
|
102
|
+
const at = (s) => html.indexOf(s);
|
|
103
|
+
assert.ok(at('settings-law-locks.js') > 0 && at('settings-law-locks.js') < at('/builders/settings.js') && at('settings-law-locks.js') < at('settings-interaction.js'));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('7: in every screen column of the civic quarter the taller building stands at the back', () => {
|
|
107
|
+
const at = City.CIVIC_SLOTS.map(([key, x, y]) => ({ key, col: x - y, depth: x + y, roof: City.ROOF[key] }));
|
|
108
|
+
for (const b of at) {
|
|
109
|
+
for (const o of at.filter((z) => z.col === b.col && z.depth > b.depth)) {
|
|
110
|
+
assert.ok(o.roof < b.roof, o.key + ' stands in front of the shorter ' + b.key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
assert.equal(new Set(City.CIVIC_SLOTS.map((s) => s[0])).size, 9, 'nine buildings');
|
|
114
|
+
assert.equal(new Set(City.CIVIC_SLOTS.map((s) => s[1] + ',' + s[2])).size, 9, 'nine lots');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('8: the plaque words are in the body face (the light hall maps labels to mono)', () => {
|
|
118
|
+
const css = read('modules', 'hall-ui', 'public', 'city.css');
|
|
119
|
+
assert.match(css, /\.city-plaque__words \{ font-family: var\(--font-body\);/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('9: the docket reads every open blocker, so its counts are complete past 50', async () => {
|
|
123
|
+
const many = Array.from({ length: 120 }, (_, i) => ({ id: String(i + 1), title: 'Blocker ' + (i + 1), body_md: '', status: 'open', created_at: '2026-10-01T00:00:00.000Z', blocking_task_ids: [] }));
|
|
124
|
+
let asked = null;
|
|
125
|
+
seams._reset();
|
|
126
|
+
try {
|
|
127
|
+
await withStubs(ideasBlockers, { listOpenBlockers: async (o) => { asked = o; return many.slice(0, o && o.limit != null ? o.limit : many.length); } }, async () => {
|
|
128
|
+
ideasDocket.contributeIdeasToDocket();
|
|
129
|
+
const c = seams.contributions('governor.docket').find((x) => x.source === 'blockers');
|
|
130
|
+
const rows = await c.read({ has: () => true });
|
|
131
|
+
assert.equal(rows.length, 120, 'all of them');
|
|
132
|
+
assert.ok(asked.limit == null || asked.limit >= 1000, 'a ceiling far past any real list');
|
|
133
|
+
});
|
|
134
|
+
} finally { seams._reset(); }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('10: the office offers who should lead to a builder who may open a board item, filed as appoint_lead', () => {
|
|
138
|
+
const living = { seats_real: true, owner: { name: 'Builder 1' }, buildings: [
|
|
139
|
+
{ key: 'treasury', name: 'Treasury', lead: { id: '7', name: 'Gus', since: '2026-10-01' }, structure: null, construction: [], answers: { ids: ['7'] } },
|
|
140
|
+
] };
|
|
141
|
+
const roster = { builders: [
|
|
142
|
+
{ id: '9', display_name: 'Zoe', status: 'active' }, { id: '7', display_name: 'Gus', status: 'active' },
|
|
143
|
+
{ id: '3', github_login: 'ann', status: 'active' }, { id: '4', display_name: 'Old', status: 'inactive' },
|
|
144
|
+
] };
|
|
145
|
+
const may = { permissions: { 'board.item.open': true } };
|
|
146
|
+
assert.equal(Office.appointPlan(living, 'treasury', roster, { permissions: {} }), null, 'not offered without the perm');
|
|
147
|
+
assert.equal(Office.appointPlan(living, 'treasury', roster, null), null);
|
|
148
|
+
const plan = Office.appointPlan(living, 'treasury', roster, may);
|
|
149
|
+
assert.deepEqual(plan.people.map((p) => p.name), ['ann', 'Zoe'], 'active people, A to Z, never the Lead in office');
|
|
150
|
+
assert.equal(plan.seated, true);
|
|
151
|
+
const body = Office.appointBody(plan, '9');
|
|
152
|
+
assert.deepEqual(body, { area: 'people', title: 'Appoint Zoe as Lead of Treasury', action: { type: 'appoint_lead', building: 'treasury', builder_id: 9 } });
|
|
153
|
+
assert.equal(decisionBoard.validateMatterAction('people', body.action).ok, true, 'the server\'s own shape');
|
|
154
|
+
assert.equal(Office.appointBody(plan, '7'), null, 'only someone on the list');
|
|
155
|
+
assert.equal(Office.appointPlan(living, 'treasury', roster, may, { leadFiled: { treasury: true } }).filed, true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('11: a habit already up for a vote reads proposed after a reload', () => {
|
|
159
|
+
const room = { open: [
|
|
160
|
+
{ id: '55', subject_type: 'constitutional_amendment', subject: { kind: 'laws', proposed: { enact: [{ text: 'Agents reply in detail', locks: [{ setting: 'verbosity', value: 'detailed' }] }] } } },
|
|
161
|
+
{ id: '56', subject_type: 'constitutional_amendment', subject: { kind: 'board', proposed: { enact: [{ locks: [{ setting: 'machinery', value: 'quiet' }] }] } } },
|
|
162
|
+
] };
|
|
163
|
+
const pending = MyAgents.pendingLaws(room);
|
|
164
|
+
assert.deepEqual(pending, { 'verbosity=detailed': '55' }, 'only laws sittings');
|
|
165
|
+
assert.deepEqual(MyAgents.pendingLaws(null), {});
|
|
166
|
+
assert.deepEqual(MyAgents.pendingLaws(false), {}, 'a 403 reads as none');
|
|
167
|
+
const me = {
|
|
168
|
+
builder: { id: '1' }, permissions: { 'board.item.open': true },
|
|
169
|
+
interaction: { fields: [{ field: 'verbosity', default_value: 'concise' }, { field: 'machinery', default_value: 'show' }], stored: { verbosity: 'detailed', machinery: 'quiet' }, effective: {} },
|
|
170
|
+
};
|
|
171
|
+
const v = MyAgents.buildView({ me, pending, proposed: {} }, {});
|
|
172
|
+
const h = (k) => v.habits.find((x) => x.key === k);
|
|
173
|
+
assert.equal(h('verbosity').proposed, true);
|
|
174
|
+
assert.equal(h('verbosity').item, '55');
|
|
175
|
+
assert.equal(h('machinery').proposed, false);
|
|
176
|
+
// a vote for ANOTHER value of the same setting is not this habit
|
|
177
|
+
const other = MyAgents.buildView({ me, pending: { 'verbosity=concise': '57' }, proposed: {} }, {});
|
|
178
|
+
assert.equal(other.habits.find((x) => x.key === 'verbosity').proposed, false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('12: two appointments racing to an unseated building end in a clean refusal, not a DB error', async () => {
|
|
182
|
+
const fake = (insertError) => ({
|
|
183
|
+
query: async (sql) => {
|
|
184
|
+
if (/^\s*INSERT INTO government_leads/.test(sql)) throw insertError;
|
|
185
|
+
if (/^\s*SELECT/.test(sql)) return { rows: [] };
|
|
186
|
+
return { rows: [] };
|
|
187
|
+
},
|
|
188
|
+
release: () => {},
|
|
189
|
+
});
|
|
190
|
+
const unique = Object.assign(new Error('duplicate key value violates unique constraint "government_leads_in_office_uniq"'), { code: '23505', constraint: 'government_leads_in_office_uniq' });
|
|
191
|
+
await withStubs(api.pool, { connect: async () => fake(unique) }, async () => {
|
|
192
|
+
const out = await govDb.applyLeadAct({ itemId: '40', action: { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'x' } });
|
|
193
|
+
assert.equal(out.done, false);
|
|
194
|
+
assert.equal(out.reason, 'lead_seated_meanwhile');
|
|
195
|
+
assert.doesNotMatch(out.what_this_means, /duplicate|constraint|23505/);
|
|
196
|
+
});
|
|
197
|
+
const other = Object.assign(new Error('boom'), { code: '23503' });
|
|
198
|
+
await withStubs(api.pool, { connect: async () => fake(other) }, async () => {
|
|
199
|
+
await assert.rejects(govDb.applyLeadAct({ itemId: '40', action: { type: 'appoint_lead', building: 'planning', builder_id: 45, charter: 'x' } }), /boom/, 'any other failure still throws');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('4: a book card steps aside when the floor desk below holds the same matter', () => {
|
|
204
|
+
const node = (attrs, kids = []) => {
|
|
205
|
+
const n = { attrs, kids, parentNode: null, getAttribute: (k) => (k in attrs ? attrs[k] : null) };
|
|
206
|
+
kids.forEach((k) => { k.parentNode = n; });
|
|
207
|
+
n.removeChild = (c) => { n.kids = n.kids.filter((x) => x !== c); c.parentNode = null; };
|
|
208
|
+
return n;
|
|
209
|
+
};
|
|
210
|
+
const all = (n) => n.kids.flatMap((k) => [k, ...all(k)]);
|
|
211
|
+
const card1 = node({ 'data-ask': 'treasury_spending:1' });
|
|
212
|
+
const card2 = node({ 'data-ask': 'blockers:9' });
|
|
213
|
+
const book = node({}, [card1, card2]);
|
|
214
|
+
const govt = node({}, [book]);
|
|
215
|
+
govt.querySelectorAll = (sel) => (sel === '[data-ask]' ? all(govt).filter((n) => n.attrs['data-ask']) : []);
|
|
216
|
+
const desk = node({}, [node({ 'data-row': 'treasury_spending:1' })]);
|
|
217
|
+
desk.querySelector = (sel) => { const m = /^\[data-row="(.*)"\]$/.exec(sel); return m ? all(desk).find((n) => n.attrs['data-row'] === m[1]) || null : null; };
|
|
218
|
+
assert.equal(Office.page.settle(govt, desk), 1);
|
|
219
|
+
assert.deepEqual(book.kids.map((k) => k.attrs['data-ask']), ['blockers:9'], 'the one the desk shows is gone; the other stays');
|
|
220
|
+
assert.equal(Office.page.settle(null, desk), 0);
|
|
221
|
+
// street.js settles the book after it fills the floor desk
|
|
222
|
+
assert.match(read('modules', 'hall-ui', 'public', 'street.js'), /fillDesk\(on, bv\); if \(OP && OP\.settle\) OP\.settle\(\$\('insideGovt'\), on\);/);
|
|
223
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// tests/hall_goals_open_first.mjs — the hall Home goals card leads with the OPEN
|
|
2
|
+
// goals the builder has joined, with the claimable work in each (task 1004755).
|
|
3
|
+
//
|
|
4
|
+
// The owner's acceptance test of criterion wa6-noise-limited found the card
|
|
5
|
+
// listing COMPLETED goals: it kept every non-archived goal, so achieved goals took
|
|
6
|
+
// slots in the five-row cap. This boots the real goals.js in a vm against a fake
|
|
7
|
+
// API and reads what it renders.
|
|
8
|
+
//
|
|
9
|
+
// Run: node tests/hall_goals_open_first.mjs
|
|
10
|
+
|
|
11
|
+
import { strict as assert } from 'node:assert';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import vm from 'node:vm';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { makeRunner } from './helpers.mjs';
|
|
18
|
+
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { claimableCountsByGoal } = require('../modules/lifecycle/claimable-counts.js');
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/goals.js'), 'utf8');
|
|
24
|
+
const { test, summary } = makeRunner();
|
|
25
|
+
|
|
26
|
+
async function renderCard({ goals, members, claimable }) {
|
|
27
|
+
const body = { innerHTML: '' };
|
|
28
|
+
const host = { innerHTML: '', querySelector: () => body };
|
|
29
|
+
let widget = null;
|
|
30
|
+
const routes = (p) => {
|
|
31
|
+
if (p === '/api/bongos/me') return { builder: { id: 7 } };
|
|
32
|
+
if (p === '/api/bongos/goals') return { goals };
|
|
33
|
+
if (p === '/api/bongos/tasks/claimable?group=goal') {
|
|
34
|
+
return { by_goal: claimableCountsByGoal(claimable), claimable_total: claimable.length };
|
|
35
|
+
}
|
|
36
|
+
if (p.startsWith('/api/bongos/tasks/claimable')) throw new Error('the card must not download the claimable feed: ' + p);
|
|
37
|
+
const m = p.match(/^\/api\/bongos\/goals\/(\d+)$/);
|
|
38
|
+
if (m) {
|
|
39
|
+
const g = goals.find((x) => String(x.id) === m[1]);
|
|
40
|
+
return { goal: g, criteria: [], members: members[g.id] || [] };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
44
|
+
const window = {
|
|
45
|
+
OTB: {
|
|
46
|
+
escapeHtml: (s) => String(s),
|
|
47
|
+
contributeHallWidget: (w) => { widget = w; },
|
|
48
|
+
},
|
|
49
|
+
BongosClient: {
|
|
50
|
+
createClient: () => ({
|
|
51
|
+
request: async (_m, p) => { const data = routes(p); return { status: data ? 200 : 404, ok: !!data, data }; },
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
location: { assign() {} },
|
|
55
|
+
};
|
|
56
|
+
vm.runInNewContext(SRC, { window, console, Promise, Map, String, Array, Math, encodeURIComponent });
|
|
57
|
+
widget.mount(host);
|
|
58
|
+
for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r));
|
|
59
|
+
return body.innerHTML;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const me = [{ builder_id: 7 }];
|
|
63
|
+
|
|
64
|
+
await test('an achieved goal the builder joined is NOT listed; the open one is', async () => {
|
|
65
|
+
const html = await renderCard({
|
|
66
|
+
goals: [
|
|
67
|
+
{ id: 1, title: 'Finished goal', status: 'achieved' },
|
|
68
|
+
{ id: 2, title: 'Live goal', status: 'open' },
|
|
69
|
+
],
|
|
70
|
+
members: { 1: me, 2: me },
|
|
71
|
+
claimable: [],
|
|
72
|
+
});
|
|
73
|
+
assert.ok(html.includes('Live goal'), 'the open joined goal is shown');
|
|
74
|
+
assert.ok(!html.includes('Finished goal'), 'the completed goal is left off the card');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await test('achieved goals no longer take the five slots ahead of open ones', async () => {
|
|
78
|
+
const goals = [];
|
|
79
|
+
for (let i = 1; i <= 5; i++) goals.push({ id: i, title: `Done ${i}`, status: 'achieved' });
|
|
80
|
+
goals.push({ id: 9, title: 'Open nine', status: 'open' });
|
|
81
|
+
const members = Object.fromEntries(goals.map((g) => [g.id, me]));
|
|
82
|
+
const html = await renderCard({ goals, members, claimable: [] });
|
|
83
|
+
assert.ok(html.includes('Open nine'));
|
|
84
|
+
assert.ok(!/Done \d/.test(html));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await test('each open goal carries how much claimable work sits in it', async () => {
|
|
88
|
+
const html = await renderCard({
|
|
89
|
+
goals: [{ id: 2, title: 'Live goal', status: 'open' }, { id: 3, title: 'Quiet goal', status: 'open' }],
|
|
90
|
+
members: { 2: me, 3: me },
|
|
91
|
+
claimable: [{ id: 10, goal_id: 2 }, { id: 11, goal_id: 2 }, { id: 12, goal_id: null }],
|
|
92
|
+
});
|
|
93
|
+
assert.match(html, /Live goal[\s\S]*2 tasks you can claim/);
|
|
94
|
+
assert.match(html, /Quiet goal[\s\S]*Nothing claimable right now/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await test('a goal the builder has not joined is not listed', async () => {
|
|
98
|
+
const html = await renderCard({
|
|
99
|
+
goals: [{ id: 4, title: 'Someone else', status: 'open' }],
|
|
100
|
+
members: { 4: [{ builder_id: 99 }] },
|
|
101
|
+
claimable: [],
|
|
102
|
+
});
|
|
103
|
+
assert.ok(!html.includes('Someone else'));
|
|
104
|
+
assert.match(html, /joined any open goals/);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await test('claimableCountsByGoal counts per goal and skips goal-less rows', () => {
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
JSON.parse(JSON.stringify(claimableCountsByGoal([{ goal_id: 2 }, { goal_id: '2' }, { goal_id: 5 }, { goal_id: null }, {}, null]))),
|
|
110
|
+
{ 2: 2, 5: 1 });
|
|
111
|
+
assert.deepEqual(claimableCountsByGoal(undefined), {});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await test('the route answers ?group=goal with counts and no rows', () => {
|
|
115
|
+
const route = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
116
|
+
const body = route.slice(route.indexOf("req.query.group === 'goal'"));
|
|
117
|
+
assert.ok(body.slice(0, 300).includes('res.json({ by_goal: claimableCountsByGoal(feed), claimable_total: feed.length })'));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
summary();
|
|
@@ -248,6 +248,23 @@ try {
|
|
|
248
248
|
assert.equal(lib.askedLine(0), '');
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
+
await test('Where your pages are (task 1004662): the server in-flight list, worded, stuck first, with how long', () => {
|
|
252
|
+
assert.deepEqual(lib.inFlightRows(PAGES, NOW).map((r) => [r.page_id, r.state, r.age, r.stuck]), [
|
|
253
|
+
['landing:index', 'Being written', '', false],
|
|
254
|
+
], 'the open round the ledger holds, no date on it so no age');
|
|
255
|
+
const rows = lib.inFlightRows({ in_flight: [
|
|
256
|
+
{ page_id: 'landing:contact', title: 'Contact', surface: 'landing', state_word: 'Approved, going live', since: day(17), stuck: true },
|
|
257
|
+
{ page_id: 'builders:work', title: 'Tasks', surface: 'builders', state_word: 'Submitted, waiting to be applied', since: day(1), stuck: false },
|
|
258
|
+
{ page_id: 'status:index', title: 'Status', surface: 'status', state_word: null, since: NOW.toISOString(), stuck: true },
|
|
259
|
+
] }, NOW);
|
|
260
|
+
assert.deepEqual(rows.map((r) => [r.title, r.state, r.age, r.stuck]), [
|
|
261
|
+
['Contact', 'Approved, going live', 'for 17 days', true],
|
|
262
|
+
['Tasks', 'Submitted, waiting to be applied', 'for 1 day', false],
|
|
263
|
+
['Status', 'Needs a look', 'since today', true],
|
|
264
|
+
]);
|
|
265
|
+
assert.deepEqual(lib.inFlightRows(null, NOW), []);
|
|
266
|
+
});
|
|
267
|
+
|
|
251
268
|
await test('a word on the page finds its own line: the most specific wins, holes alone never match, shared never', () => {
|
|
252
269
|
const sentence = 'Home, on the landing page, is where you left it. Twelve of its thirty-eight lines are yours so far.';
|
|
253
270
|
assert.equal(lib.matchLine(sentence, STUDIO_READING.lines).key, 'L0003', 'not the generic "{…} of {…}"');
|
|
@@ -335,6 +352,9 @@ try {
|
|
|
335
352
|
assert.match(body, /1 of 2 tweaked/);
|
|
336
353
|
assert.match(body, /style="width: 50%"/);
|
|
337
354
|
assert.match(body, /1 page has been asked for/);
|
|
355
|
+
assert.match(body, /Where your pages are/);
|
|
356
|
+
assert.match(body, /st-flight__state">Being written</, 'the open round shows where it is');
|
|
357
|
+
assert.match(body, /href="\/builders\/tweak-editor\?page=landing:index"/);
|
|
338
358
|
await s.clickIn('st-panel', '[data-close-panel]');
|
|
339
359
|
assert.equal(s.el('st-panel').hidden, true);
|
|
340
360
|
assert.ok(!s.html.classList.contains('st-open'));
|