@bongos/core 1.19.1034 → 1.19.1036

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 (65) hide show
  1. package/.bongos-core.json +88 -68
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +9 -1
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/adr/0339-a-projects-owner-upgrades-it-and-a-hosted-project-hands-off-to-the-hub-door.md +84 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/api/openapi.json +126 -8
  10. package/docs/api-reference.md +9 -2
  11. package/docs/copy-inventory.md +10 -10
  12. package/docs/copy-registry.json +13 -13
  13. package/docs/module-api-changelog.md +4 -0
  14. package/modules/autonomy/db.js +19 -0
  15. package/modules/autonomy/routes/autonomy.js +28 -0
  16. package/modules/government/catalog.js +2 -2
  17. package/modules/hall-ui/public/atlas.html +1 -1
  18. package/modules/hall-ui/public/blockers.html +1 -1
  19. package/modules/hall-ui/public/board-room.html +1 -1
  20. package/modules/hall-ui/public/collab.html +1 -1
  21. package/modules/hall-ui/public/copy-desk.html +1 -1
  22. package/modules/hall-ui/public/deploy.html +1 -1
  23. package/modules/hall-ui/public/diagrams.html +1 -1
  24. package/modules/hall-ui/public/drachmae.html +1 -1
  25. package/modules/hall-ui/public/fleet.html +1 -1
  26. package/modules/hall-ui/public/gate.html +1 -1
  27. package/modules/hall-ui/public/goals.html +1 -1
  28. package/modules/hall-ui/public/government.html +1 -1
  29. package/modules/hall-ui/public/idea.html +1 -1
  30. package/modules/hall-ui/public/ideas.html +1 -1
  31. package/modules/hall-ui/public/index.html +1 -1
  32. package/modules/hall-ui/public/modules.html +1 -1
  33. package/modules/hall-ui/public/people.html +1 -1
  34. package/modules/hall-ui/public/primer.html +1 -1
  35. package/modules/hall-ui/public/profile.html +1 -1
  36. package/modules/hall-ui/public/project-settings.html +1 -1
  37. package/modules/hall-ui/public/ranks.html +1 -1
  38. package/modules/hall-ui/public/roadmap.html +1 -1
  39. package/modules/hall-ui/public/roster.html +1 -1
  40. package/modules/hall-ui/public/sessions.html +1 -1
  41. package/modules/hall-ui/public/settings.html +1 -1
  42. package/modules/hall-ui/public/shell.js +36 -5
  43. package/modules/hall-ui/public/studio.html +1 -1
  44. package/modules/hall-ui/public/task.html +1 -1
  45. package/modules/hall-ui/public/watch.html +1 -1
  46. package/modules/hall-ui/public/work.html +1 -1
  47. package/modules/lifecycle/ship-assurance.js +17 -1
  48. package/modules/provisioning/core-upgrade-authz.js +85 -0
  49. package/modules/provisioning/core-upgrade.js +2 -1
  50. package/modules/provisioning/provisioning.js +4 -4
  51. package/modules/provisioning/routes/core-upgrade.js +12 -6
  52. package/package-lock.json +2 -2
  53. package/package.json +1 -1
  54. package/release-notes.json +20 -0
  55. package/scripts/gds/provision-core-upgrade.js +12 -4
  56. package/src/bongos/auth.js +8 -2
  57. package/src/bongos/core-update.js +23 -1
  58. package/src/bongos/routes/core-update.js +51 -4
  59. package/src/module-api.js +1 -1
  60. package/tests/core_upgrade_owner_door.mjs +444 -0
  61. package/tests/nav_permission_atoms.mjs +1 -1
  62. package/tests/provisioning_teardown_intent.mjs +28 -2
  63. package/tests/routine_observability_api.mjs +195 -0
  64. package/tests/scouting_page_gate.mjs +11 -3
  65. package/tests/ship_assurance.mjs +86 -3
@@ -0,0 +1,195 @@
1
+ // tests/routine_observability_api.mjs — the unattended routines are diagnosable from off
2
+ // the box (task 1002669).
3
+ //
4
+ // Every autonomy-gate decision already lands in `autonomy_runs` and every core upgrade in
5
+ // `core_upgrades`, but no route read either, so "did the routine fire, what did it
6
+ // decide, and when did the pin last move" needed an SSH session (blocker 1000106). Two
7
+ // read-only routes answer it now:
8
+ // GET /api/bongos/autonomy/runs — behind `autonomy.precheck` (Metic+), the atom the
9
+ // precheck itself uses
10
+ // GET /api/bongos/core-upgrades — behind `core.pin.move` (Archon), the atom the
11
+ // upgrade banner and the deploy door use
12
+ // No new atom, so no migration. These tests drive REAL requests through the assembled
13
+ // routers with only the two auth gates stubbed (the core_update_banner precedent: an
14
+ // arity-mangled handler 500s, and only a real request sees that), read the atom each
15
+ // gate was built with off the router stack, and pin the SQL against a stubbed pool.
16
+ //
17
+ // DB-free, no network. Run: node --test tests/routine_observability_api.mjs
18
+
19
+ import { test } from 'node:test';
20
+ import assert from 'node:assert/strict';
21
+ import { createRequire } from 'node:module';
22
+ import path from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const req = createRequire(import.meta.url);
27
+ const express = req('express');
28
+ const { attachFail } = req(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
29
+
30
+ // A permission stub that remembers which atom it was built with, so the test can read
31
+ // the gate's authority off the router instead of grepping the source for it.
32
+ const permStub = (key) => {
33
+ const mw = (_req, _res, next) => next();
34
+ mw.permKey = key;
35
+ return mw;
36
+ };
37
+ const passBuilder = (reqq, _res, next) => { reqq.builder = { id: 3, rank: 'archon' }; next(); };
38
+
39
+ async function serve(router) {
40
+ const app = express();
41
+ app.use(attachFail);
42
+ app.use(router);
43
+ const server = app.listen(0);
44
+ await new Promise((r) => server.once('listening', r));
45
+ const base = `http://127.0.0.1:${server.address().port}`;
46
+ return { base, close: () => server.close() };
47
+ }
48
+
49
+ function layerFor(router, p) {
50
+ const layer = router.stack.find((l) => l.route && l.route.path === p);
51
+ assert.ok(layer, `${p} is registered`);
52
+ return layer;
53
+ }
54
+
55
+ // ── GET /autonomy/runs ──────────────────────────────────────────────────────────────
56
+
57
+ function autonomyRouter() {
58
+ const api = req(path.join(ROOT, 'src/module-api.js'));
59
+ const saved = { b: api.requireBuilder, p: api.requirePermission };
60
+ api.requireBuilder = passBuilder;
61
+ api.requirePermission = permStub;
62
+ const routePath = req.resolve(path.join(ROOT, 'modules/autonomy/routes/autonomy.js'));
63
+ delete req.cache[routePath];
64
+ try {
65
+ return req(routePath)();
66
+ } finally {
67
+ api.requireBuilder = saved.b;
68
+ api.requirePermission = saved.p;
69
+ }
70
+ }
71
+
72
+ test('GET /autonomy/runs is gated by requireBuilder + the precheck\'s own atom', () => {
73
+ const layer = layerFor(autonomyRouter(), '/autonomy/runs');
74
+ assert.equal(layer.route.methods.get, true);
75
+ assert.equal(layer.route.stack.length, 3, 'requireBuilder + requirePermission + handler');
76
+ assert.equal(layer.route.stack[1].handle.permKey, 'autonomy.precheck');
77
+ });
78
+
79
+ test('GET /autonomy/runs returns the decisions, narrowed by routine and a clamped limit', async () => {
80
+ const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
81
+ const saved = db.readAutonomyRuns;
82
+ const seen = [];
83
+ const rows = [{ id: '7', routine: 'overnight-builder', decision: 'no-go', reason: 'no claimable work', head_sha: 'abc', claimable_count: 0, enabled: true, created_at: '2026-09-25T02:00:00.000Z' }];
84
+ db.readAutonomyRuns = async (args) => { seen.push(args); return rows; };
85
+ const s = await serve(autonomyRouter());
86
+ try {
87
+ let r = await fetch(`${s.base}/autonomy/runs?routine=overnight-builder&limit=500`);
88
+ assert.equal(r.status, 200);
89
+ assert.deepEqual(await r.json(), { routine: 'overnight-builder', limit: 100, runs: rows });
90
+ assert.deepEqual(seen.pop(), { routine: 'overnight-builder', limit: 100 }, 'a limit above 100 is clamped, not refused');
91
+
92
+ r = await fetch(`${s.base}/autonomy/runs`);
93
+ assert.equal(r.status, 200);
94
+ assert.deepEqual(seen.pop(), { routine: null, limit: 20 }, 'no routine = every routine; the default limit is 20');
95
+
96
+ await fetch(`${s.base}/autonomy/runs?limit=banana`);
97
+ assert.equal(seen.pop().limit, 20, 'a non-number falls back to the default');
98
+ await fetch(`${s.base}/autonomy/runs?limit=0`);
99
+ assert.equal(seen.pop().limit, 1);
100
+
101
+ db.readAutonomyRuns = async () => { throw new Error('relation "autonomy_runs" does not exist'); };
102
+ r = await fetch(`${s.base}/autonomy/runs`);
103
+ assert.equal(r.status, 503, 'an unreadable table is a 503 that says so, never an empty list');
104
+ const body = await r.json();
105
+ assert.equal(body.error.code, 'autonomy_runs_unreadable');
106
+ assert.ok(!JSON.stringify(body).includes('does not exist'), 'the database error text stays in the log');
107
+ } finally {
108
+ s.close();
109
+ db.readAutonomyRuns = saved;
110
+ }
111
+ });
112
+
113
+ test('readAutonomyRuns: newest first, one routine or all, ids as strings', async () => {
114
+ const api = req(path.join(ROOT, 'src/module-api.js'));
115
+ const db = req(path.join(ROOT, 'modules/autonomy/db.js'));
116
+ const saved = api.pool.query;
117
+ const calls = [];
118
+ api.pool.query = async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 42, routine: 'bfg', decision: 'go' }] }; };
119
+ try {
120
+ const out = await db.readAutonomyRuns({ routine: 'bfg', limit: 5 });
121
+ assert.deepEqual(out, [{ id: '42', routine: 'bfg', decision: 'go' }]);
122
+ assert.match(calls[0].sql, /FROM autonomy_runs/);
123
+ assert.match(calls[0].sql, /\$1::text IS NULL OR routine = \$1/);
124
+ assert.match(calls[0].sql, /ORDER BY created_at DESC/);
125
+ assert.deepEqual(calls[0].params, ['bfg', 5]);
126
+ await db.readAutonomyRuns();
127
+ assert.deepEqual(calls[1].params, [null, 20]);
128
+ assert.doesNotMatch(calls[0].sql, /\b(INSERT|UPDATE|DELETE)\b/i, 'read-only');
129
+ } finally {
130
+ api.pool.query = saved;
131
+ }
132
+ });
133
+
134
+ // ── GET /core-upgrades ──────────────────────────────────────────────────────────────
135
+
136
+ function coreUpdateRouter(deps) {
137
+ const auth = req(path.join(ROOT, 'src/bongos/auth.js'));
138
+ const saved = { b: auth.requireBuilder, p: auth.requirePermission };
139
+ auth.requireBuilder = passBuilder;
140
+ auth.requirePermission = permStub;
141
+ const routePath = req.resolve(path.join(ROOT, 'src/bongos/routes/core-update.js'));
142
+ delete req.cache[routePath];
143
+ try {
144
+ return req(routePath)(deps);
145
+ } finally {
146
+ auth.requireBuilder = saved.b;
147
+ auth.requirePermission = saved.p;
148
+ delete req.cache[routePath];
149
+ }
150
+ }
151
+
152
+ test('GET /core-upgrades is gated by requireBuilder + core.pin.move, like the banner beside it', () => {
153
+ const router = coreUpdateRouter();
154
+ const layer = layerFor(router, '/core-upgrades');
155
+ assert.equal(layer.route.stack.length, 3, 'requireBuilder + requirePermission + handler');
156
+ assert.equal(layer.route.stack[1].handle.permKey, 'core.pin.move');
157
+ assert.equal(layerFor(router, '/core-update').route.stack[1].handle.permKey, 'core.pin.move', 'the existing route is untouched');
158
+ });
159
+
160
+ test('GET /core-upgrades answers "when did the pin last move", newest first', async () => {
161
+ const seen = [];
162
+ const ledger = [
163
+ { id: '12', from_version: '1.19.1022', to_version: '1.19.1023', source_commit: 'b50d32f1', note: null, applied_at: '2026-09-24T10:00:00.000Z', source: 'door', requested_by_builder_id: '3' },
164
+ { id: '11', from_version: '1.19.1010', to_version: '1.19.1022', source_commit: null, note: 'auto-subscription (patch)', applied_at: '2026-09-23T10:00:00.000Z', source: 'cli', requested_by_builder_id: null },
165
+ ];
166
+ const s = await serve(coreUpdateRouter({ upgrades: async (args) => { seen.push(args); return ledger; } }));
167
+ try {
168
+ const r = await fetch(`${s.base}/core-upgrades?limit=2`);
169
+ assert.equal(r.status, 200, 'the handler runs — a 500 here is the asyncHandler arity bug');
170
+ const body = await r.json();
171
+ assert.equal(body.ok, true);
172
+ assert.equal(body.limit, 2);
173
+ assert.deepEqual(body.upgrades, ledger);
174
+ assert.equal(body.upgrades[0].applied_at, '2026-09-24T10:00:00.000Z', 'the first row is the last pin move');
175
+ assert.ok('running' in body, 'the running core version rides along, so behind-or-not is one read');
176
+ assert.deepEqual(seen, [{ limit: 2 }]);
177
+ } finally {
178
+ s.close();
179
+ }
180
+ });
181
+
182
+ test('readCoreUpgrades: the ledger, newest first, read-only, ids as strings', async () => {
183
+ const { readCoreUpgrades } = req(path.join(ROOT, 'src/bongos/core-update.js'));
184
+ const calls = [];
185
+ const pool = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 5, to_version: '1.19.1023', requested_by_builder_id: 3 }, { id: 4, to_version: '1.19.1022', requested_by_builder_id: null }] }; } };
186
+ const out = await readCoreUpgrades({ limit: 7, pool });
187
+ assert.deepEqual(out, [
188
+ { id: '5', to_version: '1.19.1023', requested_by_builder_id: '3' },
189
+ { id: '4', to_version: '1.19.1022', requested_by_builder_id: null },
190
+ ]);
191
+ assert.match(calls[0].sql, /FROM core_upgrades/);
192
+ assert.match(calls[0].sql, /ORDER BY applied_at DESC/);
193
+ assert.deepEqual(calls[0].params, [7]);
194
+ assert.doesNotMatch(calls[0].sql, /\b(INSERT|UPDATE|DELETE)\b/i, 'read-only');
195
+ });
@@ -174,11 +174,19 @@ await t('the OTHER page gates are untouched — their seam is never consulted at
174
174
  assert.ok(vote, 'board.vote.cast floors at xenos on purpose (ADR 0266) — admitted without the seam');
175
175
  });
176
176
 
177
- await t('only ONE gate declares a second door (a widening must be opted into, one gate at a time)', () => {
177
+ await t('only the gates WITH A PROOF declare a second door (a widening must be opted into, one gate at a time)', () => {
178
178
  const src = require('node:fs').readFileSync(new URL('../src/bongos/auth.js', import.meta.url), 'utf8');
179
- const declarations = [...src.matchAll(/alsoAdmits:\s*'([^']+)'/g)].map((m) => m[1]);
180
- assert.deepEqual(declarations, ['isProjectOwner'],
179
+ // Keyed by GATE, not just the door's name, so a third gate borrowing 'isProjectOwner'
180
+ // fails here instead of hiding behind an unchanged list of names. Each entry names the
181
+ // file that proves its widening:
182
+ // requireProjectCuratePage — this file (ADR 0210 D3);
183
+ // requireCorePinPage — tests/core_upgrade_owner_door.mjs (ADR 0339, task 1004174).
184
+ const declarations = [...src.matchAll(/(require\w+Page):\s*Object\.freeze\(\{[^}]*alsoAdmits:\s*'([^']+)'/g)]
185
+ .map((m) => `${m[1]}:${m[2]}`);
186
+ assert.deepEqual(declarations, ['requireProjectCuratePage:isProjectOwner', 'requireCorePinPage:isProjectOwner'],
181
187
  'a second alsoAdmits appearing without its own proof is how a page gate quietly widens');
188
+ const plain = [...src.matchAll(/alsoAdmits:\s*'([^']+)'/g)].length;
189
+ assert.equal(plain, declarations.length, 'every alsoAdmits sits on a named page gate this test can see');
182
190
  });
183
191
 
184
192
  seams._reset();
@@ -194,14 +194,15 @@ await t('the CLI carries the declaration through its deliberate-empty path', ()
194
194
  const { deriveShipAssurance } = require(path.join(ROOT, 'modules/lifecycle/ship-assurance.js'));
195
195
  const TIP = 'a'.repeat(40);
196
196
  console.log('\nthe tip proof does not depend on the branch/PR probe');
197
- await t('tip in main + getPublishStatus throwing (no branch on the claim) → proof', async () => {
197
+ await t('tip in main + getPublishStatus throwing → proof (the status probe is only a witness)', async () => {
198
198
  const seen = [];
199
199
  const r = await deriveShipAssurance({
200
200
  taskId: 1003285,
201
201
  body: {},
202
202
  db: { getClaimPublishedSha: async () => TIP },
203
203
  githubPush: {
204
- getPublishStatus: async () => { const e = new Error('BAD_BRANCH'); e.code = 'BAD_BRANCH'; throw e; },
204
+ findTaskPullRequest: async () => ({ branch: 'task-1003285', pr_number: 7, merged: true }),
205
+ getPublishStatus: async () => { const e = new Error('STATUS_FETCH_FAILED'); e.code = 'STATUS_FETCH_FAILED'; throw e; },
205
206
  landProvenForTask: async ({ publishedSha, status }) => {
206
207
  seen.push({ publishedSha, status });
207
208
  return publishedSha === TIP ? { proven: true, reason: 'published tip in main' } : { proven: false };
@@ -210,9 +211,28 @@ await t('tip in main + getPublishStatus throwing (no branch on the claim) → pr
210
211
  onProbeError: (e) => seen.push({ probeError: e.code }),
211
212
  });
212
213
  assert.equal(seen.some((s) => s.publishedSha === TIP && s.status === null), true, 'the proof ran with status=null');
213
- assert.equal(seen.some((s) => s.probeError === 'BAD_BRANCH'), true, 'the probe failure was still reported');
214
+ assert.equal(seen.some((s) => s.probeError === 'STATUS_FETCH_FAILED'), true, 'the probe failure was still reported');
214
215
  assert.equal(r.assurance && r.assurance.kind, 'proof');
215
216
  });
217
+ await t('tip in main + the PR search throwing → proof, and the status probe is never asked', async () => {
218
+ let statusCalls = 0;
219
+ const errs = [];
220
+ const r = await deriveShipAssurance({
221
+ taskId: 1003286,
222
+ body: {},
223
+ db: { getClaimPublishedSha: async () => TIP },
224
+ githubPush: {
225
+ findTaskPullRequest: async () => { const e = new Error('PUSH_UNCONFIGURED'); e.code = 'PUSH_UNCONFIGURED'; throw e; },
226
+ getPublishStatus: async () => { statusCalls++; return null; },
227
+ landProvenForTask: async ({ publishedSha, status }) => (publishedSha === TIP && status === null
228
+ ? { proven: true, reason: 'published tip in main' } : { proven: false }),
229
+ },
230
+ onProbeError: (e) => errs.push(e.code),
231
+ });
232
+ assert.equal(r.assurance && r.assurance.kind, 'proof');
233
+ assert.deepEqual(errs, ['PUSH_UNCONFIGURED'], 'the search failure is reported on its own');
234
+ assert.equal(statusCalls, 0, 'no branch was found, so there is nothing to ask the status probe about');
235
+ });
216
236
  await t('no tip + getPublishStatus throwing → still the 409 refusal (nothing to prove)', async () => {
217
237
  const r = await deriveShipAssurance({
218
238
  taskId: 1003253,
@@ -238,5 +258,68 @@ await t('a proof failure (compare API down) still falls through to the refusal,
238
258
  assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
239
259
  });
240
260
 
261
+ // task 1004275 — the status probe was called with { taskId, publishedSha }, which
262
+ // getPublishStatus does not read: it wants { branch }, so it threw BAD_BRANCH on EVERY
263
+ // manual ship and both PR-witness proofs could never fire here. A squash-landed or
264
+ // no-tip task was refused ship_land_not_proven while the reconciler (which resolves a
265
+ // branch) shipped it minutes later. The branch now comes from the task's own PR, found
266
+ // by title — the lookup GET /publish-status has used since task 1003764.
267
+ console.log('\nthe PR witnesses reach the proof (task 1004275)');
268
+ const realPush = require(path.join(ROOT, 'modules/lifecycle/github-push.js'));
269
+ // The REAL getPublishStatus contract, minus the network: anything but a safe branch is
270
+ // refused exactly as the real one refuses it, so the pre-fix call shape goes red here.
271
+ const statusFor = (merged) => async (args) => {
272
+ if (!args || !realPush.isSafeBranch(args.branch)) { const e = new Error('BAD_BRANCH'); e.code = 'BAD_BRANCH'; throw e; }
273
+ return { branch: args.branch, pr_state: merged ? 'closed' : 'open', merged, pr_number: 1214,
274
+ pr_title: 'task 1004275: fix the land proof', pr_body: '', merge_sha: null };
275
+ };
276
+ await t('no tip + a merged PR titled for the task → proof (the no-tip fallback now fires)', async () => {
277
+ const asked = [];
278
+ const errs = [];
279
+ const r = await deriveShipAssurance({
280
+ taskId: 1004275,
281
+ body: {},
282
+ db: { getClaimPublishedSha: async () => null },
283
+ githubPush: {
284
+ findTaskPullRequest: async (a) => { asked.push(a); return { branch: '1004275-assurance-branch-w4q8', pr_number: 1214, merged: true }; },
285
+ getPublishStatus: statusFor(true),
286
+ landProvenForTask: realPush.landProvenForTask, // no tip + merged PR → decided with no network call
287
+ },
288
+ onProbeError: (e) => errs.push(e.code),
289
+ });
290
+ assert.deepEqual(errs, [], 'no probe failed');
291
+ assert.deepEqual(asked, [{ taskId: 1004275 }], 'the PR is found by the task id');
292
+ assert.equal(r.assurance && r.assurance.kind, 'proof');
293
+ assert.match(r.assurance.reason, /matched PR references task 1004275/);
294
+ });
295
+ await t('no tip + the task\'s PR still OPEN → the 409 refusal (a PR is a witness, not a land)', async () => {
296
+ const r = await deriveShipAssurance({
297
+ taskId: 1004275,
298
+ body: {},
299
+ db: { getClaimPublishedSha: async () => null },
300
+ githubPush: {
301
+ findTaskPullRequest: async () => ({ branch: '1004275-assurance-branch-w4q8', pr_number: 1214, merged: false }),
302
+ getPublishStatus: statusFor(false),
303
+ landProvenForTask: realPush.landProvenForTask,
304
+ },
305
+ });
306
+ assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
307
+ });
308
+ await t('no tip + no PR for the task → the 409 refusal, and getPublishStatus is not called with nothing', async () => {
309
+ let statusCalls = 0;
310
+ const r = await deriveShipAssurance({
311
+ taskId: 1004276,
312
+ body: {},
313
+ db: { getClaimPublishedSha: async () => null },
314
+ githubPush: {
315
+ findTaskPullRequest: async () => null,
316
+ getPublishStatus: async () => { statusCalls++; throw new Error('BAD_BRANCH'); },
317
+ landProvenForTask: realPush.landProvenForTask,
318
+ },
319
+ });
320
+ assert.equal(r.refusal && r.refusal.code, 'ship_land_not_proven');
321
+ assert.equal(statusCalls, 0);
322
+ });
323
+
241
324
  console.log(`\n${passed} passed, ${failed} failed`);
242
325
  process.exit(failed ? 1 : 0);