@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.
- package/.bongos-core.json +88 -68
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +9 -1
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0339-a-projects-owner-upgrades-it-and-a-hosted-project-hands-off-to-the-hub-door.md +84 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +126 -8
- package/docs/api-reference.md +9 -2
- package/docs/copy-inventory.md +10 -10
- package/docs/copy-registry.json +13 -13
- package/docs/module-api-changelog.md +4 -0
- package/modules/autonomy/db.js +19 -0
- package/modules/autonomy/routes/autonomy.js +28 -0
- package/modules/government/catalog.js +2 -2
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/people.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +36 -5
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/lifecycle/ship-assurance.js +17 -1
- package/modules/provisioning/core-upgrade-authz.js +85 -0
- package/modules/provisioning/core-upgrade.js +2 -1
- package/modules/provisioning/provisioning.js +4 -4
- package/modules/provisioning/routes/core-upgrade.js +12 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +20 -0
- package/scripts/gds/provision-core-upgrade.js +12 -4
- package/src/bongos/auth.js +8 -2
- package/src/bongos/core-update.js +23 -1
- package/src/bongos/routes/core-update.js +51 -4
- package/src/module-api.js +1 -1
- package/tests/core_upgrade_owner_door.mjs +444 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/provisioning_teardown_intent.mjs +28 -2
- package/tests/routine_observability_api.mjs +195 -0
- package/tests/scouting_page_gate.mjs +11 -3
- 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
|
|
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
|
-
|
|
180
|
-
|
|
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();
|
package/tests/ship_assurance.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 === '
|
|
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);
|