@bongos/core 1.19.347 → 1.19.349
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.
- package/.bongos-core.json +33 -23
- package/docs/copy-inventory.md +22 -22
- package/docs/copy-registry.json +25 -25
- package/modules/hall-ui/public/idea.js +9 -1
- package/modules/hall-ui/public/ideas.js +17 -5
- package/modules/lifecycle/github-push.js +51 -4
- package/modules/lifecycle/routes/tasks.js +22 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/client-baseurl-guard.js +116 -0
- package/scripts/gds/deploy-divergence.js +34 -3
- package/scripts/gds/fitness.js +1 -0
- package/scripts/gds/ship-land.js +24 -2
- package/scripts/gds/update-channel.js +121 -5
- package/src/module-api.js +5 -1
- package/tests/client_baseurl_guard.mjs +122 -0
- package/tests/idea_hall_composer.mjs +11 -0
- package/tests/ship_publish_error.mjs +86 -2
- package/tests/update_channel.mjs +125 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// tests/client_baseurl_guard.mjs — the guard that stops a page 404ing every call
|
|
2
|
+
// (task 1003263).
|
|
3
|
+
//
|
|
4
|
+
// Three pages shipped the defect this guard exists to catch: ideas.js and idea.js
|
|
5
|
+
// were dead in production from the day they shipped, and collab.js was caught one
|
|
6
|
+
// commit before it would have joined them. So the guard itself is worth testing on
|
|
7
|
+
// synthetic sources rather than only on the live tree — a scan that passes because
|
|
8
|
+
// the tree happens to be clean today proves nothing about what it would REFUSE.
|
|
9
|
+
//
|
|
10
|
+
// Every case below is an in-memory file: no fixtures on disk, no dependence on the
|
|
11
|
+
// current state of modules/hall-ui/public/.
|
|
12
|
+
//
|
|
13
|
+
// Run: node tests/client_baseurl_guard.mjs
|
|
14
|
+
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { checkClientBaseUrl, scanPages } = require('../scripts/gds/client-baseurl-guard.js');
|
|
20
|
+
|
|
21
|
+
let passed = 0;
|
|
22
|
+
let failed = 0;
|
|
23
|
+
function test(name, fn) {
|
|
24
|
+
try { fn(); passed++; console.log(` ok ${name}`); }
|
|
25
|
+
catch (err) { failed++; console.error(` FAIL ${name}\n ${err.message}`); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const from = (map) => ({
|
|
29
|
+
files: Object.keys(map),
|
|
30
|
+
read: (rel) => (rel in map ? map[rel] : null),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// --- THE DEFECT ------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
test('the exact shipped defect is refused: baseUrl:\'\' plus a namespaced call', () => {
|
|
36
|
+
const { violations } = scanPages(...Object.values(from({
|
|
37
|
+
'modules/hall-ui/public/bad.js':
|
|
38
|
+
"const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin' });\n"
|
|
39
|
+
+ 'const r = await api.inbox.getInboxTemplate({});\n',
|
|
40
|
+
})));
|
|
41
|
+
assert.equal(violations.length, 1);
|
|
42
|
+
assert.match(violations[0], /bad\.js/);
|
|
43
|
+
assert.match(violations[0], /site root/, 'the message says WHY it is wrong, not just that it is');
|
|
44
|
+
assert.match(violations[0], /Drop the baseUrl override/, 'and what to do about it');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('double-quoted and backtick empty bases are the same defect', () => {
|
|
48
|
+
for (const empty of ['""', '``']) {
|
|
49
|
+
const { violations } = scanPages(...Object.values(from({
|
|
50
|
+
'public/p.js': `const api = createClient({ baseUrl: ${empty} });\napi.me.getMe({});\n`,
|
|
51
|
+
})));
|
|
52
|
+
assert.equal(violations.length, 1, `baseUrl: ${empty} must be caught`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// --- WHAT IT MUST NOT FLAG -------------------------------------------------
|
|
57
|
+
// A guard that fires on correct code gets suppressed, and then it protects nothing.
|
|
58
|
+
|
|
59
|
+
test("the escape-hatch style is CORRECT with baseUrl:'' — never flagged", () => {
|
|
60
|
+
// work.js, copy-desk.js and most of the hall do exactly this: empty base, but
|
|
61
|
+
// every path written out in full. Flagging it would be a false positive on the
|
|
62
|
+
// majority of the hall.
|
|
63
|
+
const { violations } = scanPages(...Object.values(from({
|
|
64
|
+
'modules/hall-ui/public/work.js':
|
|
65
|
+
"const API = '/api/bongos';\n"
|
|
66
|
+
+ "const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin' });\n"
|
|
67
|
+
+ "const res = await api.request('GET', `${API}/tasks`);\n",
|
|
68
|
+
})));
|
|
69
|
+
assert.deepEqual(violations, []);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('the fix itself passes — omitting baseUrl with namespaced calls is correct', () => {
|
|
73
|
+
const { violations } = scanPages(...Object.values(from({
|
|
74
|
+
'modules/hall-ui/public/ideas.js':
|
|
75
|
+
"const api = window.BongosClient.createClient({ credentials: 'same-origin', throwOnError: false });\n"
|
|
76
|
+
+ 'await api.inbox.getInboxTemplate({});\n',
|
|
77
|
+
})));
|
|
78
|
+
assert.deepEqual(violations, [], 'the shipped fix must not trip the guard that motivated it');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('a non-empty baseUrl is a deliberate choice, not the defect', () => {
|
|
82
|
+
const { violations } = scanPages(...Object.values(from({
|
|
83
|
+
'public/ext.js':
|
|
84
|
+
"const api = createClient({ baseUrl: 'https://other.example/api/bongos/v1' });\n"
|
|
85
|
+
+ 'api.me.getMe({});\n',
|
|
86
|
+
})));
|
|
87
|
+
assert.deepEqual(violations, []);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a page that builds no client at all is ignored', () => {
|
|
91
|
+
const { violations, usingClient } = scanPages(...Object.values(from({
|
|
92
|
+
'public/static.js': "document.querySelector('#x').textContent = 'hi';\n",
|
|
93
|
+
})));
|
|
94
|
+
assert.deepEqual(violations, []);
|
|
95
|
+
assert.equal(usingClient, 0);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// --- THE SCAN MUST NOT SILENTLY SUCCEED ------------------------------------
|
|
99
|
+
|
|
100
|
+
test('an empty enumeration is a hard FAIL, not a clean pass', () => {
|
|
101
|
+
// The failure mode that makes a guard worthless: git ls-files returns nothing,
|
|
102
|
+
// zero files are scanned, and the check reports green forever.
|
|
103
|
+
const r = checkClientBaseUrl({ files: [], read: () => null });
|
|
104
|
+
assert.equal(r.hardFail, true);
|
|
105
|
+
assert.match(r.violations[0], /scan defect/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('an unreadable file is skipped without being counted as scanned', () => {
|
|
109
|
+
const { scanned } = scanPages(['gone.js'], () => null);
|
|
110
|
+
assert.equal(scanned, 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('the check reports hardFail so CI actually stops', () => {
|
|
114
|
+
const r = checkClientBaseUrl(from({
|
|
115
|
+
'public/bad.js': "createClient({ baseUrl: '' });\napi.inbox.postInbox({});\n",
|
|
116
|
+
}));
|
|
117
|
+
assert.equal(r.ok, false);
|
|
118
|
+
assert.equal(r.hardFail, true, 'a page dead in prod is not a warning');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log(`\nclient_baseurl_guard: ${passed} passed, ${failed} failed`);
|
|
122
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -112,6 +112,17 @@ await test('the page gets both the FORM and the METER from the server', () => {
|
|
|
112
112
|
assert.match(IDEAS_JS, /getInboxTemplate/, 'the form is rendered from GET /inbox/template');
|
|
113
113
|
assert.match(IDEAS_JS, /postInboxScore/, 'the meter is filled from POST /inbox/score');
|
|
114
114
|
assert.match(IDEAS_JS, /postInbox\b/, 'and it files through POST /inbox');
|
|
115
|
+
|
|
116
|
+
// ...AND THOSE CALLS MUST REACH THE API. The three assertions above are regexes
|
|
117
|
+
// over the SOURCE TEXT, so they passed for months while the page 404'd on every
|
|
118
|
+
// one of them: baseUrl:'' told the client the API-relative templates were already
|
|
119
|
+
// absolute, and each request went to the site root. Naming a method proves the
|
|
120
|
+
// call is written, never that it is addressed — task 1003263.
|
|
121
|
+
assert.doesNotMatch(
|
|
122
|
+
IDEAS_JS,
|
|
123
|
+
/createClient\(\s*\{[^}]*baseUrl\s*:\s*(''|"")/,
|
|
124
|
+
"baseUrl:'' sends the generated client's API-relative paths to the site root",
|
|
125
|
+
);
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
await test('the page does not print a pass bar — that rule lives in economy', () => {
|
|
@@ -99,7 +99,19 @@ test('a "fetch first" rejection names the branch and the reset-onto-origin remed
|
|
|
99
99
|
assert.match(out, /reset/i, 'names the reset');
|
|
100
100
|
assert.match(out, /generator/i, 'and the regenerate step before re-shipping');
|
|
101
101
|
assert.match(out, /re-ship|fast-forward/i, 'and why: so the push fast-forwards');
|
|
102
|
-
|
|
102
|
+
// CONTRACT CHANGED, task 1003222. This used to assert the OPPOSITE — that the
|
|
103
|
+
// whole thing collapsed to one line — which was right while the server clipped
|
|
104
|
+
// stderr to 200 chars, and wrong once it stopped. The summary line is still
|
|
105
|
+
// collapsed and scannable; the verbatim text is quoted BELOW it, because git puts
|
|
106
|
+
// the line that names the cause last and a one-line squash buries it.
|
|
107
|
+
const [summary] = out.split('\n');
|
|
108
|
+
assert.ok(!/\n/.test(summary), 'the SUMMARY is still one scannable line');
|
|
109
|
+
assert.match(summary, /reason:/, 'and still carries a reason inline');
|
|
110
|
+
assert.match(out, /what the remote actually said/, 'the untruncated stderr is quoted below');
|
|
111
|
+
assert.ok(
|
|
112
|
+
out.includes("error: failed to push some refs to 'github.com/org/repo.git'"),
|
|
113
|
+
'the remote message survives VERBATIM, on its own line — the whole point of the task',
|
|
114
|
+
);
|
|
103
115
|
});
|
|
104
116
|
|
|
105
117
|
test('the "non-fast-forward" wording triggers the same hint through the envelope', () => {
|
|
@@ -158,5 +170,77 @@ test('1003221 a non-NFF failure gains no hint from either flag', () => {
|
|
|
158
170
|
}
|
|
159
171
|
});
|
|
160
172
|
|
|
173
|
+
|
|
174
|
+
// --- task 1003222: the remote's own words reach the builder ------------------
|
|
175
|
+
//
|
|
176
|
+
// The failure this closes: a builder saw `reason: error: failed` — clipped
|
|
177
|
+
// mid-word by a 200-char slice on top of an earlier 500-char one — while the line
|
|
178
|
+
// that named the cause (`remote: fatal error in commit_refs`) and the HTTP status
|
|
179
|
+
// existed server-side only. That cost hours AND produced a wrong first diagnosis,
|
|
180
|
+
// because the real message was never in the room.
|
|
181
|
+
|
|
182
|
+
const LONG_STDERR = [
|
|
183
|
+
'To https://github.com/org/repo.git',
|
|
184
|
+
' ! [remote rejected] b -> b (pre-receive hook declined)',
|
|
185
|
+
'error: failed to push some refs',
|
|
186
|
+
'remote: fatal error in commit_refs', // git's useful line is LAST
|
|
187
|
+
].join('\n');
|
|
188
|
+
|
|
189
|
+
test('1003222 the LAST line survives — a head-truncation would have eaten it', () => {
|
|
190
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: LONG_STDERR });
|
|
191
|
+
assert.ok(
|
|
192
|
+
out.includes('remote: fatal error in commit_refs'),
|
|
193
|
+
'the cause git printed last must reach the builder',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('1003222 the git exit code is reported — 128 and 1 are different remedies', () => {
|
|
198
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'boom', git_exit_code: 128 });
|
|
199
|
+
assert.match(out, /git exit 128/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('1003222 a GitHub HTTP status is reported when the API refused', () => {
|
|
203
|
+
const out = fmt({ error: 'pr_create_failed', github_status: 422, reason: 'No commits between main and b' });
|
|
204
|
+
assert.match(out, /GitHub HTTP 422/, '422 vs 403 vs 404 are three different fixes');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('1003222 a timeout says so rather than looking like a rejection', () => {
|
|
208
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'killed', timed_out: true });
|
|
209
|
+
assert.match(out, /TIMED OUT/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('1003222 a single-line stderr is NOT repeated in a quote block', () => {
|
|
213
|
+
// It is already fully present in the summary; quoting it again is noise.
|
|
214
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: 'fatal: repository not found' });
|
|
215
|
+
assert.ok(!out.includes('what the remote actually said'), 'no block for a one-liner');
|
|
216
|
+
assert.match(out, /fatal: repository not found/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('1003222 an omitted middle is DECLARED, never silently clipped', () => {
|
|
220
|
+
const out = fmt({
|
|
221
|
+
error: 'publish_failed', step: 'push', reason: LONG_STDERR, reason_bytes: 99999,
|
|
222
|
+
});
|
|
223
|
+
assert.match(out, /99999 bytes at the server/, 'the reader is told text is missing');
|
|
224
|
+
assert.match(out, /omitted/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('1003222 the remedy precedes the quote block, not stranded under it', () => {
|
|
228
|
+
// The remedy is the actionable sentence. Below twenty lines of git chatter it
|
|
229
|
+
// reads as a footnote to the quotation and gets missed.
|
|
230
|
+
const out = fmt({ error: 'publish_failed', step: 'push', reason: NON_FF_STDERR });
|
|
231
|
+
const remedy = out.indexOf('reset onto');
|
|
232
|
+
const quote = out.indexOf('what the remote actually said');
|
|
233
|
+
assert.ok(remedy > -1 && quote > -1, 'both present');
|
|
234
|
+
assert.ok(remedy < quote, 'remedy comes first');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('1003222 nothing regressed for a failure carrying no stderr at all', () => {
|
|
238
|
+
const out = fmt({ error: 'publish_failed' });
|
|
239
|
+
assert.match(out, /publish_failed/);
|
|
240
|
+
assert.ok(!out.includes('what the remote actually said'));
|
|
241
|
+
assert.ok(!/undefined|null|\[object/.test(out), 'no placeholder leakage');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
|
|
161
245
|
console.log(`ship_publish_error: ${passed} passed, ${failed} failed`);
|
|
162
|
-
process.exit(failed === 0 ? 0 : 1);
|
|
246
|
+
process.exit(failed === 0 ? 0 : 1);
|
package/tests/update_channel.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// 4. parseNpmViewVersions — the array / bare-string / empty shapes npm view emits.
|
|
9
9
|
// 5. listAvailableVersions — ok + fail-soft with an injected runner.
|
|
10
10
|
// 6. loadSubscriptions — missing/malformed → empty; channel default + validate; env kept; dropless.
|
|
11
|
+
// 7. describeRoster — an empty roster REPORTS WHY, and is an alarm on a control plane (task 1003158).
|
|
12
|
+
// 8. subscriptionStaleness — releases the channel was entitled to and did not take = "the upgrader stopped".
|
|
11
13
|
//
|
|
12
14
|
// DB-free + network-free (every external is injected). Run: node tests/update_channel.mjs
|
|
13
15
|
|
|
@@ -182,6 +184,129 @@ t('loadSubscriptions: an entry without a dir is dropped (cannot be upgraded)', (
|
|
|
182
184
|
assert.equal(r.instances[0].slug, 'real');
|
|
183
185
|
});
|
|
184
186
|
|
|
187
|
+
// ---- describeRoster: fails closed, no longer fails quiet (task 1003158) ----
|
|
188
|
+
//
|
|
189
|
+
// The regression these pin: before this, a missing manifest, a malformed manifest and a deliberately
|
|
190
|
+
// empty roster were INDISTINGUISHABLE — all three returned a bare `instances: []` and the routine
|
|
191
|
+
// shrugged. cloudbongos.com sat 36 releases behind for days because "nothing happened" looked
|
|
192
|
+
// identical to "nothing needed to happen". Every empty roster must now say WHY, every run.
|
|
193
|
+
|
|
194
|
+
t('loadSubscriptions: missing file reports reason manifest-absent (not just an empty list)', () => {
|
|
195
|
+
const dir = mkdtempSync(join(tmpdir(), 'otb-subs-none2-'));
|
|
196
|
+
const r = uc.loadSubscriptions({ instanceDir: dir });
|
|
197
|
+
assert.deepEqual(r.instances, []);
|
|
198
|
+
assert.equal(r.reason, uc.ROSTER_ABSENT);
|
|
199
|
+
assert.equal(r.dropped, 0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
t('loadSubscriptions: malformed JSON reports reason manifest-malformed + keeps the error', () => {
|
|
203
|
+
const dir = scratchWith('{ not valid');
|
|
204
|
+
const r = uc.loadSubscriptions({ instanceDir: dir });
|
|
205
|
+
assert.equal(r.reason, uc.ROSTER_MALFORMED);
|
|
206
|
+
assert.match(r.error, /malformed JSON/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
t('loadSubscriptions: parsed-but-unsubscribable reports roster-empty and COUNTS the drops', () => {
|
|
210
|
+
const dir = scratchWith({ instances: [{ slug: 'ghost' }, { slug: 'alsoghost' }] });
|
|
211
|
+
const r = uc.loadSubscriptions({ instanceDir: dir });
|
|
212
|
+
assert.deepEqual(r.instances, []);
|
|
213
|
+
assert.equal(r.reason, uc.ROSTER_EMPTY);
|
|
214
|
+
assert.equal(r.dropped, 2);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
t('loadSubscriptions: a usable roster reports ok', () => {
|
|
218
|
+
const dir = scratchWith({ instances: [{ slug: 'cb', dir: '/srv/cb' }] });
|
|
219
|
+
const r = uc.loadSubscriptions({ instanceDir: dir });
|
|
220
|
+
assert.equal(r.reason, uc.ROSTER_OK);
|
|
221
|
+
assert.equal(r.dropped, 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
t('describeRoster: an empty roster on a CONTROL PLANE is an alarm, not a shrug', () => {
|
|
225
|
+
const dir = mkdtempSync(join(tmpdir(), 'otb-subs-cp-'));
|
|
226
|
+
const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }), { controlPlane: true });
|
|
227
|
+
assert.equal(d.level, 'alarm');
|
|
228
|
+
assert.match(d.message, /CONTROL PLANE/);
|
|
229
|
+
assert.match(d.message, /subscribing NOTHING/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
t('describeRoster: the same empty roster on a plain instance is only a warn (shipped default)', () => {
|
|
233
|
+
const dir = mkdtempSync(join(tmpdir(), 'otb-subs-plain-'));
|
|
234
|
+
const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }));
|
|
235
|
+
assert.equal(d.level, 'warn');
|
|
236
|
+
assert.match(d.message, /shipped default/);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
t('describeRoster: a malformed manifest is an alarm everywhere', () => {
|
|
240
|
+
const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: scratchWith('{ nope') }));
|
|
241
|
+
assert.equal(d.level, 'alarm');
|
|
242
|
+
assert.match(d.message, /MALFORMED/);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
t('describeRoster: partial loss is surfaced — a healthy-looking sweep that silently skips a row', () => {
|
|
246
|
+
const dir = scratchWith({ instances: [{ slug: 'cb', dir: '/srv/cb' }, { slug: 'ghost' }] });
|
|
247
|
+
const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }));
|
|
248
|
+
assert.equal(d.level, 'warn');
|
|
249
|
+
assert.equal(d.dropped, 1);
|
|
250
|
+
assert.match(d.message, /DROPPED/);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
t('describeRoster: a clean roster is ok and says how many it holds', () => {
|
|
254
|
+
const dir = scratchWith({ instances: [{ slug: 'cb', dir: '/srv/cb' }] });
|
|
255
|
+
const d = uc.describeRoster(uc.loadSubscriptions({ instanceDir: dir }), { controlPlane: true });
|
|
256
|
+
assert.equal(d.level, 'ok');
|
|
257
|
+
assert.equal(d.count, 1);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ---- subscriptionStaleness: watching the watcher ---------------------------
|
|
261
|
+
|
|
262
|
+
const RELEASES = Array.from({ length: 41 }, (_, i) => `1.19.${250 + i}`); // 1.19.250 … 1.19.290
|
|
263
|
+
|
|
264
|
+
t('subscriptionStaleness: reproduces the incident — 1.19.254 on patch is 36 releases behind, stale', () => {
|
|
265
|
+
const st = uc.subscriptionStaleness({ installed: '1.19.254', channel: 'patch', available: RELEASES });
|
|
266
|
+
assert.equal(st.behind, 36);
|
|
267
|
+
assert.equal(st.target, '1.19.290');
|
|
268
|
+
assert.equal(st.stale, true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
t('subscriptionStaleness: a swept instance is 0 behind and not stale', () => {
|
|
272
|
+
const st = uc.subscriptionStaleness({ installed: '1.19.290', channel: 'patch', available: RELEASES });
|
|
273
|
+
assert.equal(st.behind, 0);
|
|
274
|
+
assert.equal(st.target, null);
|
|
275
|
+
assert.equal(st.stale, false);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
t('subscriptionStaleness: a busy publish-on-merge day under the threshold does NOT cry wolf', () => {
|
|
279
|
+
const st = uc.subscriptionStaleness({ installed: '1.19.287', channel: 'patch', available: RELEASES });
|
|
280
|
+
assert.equal(st.behind, 3);
|
|
281
|
+
assert.equal(st.stale, false);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
t('subscriptionStaleness: a pinned instance is never stale (declining is not neglect)', () => {
|
|
285
|
+
const st = uc.subscriptionStaleness({ installed: '1.19.254', channel: 'pinned', available: RELEASES });
|
|
286
|
+
assert.equal(st.behind, 0);
|
|
287
|
+
assert.equal(st.stale, false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
t('subscriptionStaleness: counts only what the CHANNEL may take — minors/majors are not a backlog', () => {
|
|
291
|
+
const st = uc.subscriptionStaleness({ installed: '1.19.254', channel: 'patch', available: ['1.20.0', '1.20.7', '2.0.0'] });
|
|
292
|
+
assert.equal(st.behind, 0);
|
|
293
|
+
assert.equal(st.stale, false);
|
|
294
|
+
// the same list on the minor channel IS a backlog
|
|
295
|
+
assert.equal(uc.subscriptionStaleness({ installed: '1.19.254', channel: 'minor', available: ['1.20.0', '1.20.7', '2.0.0'] }).behind, 2);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
t('subscriptionStaleness: threshold is overridable; a bad threshold falls back to the default', () => {
|
|
299
|
+
assert.equal(uc.subscriptionStaleness({ installed: '1.19.287', channel: 'patch', available: RELEASES, threshold: 1 }).stale, true);
|
|
300
|
+
assert.equal(uc.subscriptionStaleness({ installed: '1.19.287', channel: 'patch', available: RELEASES, threshold: 'nonsense' }).threshold, uc.DEFAULT_STALENESS_THRESHOLD);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
t('describeStaleness: silent when healthy, explicit about "the upgrader stopped" when not', () => {
|
|
304
|
+
assert.equal(uc.describeStaleness(uc.subscriptionStaleness({ installed: '1.19.290', channel: 'patch', available: RELEASES })), null);
|
|
305
|
+
const msg = uc.describeStaleness(uc.subscriptionStaleness({ installed: '1.19.254', channel: 'patch', available: RELEASES }), { slug: 'cloudbongos.com' });
|
|
306
|
+
assert.match(msg, /cloudbongos\.com is 36 patch releases behind/);
|
|
307
|
+
assert.match(msg, /upgrader has most likely stopped/);
|
|
308
|
+
});
|
|
309
|
+
|
|
185
310
|
console.log(`\nupdate_channel: ${passed} passed, ${failed} failed`);
|
|
186
311
|
process.exit(failed ? 1 : 0);
|
|
187
312
|
|