@bongos/core 1.19.307 → 1.19.309

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.
@@ -4,7 +4,7 @@ const auth = require('../auth');
4
4
  module.exports = function buildHealthzRouter() {
5
5
  const router = express.Router();
6
6
 
7
- // rank: public — liveness probe; status.amazonprimea.com hits this anonymously.
7
+ // rank: public — liveness probe; the status dashboard (status.<apex>) hits this anonymously.
8
8
  router.get('/healthz', async (_req, res) => {
9
9
  res.json({
10
10
  ok: true,
@@ -140,7 +140,7 @@ module.exports = function buildPublicRouter() {
140
140
  res.json(clientModules());
141
141
  });
142
142
 
143
- // rank: public — read-only surface for status.amazonprimea.com.
143
+ // rank: public — read-only surface for the status dashboard (status.<apex>).
144
144
  router.get('/public/versions', async (req, res) => {
145
145
  // R14 (#2001 / ADR 0119): adopt the shared pagination contract (was unbounded).
146
146
  // Small list → fetch + slice; `page` is additive (readers of .versions unchanged).
@@ -378,7 +378,7 @@ module.exports = function buildPublicRouter() {
378
378
  // Single-task resolver for "#NNN" deep links. Our docs reference GDS task IDs
379
379
  // as bare "#NNN"; read on github.com those collide with GitHub's own PR/issue
380
380
  // autolinker (a separate number space) and mis-link, so they are rewritten to
381
- // status-dashboard permalinks (status.amazonprimea.com/#/task/N) that resolve
381
+ // status-dashboard permalinks (status.<apex>/#/task/N) that resolve
382
382
  // here. Privacy gate: a full title/summary is exposed ONLY for
383
383
  // already-public work (status shipped|confirmed — same surface as
384
384
  // /public/recent-shipped). In-flight tasks return status + version with the
package/src/module-api.js CHANGED
@@ -1041,8 +1041,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1041
1041
  // landed since 1.19.305 with no explicit bump. run 32662000590. (task 1002620)
1042
1042
  // 1.19.307 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1043
1043
  // landed since 1.19.306 with no explicit bump. run 32663876767. (task 1002620)
1044
+ // 1.19.308 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1045
+ // landed since 1.19.307 with no explicit bump. run 32664559221. (task 1002620)
1046
+ // 1.19.309 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1047
+ // landed since 1.19.308 with no explicit bump. run 32664868080. (task 1002620)
1044
1048
  // ---------------------------------------------------------------------------
1045
- const CORE_VERSION = '1.19.307'; // CI auto-patch carrier (ADR 0161); see changelog above
1049
+ const CORE_VERSION = '1.19.309'; // CI auto-patch carrier (ADR 0161); see changelog above
1046
1050
 
1047
1051
  // A namespaced logger so a module's log lines are attributable + consistent.
1048
1052
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -200,6 +200,25 @@ t('buildRebrandRules: partial target (only publicOrigin) → subdomains fall bac
200
200
  assert.equal(out, 'acme.example.org', 'no builders.acme.example.org mangling');
201
201
  });
202
202
 
203
+ // task 1002672: the bare UA brand token (no TLD — none of the host rules match it).
204
+ // A founder-branded hook UA like `amazonprimea-gds-cli/1.0` must neutralize on
205
+ // materialize; a bare founder word in prose (no `-gds` suffix) must NOT be touched.
206
+ t('buildRebrandRules: User-Agent brand token rewritten; bare brand word in prose untouched', () => {
207
+ const from = { ...OTB, repo: { name: 'amazonprimea' } };
208
+ const to = { domains: { publicOrigin: 'https://acme.example.org' }, envPrefix: 'ACME', repo: { name: 'acme-city' } };
209
+ const rules = m.buildRebrandRules(from, to);
210
+ assert.equal(m.rebrandText(`'User-Agent': 'amazonprimea-gds-cli/1.0'`, rules), `'User-Agent': 'acme-city-gds-cli/1.0'`);
211
+ assert.equal(m.rebrandText('amazonprimea-gds/1.0', rules), 'acme-city-gds/1.0');
212
+ assert.equal(m.rebrandText('the amazonprimea style guide', rules), 'the amazonprimea style guide', 'prose token without -gds suffix stays');
213
+ // envPrefix fallback when repo.name is absent on either side.
214
+ const rules2 = m.buildRebrandRules(OTB, { domains: { publicOrigin: 'https://acme.example.org' }, envPrefix: 'ACME' });
215
+ assert.equal(m.rebrandText('otb-gds-cli/1.0', rules2), 'acme-gds-cli/1.0');
216
+ // No degenerate bare `-gds` rule when a side resolves to nothing.
217
+ const rules3 = m.buildRebrandRules({ domains: {} }, { domains: { publicOrigin: 'https://acme.example.org' }, envPrefix: 'ACME' });
218
+ assert.ok(!rules3.some((r) => r.pattern.source === '-gds'), 'no bare -gds rule');
219
+ assert.equal(m.rebrandText('plain-gds/1.0 text', m.buildRebrandRules({}, {})), 'plain-gds/1.0 text');
220
+ });
221
+
203
222
  t('buildRebrandRules: source === target is an empty ruleset → rebrandText is a no-op', () => {
204
223
  assert.equal(m.buildRebrandRules(OTB, OTB).length, 0);
205
224
  const s = 'https://amazonprimea.com ~/.config/otb';
@@ -0,0 +1,362 @@
1
+ // tests/go_live.mjs
2
+ //
3
+ // task 1003189 — go-live is one command.
4
+ //
5
+ // What is worth pinning here is NOT the happy path (that needs a real box) but the
6
+ // REFUSALS. The whole value of this script is that it stops instead of guessing, and
7
+ // every halt below is a trap this pipeline has actually paid for at least once:
8
+ //
9
+ // UNRELEASED_ON_MAIN — the ten-day stale-prod incident (blocker 1000097), and
10
+ // the publish push-race that leaves main's code carried by
11
+ // no release at all.
12
+ // MARKER_ABSENT — "the release current when your fix merges usually does
13
+ // NOT contain your fix" (ADR 0161 carriers bump on their
14
+ // own cadence). Deploying it is a clean go-live that
15
+ // changes nothing, and every downstream check agrees.
16
+ // ALREADY_LIVE / LIVE_AHEAD — a no-op bump reported as success.
17
+ // DIRTY_CHECKOUT — CORE_VERSION read from a dirty tree is not what shipped.
18
+ //
19
+ // The shell-safety tests are the other half. This script composes a command that runs
20
+ // as root over ssh, so a config value carrying `$(...)`, a backtick or a `;` must be
21
+ // REFUSED, not quoted-and-hoped-for (the #681 injection class).
22
+ //
23
+ // Run: node --test tests/go_live.mjs
24
+
25
+ import { test } from 'node:test';
26
+ import assert from 'node:assert/strict';
27
+ import { createRequire } from 'node:module';
28
+ import path from 'node:path';
29
+ import { fileURLToPath } from 'node:url';
30
+
31
+ const require = createRequire(import.meta.url);
32
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
33
+ const G = require('../scripts/gds/go-live.js');
34
+
35
+ const okTarget = {
36
+ ssh: 'my-box',
37
+ instanceDir: '/home/me/inst',
38
+ service: 'inst.service',
39
+ healthUrl: 'http://127.0.0.1:3002/healthz',
40
+ versionUrl: 'http://127.0.0.1:3002/version',
41
+ deployTimer: 'inst-deploy.timer',
42
+ };
43
+
44
+ // A divergence result shaped like deploy-divergence.check() returns.
45
+ const div = (over = {}) => ({
46
+ ok: true, onMain: '1.19.306', live: { coreVersion: '1.19.305' },
47
+ hasUnreleased: false, unreleased: { count: 0 }, ...over,
48
+ });
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Halt matrix
52
+ // ---------------------------------------------------------------------------
53
+
54
+ test('a clean gap plans with no halts', () => {
55
+ const p = G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306' });
56
+ assert.equal(p.halts.length, 0, JSON.stringify(p.halts));
57
+ assert.equal(p.from, '1.19.305');
58
+ });
59
+
60
+ test('unreleased commits on main HALT the go-live', () => {
61
+ const p = G.planGoLive({
62
+ div: div({ hasUnreleased: true, unreleased: { count: 7 } }),
63
+ target: okTarget, targetVersion: '1.19.306',
64
+ });
65
+ const h = p.halts.find((x) => x.code === 'UNRELEASED_ON_MAIN');
66
+ assert.ok(h, 'expected UNRELEASED_ON_MAIN');
67
+ // The detail must name the push-race, or the reader assumes it is only timing.
68
+ assert.match(h.detail, /publish/i);
69
+ assert.match(h.detail, /7 commit/);
70
+ });
71
+
72
+ test('--allow-unreleased downgrades that halt to a note rather than dropping it', () => {
73
+ const p = G.planGoLive({
74
+ div: div({ hasUnreleased: true, unreleased: { count: 7 } }),
75
+ target: okTarget, targetVersion: '1.19.306', allowUnreleased: true,
76
+ });
77
+ assert.equal(p.halts.find((x) => x.code === 'UNRELEASED_ON_MAIN'), undefined);
78
+ const n = p.notes.find((x) => x.code === 'UNRELEASED_ON_MAIN');
79
+ assert.ok(n && n.downgraded, 'the condition must still be REPORTED, just not fatal');
80
+ });
81
+
82
+ test('a no-op bump is a halt, not a success', () => {
83
+ const p = G.planGoLive({
84
+ div: div({ live: { coreVersion: '1.19.306' } }), target: okTarget, targetVersion: '1.19.306',
85
+ });
86
+ assert.ok(p.halts.find((x) => x.code === 'ALREADY_LIVE'));
87
+ });
88
+
89
+ test('live ahead of the target halts rather than silently downgrading', () => {
90
+ const p = G.planGoLive({
91
+ div: div({ live: { coreVersion: '1.19.310' } }), target: okTarget, targetVersion: '1.19.306',
92
+ });
93
+ assert.ok(p.halts.find((x) => x.code === 'LIVE_AHEAD'));
94
+ });
95
+
96
+ test('a dirty core checkout halts — CORE_VERSION there is not what shipped', () => {
97
+ const p = G.planGoLive({
98
+ div: div(), target: okTarget, targetVersion: '1.19.306', dirtyCheckout: ' M src/module-api.js',
99
+ });
100
+ assert.ok(p.halts.find((x) => x.code === 'DIRTY_CHECKOUT'));
101
+ });
102
+
103
+ test('an undetermined divergence halts and does not fall through to a bump', () => {
104
+ const p = G.planGoLive({
105
+ div: { undetermined: true, reason: 'could not read /version' }, target: okTarget, targetVersion: '1.19.306',
106
+ });
107
+ assert.equal(p.halts.length, 1);
108
+ assert.equal(p.halts[0].code, 'DIVERGENCE_UNDETERMINED');
109
+ assert.equal(p.from, null, 'must not invent a from-version');
110
+ });
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Shell safety — this builds a command that runs as root over ssh
114
+ // ---------------------------------------------------------------------------
115
+
116
+ test('config values carrying shell metacharacters are REFUSED', () => {
117
+ for (const evil of [
118
+ '$(touch /tmp/pwned)',
119
+ '`id`',
120
+ 'box; rm -rf /',
121
+ 'box && curl evil.example',
122
+ 'box\nrm -rf /',
123
+ 'box|tee /etc/passwd',
124
+ "box'quote",
125
+ ]) {
126
+ const errs = G.validateTarget('t', { ...okTarget, ssh: evil });
127
+ assert.ok(errs.length > 0, `expected refusal for ${JSON.stringify(evil)}`);
128
+ assert.match(errs.join(' '), /unsafe in a shell/);
129
+ }
130
+ });
131
+
132
+ test('env values and keys are validated too', () => {
133
+ assert.ok(G.validateTarget('t', { ...okTarget, env: { PGDATABASE: '$(id)' } }).length > 0);
134
+ assert.ok(G.validateTarget('t', { ...okTarget, env: { 'BAD KEY': 'x' } }).length > 0);
135
+ assert.equal(G.validateTarget('t', { ...okTarget, env: { PGDATABASE: 'mydb' } }).length, 0);
136
+ });
137
+
138
+ test('a typo’d field is reported, never silently ignored', () => {
139
+ // A misspelled healthUrl that is quietly dropped would DISABLE the health gate —
140
+ // exactly the quiet failure this pipeline keeps being bitten by.
141
+ const errs = G.validateTarget('t', { ...okTarget, healthURL: 'http://x/healthz' });
142
+ assert.match(errs.join(' '), /unknown field "healthURL"/);
143
+ });
144
+
145
+ test('missing required fields are named individually', () => {
146
+ const errs = G.validateTarget('t', { ssh: 'box' });
147
+ for (const f of ['instanceDir', 'service', 'healthUrl', 'versionUrl', 'deployTimer']) {
148
+ assert.match(errs.join(' '), new RegExp(`missing required field "${f}"`));
149
+ }
150
+ });
151
+
152
+ test('a valid target passes clean', () => {
153
+ assert.deepEqual(G.validateTarget('t', okTarget), []);
154
+ });
155
+
156
+ test('asOwner composes the documented sudo -u … env … shape', () => {
157
+ const cmd = G.asOwner({ runAs: 'me', env: { PGDATABASE: 'mydb' } }, 'echo hi');
158
+ assert.match(cmd, /^sudo -u me -H env PGDATABASE=mydb HOME=\/home\/me echo hi$/);
159
+ // PGDATABASE is load-bearing: without it migrate targets a stale default and
160
+ // the upgrade auto-rolls-back. It must survive into the command.
161
+ assert.ok(cmd.includes('PGDATABASE=mydb'));
162
+ });
163
+
164
+ test('asOwner with no runAs still carries env', () => {
165
+ assert.equal(G.asOwner({ env: { A: 'b' } }, 'x'), 'env A=b x');
166
+ assert.equal(G.asOwner({}, 'x'), 'x');
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Config loading
171
+ // ---------------------------------------------------------------------------
172
+
173
+ test('an absent config reports NO_TARGET_CONFIG with a template, not a stack trace', () => {
174
+ const r = G.loadConfig({ configPath: 'config/definitely-not-here.json', exists: () => false });
175
+ assert.equal(r.error, 'NO_TARGET_CONFIG');
176
+ assert.ok(r.template.includes('instanceDir'));
177
+ });
178
+
179
+ test('a malformed config is reported as malformed, not treated as empty', () => {
180
+ const r = G.loadConfig({ configPath: 'x.json', exists: () => true, readFile: () => '{ not json' });
181
+ assert.equal(r.error, 'CONFIG_MALFORMED');
182
+ });
183
+
184
+ test('a config without targets is malformed rather than a silent no-op', () => {
185
+ const r = G.loadConfig({ configPath: 'x.json', exists: () => true, readFile: () => '{"defaultTarget":"web"}' });
186
+ assert.equal(r.error, 'CONFIG_MALFORMED');
187
+ assert.match(r.detail, /targets/);
188
+ });
189
+
190
+ test('the shipped template is itself valid config', () => {
191
+ const r = G.loadConfig({ configPath: 'x.json', exists: () => true, readFile: () => G.CONFIG_TEMPLATE });
192
+ assert.ok(r.ok, `template did not parse: ${r.detail || ''}`);
193
+ const sel = G.selectTarget(r.config, undefined);
194
+ assert.ok(sel.ok, `template target invalid: ${sel.detail || ''}`);
195
+ assert.equal(sel.name, 'web');
196
+ });
197
+
198
+ test('an unknown target names what IS available', () => {
199
+ const r = G.loadConfig({ configPath: 'x.json', exists: () => true, readFile: () => G.CONFIG_TEMPLATE });
200
+ const sel = G.selectTarget(r.config, 'nope');
201
+ assert.equal(sel.error, 'UNKNOWN_TARGET');
202
+ assert.match(sel.detail, /web/);
203
+ });
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Markers + version parsing
207
+ // ---------------------------------------------------------------------------
208
+
209
+ test('parseMarker splits on the FIRST = so a needle may contain one', () => {
210
+ assert.deepEqual(G.parseMarker('src/a.js=const X = 1'), { path: 'src/a.js', needle: 'const X = 1' });
211
+ });
212
+
213
+ test('a malformed marker is rejected', () => {
214
+ assert.ok(G.parseMarker('noequals').error);
215
+ assert.ok(G.parseMarker('=needle').error);
216
+ assert.ok(G.parseMarker('path=').error);
217
+ assert.equal(G.parseMarker(''), null);
218
+ });
219
+
220
+ test('the version pattern accepts releases and rejects free text', () => {
221
+ for (const v of ['1.19.306', '1.0.0', '2.3.4-rc.1']) assert.ok(G.SEMVER_RE.test(v), v);
222
+ for (const v of ['latest', '1.19', 'v1.19.306', '1.19.306; rm -rf /', '']) assert.ok(!G.SEMVER_RE.test(v), v);
223
+ });
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Rendering — the report is the product; it must not claim more than it proved
227
+ // ---------------------------------------------------------------------------
228
+
229
+ test('the plan names every halt it found', () => {
230
+ const p = G.planGoLive({
231
+ div: div({ hasUnreleased: true, unreleased: { count: 3 } }),
232
+ target: okTarget, targetVersion: '1.19.306', dirtyCheckout: ' M x',
233
+ });
234
+ const out = G.renderPlan(p, { targetName: 'web' });
235
+ assert.match(out, /UNRELEASED_ON_MAIN/);
236
+ assert.match(out, /DIRTY_CHECKOUT/);
237
+ assert.match(out, /will NOT proceed/);
238
+ });
239
+
240
+ test('a clean plan tells you the next command', () => {
241
+ const out = G.renderPlan(G.planGoLive({ div: div(), target: okTarget, targetVersion: '1.19.306' }), { targetName: 'web' });
242
+ assert.match(out, /--apply/);
243
+ });
244
+
245
+ test('an uncommitted pin is reported as the durability failure it is', () => {
246
+ const out = G.renderReport({
247
+ targetName: 'web', from: '1.19.305', to: '1.19.306', liveVersion: '1.19.306',
248
+ pinCommitted: false, steps: [],
249
+ });
250
+ assert.match(out, /revert this bump/);
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // The content proof must fail CLOSED
255
+ //
256
+ // This is the bug that got through twice. `grep -c` PRINTS 0 and EXITS 1 on no
257
+ // match, so the obvious `|| echo 0` emitted "0\n0" — which Number() turns into
258
+ // NaN, and NaN === 0 is false, so an ABSENT marker sailed through as no-halt.
259
+ // A content proof that fails open is worse than none: it certifies a release
260
+ // that does not contain the change, and every downstream check agrees.
261
+ // ---------------------------------------------------------------------------
262
+
263
+ test('a clean count reads as that count', () => {
264
+ assert.equal(G.parseMarkerHits('3'), 3);
265
+ assert.equal(G.parseMarkerHits('0'), 0);
266
+ assert.equal(G.parseMarkerHits(' 12 \n'), 12);
267
+ });
268
+
269
+ test('a double-printed zero reads as 0 (absent), never NaN', () => {
270
+ const v = G.parseMarkerHits('0\n0');
271
+ assert.equal(v, 0);
272
+ assert.ok(!Number.isNaN(v), 'NaN here is what let an absent marker pass');
273
+ });
274
+
275
+ test('anything that is not a count is UNCHECKED, never a confident 0', () => {
276
+ for (const junk of ['', null, undefined, 'PACK_FAILED', 'PACK_FAILED npm error E404', 'tar: not found', 'abc']) {
277
+ assert.equal(G.parseMarkerHits(junk), 'PACK_FAILED', JSON.stringify(junk));
278
+ }
279
+ });
280
+
281
+ test("'could not check' is a distinct halt from 'absent'", () => {
282
+ // They must never collapse: one says fix your fetch, the other says pick a
283
+ // later release. Conflating them sends you chasing a version that is not there.
284
+ assert.ok(G.HALTS.MARKER_UNCHECKED);
285
+ assert.ok(G.HALTS.MARKER_ABSENT);
286
+ assert.notEqual(G.HALTS.MARKER_UNCHECKED, G.HALTS.MARKER_ABSENT);
287
+ });
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // The NPM_TOKEN forwarding trap
291
+ //
292
+ // `bongos upgrade --registry` needs NPM_TOKEN or npm 404s the PRIVATE core and the
293
+ // upgrade auto-rolls-back — which reads as "the release is broken" when it is only
294
+ // "the token never arrived". Sourcing the root-only env file in the OUTER shell is
295
+ // necessary and NOT sufficient: sudo's env_reset drops it, and an explicit
296
+ // `env K=V` list that omits it drops it too. This cost a real failed go-live.
297
+ // ---------------------------------------------------------------------------
298
+
299
+ test('a forwarded var is passed BY REFERENCE, never by value', () => {
300
+ const cmd = G.asOwner({ runAs: 'lars', env: { PGDATABASE: 'cb' } }, 'npm install', { forward: ['NPM_TOKEN'] });
301
+ assert.match(cmd, /NPM_TOKEN="\$NPM_TOKEN"/);
302
+ // The point of by-reference: the secret's VALUE must never appear in the script text.
303
+ assert.ok(!cmd.includes('npm_'), 'a token value must never be interpolated here');
304
+ });
305
+
306
+ test('forwarding is only wired when the target names an env file to source from', () => {
307
+ // Otherwise K="$K" expands to empty and silently does nothing — a quiet no-op
308
+ // is worse than not trying.
309
+ assert.deepEqual(G.npmForward({ npmEnvFile: '/etc/x/npm.env' }), ['NPM_TOKEN']);
310
+ assert.deepEqual(G.npmForward({}), []);
311
+ });
312
+
313
+ test('an unvetted variable name is never interpolated', () => {
314
+ const cmd = G.asOwner({ runAs: 'me' }, 'x', { forward: ['GOOD', 'bad name', '$(id)', '1BAD'] });
315
+ assert.match(cmd, /GOOD="\$GOOD"/);
316
+ assert.ok(!cmd.includes('bad name') && !cmd.includes('$(id)') && !cmd.includes('1BAD'));
317
+ });
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // A failed run must say what the tool said
321
+ // ---------------------------------------------------------------------------
322
+
323
+ test('the report surfaces the failing tool’s own words', () => {
324
+ const out = G.renderReport({
325
+ ok: false, failedAt: 'UPGRADE', targetName: 'web', from: '1.19.305', to: '1.19.306',
326
+ rolledBack: true, steps: [{ name: 'upgrade', ok: false, detail: 'AUTO-ROLLBACK ran' }],
327
+ raw: { stdout: 'npm error 404 Not Found - GET https://registry.npmjs.org/@bongos%2fcore\nup to date in 2s', stderr: '' },
328
+ });
329
+ assert.match(out, /404 Not Found/, 'the diagnosis must reach the reader');
330
+ assert.match(out, /not broken/, 'a rollback must be distinguished from an outage');
331
+ });
332
+
333
+ test('failureExcerpt prefers diagnostic lines over trailing noise', () => {
334
+ const raw = { stdout: ['ok a', 'ok b', 'npm error code E404', 'up to date in 2s'].join('\n'), stderr: '' };
335
+ const ex = G.failureExcerpt(raw);
336
+ assert.ok(ex.some((l) => l.includes('E404')));
337
+ assert.ok(!ex.some((l) => l === 'ok a'));
338
+ });
339
+
340
+ test('failureExcerpt is safe on an empty or missing result', () => {
341
+ assert.deepEqual(G.failureExcerpt(null), []);
342
+ assert.deepEqual(G.failureExcerpt({ stdout: '', stderr: '' }), []);
343
+ });
344
+
345
+ // ---------------------------------------------------------------------------
346
+ // A stale origin/main ref silently retargets the whole go-live
347
+ // ---------------------------------------------------------------------------
348
+
349
+ test('refreshMain reports a failed fetch instead of throwing', () => {
350
+ const r = G.refreshMain({ run: () => ({ status: 1, stderr: 'fatal: could not read from remote\n' }) });
351
+ assert.equal(r.ok, false);
352
+ assert.match(r.reason, /could not read from remote/);
353
+ });
354
+
355
+ test('refreshMain succeeds quietly', () => {
356
+ assert.deepEqual(G.refreshMain({ run: () => ({ status: 0, stdout: '', stderr: '' }) }), { ok: true });
357
+ });
358
+
359
+ test('a spawn error is handled, not thrown', () => {
360
+ const r = G.refreshMain({ run: () => ({ error: new Error('ENOENT'), status: null }) });
361
+ assert.equal(r.ok, false);
362
+ });