@bongos/core 1.19.954 → 1.19.956

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,292 @@
1
+ // tests/provisioning_runner_drift_notice.mjs
2
+ //
3
+ // An intent-runner unit pointed at a different core than the instance installed now says
4
+ // so, once, without anybody looking (task 1004000 — the follow-up the task 1002722 grade
5
+ // asked for).
6
+ //
7
+ // THE DONE-WHEN IS TWO SENTENCES AND BOTH ARE HERE: "a unit repointed at a different core
8
+ // than the installed one produces one notice within one tick, and a healthy host produces
9
+ // none." The first is asserted through the real sweep tick, with a fake `systemctl`
10
+ // reader, so what is proved is the wiring and not only the planner. The second is
11
+ // asserted the same way — silence is a result, and the only way to know it is not the
12
+ // silence of a check that never ran is to prove the tick DID run and chose to say
13
+ // nothing.
14
+ //
15
+ // Also pinned, because each is a way this kind of alert goes wrong:
16
+ // • ONE notice, not one every five minutes — the state suppresses a repeat
17
+ // • a FIRST SIGHT is announced, unlike liveness's deliberate silence about an outage
18
+ // that predates the process (the reason is in runner-drift-notify.js's header, and a
19
+ // test that got this backwards would look just as green)
20
+ // • `unresolved` is NOT drift — it is the absence of a verdict, and announcing it would
21
+ // be a siren over nothing; it reaches the log line instead
22
+ // • no systemd → nothing said and no state touched, so a checker that cannot run never
23
+ // clears an announcement it also cannot re-make
24
+ // • a unit that VANISHES is not a recovery — "it is gone" and "it is fixed" are
25
+ // different facts and only one is good news
26
+ // • a drift check that throws does not take the liveness sweep with it
27
+ //
28
+ // DB-free and network-free: the systemctl reader and the notifier are injected.
29
+ //
30
+ // Run: node tests/provisioning_runner_drift_notice.mjs
31
+
32
+ import { strict as assert } from 'node:assert';
33
+ import { createRequire } from 'node:module';
34
+ import { readFileSync } from 'node:fs';
35
+
36
+ const require = createRequire(import.meta.url);
37
+ const notify = require('../modules/provisioning/pollers/runner-drift-notify.js');
38
+ const sweep = require('../modules/provisioning/pollers/liveness-sweep.js');
39
+
40
+ let passed = 0, failed = 0;
41
+ function t(name, fn) {
42
+ try { fn(); passed++; console.log(` PASS ${name}`); }
43
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
44
+ }
45
+ async function ta(name, fn) {
46
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
47
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
48
+ }
49
+
50
+ const DRIFTED = { name: 'bongos-intent-runner.service', status: 'drift', runnerVersion: '1.19.900', instanceVersion: '1.19.953', reason: 'runs core 1.19.900 but the instance installed 1.19.953' };
51
+ const HEALTHY = { name: 'bongos-intent-runner.service', status: 'ok', runnerVersion: '1.19.953', instanceVersion: '1.19.953', reason: 'runs core 1.19.953' };
52
+ const UNRESOLVED = { name: 'bongos-intent-runner.service', status: 'unresolved', runnerVersion: null, instanceVersion: '1.19.953', reason: 'ExecStart does not run a .js script directly' };
53
+
54
+ console.log('the planner:');
55
+
56
+ t('a drifted unit is announced on FIRST SIGHT, and the message names the installed version', () => {
57
+ const state = new Set();
58
+ const r = notify.planDriftNotices([DRIFTED], state);
59
+ assert.equal(r.notices.length, 1);
60
+ assert.equal(r.notices[0].kind, 'drift');
61
+ assert.match(r.notices[0].text, /older version/);
62
+ assert.match(r.notices[0].text, /1\.19\.953/, 'the installed version is in the message — it is what the runner should be on');
63
+ assert.match(r.notices[0].text, /bongos-intent-runner\.service/, 'and the unit name, once, for whoever fixes it');
64
+ assert.equal(r.drifting, 1);
65
+ // The opposite of liveness's rule, deliberately: drift that predates this process is
66
+ // exactly what nobody noticed, so a restart re-announcing it is correct.
67
+ assert.deepEqual([...state], ['bongos-intent-runner.service']);
68
+ });
69
+
70
+ t('the SECOND tick says nothing — one notice, not one every five minutes', () => {
71
+ const state = new Set();
72
+ notify.planDriftNotices([DRIFTED], state);
73
+ const again = notify.planDriftNotices([DRIFTED], state);
74
+ assert.deepEqual(again.notices, []);
75
+ assert.equal(again.drifting, 1, 'still counted in the account, just not re-announced');
76
+ });
77
+
78
+ t('a recovery is announced once, and then the unit is forgotten', () => {
79
+ const state = new Set();
80
+ notify.planDriftNotices([DRIFTED], state);
81
+ const fixed = notify.planDriftNotices([HEALTHY], state);
82
+ assert.equal(fixed.notices.length, 1);
83
+ assert.equal(fixed.notices[0].kind, 'clear');
84
+ assert.match(fixed.notices[0].text, /back on the current version/);
85
+ assert.deepEqual([...state], [], 'forgotten, so a later drift is a fresh first sight');
86
+ assert.deepEqual(notify.planDriftNotices([HEALTHY], state).notices, [], 'and the all-clear is not repeated either');
87
+ });
88
+
89
+ t('a unit that VANISHES is not a recovery', () => {
90
+ // "The unit is gone" and "the unit is fixed" are different facts and only one of them
91
+ // is good news. The state is still cleared, so a reinstated unit is a fresh first sight.
92
+ const state = new Set();
93
+ notify.planDriftNotices([DRIFTED], state);
94
+ const gone = notify.planDriftNotices([], state);
95
+ assert.deepEqual(gone.notices, [], 'no all-clear for a unit nobody fixed');
96
+ assert.deepEqual([...state], []);
97
+ });
98
+
99
+ t('several units drifting at once are ONE message, because they have one cause', () => {
100
+ const state = new Set();
101
+ const units = [DRIFTED, { ...DRIFTED, name: 'bongos-box-intent-runner.service' }];
102
+ const r = notify.planDriftNotices(units, state);
103
+ assert.equal(r.notices.length, 1, 'one message, not one per unit');
104
+ assert.match(r.notices[0].text, /2 background runners/);
105
+ assert.match(r.notices[0].text, /one upgrade that moved the code/, 'and it names the usual cause');
106
+ assert.equal(r.notices[0].units.length, 2);
107
+ });
108
+
109
+ t("'unresolved' is NOT drift — no notice, but it does reach the log line", () => {
110
+ const state = new Set();
111
+ const r = notify.planDriftNotices([UNRESOLVED], state);
112
+ assert.deepEqual(r.notices, [], 'the absence of a verdict is not a bad verdict');
113
+ assert.equal(r.unresolved, 1);
114
+ assert.equal(r.drifting, 0);
115
+ assert.deepEqual([...state], []);
116
+ // Visible without being urgent — and a host that is ALL unresolved must not read as
117
+ // healthy, which is why the line exists at all.
118
+ assert.match(notify.formatDriftLine(r), /unresolved=1 \(cannot tell — not a verdict\)/);
119
+ });
120
+
121
+ t('no systemd says nothing, touches no state, and reports systemd:false', () => {
122
+ const state = new Set(['bongos-intent-runner.service']);
123
+ for (const nothing of [null, undefined]) {
124
+ const r = notify.planDriftNotices(nothing, state);
125
+ assert.deepEqual(r.notices, []);
126
+ assert.equal(r.systemd, false);
127
+ assert.deepEqual([...state], ['bongos-intent-runner.service'],
128
+ 'a checker that cannot run must not clear an announcement it also cannot re-make');
129
+ }
130
+ });
131
+
132
+ t('a healthy host produces no log line either — no narration every five minutes', () => {
133
+ const r = notify.planDriftNotices([HEALTHY], new Set());
134
+ assert.deepEqual(r.notices, []);
135
+ assert.equal(notify.formatDriftLine(r), null, 'a vanilla control plane stays quiet');
136
+ assert.equal(notify.formatDriftLine({ systemd: true, checked: 0, drifting: 0, unresolved: 0 }), null, 'and so does a host with no runner units');
137
+ assert.match(notify.formatDriftLine({ systemd: true, checked: 2, drifting: 1, unresolved: 0 }), /runners=2 · drift=1/);
138
+ });
139
+
140
+ console.log('\nthrough the real sweep tick:');
141
+
142
+ // The sweep's own dependency seam, with no projects to probe: this tick is about the
143
+ // runners. `collectRunnerDrift` is what the production path reads systemd through.
144
+ function tickDeps({ units, notifySpy, logs }) {
145
+ return {
146
+ // `pool`, not `db` — sweepOnce's own seam name. Passing the wrong one silently falls
147
+ // back to the real api.pool and the tick tries to reach Postgres, which is how these
148
+ // five cases first failed.
149
+ pool: { query: async () => ({ rows: [] }) },
150
+ log: (m) => logs.push(String(m)),
151
+ notify: (n) => notifySpy.push(n),
152
+ collectRunnerDrift: async () => units,
153
+ instanceRoot: '/srv/instance',
154
+ };
155
+ }
156
+
157
+ // No active instances to probe: this tick is about the runners, and the roster read is
158
+ // the only other thing sweepOnce does before reaching them.
159
+ const provisioning = require('../modules/provisioning/provisioning.js');
160
+ provisioning.listActiveInstancesForLiveness = async () => [];
161
+
162
+ await ta('DONE-WHEN 1: a repointed unit produces ONE notice within ONE tick', async () => {
163
+ sweep.resetNoticeState();
164
+ const notifySpy = [], logs = [];
165
+ const out = await sweep.sweepOnce(tickDeps({ units: [DRIFTED], notifySpy, logs }));
166
+ assert.equal(notifySpy.length, 1, `one notice from one tick, got ${notifySpy.length}`);
167
+ assert.equal(notifySpy[0].kind, 'drift');
168
+ assert.equal(out.runnerDrift, 1, 'and the tick reports it in its account');
169
+ assert.ok(logs.some((l) => /\[drift\].*drift=1/.test(l)), 'the journal carries the line too');
170
+ });
171
+
172
+ await ta('DONE-WHEN 2: a healthy host produces NONE — and the tick demonstrably ran', async () => {
173
+ sweep.resetNoticeState();
174
+ const notifySpy = [], logs = [];
175
+ let asked = 0;
176
+ const deps = tickDeps({ units: [HEALTHY], notifySpy, logs });
177
+ const out = await sweep.sweepOnce({ ...deps, collectRunnerDrift: async () => { asked++; return [HEALTHY]; } });
178
+ assert.equal(notifySpy.length, 0, 'nothing is said');
179
+ assert.equal(asked, 1, 'and this is NOT the silence of a check that never ran');
180
+ assert.equal(out.runnerDrift, 0);
181
+ assert.ok(!logs.some((l) => /\[drift\]/.test(l)), 'no line either — silence all the way down');
182
+ });
183
+
184
+ await ta('the same drift over two ticks is announced once', async () => {
185
+ sweep.resetNoticeState();
186
+ const notifySpy = [], logs = [];
187
+ const deps = tickDeps({ units: [DRIFTED], notifySpy, logs });
188
+ await sweep.sweepOnce(deps);
189
+ await sweep.sweepOnce(deps);
190
+ assert.equal(notifySpy.length, 1, `the second tick must not re-announce, got ${notifySpy.length}`);
191
+ });
192
+
193
+ await ta('a drift check that THROWS does not take the sweep with it', async () => {
194
+ sweep.resetNoticeState();
195
+ const notifySpy = [], logs = [];
196
+ const deps = tickDeps({ units: [], notifySpy, logs });
197
+ const out = await sweep.sweepOnce({ ...deps, collectRunnerDrift: async () => { throw new Error('systemctl exploded'); } });
198
+ assert.ok(out, 'the tick still returns its liveness account');
199
+ assert.equal(out.swept, 0);
200
+ // Logged, not swallowed: silence here is indistinguishable from a healthy host, which
201
+ // is the exact failure this task was filed about.
202
+ assert.ok(logs.some((l) => /\[drift\] check failed/.test(l) && /systemctl exploded/.test(l)),
203
+ `the failure is said out loud: ${logs.join(' | ')}`);
204
+ });
205
+
206
+ await ta('the check is not gated on there being tenant projects', async () => {
207
+ // A control plane with no tenants still has runners worth checking — and that is the
208
+ // host this runs on.
209
+ sweep.resetNoticeState();
210
+ const notifySpy = [], logs = [];
211
+ const out = await sweep.sweepOnce(tickDeps({ units: [DRIFTED], notifySpy, logs }));
212
+ assert.equal(out.swept, 0, 'no projects at all');
213
+ assert.equal(notifySpy.length, 1, 'and the drift was still found');
214
+ });
215
+
216
+ console.log('\nwiring:');
217
+
218
+ t('the module crosses NO require boundary into core — the checker is a subprocess', () => {
219
+ // A module may require only src/module-api.js (ADR 0083, docs/modules-contract.md, and
220
+ // this module's own CLAUDE.md header). scripts/ is core-package territory, so the
221
+ // checker is read the way the sibling reads scripts/discord/post.js: as a subprocess.
222
+ // fitness's checkModulesImportOnlyApi does not currently catch a scripts/ target, so
223
+ // this assertion is the only thing standing between the rule and a silent regression.
224
+ for (const f of ['runner-drift-notify.js', 'liveness-sweep.js']) {
225
+ const src = readFileSync(new URL(`../modules/provisioning/pollers/${f}`, import.meta.url), 'utf8');
226
+ const requires = [...src.matchAll(/require\((['"])([^'"]+)\1\)/g)].map((m) => m[2]);
227
+ for (const spec of requires) {
228
+ // src/module-api(.js) is THE doorway and is allowed; everything else under src/ or
229
+ // scripts/ is not.
230
+ if (/^(\.\.\/)+src\/module-api(\.js)?$/.test(spec)) continue;
231
+ assert.ok(!/(^|\/)scripts\//.test(spec) && !/(^|\/)src\//.test(spec),
232
+ `${f} requires "${spec}" — a module may require only src/module-api.js; spawn a script instead (ADR 0083)`);
233
+ }
234
+ }
235
+ const notifySrc = readFileSync(new URL('../modules/provisioning/pollers/runner-drift-notify.js', import.meta.url), 'utf8');
236
+ assert.match(notifySrc, /execFile/, 'the checker is spawned');
237
+ assert.match(notifySrc, /runner-drift\.js', '--instance-root'|scripts\/gds\/runner-drift\.js/, 'and it is the checker that is spawned');
238
+ });
239
+
240
+ t('postNotice is NOT re-exported — the sweep takes it straight from its owner', () => {
241
+ // A re-export with no production caller is API surface to keep true for nothing.
242
+ assert.equal(notify.postNotice, undefined);
243
+ const sweepSrc = readFileSync(new URL('../modules/provisioning/pollers/liveness-sweep.js', import.meta.url), 'utf8');
244
+ assert.match(sweepSrc, /const \{ planNotices, postNotice \} = require\('\.\/liveness-notify'\);/,
245
+ 'one Discord path, imported from the file that owns it');
246
+ });
247
+
248
+ await ta('readRunnerDrift: a NON-ZERO exit is the drift signal, not a failed read', async () => {
249
+ // runner-drift.js exits 1 exactly WHEN it finds drift — that is its contract as a
250
+ // CI-style gate. Treating a failed exit as a failed read would discard the only result
251
+ // worth reporting, so the exit code is ignored whenever stdout parses.
252
+ const payload = JSON.stringify({ instanceRoot: '/srv/instance', systemd: true, units: [DRIFTED] });
253
+ const execFileImpl = (_cmd, _args, _opts, cb) => cb(Object.assign(new Error('Command failed'), { code: 1 }), payload, '');
254
+ const units = await notify.readRunnerDrift({ instanceRoot: '/srv/instance', execFileImpl });
255
+ assert.equal(units.length, 1);
256
+ assert.equal(units[0].status, 'drift');
257
+ });
258
+
259
+ await ta('readRunnerDrift: it spawns the checker with the instance root, from the repo root', async () => {
260
+ let seen = null;
261
+ const execFileImpl = (cmd, args, opts, cb) => { seen = { cmd, args, opts }; cb(null, JSON.stringify({ systemd: true, units: [] }), ''); };
262
+ const units = await notify.readRunnerDrift({ instanceRoot: '/srv/instance', execFileImpl });
263
+ assert.deepEqual(units, []);
264
+ assert.equal(seen.cmd, 'node');
265
+ assert.deepEqual(seen.args, ['scripts/gds/runner-drift.js', '--instance-root', '/srv/instance', '--json']);
266
+ assert.match(seen.opts.cwd, /cloud-bongos/, 'invoked from the repo root regardless of cwd');
267
+ assert.ok(seen.opts.timeout > 0, 'a hung systemctl costs the check, not the tick');
268
+ });
269
+
270
+ await ta('readRunnerDrift: "could not look" is NULL, never an empty array', async () => {
271
+ // "No runner units" and "could not ask" must not read the same — planDriftNotices
272
+ // treats them differently, and an empty array would silently clear announcements.
273
+ const cases = [
274
+ ['unparseable stdout', (_c, _a, _o, cb) => cb(null, 'systemctl: command not found', '')],
275
+ ['no systemd', (_c, _a, _o, cb) => cb(null, JSON.stringify({ systemd: false, units: [] }), '')],
276
+ ['empty stdout', (_c, _a, _o, cb) => cb(new Error('timeout'), '', '')],
277
+ ['a throwing spawn', () => { throw new Error('ENOENT'); }],
278
+ ];
279
+ for (const [why, execFileImpl] of cases) {
280
+ assert.equal(await notify.readRunnerDrift({ instanceRoot: '/x', execFileImpl }), null, why);
281
+ }
282
+ // And the empty-array case really is distinct: a host with systemd and no units.
283
+ assert.deepEqual(await notify.readRunnerDrift({ instanceRoot: '/x', execFileImpl: (_c, _a, _o, cb) => cb(null, JSON.stringify({ systemd: true, units: [] }), '') }), []);
284
+ });
285
+
286
+ t('the sweep clears BOTH notice states on reset, or a test leaks into the next one', () => {
287
+ const src = readFileSync(new URL('../modules/provisioning/pollers/liveness-sweep.js', import.meta.url), 'utf8');
288
+ assert.match(src, /function resetNoticeState\(\) \{ noticeState\.clear\(\); driftState\.clear\(\); \}/);
289
+ });
290
+
291
+ console.log(`\nprovisioning_runner_drift_notice: ${passed} passed, ${failed} failed`);
292
+ if (failed) process.exit(1);