@bongos/core 1.19.1010 → 1.19.1012

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,265 @@
1
+ // tests/sky_read.mjs — GET /sky, the whole project sky in one read (task 1004230,
2
+ // sky part 2 of 5; goal 1000095; ADR 0331, ADR 0227 §11).
3
+ //
4
+ // What this file pins is what the picture cannot show: that the wells and the
5
+ // routes ARE the goal graph's (same read, not a copy); that a private goal stays
6
+ // a well but loses every piece of prose, not only its title; that a task's
7
+ // criterion link is the SAME link the version progress rollup computes, from the
8
+ // same rows; that the payload is compact; that the cache is per viewer and
9
+ // expires; and that identity comes from the authenticated request.
10
+ //
11
+ // DB-free: the pure shaper is run directly, the SQL against a recording fake
12
+ // pool, the handler against stub collaborators (the goal_graph.mjs precedent).
13
+ //
14
+ // Run: node tests/sky_read.mjs
15
+ process.env.NODE_ENV = 'test';
16
+ import assert from 'node:assert/strict';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { createRequire } from 'node:module';
21
+ import { makeRunner } from './helpers.mjs';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const skyLib = require(path.join(ROOT, 'modules/lifecycle/sky.js'));
26
+ const { shapeSky, foldCriteria, skyRows, sky, skyCache, skyHandler } = skyLib;
27
+ const { buildCriterionProgress } = require(path.join(ROOT, 'modules/lifecycle/done-when.js'));
28
+ const { CUT_STATUSES, DONE_STATUSES } = require(path.join(ROOT, 'modules/lifecycle/task-status.js'));
29
+ const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules/lifecycle/module.json'), 'utf8'));
30
+ const ROUTE_SRC = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/sky.js'), 'utf8');
31
+
32
+ const { test, summary } = makeRunner();
33
+
34
+ const CATS = new Map([['7', { id: 7, slug: 'security', label: 'Security' }]]);
35
+ const g = (id, extra = {}) => ({ goal_id: String(id), title: `Goal ${id}`, status: 'open', version_id: 'V1', category_id: null, visible: true, task_total: 4, task_done: 1, ...extra });
36
+ const e = (from, to, count = 1) => ({ from_goal_id: String(from), to_goal_id: String(to), count });
37
+ const x = (id, extra = {}) => ({ goal_id: String(id), subtitle: `sub ${id}`, created_at: new Date('2026-09-01T00:00:00Z'), ...extra });
38
+ // A flat (criterion, task) row in the shape both the rollup and the sky read.
39
+ const cr = (id, goalId, taskId, taskStatus, extra = {}) => ({ id: String(id), goal_id: String(goalId), criterion_id: `slug-${id}`, criterion_md: `Criterion ${id}`, satisfied: false, sort_order: Number(id), task_id: taskId == null ? null : String(taskId), task_status: taskStatus ?? null, ...extra });
40
+ const t = (id, goalId, extra = {}) => ({ id: String(id), goal_id: String(goalId), title: `Task ${id}`, status: 'ready', credits_reward: 30, created_on: '2026-09-02', idea_id: null, ...extra });
41
+
42
+ // ---- shapeSky -------------------------------------------------------------------
43
+
44
+ await test('the shape: wells carry their criteria and their compact tasks; edges are the graph\'s; counts add up', () => {
45
+ const out = shapeSky({
46
+ goals: [g(1), g(2)], edges: [e(1, 2, 3)], extras: [x(1), x(2)],
47
+ criteria: [cr(10, 1, 100, 'shipped'), cr(10, 1, 101, 'ready'), cr(11, 2, null)],
48
+ tasks: [t(100, 1, { status: 'shipped', idea_id: '555' }), t(101, 1), t(200, 2)],
49
+ });
50
+ assert.deepEqual(Object.keys(out), ['goals', 'edges', 'counts']);
51
+ assert.deepEqual(out.edges, [{ from: '1', to: '2', count: 3 }]);
52
+ assert.deepEqual(out.counts, { goals: 2, edges: 1, criteria: 2, tasks: 3 });
53
+ const w1 = out.goals[0];
54
+ assert.deepEqual(Object.keys(w1).sort(), ['category', 'created_at', 'criteria', 'goal_id', 'status', 'subtitle', 'task_done', 'task_total', 'tasks', 'title', 'version_id']);
55
+ assert.equal(w1.subtitle, 'sub 1');
56
+ assert.equal(w1.created_at, '2026-09-01T00:00:00.000Z');
57
+ assert.deepEqual(w1.criteria, [{ id: '10', criterion_id: 'slug-10', criterion_md: 'Criterion 10', satisfied: false, task_ids: ['100', '101'], pending_review: false }]);
58
+ 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.equal(w1.tasks[1].idea_id, null, 'a task not born of an idea carries null, not an empty string');
60
+ assert.deepEqual(out.goals[1].criteria[0].task_ids, [], 'a criterion with no linked task is an empty ring, not a missing one');
61
+ });
62
+
63
+ await test('nothing but the compact fields rides on a task — no description, no value summary, no notes', () => {
64
+ const out = shapeSky({ goals: [g(1)], extras: [x(1)], tasks: [t(1, 1, { description: 'long prose', value_summary: 'more prose', notes: 'and more' })] });
65
+ assert.deepEqual(Object.keys(out.goals[0].tasks[0]).sort(), ['created_on', 'credits_reward', 'id', 'idea_id', 'status', 'title']);
66
+ });
67
+
68
+ await test('a ring or a task under a goal that is not a well is dropped, not drawn to nowhere', () => {
69
+ const out = shapeSky({ goals: [g(1)], extras: [x(1)], criteria: [cr(10, 99, 5, 'ready')], tasks: [t(5, 99)] });
70
+ assert.deepEqual(out.counts, { goals: 1, edges: 0, criteria: 0, tasks: 0 });
71
+ });
72
+
73
+ await test('pending_review is derived the way the rollup derives it: every linked task terminal, at least one shipped, not yet satisfied', () => {
74
+ const out = shapeSky({
75
+ goals: [g(1)], extras: [x(1)],
76
+ criteria: [cr(10, 1, 1, 'shipped'), cr(10, 1, 2, 'abandoned'), cr(11, 1, 3, 'shipped'), cr(11, 1, 4, 'active'), cr(12, 1, 5, 'abandoned'), cr(13, 1, 6, 'shipped', { satisfied: true })],
77
+ });
78
+ const by = Object.fromEntries(out.goals[0].criteria.map((c) => [c.id, c]));
79
+ assert.equal(by['10'].pending_review, true, 'shipped + abandoned: abandoned is terminal, so nothing remains');
80
+ assert.equal(by['11'].pending_review, false, 'an active task still gates it');
81
+ assert.equal(by['12'].pending_review, true, 'all-abandoned DOES surface for review — the review flag is deliberately wider than auto-close (task 1002651, ADR 0183), and the sky must not narrow it');
82
+ assert.equal(by['13'].pending_review, false, 'already satisfied');
83
+ assert.equal(by['13'].satisfied, true);
84
+ });
85
+
86
+ await test('a PRIVATE goal the viewer cannot see stays a well with its counts, rings and tasks — but EVERY piece of prose is null, not only the title', () => {
87
+ const secret = { goal_id: '1', title: 'Secret roadmap', status: 'open', version_id: 'V1', category_id: 7, visible: false, task_total: 2, task_done: 0 };
88
+ const out = shapeSky({
89
+ goals: [secret, g(2)], edges: [e(2, 1)], extras: [x(1, { subtitle: 'the secret plan' }), x(2)],
90
+ criteria: [cr(10, 1, 100, 'ready', { criterion_md: 'secret criterion text' })],
91
+ tasks: [t(100, 1, { title: 'secret task title' })],
92
+ }, CATS);
93
+ const hidden = out.goals[0];
94
+ assert.equal(hidden.title, null);
95
+ assert.equal(hidden.subtitle, null);
96
+ assert.equal(hidden.category, null);
97
+ assert.equal(hidden.task_total, 2, 'counts are structure, not secrets (ADR 0112)');
98
+ assert.equal(hidden.criteria.length, 1, 'the ring is still drawn');
99
+ assert.equal(hidden.criteria[0].criterion_md, null, 'its text is not');
100
+ assert.deepEqual(hidden.criteria[0].task_ids, ['100'], 'the link survives — an id is not prose');
101
+ assert.equal(hidden.tasks[0].title, null);
102
+ assert.equal(hidden.tasks[0].status, 'ready');
103
+ assert.deepEqual(out.edges, [{ from: '2', to: '1', count: 1 }], 'and its routes still draw, or the structure would lie');
104
+ const json = JSON.stringify(out);
105
+ for (const leak of ['Secret roadmap', 'the secret plan', 'secret criterion text', 'secret task title', 'Security']) {
106
+ assert.equal(json.includes(leak), false, `"${leak}" is nowhere in the payload`);
107
+ }
108
+ });
109
+
110
+ await test('an empty corpus is a valid, empty sky', () => {
111
+ assert.deepEqual(shapeSky({}), { goals: [], edges: [], counts: { goals: 0, edges: 0, criteria: 0, tasks: 0 } });
112
+ });
113
+
114
+ // ---- the criterion→task link agrees with the progress rollup ------------------------
115
+
116
+ await test('a task\'s criterion link is the SAME link the version progress rollup computes, from the same flat rows', () => {
117
+ // The rollup's rows carry cnum + task_title; the sky's carry goal_id + task_status.
118
+ // One set of rows, both projections, so the two folds see one truth.
119
+ const pairs = [[10, 1, 100, 'shipped'], [10, 1, 101, 'ready'], [11, 1, null, null], [12, 2, 200, 'active'], [12, 2, 201, 'shipped'], [12, 2, 202, 'abandoned']];
120
+ const rollupRows = pairs.map(([c, goal, task, status], i) => ({ cnum: c - 9, id: String(c), criterion_id: `slug-${c}`, criterion_md: `Criterion ${c}`, satisfied: false, goal_id: String(goal), task_id: task == null ? null : String(task), task_title: task == null ? null : `Task ${task}`, task_status: status }));
121
+ const skyRowsFlat = pairs.map(([c, goal, task, status]) => cr(c, goal, task, status));
122
+
123
+ const rollup = buildCriterionProgress('V1', rollupRows, 0, [{ id: '1', title: 'A', status: 'open' }, { id: '2', title: 'B', status: 'open' }]);
124
+ const folded = foldCriteria(skyRowsFlat);
125
+
126
+ const linkFromRollup = Object.fromEntries(rollup.criteria.map((c) => [c.id, c.tasks.map((tk) => String(tk.id))]));
127
+ const linkFromSky = Object.fromEntries(folded.map((c) => [c.id, c.task_ids]));
128
+ assert.deepEqual(linkFromSky, linkFromRollup);
129
+ // and the derived flag agrees too, criterion by criterion
130
+ const flagFromRollup = Object.fromEntries(rollup.criteria.map((c) => [c.id, c.pending_review]));
131
+ const flagFromSky = Object.fromEntries(folded.map((c) => [c.id, c.pending_review]));
132
+ assert.deepEqual(flagFromSky, flagFromRollup);
133
+ assert.equal(flagFromSky['12'], false, 'one active task keeps ring 12 open on both sides');
134
+ });
135
+
136
+ // ---- the SQL, against a recording pool -----------------------------------------------
137
+
138
+ function fakePool({ goalRows = [], edgeRows = [], extraRows = [], criteriaRows = [], taskRows = [] } = {}) {
139
+ const calls = [];
140
+ return {
141
+ calls,
142
+ async query(sql, params) {
143
+ calls.push({ sql, params });
144
+ if (/FROM dependencies/.test(sql)) return { rows: edgeRows };
145
+ if (/FROM done_when_criteria/.test(sql)) return { rows: criteriaRows };
146
+ if (/subtitle/.test(sql)) return { rows: extraRows };
147
+ if (/FROM tasks t/.test(sql)) return { rows: taskRows };
148
+ return { rows: goalRows };
149
+ },
150
+ };
151
+ }
152
+
153
+ await test('the wells and the routes come from goal-graph.js\'s OWN read — the viewer, the Archon flag and the shared buckets ride as its bind parameters', async () => {
154
+ const pool = fakePool();
155
+ await skyRows({ versionId: 'V1', viewerId: 42, viewerIsArchon: true }, { pool });
156
+ const goalsQ = pool.calls.find((c) => /FROM goals g\s+JOIN tasks t/.test(c.sql));
157
+ assert.ok(goalsQ, 'the graph\'s goals query ran');
158
+ assert.deepEqual(goalsQ.params, ['V1', true, 42, CUT_STATUSES, DONE_STATUSES]);
159
+ assert.ok(pool.calls.some((c) => /FROM dependencies/.test(c.sql)), 'and its edges query');
160
+ assert.equal(pool.calls.length, 5, 'five queries, however many goals, criteria or tasks there are');
161
+ for (const { sql } of pool.calls) assert.equal(/\b42\b/.test(sql), false, 'no viewer id in any SQL text');
162
+ });
163
+
164
+ await test('the criterion→task join is read ONCE for every goal-bearing criterion — one query, flat rows, never per criterion or per version', async () => {
165
+ const pool = fakePool();
166
+ await skyRows({}, { pool });
167
+ const crit = pool.calls.filter((c) => /FROM done_when_criteria/.test(c.sql));
168
+ assert.equal(crit.length, 1);
169
+ assert.match(crit[0].sql, /LEFT JOIN task_criteria tc ON tc\.criterion_id = c\.id/);
170
+ assert.match(crit[0].sql, /c\.goal_id IS NOT NULL/);
171
+ 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)');
172
+ });
173
+
174
+ await test('the task query is compact by construction: no description, no value_summary, and the date is truncated in SQL', async () => {
175
+ const pool = fakePool();
176
+ await skyRows({}, { pool });
177
+ const tq = pool.calls.find((c) => /FROM tasks t\s+JOIN goals g/.test(c.sql));
178
+ const select = tq.sql.slice(tq.sql.indexOf('SELECT'), tq.sql.indexOf('FROM'));
179
+ assert.equal(/description|value_summary|notes|handoff/.test(select), false, select);
180
+ assert.match(select, /created_at::date/);
181
+ assert.match(select, /source_ref ~ '\^idea:\[0-9\]\+\$'/, 'the idea id comes from a strict source_ref match');
182
+ });
183
+
184
+ await test('a repeated ?version= (an array) binds as its first value on every query — never a driver-level 500', async () => {
185
+ const pool = fakePool();
186
+ await skyRows({ versionId: ['V1', 'V2'] }, { pool });
187
+ for (const { params } of pool.calls) assert.equal(params[0], 'V1');
188
+ });
189
+
190
+ await test('the payload is the shaped sky, categories included, from one fake read', async () => {
191
+ const pool = fakePool({
192
+ goalRows: [{ goal_id: '1', title: 'A', status: 'open', version_id: 'V1', category_id: 7, visible: true, task_total: 1, task_done: 0 }],
193
+ extraRows: [x(1)], criteriaRows: [cr(10, 1, 100, 'ready')], taskRows: [t(100, 1)],
194
+ });
195
+ const out = await sky({ viewerId: 1 }, { pool, categoryIndex: async () => CATS });
196
+ assert.equal(out.goals[0].category.label, 'Security');
197
+ assert.deepEqual(out.goals[0].criteria[0].task_ids, ['100']);
198
+ assert.equal(out.goals[0].tasks[0].title, 'Task 100');
199
+ });
200
+
201
+ // ---- the cache and the handler -------------------------------------------------------
202
+
203
+ function run(handler, req) {
204
+ const res = { body: null, failed: null, headers: {}, set(k, v) { this.headers[k] = v; return this; }, json(b) { this.body = b; return this; }, fail(code, o) { this.failed = { code, o }; return this; } };
205
+ return handler(req, res).then(() => res);
206
+ }
207
+
208
+ await test('the handler reads identity from the authenticated request, never the query', async () => {
209
+ let seen = null;
210
+ const h = skyHandler({ read: async (o) => { seen = o; return { goals: [], edges: [], counts: {} }; }, log: { error() {} } });
211
+ const res = await run(h, { builder: { id: 7, rank: 'metic' }, query: { version: 'V2', viewerId: '999', viewerIsArchon: 'true' } });
212
+ assert.deepEqual(seen, { versionId: 'V2', viewerId: 7, viewerIsArchon: false });
213
+ assert.equal(res.body.cache.hit, false);
214
+ assert.equal(res.body.cache.ttl_seconds, 60);
215
+ assert.equal(res.headers['Cache-Control'], 'private, max-age=60', 'private — the payload is shaped for one viewer');
216
+ });
217
+
218
+ await test('the cache serves a second read within 60s without touching the database, says so, and expires after', async () => {
219
+ let now = 1_000_000;
220
+ let reads = 0;
221
+ const cache = skyCache({ now: () => now });
222
+ const h = skyHandler({ read: async () => { reads += 1; return { goals: [], edges: [], counts: {} }; }, log: { error() {} }, cache });
223
+ const req = { builder: { id: 7, rank: 'metic' }, query: {} };
224
+ const a = await run(h, req);
225
+ const b = await run(h, req);
226
+ assert.equal(reads, 1);
227
+ assert.equal(b.body.cache.hit, true);
228
+ assert.equal(b.body.cache.taken_at, a.body.cache.taken_at);
229
+ now += 61_000;
230
+ const c = await run(h, req);
231
+ assert.equal(reads, 2, 'expired — read again');
232
+ assert.equal(c.body.cache.hit, false);
233
+ });
234
+
235
+ await test('the cache is PER VIEWER — a payload shaped for one builder is never handed to another, and an Archon\'s never to a non-Archon', async () => {
236
+ const cache = skyCache({ now: () => 5 });
237
+ const seen = [];
238
+ const h = skyHandler({ read: async (o) => { seen.push(o); return { goals: [{ goal_id: '1', title: o.viewerIsArchon ? 'SEEN' : null }], edges: [], counts: {} }; }, log: { error() {} }, cache });
239
+ const archon = await run(h, { builder: { id: 1, rank: 'archon' }, query: {} });
240
+ const metic = await run(h, { builder: { id: 2, rank: 'metic' }, query: {} });
241
+ const other = await run(h, { builder: { id: 3, rank: 'metic' }, query: {} });
242
+ assert.equal(seen.length, 3, 'three viewers, three reads');
243
+ assert.equal(archon.body.goals[0].title, 'SEEN');
244
+ assert.equal(metic.body.goals[0].title, null);
245
+ assert.equal(other.body.cache.hit, false);
246
+ assert.notEqual(cache.key({ viewerId: 1, viewerIsArchon: true }), cache.key({ viewerId: 1, viewerIsArchon: false }));
247
+ assert.notEqual(cache.key({ viewerId: 2 }), cache.key({ viewerId: 2, versionId: 'V1' }), 'a version filter is its own entry');
248
+ });
249
+
250
+ await test('a failing read is a 500 with no internals in the body', async () => {
251
+ const h = skyHandler({ read: async () => { throw new Error('SELECT exploded at goals.secret_column'); }, log: { error() {} } });
252
+ const res = await run(h, { builder: { id: 7, rank: 'metic' }, query: {} });
253
+ assert.equal(res.failed.code, 'sky_failed');
254
+ assert.equal(res.failed.o.status, 500);
255
+ assert.equal(JSON.stringify(res.failed).includes('secret_column'), false);
256
+ });
257
+
258
+ // ---- the mount -----------------------------------------------------------------------
259
+
260
+ await test('the route is contributed by the lifecycle manifest and gated to a signed-in builder', () => {
261
+ assert.ok(manifest.contributes.routes.includes('sky'), 'module.json contributes "sky"');
262
+ assert.match(ROUTE_SRC, /router\.get\('\/sky',\s*api\.requireBuilder,\s*sky\.skyHandler\(/);
263
+ });
264
+
265
+ summary();