@bongos/core 1.19.491 → 1.19.492

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.
@@ -0,0 +1,108 @@
1
+ // tests/task_detail_includes.mjs — the task read's two new opt-in siblings
2
+ // (task 1003437, goal 1000081): ?include=session_logs and ?include=claims on
3
+ // GET /tasks/:id, and the two reads behind them.
4
+ //
5
+ // WHY. The hall's task page grew a grade panel and a session log (the owner's
6
+ // two net-new regions), and the claim block finally shows the whole ledger. The
7
+ // grade already rode the read behind ?include=grade; the session log and the
8
+ // claim ledger had NO read surface at all — session_logs was write-only from
9
+ // the API's point of view, and claims exposed only the most recent holder. The
10
+ // endpoint is a CRITICAL §4 supervisor hot path, so every sibling is opt-in:
11
+ // a caller that does not ask pays for nothing.
12
+ //
13
+ // What it locks:
14
+ // * THE ROUTE honours both include keys, calls the two reads, and sets each
15
+ // sibling only when asked (an unasked key is ABSENT, not null).
16
+ // * THE READS are keyed by task_id, newest first, joined to builders for the
17
+ // name — and, like getLastClaimHolder (#678), never return a rank.
18
+ // * THE FACADE (modules/lifecycle/db.js) re-exports both, so the route's
19
+ // `db.getTaskSessionLogs` / `db.getTaskClaims` resolve.
20
+
21
+ import { test } from 'node:test';
22
+ import assert from 'node:assert/strict';
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { createRequire } from 'node:module';
26
+ import { fileURLToPath } from 'node:url';
27
+
28
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
30
+ const require = createRequire(import.meta.url);
31
+
32
+ const routeSrc = read('modules/lifecycle/routes/tasks.js');
33
+ const handler = routeSrc.slice(routeSrc.indexOf("router.get('/tasks/:id',"), routeSrc.indexOf("// GET /tasks/:id/dependencies"));
34
+
35
+ test('GET /tasks/:id honours ?include=session_logs and ?include=claims as opt-in siblings', () => {
36
+ assert.ok(handler.length > 0, 'the handler is where it was');
37
+ assert.match(handler, /includes\.includes\('session_logs'\)/);
38
+ assert.match(handler, /includes\.includes\('claims'\)/);
39
+ assert.match(handler, /wantSessionLogs \? db\.getTaskSessionLogs\(id\) : Promise\.resolve\(null\)/, 'the read runs only when asked');
40
+ assert.match(handler, /wantClaims \? db\.getTaskClaims\(id\) : Promise\.resolve\(null\)/);
41
+ assert.match(handler, /if \(wantSessionLogs\) body\.session_logs = sessionLogs;/, 'set only when asked — absent otherwise');
42
+ assert.match(handler, /if \(wantClaims\) body\.claims = claims;/);
43
+ assert.match(handler, /auth\.requireBuilder/, 'the gate is unchanged: any signed-in builder');
44
+ });
45
+
46
+ test('the lifecycle facade re-exports both reads', () => {
47
+ const db = require('../modules/lifecycle/db.js');
48
+ assert.equal(typeof db.getTaskSessionLogs, 'function');
49
+ assert.equal(typeof db.getTaskClaims, 'function');
50
+ const reads = require('../modules/lifecycle/db-claim-reads.js');
51
+ assert.equal(db.getTaskSessionLogs, reads.getTaskSessionLogs);
52
+ assert.equal(db.getTaskClaims, reads.getTaskClaims);
53
+ });
54
+
55
+ // The reads run against the module-api pool the module destructured at load; the
56
+ // pool OBJECT is shared, so swapping its query for the test's duration is enough.
57
+ async function withQuery(fn, body) {
58
+ const api = require('../src/module-api');
59
+ const orig = api.pool.query;
60
+ const calls = [];
61
+ api.pool.query = async (sql, params) => { calls.push({ sql: String(sql).replace(/\s+/g, ' '), params }); return fn(sql, params); };
62
+ try { return await body(calls); } finally { api.pool.query = orig; }
63
+ }
64
+
65
+ test('getTaskSessionLogs: by task_id, newest first, the builder joined for a name, no rank', async () => {
66
+ const reads = require('../modules/lifecycle/db-claim-reads.js');
67
+ const rows = [{ id: '2', outcome: 'shipped' }, { id: '1', outcome: 'partial' }];
68
+ await withQuery(async () => ({ rows }), async (calls) => {
69
+ const out = await reads.getTaskSessionLogs(1003437);
70
+ assert.deepEqual(out, rows, 'rows pass through untouched');
71
+ assert.equal(calls.length, 1);
72
+ const { sql, params } = calls[0];
73
+ assert.deepEqual(params, [1003437]);
74
+ assert.match(sql, /FROM session_logs s/);
75
+ assert.match(sql, /JOIN builders b ON b\.id = s\.builder_id/);
76
+ assert.match(sql, /WHERE s\.task_id = \$1/);
77
+ assert.match(sql, /ORDER BY s\.ended_at DESC, s\.id DESC/);
78
+ for (const col of ['s.started_at', 's.ended_at', 's.outcome', 's.notes_md', 's.confidence', 'b.github_login', 'b.display_name']) {
79
+ assert.ok(sql.includes(col), `selects ${col}`);
80
+ }
81
+ assert.doesNotMatch(sql, /\brank\b/, 'rank is sensitive metadata and stays off this read (#678)');
82
+ });
83
+ });
84
+
85
+ test('getTaskClaims: the whole ledger by task_id, newest first, the builder joined, no rank', async () => {
86
+ const reads = require('../modules/lifecycle/db-claim-reads.js');
87
+ const rows = [{ claim_id: '2' }, { claim_id: '1' }];
88
+ await withQuery(async () => ({ rows }), async (calls) => {
89
+ const out = await reads.getTaskClaims(1003437);
90
+ assert.deepEqual(out, rows);
91
+ const { sql, params } = calls[0];
92
+ assert.deepEqual(params, [1003437]);
93
+ assert.match(sql, /FROM claims c/);
94
+ assert.match(sql, /JOIN builders b ON b\.id = c\.builder_id/);
95
+ assert.match(sql, /WHERE c\.task_id = \$1/);
96
+ assert.match(sql, /ORDER BY c\.claimed_at DESC, c\.id DESC/);
97
+ assert.doesNotMatch(sql, /LIMIT/, 'the ledger, not the last holder');
98
+ assert.doesNotMatch(sql, /\brank\b/);
99
+ });
100
+ });
101
+
102
+ test('an empty ledger is an empty array, never null — the page tells "none" from "not asked"', async () => {
103
+ const reads = require('../modules/lifecycle/db-claim-reads.js');
104
+ await withQuery(async () => ({ rows: [] }), async () => {
105
+ assert.deepEqual(await reads.getTaskSessionLogs(1), []);
106
+ assert.deepEqual(await reads.getTaskClaims(1), []);
107
+ });
108
+ });