@bongos/core 1.21.169 → 1.21.171
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 +30 -30
- package/docs/adr/0344-a-tester-is-a-project-that-opts-in-once.md +12 -0
- package/docs/copy-inventory.md +22 -20
- package/docs/copy-registry.json +41 -23
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-readings.json +16 -16
- package/modules/copy-desk/page-status.js +38 -1
- package/modules/hall-ui/public/approval-queue-lib.js +15 -1
- package/modules/hall-ui/public/approval-queue.css +9 -0
- package/modules/hall-ui/public/approval-queue.js +18 -2
- package/modules/hall-ui/public/brand-apply.js +13 -6
- package/modules/hall-ui/public/sky-data.js +5 -4
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/lifecycle/sky.js +8 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/gds/module-assess-installs.js +94 -9
- package/src/module-api.js +1 -1
- package/tests/goal_map_page.mjs +5 -5
- package/tests/hall_approval_queue.mjs +35 -0
- package/tests/hall_sky.mjs +16 -4
- package/tests/module_assess_installs.mjs +72 -6
- package/tests/reward_mode_surfacing.mjs +16 -3
- package/tests/sky_read.mjs +13 -2
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.171'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/goal_map_page.mjs
CHANGED
|
@@ -60,14 +60,14 @@ function well(over = {}) {
|
|
|
60
60
|
{ id: '71', criterion_id: 'c2', criterion_md: 'The second ring is open', satisfied: false, pending_review: false, task_ids: ['501', '502'] },
|
|
61
61
|
],
|
|
62
62
|
tasks: [
|
|
63
|
-
{ id: '500', title: 'The task', status: 'shipped', credits_reward: 60, created_on: '2026-07-01', idea_id: '10' },
|
|
63
|
+
{ id: '500', title: 'The task', status: 'shipped', credits_reward: 60, credits_awarded: 60, created_on: '2026-07-01', idea_id: '10' },
|
|
64
64
|
{ id: '501', title: 'A held task', status: 'active', credits_reward: 40, created_on: '2026-08-01', idea_id: null },
|
|
65
65
|
{ id: '502', title: 'A claimable task', status: 'ready', credits_reward: 30, created_on: '2026-08-02', idea_id: null },
|
|
66
66
|
],
|
|
67
67
|
...over,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
|
-
const tasksOf = (prefix, n, status, created = '2026-05-01') => Array.from({ length: n }, (_, i) => ({ id: `${prefix}${i}`, title: `task ${prefix}${i}`, status, credits_reward: 20, created_on: created, idea_id: null }));
|
|
70
|
+
const tasksOf = (prefix, n, status, created = '2026-05-01') => Array.from({ length: n }, (_, i) => ({ id: `${prefix}${i}`, title: `task ${prefix}${i}`, status, credits_reward: 20, credits_awarded: status === 'shipped' ? 20 : 0, created_on: created, idea_id: null }));
|
|
71
71
|
// A quiet well: nothing held, nothing landed this week.
|
|
72
72
|
const quiet = (over = {}) => well({ goal_id: '9', title: 'A quiet well — nothing moves', task_total: 4, task_done: 4, criteria: [], tasks: tasksOf('9-', 4, 'shipped'), ...over });
|
|
73
73
|
const settled = (over = {}) => well({ goal_id: '8', title: 'A settled system — it finished', status: 'achieved', task_total: 3, task_done: 3, tasks: tasksOf('8-', 3, 'shipped'), created_at: '2026-03-01T00:00:00.000Z', ...over });
|
|
@@ -142,7 +142,7 @@ await test('a goal is a well sized by its credits; settled and collapsed goals k
|
|
|
142
142
|
assert.equal(o.mass, 60);
|
|
143
143
|
assert.ok(Math.abs(a.r - o.r * 0.8) < 1e-9, 'a settled system is 0.8× the well it would have been');
|
|
144
144
|
assert.ok(Math.abs(c.r - o.r * 0.5) < 1e-9, 'a collapsed goal is half');
|
|
145
|
-
const big = await boot({ sky: sky([well(same({ goal_id: '1' })), well({ ...same({ goal_id: '4' }), tasks: tasksOf('4-', 3, 'shipped').map((t) => ({ ...t, credits_reward: 400 })) })]) });
|
|
145
|
+
const big = await boot({ sky: sky([well(same({ goal_id: '1' })), well({ ...same({ goal_id: '4' }), tasks: tasksOf('4-', 3, 'shipped').map((t) => ({ ...t, credits_reward: 400, credits_awarded: 400 })) })]) });
|
|
146
146
|
assert.ok(goal(big, '4').r > goal(big, '1').r, 'more credits, a bigger well');
|
|
147
147
|
});
|
|
148
148
|
|
|
@@ -227,7 +227,7 @@ await test('EXPLORE IS ON BY DEFAULT: the sky asks where you begin, and the ledg
|
|
|
227
227
|
assert.equal(p.el('start').hidden, false, '"where do you begin" is up');
|
|
228
228
|
const opts = p.el('startOpts').querySelectorAll('button');
|
|
229
229
|
assert.equal(opts[0].textContent, 'where my thoughts are');
|
|
230
|
-
assert.deepEqual(opts.slice(1, -1).map((b) => b.dataset.start), ['
|
|
230
|
+
assert.deepEqual(opts.slice(1, -1).map((b) => b.dataset.start), ['9', '7'], 'one door per open well, biggest first (task 1004781: 7 weighs only the 60 it paid now)');
|
|
231
231
|
assert.equal(opts.at(-1).textContent, 'show me everything');
|
|
232
232
|
assert.equal(SKY.seen(goal(p, '7')), false, 'nothing is reached yet');
|
|
233
233
|
p.key('l');
|
|
@@ -283,7 +283,7 @@ await test('THE LEDGER IS THE ACCESSIBLE PATH: every goal is a row, both directi
|
|
|
283
283
|
assert.ok(rows.includes(SKY.esc(g.title)), `${g.title} is named whole`);
|
|
284
284
|
}
|
|
285
285
|
const rowOf = (id) => (new RegExp(`data-id="${id}"[\\s\\S]*?</button>`).exec(rows) || [''])[0];
|
|
286
|
-
assert.match(rowOf('7'), /alive · 1\/3 tasks ·
|
|
286
|
+
assert.match(rowOf('7'), /alive · 1\/3 tasks · 60 cr · 1\/2 rings · waits on 1/, 'the waiting goal (alive: a task is held) counts what it waits on');
|
|
287
287
|
assert.match(rowOf('9'), /waited on by 1/, 'the goal waited on counts it the other way');
|
|
288
288
|
assert.match(rowOf('8'), /settled/); assert.match(rowOf('6'), /collapsed/);
|
|
289
289
|
const filters = p.el('filters').querySelectorAll('button').map((b) => [b.dataset.f, b.textContent.replace(/\s*\d+$/, '').trim()]);
|
|
@@ -311,6 +311,41 @@ await test('empty, closed with Escape, and focus goes back where it was', async
|
|
|
311
311
|
assert.deepEqual(b.counts, [0]);
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
+
// task 1004765: a round SUBMITTED but never applied sat unseen for weeks. The
|
|
315
|
+
// server lists it apart (not_applied), the panel shows it below the reviewable
|
|
316
|
+
// page, marked, with how long it has waited, and with no verb; the door counts
|
|
317
|
+
// only what can be reviewed.
|
|
318
|
+
await test('submitted but not applied: listed apart, marked, how long, no verbs, not in the door count', async () => {
|
|
319
|
+
const NOW = Date.parse('2026-10-20T12:00:00Z');
|
|
320
|
+
const submitted = {
|
|
321
|
+
id: 60, status: 'ready', holder_id: null, artist_id: ME, artist_login: 'me', updated_at: '2026-09-29T08:00:00Z',
|
|
322
|
+
source_ref: pages.roundSourceRef('landing:projects', 2),
|
|
323
|
+
description: ['Page tweak round 2.', pages.composeBatchBlock({ page_id: 'landing:projects', reading_hash: 'h', submitted_at: '2026-09-29T08:00:00Z', lines: [{ key: 'L0002', section: 'list', before: 'Every project', after: 'All of them', placement: 'placed', file: 'x.html', line: 1, string_id: 'a' }] })].join('\n\n'),
|
|
324
|
+
};
|
|
325
|
+
const tasks = [...TASKS.filter((t) => t.id !== 42), submitted];
|
|
326
|
+
const data = pageStatus.composeApprovals({ inventory: INVENTORY, tasks, builderId: ME, crafts: ['artist'], now: NOW });
|
|
327
|
+
assert.equal(data.count, 1, 'the door counts reviewable rounds only');
|
|
328
|
+
assert.deepEqual(data.queue.map((q) => q.task_id), ['41']);
|
|
329
|
+
assert.deepEqual(data.not_applied.map((r) => [r.page_id, r.task_id, r.state_word, r.waited_days, r.line_count]),
|
|
330
|
+
[['landing:projects', '60', 'Submitted, waiting to be applied', 21, 1]]);
|
|
331
|
+
|
|
332
|
+
const b = bootPanel({ answers: { [APPROVALS]: { ok: true, ...data }, [vis(41)]: slotsFor(41) } });
|
|
333
|
+
await b.panel.open(); await settle(); await settle();
|
|
334
|
+
assert.equal(text(b.q('.aq-pending__h')), 'Submitted, not applied yet (1)');
|
|
335
|
+
const row = b.q('.aq-pending__row');
|
|
336
|
+
assert.match(text(row), /Projects/);
|
|
337
|
+
assert.match(text(row), /not applied yet/);
|
|
338
|
+
assert.match(text(row), /Submitted, waiting to be applied · waiting 21 days\. The words are waiting to be applied/);
|
|
339
|
+
assert.equal(row.querySelectorAll('button').length, 0, 'no approve or send-back on a page nobody applied');
|
|
340
|
+
assert.deepEqual(b.counts, [1]);
|
|
341
|
+
|
|
342
|
+
const empty = bootPanel({ answers: { [APPROVALS]: { ok: true, ...pageStatus.composeApprovals({ inventory: INVENTORY, tasks: [submitted], builderId: ME, crafts: ['artist'], now: NOW }) } } });
|
|
343
|
+
await empty.panel.open(); await settle();
|
|
344
|
+
assert.equal(text(empty.q('.aq-title')), 'Nothing waiting', 'nothing to review');
|
|
345
|
+
assert.ok(empty.q('.aq-pending__row'), 'but the submitted page is still shown');
|
|
346
|
+
assert.deepEqual(empty.counts, [0]);
|
|
347
|
+
});
|
|
348
|
+
|
|
314
349
|
// ── the writes: only the host's, and the host's are the studio's allowlist ──
|
|
315
350
|
|
|
316
351
|
await test('the panel writes only through the writes it is handed; the studio hands it its own WRITE_ROUTES', () => {
|
package/tests/hall_sky.mjs
CHANGED
|
@@ -92,7 +92,7 @@ function well(over = {}) {
|
|
|
92
92
|
{ id: '71', criterion_id: 'c2', criterion_md: 'The second ring is open', satisfied: false, pending_review: false, task_ids: ['501', '502'] },
|
|
93
93
|
],
|
|
94
94
|
tasks: [
|
|
95
|
-
{ id: '500', title: 'The task', status: 'shipped', credits_reward: 60, created_on: '2026-07-01', idea_id: '10' },
|
|
95
|
+
{ id: '500', title: 'The task', status: 'shipped', credits_reward: 60, credits_awarded: 6, created_on: '2026-07-01', idea_id: '10' },
|
|
96
96
|
{ id: '501', title: 'A held task', status: 'active', credits_reward: 40, created_on: '2026-08-01', idea_id: null },
|
|
97
97
|
{ id: '502', title: 'A claimable task', status: 'ready', credits_reward: 30, created_on: '2026-08-02', idea_id: null },
|
|
98
98
|
],
|
|
@@ -211,7 +211,18 @@ await test('the rings are the criteria, closing as each is satisfied and thick w
|
|
|
211
211
|
const g = p.S().goalById.get('7');
|
|
212
212
|
assert.equal(g.crit.length, 2);
|
|
213
213
|
same(g.crit.map((c) => c.satisfied), [true, false]);
|
|
214
|
-
|
|
214
|
+
// task 1004781 — a ring weighs what its tasks were PAID: the shipped task paid 6 (its 60 was a guess), and
|
|
215
|
+
// the held and claimable tasks have paid nothing yet, so their estimates stay off the ring.
|
|
216
|
+
same(g.crit.map((c) => c.credits), [6, 0], 'a ring\'s credits are what the tasks on it were paid');
|
|
217
|
+
assert.equal(g.tasks.find((t) => t.id === '500').estimate, null, 'a settled task carries no estimate');
|
|
218
|
+
assert.equal(g.tasks.find((t) => t.id === '501').estimate, 40, 'live work keeps its estimate, apart from the total');
|
|
219
|
+
const SKY = p.SKY();
|
|
220
|
+
SKY.pick({ kind: 'task', ref: g.tasks.find((t) => t.id === '501') }, false);
|
|
221
|
+
assert.match(p.panel(), /40 <span class="lbl">Est\.<\/span>/, 'the panel labels a live task\'s figure as an estimate');
|
|
222
|
+
assert.doesNotMatch(p.panel(), /credits paid/);
|
|
223
|
+
SKY.pick({ kind: 'task', ref: g.tasks.find((t) => t.id === '500') }, false);
|
|
224
|
+
assert.match(p.panel(), /credits paid[\s\S]*?6/, 'and a settled task\'s as what it was paid');
|
|
225
|
+
assert.doesNotMatch(p.panel(), /Est\./);
|
|
215
226
|
assert.equal(g.tasks.find((t) => t.id === '500').ci, 0, 'a task knows which ring it gates');
|
|
216
227
|
assert.equal(g.tasks.find((t) => t.id === '500').sourceRef, '10', 'and which thought it was born of');
|
|
217
228
|
});
|
|
@@ -230,8 +241,9 @@ await test('abandoned work is listed at zero and adds nothing to a well or its r
|
|
|
230
241
|
const g = p.S().goalById.get('7');
|
|
231
242
|
assert.equal(g.tasks.find((t) => t.id === '503').credits, 0, 'still listed, at zero');
|
|
232
243
|
assert.equal(g.tasks.find((t) => t.id === '504').credits, 0, 'a merged-away task is listed at zero too');
|
|
233
|
-
assert.equal(g.credits,
|
|
234
|
-
same(g.crit.map((c) => c.credits), [
|
|
244
|
+
assert.equal(g.credits, 6, 'the well\'s mass is what was paid only');
|
|
245
|
+
same(g.crit.map((c) => c.credits), [6, 0], 'and the ring it sat on is not thickened by it');
|
|
246
|
+
assert.equal(g.tasks.find((t) => t.id === '503').estimate, null, 'abandoned work carries no estimate either');
|
|
235
247
|
});
|
|
236
248
|
|
|
237
249
|
await test('a private goal keeps its structure and none of its prose', async () => {
|
|
@@ -20,7 +20,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
20
20
|
|
|
21
21
|
const require = createRequire(import.meta.url);
|
|
22
22
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
-
const { MIN_HOLDERS, assessInstall, assessReliability, recordInstallSignals } = require('../scripts/gds/module-assess-installs.js');
|
|
23
|
+
const { MIN_HOLDERS, PRIOR_TESTERS, assessInstall, assessReliability, assessTesterFeedback, recordInstallSignals } = require('../scripts/gds/module-assess-installs.js');
|
|
24
24
|
const reports = require('../src/bongos/module-usage-reports.js');
|
|
25
25
|
const { ERROR_CODE_RE } = require('../src/bongos/module-usage-reports.js');
|
|
26
26
|
const { cmdInstall } = require('../scripts/gds/module.js');
|
|
@@ -146,11 +146,13 @@ test('the switch: off writes an opt-out row, on deletes it', async () => {
|
|
|
146
146
|
|
|
147
147
|
// ---- the re-score ----
|
|
148
148
|
|
|
149
|
-
function scoringDb({ latest, days, current = [] }) {
|
|
149
|
+
function scoringDb({ latest, days, current = [], testerInstalls = [], testerDays = [] }) {
|
|
150
150
|
return fakeDb([
|
|
151
|
-
[/FROM module_install_reports r/, () => ({ rows: latest })],
|
|
152
|
-
[/FROM module_runtime_error_counts r/, () => ({ rows: days })],
|
|
153
|
-
[/
|
|
151
|
+
[/FROM module_install_reports r\s/, () => ({ rows: latest })],
|
|
152
|
+
[/FROM module_runtime_error_counts r\s/, () => ({ rows: days })],
|
|
153
|
+
[/FROM module_install_reports ir/, () => ({ rows: testerInstalls })],
|
|
154
|
+
[/FROM module_runtime_error_counts ec/, () => ({ rows: testerDays })],
|
|
155
|
+
[/part IN \('install', 'reliability', 'tester_feedback'\)/, () => ({ rows: current })],
|
|
154
156
|
[/INSERT INTO module_assessment_signals/, () => ({ rows: [] })],
|
|
155
157
|
[/SELECT DISTINCT ON \(part\) id, part, outcome, score, sample_size/, () => ({ rows: [] })],
|
|
156
158
|
[/FROM module_assessment_scores/, () => ({ rows: [] })],
|
|
@@ -173,7 +175,10 @@ test('re-score: writes a signal row only for a part whose result moved, then re-
|
|
|
173
175
|
const latest = [{ ok: true }, { ok: true }, { ok: false, error_code: 'place_failed' }];
|
|
174
176
|
const db = scoringDb({
|
|
175
177
|
latest, days: [],
|
|
176
|
-
current: [
|
|
178
|
+
current: [
|
|
179
|
+
{ part: 'reliability', outcome: 'no_data', score: null, sample_size: 0 },
|
|
180
|
+
{ part: 'tester_feedback', outcome: 'no_data', score: null, sample_size: 0 },
|
|
181
|
+
],
|
|
177
182
|
});
|
|
178
183
|
const res = await recordInstallSignals(5, { db });
|
|
179
184
|
assert.deepEqual(res.written, ['install'], 'reliability is unchanged, so no new row');
|
|
@@ -184,12 +189,73 @@ test('re-score: writes a signal row only for a part whose result moved, then re-
|
|
|
184
189
|
const again = scoringDb({ latest, days: [], current: [
|
|
185
190
|
{ part: 'install', outcome: 'scored', score: 67, sample_size: 3 },
|
|
186
191
|
{ part: 'reliability', outcome: 'no_data', score: null, sample_size: 0 },
|
|
192
|
+
{ part: 'tester_feedback', outcome: 'no_data', score: null, sample_size: 0 },
|
|
187
193
|
] });
|
|
188
194
|
const res2 = await recordInstallSignals(5, { db: again });
|
|
189
195
|
assert.deepEqual(res2.written, []);
|
|
190
196
|
assert.ok(!again.calls.some((c) => /INSERT/.test(c.sql)), 'nothing moved, nothing written');
|
|
191
197
|
});
|
|
192
198
|
|
|
199
|
+
// ---- tester feedback (task 1003806, ADR 0344 D5) ----
|
|
200
|
+
|
|
201
|
+
const testers = (n, { failed = 0, erroring = 0 } = {}) => {
|
|
202
|
+
const installs = Array.from({ length: n }, (_, i) => ({ holder_kind: 'instance', holder_ref: `p${i}`, ok: i >= failed, error_code: i < failed ? 'place_failed' : null }));
|
|
203
|
+
const days = Array.from({ length: erroring }, (_, i) => ({ holder_kind: 'instance', holder_ref: `p${n - 1 - i}`, error_count: 2 }));
|
|
204
|
+
return { installs, days };
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
test('Tester feedback: no tester reports is no_data, and fewer than MIN_HOLDERS is pending', () => {
|
|
208
|
+
assert.equal(assessTesterFeedback({}).outcome, 'no_data');
|
|
209
|
+
const s = assessTesterFeedback(testers(MIN_HOLDERS - 1));
|
|
210
|
+
assert.equal(s.outcome, 'pending');
|
|
211
|
+
assert.equal(s.score, null);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('Tester feedback: a tester it went well for installed cleanly AND hit no runtime errors', () => {
|
|
215
|
+
const s = assessTesterFeedback(testers(20, { failed: 2, erroring: 3 }));
|
|
216
|
+
assert.equal(s.detail.went_well, 15);
|
|
217
|
+
assert.equal(s.detail.install_failed, 2);
|
|
218
|
+
assert.equal(s.detail.with_errors, 3);
|
|
219
|
+
assert.equal(s.detail.errors, 6);
|
|
220
|
+
assert.equal(s.score, Math.round((100 * (15 + PRIOR_TESTERS / 2)) / (20 + PRIOR_TESTERS)));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Tester feedback: a tester who only sent error counts still counts once', () => {
|
|
224
|
+
const s = assessTesterFeedback({ installs: [], days: [
|
|
225
|
+
{ holder_kind: 'instance', holder_ref: 'a', error_count: 0 },
|
|
226
|
+
{ holder_kind: 'instance', holder_ref: 'a', error_count: 0 },
|
|
227
|
+
{ holder_kind: 'instance', holder_ref: 'b', error_count: 0 },
|
|
228
|
+
{ holder_kind: 'builder', holder_ref: 'a', error_count: 1 },
|
|
229
|
+
] });
|
|
230
|
+
assert.equal(s.sample_size, 3);
|
|
231
|
+
assert.equal(s.detail.went_well, 2);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('Tester feedback: sample size weighs — a few friendly testers cannot outrank many real ones', () => {
|
|
235
|
+
const few = assessTesterFeedback(testers(3)).score;
|
|
236
|
+
const many = assessTesterFeedback(testers(200, { failed: 10 })).score;
|
|
237
|
+
assert.ok(many > few, `200 testers at 95% (${many}) must beat 3 at 100% (${few})`);
|
|
238
|
+
assert.ok(assessTesterFeedback(testers(200)).score >= 95, 'a large clean sample still scores high');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('re-score: tester reports are read only inside the tester window, author excluded', async () => {
|
|
242
|
+
const t4 = testers(4);
|
|
243
|
+
const db = scoringDb({ latest: [], days: [], testerInstalls: t4.installs, testerDays: t4.days });
|
|
244
|
+
const res = await recordInstallSignals(5, { db });
|
|
245
|
+
assert.ok(res.written.includes('tester_feedback'));
|
|
246
|
+
assert.equal(res.signals.tester_feedback.score, Math.round((100 * (4 + PRIOR_TESTERS / 2)) / (4 + PRIOR_TESTERS)));
|
|
247
|
+
for (const [table, a, when] of [['module_install_reports', 'ir', /ir\.reported_at >= t\.start AND \(t\.stop IS NULL OR ir\.reported_at < t\.stop\)/],
|
|
248
|
+
['module_runtime_error_counts', 'ec', /ec\.day >= t\.start::date AND \(t\.stop IS NULL OR ec\.day <= t\.stop::date\)/]]) {
|
|
249
|
+
const sql = db.calls.find((c) => c.sql.includes(`FROM ${table} ${a}`)).sql;
|
|
250
|
+
assert.match(sql, /c1\.channel = 'tester'/, `${table}: the window opens on the tester channel`);
|
|
251
|
+
assert.match(sql, /c2\.channel <> 'tester' AND c2\.set_at > s\.start/, `${table}: and closes when it leaves it`);
|
|
252
|
+
assert.match(sql, when, `${table}: only reports inside the window`);
|
|
253
|
+
assert.ok(sql.includes(`NOT (${a}.holder_kind = 'builder' AND ${a}.holder_ref = m.author_id::text)`), `${table}: excludes the author`);
|
|
254
|
+
}
|
|
255
|
+
const insert = db.calls.find((c) => /INSERT INTO module_assessment_signals/.test(c.sql) && c.params[1] === 'tester_feedback');
|
|
256
|
+
assert.deepEqual(insert.params.slice(0, 5), [5, 'tester_feedback', 'scored', res.signals.tester_feedback.score, 4]);
|
|
257
|
+
});
|
|
258
|
+
|
|
193
259
|
// ---- the migration: there is nowhere to put anything but counts ----
|
|
194
260
|
|
|
195
261
|
test('core_273: three additive tables whose columns hold counts and codes only', () => {
|
|
@@ -136,14 +136,27 @@ function loadBrandApply(rewardBlock) {
|
|
|
136
136
|
const SHIPPED_TASK = { status: 'shipped', credits_awarded: 0, credits_reward: 60 };
|
|
137
137
|
const READY_TASK = { status: 'ready', credits_reward: 60 };
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
// task 1004781 — a settled task shows what it was PAID, never its estimate:
|
|
140
|
+
// the 60 is not a payout, so a shipped task with nothing in the ledger is 0,
|
|
141
|
+
// and that 0 is an actual figure (plain label). Only live work is "Est.".
|
|
142
|
+
test('hall: cost-plus-only shows a SHIPPED task as what it paid (0), not the 60 estimate', () => {
|
|
140
143
|
const B = loadBrandApply(CPO);
|
|
141
|
-
assert.equal(B.
|
|
142
|
-
|
|
144
|
+
assert.equal(B.taskCreditValue(SHIPPED_TASK), 0, 'the estimate must never stand in for a payout');
|
|
145
|
+
assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits');
|
|
146
|
+
assert.equal(B.taskCreditValue({ ...SHIPPED_TASK, credits_awarded: 7 }), 7, 'a real payout shows as itself');
|
|
147
|
+
assert.equal(B.taskCreditValue(READY_TASK), 60, 'live work keeps its estimate');
|
|
143
148
|
assert.equal(B.taskCreditLabel(READY_TASK), 'credits Est.');
|
|
144
149
|
assert.equal(B.perTaskCredit(), false);
|
|
145
150
|
});
|
|
146
151
|
|
|
152
|
+
test('hall: a settled task never falls back to its estimate, on any reward mode', () => {
|
|
153
|
+
for (const mode of [CPO, PAYS, undefined]) {
|
|
154
|
+
const B = loadBrandApply(mode);
|
|
155
|
+
assert.equal(B.taskCreditValue(SHIPPED_TASK), 0);
|
|
156
|
+
assert.equal(B.taskCreditValue({ status: 'confirmed', credits_reward: 30 }), 0, 'no ledger figure ⇒ 0');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
147
160
|
test('hall: a PAYING instance keeps the original settled/unsettled split', () => {
|
|
148
161
|
const B = loadBrandApply(PAYS);
|
|
149
162
|
assert.equal(B.taskCreditLabel(SHIPPED_TASK), 'credits', 'settled ⇒ plain label, unchanged');
|
package/tests/sky_read.mjs
CHANGED
|
@@ -56,14 +56,14 @@ await test('the shape: wells carry their criteria and their compact tasks; edges
|
|
|
56
56
|
assert.equal(w1.created_at, '2026-09-01T00:00:00.000Z');
|
|
57
57
|
// task 1004402: each ring also carries its UAT state + backend-only flag (ADR 0351).
|
|
58
58
|
assert.deepEqual(w1.criteria, [{ id: '10', criterion_id: 'slug-10', criterion_md: 'Criterion 10', satisfied: false, backend_only: false, task_ids: ['100', '101'], pending_review: false, uat_state: 'open' }]);
|
|
59
|
-
assert.deepEqual(w1.tasks[0], { id: '100', title: 'Task 100', status: 'shipped', credits_reward: 30, created_on: '2026-09-02', idea_id: '555' });
|
|
59
|
+
assert.deepEqual(w1.tasks[0], { id: '100', title: 'Task 100', status: 'shipped', credits_reward: 30, credits_awarded: 0, created_on: '2026-09-02', idea_id: '555' });
|
|
60
60
|
assert.equal(w1.tasks[1].idea_id, null, 'a task not born of an idea carries null, not an empty string');
|
|
61
61
|
assert.deepEqual(out.goals[1].criteria[0].task_ids, [], 'a criterion with no linked task is an empty ring, not a missing one');
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
await test('nothing but the compact fields rides on a task — no description, no value summary, no notes', () => {
|
|
65
65
|
const out = shapeSky({ goals: [g(1)], extras: [x(1)], tasks: [t(1, 1, { description: 'long prose', value_summary: 'more prose', notes: 'and more' })] });
|
|
66
|
-
assert.deepEqual(Object.keys(out.goals[0].tasks[0]).sort(), ['created_on', 'credits_reward', 'id', 'idea_id', 'status', 'title']);
|
|
66
|
+
assert.deepEqual(Object.keys(out.goals[0].tasks[0]).sort(), ['created_on', 'credits_awarded', 'credits_reward', 'id', 'idea_id', 'status', 'title']);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
await test('a ring or a task under a goal that is not a well is dropped, not drawn to nowhere', () => {
|
|
@@ -177,6 +177,17 @@ await test('the criterion→task join is read ONCE for every goal-bearing criter
|
|
|
177
177
|
assert.match(crit[0].sql, /LEFT JOIN tasks t\s+ON t\.id = tc\.task_id/, 'a criterion with no task still arrives (the rollup\'s LEFT JOIN)');
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
+
// task 1004781 — the sky sizes wells by what the ledger PAID, so the read
|
|
181
|
+
// carries the payout (the shared subquery, settled-only) beside the estimate.
|
|
182
|
+
await test('a task carries what the ledger paid for it, from the shared payout subquery', async () => {
|
|
183
|
+
const out = shapeSky({ goals: [g(1)], extras: [x(1)], tasks: [t(100, 1, { status: 'shipped', credits_awarded: '7' })] });
|
|
184
|
+
assert.equal(out.goals[0].tasks[0].credits_awarded, 7);
|
|
185
|
+
const pool = fakePool();
|
|
186
|
+
await skyRows({}, { pool });
|
|
187
|
+
const tq = pool.calls.find((c) => /FROM tasks t\s+JOIN goals g/.test(c.sql));
|
|
188
|
+
assert.match(tq.sql, /FROM credit_log cl[\s\S]*AS credits_awarded/);
|
|
189
|
+
});
|
|
190
|
+
|
|
180
191
|
await test('the task query is compact by construction: no description, no value_summary, and the date is truncated in SQL', async () => {
|
|
181
192
|
const pool = fakePool();
|
|
182
193
|
await skyRows({}, { pool });
|