@bongos/core 1.20.5 → 1.20.6

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.
Files changed (55) hide show
  1. package/.bongos-core.json +102 -52
  2. package/.claude/skills/goal-review/SKILL.md +16 -24
  3. package/.claude/skills/goal-uat/SKILL.md +84 -0
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +12 -0
  6. package/clients/bongos-client/index.cjs +12 -0
  7. package/clients/bongos-client/index.d.ts +18 -1
  8. package/clients/bongos-client/index.mjs +12 -0
  9. package/docs/adr/0183-criteria-close-themselves.md +1 -1
  10. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  11. package/docs/adr/0351-a-criterion-closes-on-a-uat.md +73 -0
  12. package/docs/adr/README.md +28 -0
  13. package/docs/api/openapi.json +385 -5
  14. package/docs/api-reference.md +14 -4
  15. package/docs/architecture.md +7 -0
  16. package/docs/copy-inventory.md +22 -22
  17. package/docs/copy-registry.json +23 -23
  18. package/docs/file-map.md +2 -1
  19. package/docs/module-api-changelog.md +3 -1
  20. package/docs/page-readings.json +3 -3
  21. package/modules/hall-ui/public/goals-page.js +8 -3
  22. package/modules/hall-ui/public/tweak-editor.css +4 -7
  23. package/modules/hall-ui/public/tweak-editor.html +3 -3
  24. package/modules/hall-ui/public/tweak-editor.js +3 -0
  25. package/modules/lifecycle/criterion-uat-db.js +267 -0
  26. package/modules/lifecycle/criterion-uat.js +303 -0
  27. package/modules/lifecycle/db-goals.js +25 -12
  28. package/modules/lifecycle/done-when.js +84 -17
  29. package/modules/lifecycle/migrations/lifecycle_015_criterion_uat.sql +73 -0
  30. package/modules/lifecycle/module.json +1 -0
  31. package/modules/lifecycle/routes/criterion-uat.js +169 -0
  32. package/modules/lifecycle/routes/done-when.js +25 -1
  33. package/modules/npm-release/module.json +3 -1
  34. package/modules/npm-release/routes/task-where.js +18 -1
  35. package/modules/npm-release/work.js +38 -7
  36. package/modules/specialities/routes/specialities.js +8 -1
  37. package/modules/specialities/specialities.js +26 -1
  38. package/package-lock.json +2 -2
  39. package/package.json +1 -1
  40. package/release-notes.json +18 -0
  41. package/scripts/gds/cli-lib.js +4 -1
  42. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  43. package/scripts/gds/status.js +14 -3
  44. package/scripts/gds/uat.js +120 -0
  45. package/src/bongos/route-rank-check.js +9 -0
  46. package/src/module-api.js +1 -1
  47. package/tests/auto_satisfy_criteria.mjs +4 -2
  48. package/tests/criterion_uat.mjs +467 -0
  49. package/tests/criterion_uat_routes.mjs +232 -0
  50. package/tests/fitness.mjs +3 -1
  51. package/tests/goal_achievement.mjs +4 -2
  52. package/tests/goal_routes.mjs +4 -2
  53. package/tests/ideator_full_idea_shapes_space_proof.mjs +3 -2
  54. package/tests/npm_release_where.mjs +18 -2
  55. package/tests/speciality_session_skills.mjs +150 -0
@@ -0,0 +1,467 @@
1
+ // tests/criterion_uat.mjs — task 1004392 / ADR 0351: a criterion closes on a UAT.
2
+ //
3
+ // What these protect, in order of how badly it would hurt:
4
+ //
5
+ // 1. THE CLOSE NEEDS A CURRENT SIGN-OFF. autoSatisfyShippedCriteria (the ship
6
+ // hook and the reconciler sweep) must not close a criterion on shipped tasks
7
+ // alone any more, and a sign-off OLDER than the newest linked ship must not
8
+ // count. Losing either reopens the exact hole wa7-government fell through.
9
+ // 2. THE SIGNER WALL. Whoever shipped linked work is refused; the project owner
10
+ // is the one exception (a solo project must not deadlock). Asserted in the
11
+ // pure ladder AND through the real signOff transaction, so a refusal that
12
+ // exists in the ladder but is never consulted cannot pass.
13
+ // 3. THE STATES people read ("Awaiting UAT", "Closed before UAT"), and that an
14
+ // all-abandoned criterion is NOT awaiting a UAT (nothing was delivered).
15
+ // 4. THE RECORDING STORE: type + magic bytes + cap, and a name shape that equals
16
+ // the migration's CHECK.
17
+ //
18
+ // DB-free: fake executors script the rows and record the SQL. The real-SQL
19
+ // semantics are the integration lane's; the shapes and the branching are here.
20
+ //
21
+ // Run: node tests/criterion_uat.mjs
22
+
23
+ import { strict as assert } from 'node:assert';
24
+ import { createRequire } from 'node:module';
25
+ import { readFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs';
26
+ import { tmpdir } from 'node:os';
27
+ import { join } from 'node:path';
28
+ import { makeRunner } from './helpers.mjs';
29
+
30
+ process.env.NODE_ENV = 'test';
31
+ const require = createRequire(import.meta.url);
32
+ const uat = require('../modules/lifecycle/criterion-uat.js');
33
+ const uatDb = require('../modules/lifecycle/criterion-uat-db.js');
34
+ const doneWhen = require('../modules/lifecycle/done-when.js');
35
+ const { test, summary } = makeRunner();
36
+
37
+ const S = uat.UAT_STATES;
38
+
39
+ // ---- 1. the states ----------------------------------------------------------
40
+
41
+ await test('STATE: shipped work with no sign-off reads Awaiting UAT', () => {
42
+ assert.equal(uat.uatState({ satisfied: false, linked: 3, unshipped: 0, shipped: 2 }), S.AWAITING_UAT);
43
+ });
44
+
45
+ await test('STATE: open linked work, or none linked, reads Open', () => {
46
+ assert.equal(uat.uatState({ satisfied: false, linked: 3, unshipped: 1, shipped: 2 }), S.OPEN);
47
+ assert.equal(uat.uatState({ satisfied: false, linked: 0, unshipped: 0, shipped: 0 }), S.OPEN);
48
+ });
49
+
50
+ await test('STATE: ALL-ABANDONED is not awaiting a UAT — nothing was delivered to test', () => {
51
+ // ADR 0183's delivery guard, kept as the Code check. /goal-review owns this row.
52
+ assert.equal(uat.uatState({ satisfied: false, linked: 2, unshipped: 0, shipped: 0 }), S.OPEN);
53
+ assert.equal(uat.codeMet({ linked: 2, unshipped: 0, shipped: 0 }), false);
54
+ });
55
+
56
+ await test('STATE: a satisfied criterion reads by the kind of its newest sign-off', () => {
57
+ const sat = { satisfied: true, linked: 1, unshipped: 0, shipped: 1 };
58
+ assert.equal(uat.uatState({ ...sat, latestKind: 'uat' }), S.SATISFIED_UAT);
59
+ assert.equal(uat.uatState({ ...sat, latestKind: 'backend_signoff' }), S.SATISFIED_BACKEND);
60
+ assert.equal(uat.uatState({ ...sat, latestKind: 'override' }), S.SATISFIED_OVERRIDE);
61
+ // The owner's decision 1: already-closed criteria stay closed and say so.
62
+ assert.equal(uat.uatState({ ...sat, latestKind: null }), S.CLOSED_BEFORE_UAT);
63
+ assert.equal(uat.stateLabel(S.CLOSED_BEFORE_UAT), 'Closed before UAT');
64
+ assert.equal(uat.stateLabel(S.AWAITING_UAT), 'Awaiting UAT');
65
+ });
66
+
67
+ await test('STATE: /status rollup carries uat_state + awaiting_uat + backend_only', () => {
68
+ const rows = [
69
+ { cnum: 1, id: 1, criterion_id: 'a', criterion_md: 'A', satisfied: false, goal_id: null, uat_kind: null, backend_only: false, task_id: 11, task_title: 't', task_status: 'shipped' },
70
+ { cnum: 2, id: 2, criterion_id: 'b', criterion_md: 'B', satisfied: true, goal_id: null, uat_kind: null, backend_only: true, task_id: 12, task_title: 't', task_status: 'shipped' },
71
+ { cnum: 3, id: 3, criterion_id: 'c', criterion_md: 'C', satisfied: false, goal_id: null, uat_kind: null, backend_only: false, task_id: 13, task_title: 't', task_status: 'abandoned' },
72
+ ];
73
+ const out = doneWhen.buildCriterionProgress('V', rows);
74
+ const [a, b, c] = out.criteria;
75
+ assert.equal(a.uat_state, S.AWAITING_UAT);
76
+ assert.equal(a.awaiting_uat, true);
77
+ assert.equal(b.uat_state, S.CLOSED_BEFORE_UAT);
78
+ assert.equal(b.backend_only, true);
79
+ // The asymmetry survives: all-abandoned is flagged for a person, not for a UAT.
80
+ assert.equal(c.pending_review, true);
81
+ assert.equal(c.awaiting_uat, false);
82
+ assert.ok(!('uat_kind' in a), 'the raw sign-off kind is folded into uat_state, not leaked beside it');
83
+ });
84
+
85
+ // ---- 2. the Live fold ------------------------------------------------------
86
+
87
+ await test('LIVE: no reading, or a partial one, is unavailable (the signer attests)', () => {
88
+ assert.deepEqual(uat.foldLiveReadings(null), { available: false });
89
+ assert.deepEqual(uat.foldLiveReadings([{ task_id: '1', stage: 'released', partial: true }]), { available: false });
90
+ });
91
+
92
+ await test('LIVE: every task in a live stage is live; any other stage names the task', () => {
93
+ assert.deepEqual(uat.foldLiveReadings([{ task_id: '1', stage: 'released' }, { task_id: '2', stage: 'live_unreleased' }]), { available: true, live: true });
94
+ const r = uat.foldLiveReadings([{ task_id: '1', stage: 'released' }, { task_id: '2', stage: 'not_live' }]);
95
+ assert.equal(r.live, false);
96
+ assert.deepEqual(r.not_live, [{ task_id: '2', stage: 'not_live' }]);
97
+ });
98
+
99
+ // ---- 3. the sign-off ladder (pure) -----------------------------------------
100
+
101
+ const MET = { linked: 2, unshipped: 0, shipped: 2 };
102
+ const base = {
103
+ criterion: { satisfied: false }, counts: MET, backendOnly: false, kind: 'uat',
104
+ signerId: 50, ownerId: 3, shipperIds: ['7', '8'], recording: 'uat-1-0123456789abcdef.mp4',
105
+ live: { available: true, live: true },
106
+ };
107
+ const code = (over) => { const r = uat.signoffRefusal({ ...base, ...over }); return r && r.code; };
108
+
109
+ await test('LADDER: a non-shipper with a recording on live work is allowed', () => {
110
+ assert.equal(uat.signoffRefusal(base), null);
111
+ });
112
+
113
+ await test('LADDER: whoever shipped linked work is refused — ids compare as strings', () => {
114
+ assert.equal(code({ signerId: 7 }), 'signer_shipped_this_work');
115
+ assert.equal(code({ signerId: '8' }), 'signer_shipped_this_work');
116
+ });
117
+
118
+ await test('LADDER: the project owner may sign off work they shipped (solo projects)', () => {
119
+ assert.equal(code({ signerId: 3, shipperIds: ['3'] }), null);
120
+ });
121
+
122
+ await test('LADDER: nothing to sign — closed, open work, or nothing delivered', () => {
123
+ assert.equal(code({ criterion: { satisfied: true } }), 'criterion_already_satisfied');
124
+ assert.equal(code({ counts: { linked: 2, unshipped: 1, shipped: 1 } }), 'criterion_not_awaiting_uat');
125
+ assert.equal(code({ counts: { linked: 2, unshipped: 0, shipped: 0 } }), 'criterion_not_awaiting_uat');
126
+ assert.equal(code({ criterion: null }), 'criterion_not_found');
127
+ });
128
+
129
+ await test('LADDER: the kind must match the criterion', () => {
130
+ assert.equal(code({ backendOnly: true }), 'criterion_is_backend_only');
131
+ assert.equal(code({ kind: 'backend_signoff', recording: null }), 'criterion_needs_recording');
132
+ assert.equal(code({ kind: 'backend_signoff', recording: null, backendOnly: true }), null);
133
+ });
134
+
135
+ await test('LADDER: a UAT needs its recording', () => {
136
+ assert.equal(code({ recording: null }), 'uat_recording_required');
137
+ });
138
+
139
+ await test('LADDER: not live is refused where readable; attestation is required where not', () => {
140
+ assert.equal(code({ live: { available: true, live: false, not_live: [] } }), 'work_not_live');
141
+ assert.equal(code({ live: { available: false } }), 'live_attestation_required');
142
+ assert.equal(code({ live: { available: false }, liveAttested: true }), null);
143
+ assert.equal(uat.liveBasis({ available: true, live: true }), 'deploy_reading');
144
+ assert.equal(uat.liveBasis({ available: false }), 'attested');
145
+ });
146
+
147
+ await test('LADDER: the shipper refusal outranks missing evidence (they are told the real reason)', () => {
148
+ assert.equal(code({ signerId: 7, recording: null, live: { available: false } }), 'signer_shipped_this_work');
149
+ });
150
+
151
+ // ---- 4. the close path ------------------------------------------------------
152
+
153
+ function fakeExec(...results) {
154
+ const calls = [];
155
+ return { calls, async query(sql, params) { calls.push({ sql, params }); return results[calls.length - 1] || { rows: [] }; } };
156
+ }
157
+
158
+ await test('CLOSE (sql): the auto-close requires a CURRENT uat/backend sign-off row', async () => {
159
+ const exec = fakeExec({ rows: [] });
160
+ await doneWhen.autoSatisfyShippedCriteria(exec, {});
161
+ const sql = exec.calls[0].sql;
162
+ assert.match(sql, /EXISTS \(SELECT 1 FROM lifecycle_criterion_uats u/, 'a sign-off row must exist');
163
+ assert.match(sql, /u\.kind IN \('uat', 'backend_signoff'\)/, 'an override row is not a candidate — the override route satisfies directly');
164
+ assert.match(sql, /u\.created_at >= COALESCE\(candidate\.last_ship/, 'a sign-off older than the newest linked ship is stale');
165
+ assert.match(sql, /max\(t\.shipped_at\) FILTER \(WHERE t\.status = 'shipped'\) AS last_ship/);
166
+ // The delivery guard (ADR 0183) is still there.
167
+ assert.match(sql, /count\(\*\) FILTER \(WHERE t\.status = 'shipped'\) >= 1/);
168
+ });
169
+
170
+ await test('CLOSE: the criterionId scope binds $2 and leaves the task scope null', async () => {
171
+ const exec = fakeExec({ rows: [] });
172
+ await doneWhen.autoSatisfyShippedCriteria(exec, { criterionId: 42 });
173
+ assert.deepEqual(exec.calls[0].params, [null, 42]);
174
+ assert.match(exec.calls[0].sql, /\$2::bigint IS NULL OR c\.id = \$2::bigint/);
175
+ const shipScoped = fakeExec({ rows: [] });
176
+ await doneWhen.autoSatisfyShippedCriteria(shipScoped, { taskId: 9 });
177
+ assert.deepEqual(shipScoped.calls[0].params, [9, null], 'the ship hook still scopes by task');
178
+ await assert.rejects(() => doneWhen.autoSatisfyShippedCriteria(fakeExec(), { criterionId: 'x' }), /criterionId must be numeric/);
179
+ });
180
+
181
+ // ---- 5. signOff, through its real transaction ------------------------------
182
+
183
+ // A SQL-routed fake: the same object is the pool and the transaction client.
184
+ function signoffDb({ linked, linkedAfter = null, satisfied = false, backendOnly = false, closes = true }) {
185
+ const calls = [];
186
+ let linkedReads = 0;
187
+ const db = {
188
+ calls,
189
+ async query(sql, params) {
190
+ calls.push({ sql, params });
191
+ if (/FROM task_criteria tc/.test(sql) && /shipped_by/.test(sql)) {
192
+ linkedReads += 1;
193
+ return { rows: linkedReads > 1 && linkedAfter ? linkedAfter : linked };
194
+ }
195
+ if (/FROM done_when_criteria WHERE id = \$1 FOR UPDATE/.test(sql)) return { rows: [{ id: params[0], satisfied, goal_id: null, version_id: 'V' }] };
196
+ if (/lifecycle_criterion_backend_only/.test(sql)) return { rows: backendOnly ? [{ '?column?': 1 }] : [] };
197
+ if (/INSERT INTO lifecycle_criterion_uats/.test(sql)) return { rows: [{ id: 1, kind: params[1], live_basis: params[4] }] };
198
+ if (/WITH candidate AS/.test(sql)) return { rows: closes ? [{ id: params[1], goal_id: null }] : [] };
199
+ return { rows: [] };
200
+ },
201
+ };
202
+ return db;
203
+ }
204
+ const withTx = (db) => async (fn) => fn(db);
205
+ const SHIPPED = [
206
+ { id: 11, status: 'shipped', shipped_by: 7, shipped_at: new Date() },
207
+ { id: 12, status: 'shipped', shipped_by: 8, shipped_at: new Date() },
208
+ ];
209
+ const REC = 'uat-5-0123456789abcdef.webm';
210
+ const inserts = (db) => db.calls.filter((c) => /INSERT INTO lifecycle_criterion_uats/.test(c.sql));
211
+
212
+ await test('SIGNOFF: a non-shipper signs, the row is written, and the criterion closes in the same tx', async () => {
213
+ const db = signoffDb({ linked: SHIPPED });
214
+ const out = await uatDb.signOff(
215
+ { criterionId: 5, kind: 'uat', signerId: 50, recording: REC, liveAttested: true, note: ' checked the page ' },
216
+ { pool: db, withTx: withTx(db), deployPort: null, ownerId: 3 },
217
+ );
218
+ assert.equal(out.ok, true, JSON.stringify(out.refusal));
219
+ const ins = inserts(db);
220
+ assert.equal(ins.length, 1);
221
+ assert.deepEqual(ins[0].params, [5, 'uat', 50, REC, 'attested', 'checked the page']);
222
+ const close = db.calls.find((c) => /WITH candidate AS/.test(c.sql));
223
+ assert.ok(close, 'the sign-off must run the close');
224
+ assert.deepEqual(close.params, [null, 5], 'scoped to THIS criterion');
225
+ assert.ok(db.calls.indexOf(close) > db.calls.indexOf(ins[0]), 'the close runs after the row it depends on');
226
+ assert.equal(out.closed.criteria.length, 1);
227
+ });
228
+
229
+ await test('SIGNOFF: a shipper is refused and NOTHING is written', async () => {
230
+ const db = signoffDb({ linked: SHIPPED });
231
+ const out = await uatDb.signOff(
232
+ { criterionId: 5, kind: 'uat', signerId: 7, recording: REC, liveAttested: true },
233
+ { pool: db, withTx: withTx(db), deployPort: null, ownerId: 3 },
234
+ );
235
+ assert.equal(out.ok, false);
236
+ assert.equal(out.refusal.code, 'signer_shipped_this_work');
237
+ assert.equal(inserts(db).length, 0);
238
+ assert.ok(!db.calls.some((c) => /WITH candidate AS/.test(c.sql)), 'a refusal never runs the close');
239
+ });
240
+
241
+ await test('SIGNOFF: the project owner may sign off their own shipped work', async () => {
242
+ const db = signoffDb({ linked: [{ id: 11, status: 'shipped', shipped_by: 3, shipped_at: new Date() }] });
243
+ const out = await uatDb.signOff(
244
+ { criterionId: 5, kind: 'uat', signerId: 3, recording: REC, liveAttested: true },
245
+ { pool: db, withTx: withTx(db), deployPort: null, ownerId: 3 },
246
+ );
247
+ assert.equal(out.ok, true, JSON.stringify(out.refusal));
248
+ });
249
+
250
+ await test('SIGNOFF: the deploy reading refuses work that is not live, and names it', async () => {
251
+ const db = signoffDb({ linked: SHIPPED });
252
+ const deployPort = { readTaskWhere: async (id) => ({ stage: id === '12' ? 'not_live' : 'released', partial: false }) };
253
+ const out = await uatDb.signOff(
254
+ { criterionId: 5, kind: 'uat', signerId: 50, recording: REC },
255
+ { pool: db, withTx: withTx(db), deployPort, ownerId: 3 },
256
+ );
257
+ assert.equal(out.refusal.code, 'work_not_live');
258
+ assert.deepEqual(out.refusal.not_live, [{ task_id: '12', stage: 'not_live' }]);
259
+ });
260
+
261
+ await test('SIGNOFF: a live reading is recorded as deploy_reading, and a THROWING reader falls back to attestation', async () => {
262
+ const live = signoffDb({ linked: SHIPPED });
263
+ const ok = await uatDb.signOff(
264
+ { criterionId: 5, kind: 'uat', signerId: 50, recording: REC },
265
+ { pool: live, withTx: withTx(live), deployPort: { readTaskWhere: async () => ({ stage: 'released' }) }, ownerId: 3 },
266
+ );
267
+ assert.equal(ok.ok, true);
268
+ assert.equal(inserts(live)[0].params[4], 'deploy_reading');
269
+ const broken = signoffDb({ linked: SHIPPED });
270
+ const out = await uatDb.signOff(
271
+ { criterionId: 5, kind: 'uat', signerId: 50, recording: REC },
272
+ { pool: broken, withTx: withTx(broken), deployPort: { readTaskWhere: async () => { throw new Error('registry down'); } }, ownerId: 3 },
273
+ );
274
+ assert.equal(out.refusal.code, 'live_attestation_required', 'an outage moves Live to the signer, never blocks them outright');
275
+ });
276
+
277
+ await test('SIGNOFF: linked work that changed between the reading and the lock is refused', async () => {
278
+ const db = signoffDb({ linked: SHIPPED, linkedAfter: [...SHIPPED, { id: 13, status: 'shipped', shipped_by: 9, shipped_at: new Date() }] });
279
+ const out = await uatDb.signOff(
280
+ { criterionId: 5, kind: 'uat', signerId: 50, recording: REC, liveAttested: true },
281
+ { pool: db, withTx: withTx(db), deployPort: null, ownerId: 3 },
282
+ );
283
+ assert.equal(out.refusal.code, 'linked_work_changed');
284
+ assert.equal(inserts(db).length, 0);
285
+ });
286
+
287
+ await test('SIGNOFF: a recording uploaded for another criterion is refused before any read', async () => {
288
+ const db = signoffDb({ linked: SHIPPED });
289
+ const out = await uatDb.signOff(
290
+ { criterionId: 6, kind: 'uat', signerId: 50, recording: REC, liveAttested: true },
291
+ { pool: db, withTx: withTx(db), deployPort: null, ownerId: 3 },
292
+ );
293
+ assert.equal(out.refusal.code, 'recording_not_for_this_criterion');
294
+ assert.equal(db.calls.length, 0);
295
+ });
296
+
297
+ await test('SIGNOFF: an override cannot be submitted through the sign-off path', async () => {
298
+ const out = await uatDb.signOff({ criterionId: 5, kind: 'override', signerId: 50 }, {});
299
+ assert.equal(out.refusal.code, 'bad_kind');
300
+ });
301
+
302
+ // ---- 6. the recording store -------------------------------------------------
303
+
304
+ const mp4 = (n = 64) => { const b = Buffer.alloc(n); b.write('ftyp', 4, 'latin1'); return b; };
305
+ const webm = (n = 64) => { const b = Buffer.alloc(n); b[0] = 0x1a; b[1] = 0x45; b[2] = 0xdf; b[3] = 0xa3; return b; };
306
+
307
+ await test('STORE: mp4 and webm pass; a mislabelled or foreign file does not', () => {
308
+ assert.equal(uat.screenRecording(mp4(), 'video/mp4').ok, true);
309
+ assert.equal(uat.screenRecording(webm(), 'video/webm; codecs=vp9').ok, true);
310
+ assert.equal(uat.screenRecording(webm(), 'video/mp4').reason, 'sniff_mismatch');
311
+ assert.equal(uat.screenRecording(mp4(), 'image/png').reason, 'bad_type');
312
+ assert.equal(uat.screenRecording(Buffer.alloc(0), 'video/mp4').reason, 'empty');
313
+ });
314
+
315
+ await test('STORE: the 100 MB cap is exact', () => {
316
+ assert.equal(uat.MAX_RECORDING_BYTES, 100 * 1024 * 1024);
317
+ assert.equal(uat.screenRecording(mp4(uat.MAX_RECORDING_BYTES), 'video/mp4').ok, true);
318
+ assert.equal(uat.screenRecording(mp4(uat.MAX_RECORDING_BYTES + 1), 'video/mp4').reason, 'too_big');
319
+ });
320
+
321
+ await test('STORE: a stored name is server-minted, carries its criterion, and resolves only inside the dir', async () => {
322
+ const dir = mkdtempSync(join(tmpdir(), 'uat-store-'));
323
+ try {
324
+ const r = await uat.persistRecording(mp4(), { criterionId: 77, contentType: 'video/mp4', dir });
325
+ assert.equal(r.ok, true);
326
+ assert.match(r.file, /^uat-77-[0-9a-f]{16}\.mp4$/);
327
+ assert.equal(uat.criterionIdFromName(r.file), 77);
328
+ assert.ok(existsSync(uat.resolveSafe(r.file, { dir })));
329
+ for (const bad of ['../uat-1-0123456789abcdef.mp4', 'uat-1-0123456789abcdef.mp4/..', 'uat-1-xyz.mp4', 'task-1-0123456789abcdef.png']) {
330
+ assert.equal(uat.resolveSafe(bad, { dir }), null, bad);
331
+ }
332
+ } finally {
333
+ rmSync(dir, { recursive: true, force: true });
334
+ }
335
+ });
336
+
337
+ await test('STORE: the app and the migration CHECK accept and refuse the same names', () => {
338
+ const sql = readFileSync(new URL('../modules/lifecycle/migrations/lifecycle_015_criterion_uat.sql', import.meta.url), 'utf8');
339
+ // This pattern uses only syntax POSIX and JS regexes read identically.
340
+ const check = new RegExp(/recording_file ~ '([^']+)'/.exec(sql)[1]);
341
+ const names = [
342
+ 'uat-1-0123456789abcdef.mp4', 'uat-123456789012-fedcba9876543210.webm',
343
+ 'uat-1234567890123-0123456789abcdef.mp4', 'uat-1-0123456789ABCDEF.mp4', 'uat-1-0123456789abcdef.mov',
344
+ 'uat--0123456789abcdef.mp4', 'uat-1-0123456789abcde.mp4', 'x/uat-1-0123456789abcdef.mp4', 'uat-1-0123456789abcdef.mp4x',
345
+ ];
346
+ for (const n of names) assert.equal(uat.isSafeName(n), check.test(n), `app and DB disagree on ${n}`);
347
+ assert.ok(names.some((n) => uat.isSafeName(n)) && names.some((n) => !uat.isSafeName(n)), 'the probe set must hold both answers');
348
+ });
349
+
350
+ // ---- 7. the read behind GET /done-when/:id/uat ------------------------------
351
+
352
+ // A SQL-routed fake for uatView: the criterion row, its linked tasks, its
353
+ // sign-offs (newest first, as listSignoffs orders them), and the backend flag.
354
+ function viewDb({ satisfied = false, linked, signoffs = [], backendOnly = false }) {
355
+ return {
356
+ async query(sql) {
357
+ if (/FROM done_when_criteria WHERE id = \$1/.test(sql)) return { rows: [{ id: 5, criterion_id: 'c', criterion_md: 'md', satisfied, goal_id: 3, version_id: 'V' }] };
358
+ if (/FROM task_criteria tc/.test(sql)) return { rows: linked };
359
+ if (/FROM lifecycle_criterion_uats u/.test(sql)) return { rows: signoffs };
360
+ if (/lifecycle_criterion_backend_only/.test(sql)) return { rows: backendOnly ? [{ x: 1 }] : [] };
361
+ throw new Error(`unexpected SQL: ${sql}`);
362
+ },
363
+ };
364
+ }
365
+ const T0 = new Date('2026-09-20T10:00:00Z');
366
+ const T1 = new Date('2026-09-25T10:00:00Z');
367
+
368
+ await test('VIEW: a sign-off older than the newest linked ship is reported STALE, never as the UAT check', async () => {
369
+ const linked = [{ id: 11, status: 'shipped', shipped_by: 7, shipped_at: T0 }, { id: 12, status: 'shipped', shipped_by: 8, shipped_at: T1 }];
370
+ const stale = { id: 1, kind: 'uat', signed_off_by: 50, created_at: new Date('2026-09-22T10:00:00Z') };
371
+ const v = await uatDb.uatView(5, { pool: viewDb({ linked, signoffs: [stale] }), deployPort: null });
372
+ assert.equal(v.uat_state, S.AWAITING_UAT);
373
+ assert.equal(v.checks.code, true);
374
+ assert.equal(v.checks.uat, null, 'work shipped after the sign-off, so it does not count');
375
+ assert.equal(v.checks.stale_uat.id, 1, 'and the reader is told why');
376
+ assert.deepEqual(v.checks.live, { available: false }, 'no deploy reading here: the signer will attest');
377
+ });
378
+
379
+ await test('VIEW: a current sign-off is the UAT check, and an override row never is', async () => {
380
+ const linked = [{ id: 11, status: 'shipped', shipped_by: 7, shipped_at: T0 }];
381
+ const override = { id: 3, kind: 'override', created_at: T1 };
382
+ const current = { id: 2, kind: 'backend_signoff', created_at: T1 };
383
+ const v = await uatDb.uatView(5, { pool: viewDb({ satisfied: true, linked, signoffs: [override, current], backendOnly: true }) });
384
+ assert.equal(v.checks.uat.id, 2);
385
+ assert.equal(v.checks.stale_uat, null);
386
+ assert.equal(v.backend_only, true);
387
+ assert.equal(v.uat_state, S.SATISFIED_OVERRIDE, 'the state reads by the NEWEST row, which is the override');
388
+ assert.equal(v.checks.live, null, 'Live is only read while a criterion is awaiting UAT');
389
+ });
390
+
391
+ await test('VIEW: Live is read from the deploy port while awaiting, and an unknown criterion is null', async () => {
392
+ const linked = [{ id: 11, status: 'shipped', shipped_by: 7, shipped_at: T0 }];
393
+ const port = { readTasksWhere: async (ids) => Object.fromEntries(ids.map((id) => [id, { stage: 'released' }])) };
394
+ const v = await uatDb.uatView(5, { pool: viewDb({ linked }), deployPort: port });
395
+ assert.deepEqual(v.checks.live, { available: true, live: true });
396
+ const none = { async query() { return { rows: [] }; } };
397
+ assert.equal(await uatDb.uatView(6, { pool: none }), null);
398
+ });
399
+
400
+ // ---- 8. readLive: one batch read, and every failure is a PARTIAL -------------
401
+
402
+ await test('LIVE READ: the batch port is asked ONCE for every task, and a missing task is partial', async () => {
403
+ const calls = [];
404
+ const port = {
405
+ readTasksWhere: async (ids) => { calls.push(ids); return { 11: { stage: 'released' } }; },
406
+ readTaskWhere: async () => { throw new Error('the per-task form must not be used when the batch exists'); },
407
+ };
408
+ const r = await uatDb.readLive(['11', '12'], { deployPort: port });
409
+ assert.equal(calls.length, 1);
410
+ assert.deepEqual(r, [{ task_id: '11', stage: 'released', partial: false }, { task_id: '12', stage: null, partial: true }]);
411
+ const thrown = await uatDb.readLive(['11'], { deployPort: { readTasksWhere: async () => { throw new Error('down'); } } });
412
+ assert.deepEqual(thrown, [{ task_id: '11', stage: null, partial: true }]);
413
+ assert.equal(await uatDb.readLive(['11'], { deployPort: null }), null, 'no provider, no reading');
414
+ });
415
+
416
+ // ---- 9. backend-only: set while OPEN, never once the criterion is awaiting ---
417
+
418
+ function flagDb({ satisfied = false, linked }) {
419
+ const calls = [];
420
+ return {
421
+ calls,
422
+ async query(sql, params) {
423
+ calls.push({ sql, params });
424
+ if (/FROM done_when_criteria WHERE id = \$1 FOR UPDATE/.test(sql)) return { rows: [{ id: params[0], satisfied, goal_id: 3, version_id: 'V' }] };
425
+ if (/FROM task_criteria tc/.test(sql)) return { rows: linked };
426
+ return { rows: [] };
427
+ },
428
+ };
429
+ }
430
+ const flagWrites = (db) => db.calls.filter((c) => /lifecycle_criterion_backend_only/.test(c.sql));
431
+ const allow = async () => null;
432
+
433
+ await test('BACKEND-ONLY: an open criterion can be marked and unmarked', async () => {
434
+ const open = [{ id: 11, status: 'active', shipped_by: null, shipped_at: null }];
435
+ const on = flagDb({ linked: open });
436
+ const r = await uatDb.setBackendOnly({ criterionId: 5, builderId: 50, backendOnly: true, authorize: allow }, { pool: on, withTx: withTx(on) });
437
+ assert.deepEqual(r, { ok: true, backend_only: true });
438
+ assert.match(flagWrites(on)[0].sql, /INSERT INTO lifecycle_criterion_backend_only/);
439
+ assert.deepEqual(flagWrites(on)[0].params, [5, 50]);
440
+ const off = flagDb({ linked: open });
441
+ await uatDb.setBackendOnly({ criterionId: 5, builderId: 50, backendOnly: false, authorize: allow }, { pool: off, withTx: withTx(off) });
442
+ assert.match(flagWrites(off)[0].sql, /DELETE FROM lifecycle_criterion_backend_only/);
443
+ });
444
+
445
+ await test('BACKEND-ONLY: LOCKED once the criterion is awaiting UAT or closed — the closer cannot skip the recording', async () => {
446
+ const shipped = [{ id: 11, status: 'shipped', shipped_by: 7, shipped_at: T0 }];
447
+ for (const [label, db] of [['awaiting', flagDb({ linked: shipped })], ['closed', flagDb({ satisfied: true, linked: shipped })]]) {
448
+ const r = await uatDb.setBackendOnly({ criterionId: 5, builderId: 50, backendOnly: true, authorize: allow }, { pool: db, withTx: withTx(db) });
449
+ assert.equal(r.refusal.code, 'backend_only_locked', label);
450
+ assert.equal(flagWrites(db).length, 0, `${label}: nothing written`);
451
+ }
452
+ });
453
+
454
+ await test('BACKEND-ONLY: the authority gate runs INSIDE the transaction, before any read of the work, and a refusal writes nothing', async () => {
455
+ const db = flagDb({ linked: [] });
456
+ const seen = [];
457
+ const r = await uatDb.setBackendOnly({
458
+ criterionId: 5, builderId: 50, backendOnly: true,
459
+ authorize: async (client, crit) => { seen.push([client === db, crit.goal_id]); return { code: 'goal_not_yours', status: 403, message: 'no' }; },
460
+ }, { pool: db, withTx: withTx(db) });
461
+ assert.equal(r.refusal.code, 'goal_not_yours');
462
+ assert.deepEqual(seen, [[true, 3]], 'the gate saw the locked row on the transaction client');
463
+ assert.equal(flagWrites(db).length, 0);
464
+ assert.ok(!db.calls.some((c) => /FROM task_criteria/.test(c.sql)), 'a refused caller learns nothing about the linked work');
465
+ });
466
+
467
+ summary();