@bongos/core 1.20.36 → 1.20.38

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 (53) hide show
  1. package/.bongos-core.json +104 -49
  2. package/.claude/skills/backlog-review/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +66 -0
  4. package/.claude/skills/collab-review/SKILL.md +1 -1
  5. package/.claude/skills/priority-session/SKILL.md +1 -1
  6. package/.claude/skills/session-handoff/SKILL.md +1 -1
  7. package/.claude/skills/status/SKILL.md +1 -1
  8. package/bin/bongos.js +1 -0
  9. package/clients/bongos-client/README.md +1 -1
  10. package/clients/bongos-client/bongos-client.global.js +8 -0
  11. package/clients/bongos-client/index.cjs +8 -0
  12. package/clients/bongos-client/index.d.ts +12 -1
  13. package/clients/bongos-client/index.mjs +8 -0
  14. package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
  15. package/docs/adr/0357-the-update-rule-set-on-deploy-is-the-one-the-sweep-follows.md +41 -0
  16. package/docs/adr/README.md +1 -0
  17. package/docs/api/openapi.json +188 -2
  18. package/docs/api-reference.md +12 -4
  19. package/docs/architecture.md +2 -0
  20. package/docs/copy-inventory.md +9 -9
  21. package/docs/copy-registry.json +9 -9
  22. package/docs/file-map.md +1 -0
  23. package/docs/module-api-changelog.md +4 -0
  24. package/docs/page-readings.json +2 -2
  25. package/docs/recipes/upgrading-the-core.md +2 -0
  26. package/modules/autonomy/db.js +39 -8
  27. package/modules/autonomy/fence.js +26 -4
  28. package/modules/autonomy/migrations/autonomy_003_priority_goal.sql +32 -0
  29. package/modules/autonomy/routes/autonomy.js +28 -1
  30. package/modules/hall-ui/public/gate.js +33 -4
  31. package/modules/ideas/routing.js +8 -1
  32. package/modules/platform-identity/instance-feedback.js +142 -0
  33. package/modules/platform-identity/module.json +1 -1
  34. package/modules/platform-identity/routes/sso.js +62 -0
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/release-notes.json +30 -0
  38. package/scripts/gds/autobongos-run.js +121 -16
  39. package/scripts/gds/feedback-send.js +131 -0
  40. package/scripts/gds/module-assess-tests.js +248 -0
  41. package/scripts/gds/update-channel.js +82 -0
  42. package/src/bongos/routes/feedback.js +110 -0
  43. package/src/bongos/routes.js +2 -0
  44. package/src/module-api.js +1 -1
  45. package/tests/autobongos_fence.mjs +62 -0
  46. package/tests/autobongos_loop.mjs +281 -0
  47. package/tests/autonomy_fence_priority.mjs +211 -0
  48. package/tests/bongos_cli.mjs +7 -1
  49. package/tests/filing_abuse_matrix.mjs +7 -2
  50. package/tests/idea_routing.mjs +1 -1
  51. package/tests/instance_feedback.mjs +327 -0
  52. package/tests/module_assess_tests.mjs +232 -0
  53. package/tests/update_channel_db.mjs +124 -0
@@ -0,0 +1,124 @@
1
+ // tests/update_channel_db.mjs — the unattended sweep follows the update rule set on /deploy
2
+ // (task 1004468, ADR 0357).
3
+ //
4
+ // WHY. /deploy writes provisioning_instances.update_channel (task 1004445), and the Upgrade button
5
+ // obeys it — but the sweep read only the roster file, so cloudbongos.com was set to `minor` on the
6
+ // site and still swept as `patch` (task 1004451's failed proof). These pin the rule: a project row
7
+ // wins, the roster is the fallback, and every fallback says why.
8
+ //
9
+ // Run: node --test tests/update_channel_db.mjs
10
+
11
+ import { test } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { createRequire } from 'node:module';
14
+ import { readFileSync } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const require = createRequire(import.meta.url);
19
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
20
+ const uc = require('../scripts/gds/update-channel.js');
21
+
22
+ const AVAILABLE = ['1.20.36', '1.20.37', '1.21.0', '2.0.0'];
23
+ const entry = (over = {}) => ({ slug: 'cloudbongos', dir: '/home/lars/cloudbongos', channel: 'patch', env: {}, ...over });
24
+ const dbOf = (rows) => ({ ok: true, channels: new Map(Object.entries(rows)) });
25
+ const targetFor = (inst) => uc.resolveChannelTarget({ installed: '1.20.37', channel: inst.channel, available: AVAILABLE });
26
+
27
+ test('DB minor + roster patch: the /deploy rule wins and a 1.21.0 target is chosen', () => {
28
+ const [inst] = uc.applyDbChannels([entry()], dbOf({ cloudbongos: 'minor' }));
29
+ assert.equal(inst.channel, 'minor');
30
+ assert.equal(inst.channelSource, uc.CHANNEL_SOURCE_DEPLOY);
31
+ assert.equal(inst.rosterChannel, 'patch');
32
+ assert.match(inst.channelNote, /set on \/deploy \(roster says patch, ignored\)/);
33
+ assert.equal(targetFor(inst), '1.21.0');
34
+ // Control: the roster alone would have held it on the patch line.
35
+ assert.equal(targetFor(entry()), null);
36
+ });
37
+
38
+ test('no project row for the slug: the roster wins, and the log says why', () => {
39
+ const [inst] = uc.applyDbChannels([entry({ slug: 'charter', channel: 'patch' })], dbOf({ cloudbongos: 'minor' }));
40
+ assert.equal(inst.channel, 'patch');
41
+ assert.equal(inst.channelSource, uc.CHANNEL_SOURCE_ROSTER);
42
+ assert.match(inst.channelNote, /no project row for this slug/);
43
+ });
44
+
45
+ test('DB pinned: the sweep skips it even when the roster says minor', () => {
46
+ const [inst] = uc.applyDbChannels([entry({ channel: 'minor' })], dbOf({ cloudbongos: 'pinned' }));
47
+ assert.equal(inst.channel, 'pinned');
48
+ assert.equal(targetFor(inst), null);
49
+ // subscribe.js skips on exactly this value, and decides it AFTER the DB rule is applied.
50
+ const src = readFileSync(path.join(ROOT, '.claude/scheduled-tasks/core-update-subscription/subscribe.js'), 'utf8');
51
+ const applied = src.indexOf('channel.applyDbChannels(');
52
+ const skip = src.indexOf("inst.channel === 'pinned'");
53
+ assert.ok(applied > 0 && skip > applied, 'the pinned skip must read the channel after the /deploy rule is applied');
54
+ });
55
+
56
+ test('database unreachable: every entry keeps its roster channel and carries the reason', () => {
57
+ const insts = uc.applyDbChannels([entry(), entry({ slug: 'charter', channel: 'minor' })], { ok: false, reason: 'psql not found' });
58
+ assert.deepEqual(insts.map((i) => i.channel), ['patch', 'minor']);
59
+ for (const i of insts) assert.match(i.channelNote, /from the roster — psql not found/);
60
+ });
61
+
62
+ test('an unknown value in the row normalises like any channel (never widens)', () => {
63
+ const [inst] = uc.applyDbChannels([entry()], dbOf({ cloudbongos: 'major' }));
64
+ assert.equal(inst.channel, uc.normalizeChannel('major'));
65
+ assert.notEqual(inst.channel, 'minor');
66
+ });
67
+
68
+ test('readDbChannels: no database named → not read, with the reason, and psql is never spawned', () => {
69
+ let spawned = false;
70
+ const r = uc.readDbChannels({ env: {}, run: () => { spawned = true; } });
71
+ assert.equal(r.ok, false);
72
+ assert.match(r.reason, /no DATABASE_URL or PGDATABASE/);
73
+ assert.equal(spawned, false);
74
+ });
75
+
76
+ test('readDbChannels: parses psql -At tab output into slug → channel, from the sweep env only', () => {
77
+ let args;
78
+ const run = (cmd, a) => { args = [cmd, ...a]; return { status: 0, stdout: 'cloudbongos\tminor\ncharter\tpatch\n\n', stderr: '' }; };
79
+ const r = uc.readDbChannels({ env: { PGDATABASE: 'cloudbongos' }, run });
80
+ assert.equal(r.ok, true);
81
+ assert.equal(r.channels.get('cloudbongos'), 'minor');
82
+ assert.equal(r.channels.get('charter'), 'patch');
83
+ assert.equal(r.channels.size, 2);
84
+ assert.deepEqual(args.slice(0, 3), ['psql', '-d', 'cloudbongos']);
85
+ assert.ok(args.includes('ON_ERROR_STOP=1'));
86
+ });
87
+
88
+ test('readDbChannels: DATABASE_URL is preferred over PGDATABASE', () => {
89
+ let target;
90
+ uc.readDbChannels({ env: { DATABASE_URL: 'postgres://u@h/db', PGDATABASE: 'x' }, run: (_c, a) => { target = a[1]; return { status: 0, stdout: '' }; } });
91
+ assert.equal(target, 'postgres://u@h/db');
92
+ });
93
+
94
+ test('readDbChannels: a missing table or psql fails soft with a one-line reason', () => {
95
+ const noTable = uc.readDbChannels({ env: { PGDATABASE: 'db' }, run: () => ({ status: 1, stdout: '', stderr: 'ERROR: relation "provisioning_instances" does not exist\nLINE 1: ...' }) });
96
+ assert.equal(noTable.ok, false);
97
+ assert.equal(noTable.reason, 'psql exit 1: no provisioning_instances table (provisioning module off?)');
98
+ const noPsql = uc.readDbChannels({ env: { PGDATABASE: 'db' }, run: () => ({ error: Object.assign(new Error('spawn psql ENOENT'), { code: 'ENOENT' }) }) });
99
+ assert.deepEqual(noPsql, { ok: false, reason: 'psql not found' });
100
+ });
101
+
102
+ test('readDbChannels: psql stderr never reaches the reason (it can name a DB user or host)', () => {
103
+ const stderr = 'psql: error: connection to server at "db.internal.example" (10.0.0.5), port 5432 failed: FATAL: password authentication failed for user "cloudbongos_admin"';
104
+ const r = uc.readDbChannels({ env: { DATABASE_URL: 'postgres://x' }, run: () => ({ status: 2, stdout: '', stderr }) });
105
+ assert.equal(r.reason, "psql exit 2: the sweep's database login was refused");
106
+ for (const leak of ['db.internal.example', '10.0.0.5', 'cloudbongos_admin']) assert.ok(!r.reason.includes(leak), `reason leaked ${leak}`);
107
+ assert.equal(uc.psqlFailureClass('something nobody anticipated for user "secret_user"'), 'query failed');
108
+ });
109
+
110
+ test('channelLine: the sweep log names the channel and where it came from', () => {
111
+ const [fromDeploy, fromRoster] = uc.applyDbChannels(
112
+ [entry(), entry({ slug: 'charter', channel: 'patch' })], dbOf({ cloudbongos: 'minor' }));
113
+ assert.equal(uc.channelLine(fromDeploy), ' • cloudbongos: channel minor, set on /deploy (roster says patch, ignored)');
114
+ assert.equal(uc.channelLine(fromRoster), ' • charter: channel patch, from the roster — no project row for this slug');
115
+ const src = readFileSync(path.join(ROOT, '.claude/scheduled-tasks/core-update-subscription/subscribe.js'), 'utf8');
116
+ assert.match(src, /console\.log\(channel\.channelLine\(inst\)\)/);
117
+ });
118
+
119
+ test('subscribe.js reads the DB from its own env, never an entry env (which names that instance\'s DB)', () => {
120
+ const src = readFileSync(path.join(ROOT, '.claude/scheduled-tasks/core-update-subscription/subscribe.js'), 'utf8');
121
+ assert.match(src, /channel\.readDbChannels\(\{ env: process\.env \}\)/);
122
+ // A core too old to carry the reader must keep the roster rather than crash the sweep.
123
+ assert.match(src, /typeof channel\.readDbChannels === 'function'/);
124
+ });