@bongos/core 1.19.1033 → 1.19.1035
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 +204 -99
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/grade-recover/SKILL.md +2 -1
- package/.claude/skills/grader-health/SKILL.md +4 -0
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- 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 +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +15 -0
- package/docs/adr/README.md +1 -1
- package/docs/api/openapi.json +120 -4
- package/docs/api-reference.md +9 -2
- package/docs/architecture.md +3 -2
- package/docs/canonical-permissions.md +2 -2
- package/docs/copy-inventory.md +34 -30
- package/docs/copy-registry.json +69 -33
- package/docs/module-api-changelog.md +5 -1
- package/docs/recipes/local-dev.md +45 -3
- package/docs/recipes/packaging-the-core.md +1 -1
- package/modules/autonomy/cadence.js +52 -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/government/protected-surfaces.json +1 -0
- package/modules/grading/grade-engine.js +267 -0
- package/modules/grading/grader-subagent.js +4 -3
- package/modules/grading/grader-workers/index.js +67 -11
- package/modules/grading/grader-workers/worker-verdict.js +32 -2
- package/modules/grading/grader.js +109 -88
- package/modules/hall-ui/public/deploy.js +35 -0
- package/modules/hall-ui/public/project-settings.css +7 -0
- package/modules/lifecycle/routes/tasks.js +19 -4
- package/modules/lifecycle/ship-assurance.js +17 -1
- package/modules/provisioning/control-plane-guard.js +14 -13
- package/modules/provisioning/core-upgrade.js +30 -17
- package/modules/provisioning/public-refusal.js +44 -0
- package/modules/provisioning/routes/disconnect.js +2 -1
- package/modules/provisioning/routes/env-manifest.js +5 -4
- package/modules/provisioning/routes/provisioning.js +16 -15
- package/modules/provisioning/routes/repo-private.js +2 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +80 -0
- package/scripts/gds/autobongos-run.js +10 -2
- package/scripts/gds/backfill-cost.js +20 -9
- package/scripts/gds/claim.js +2 -2
- package/scripts/gds/credential-presence.js +281 -0
- package/scripts/gds/deploy-main-audit-gate.sh +11 -4
- package/scripts/gds/dev-lib.js +212 -5
- package/scripts/gds/dev.js +102 -50
- package/scripts/gds/doctor.js +3 -2
- package/scripts/gds/grade-correlation-audit.js +280 -0
- package/scripts/gds/grade-replay.js +237 -0
- package/scripts/gds/hooks/pre-push.js +7 -3
- package/scripts/gds/main-audit.js +19 -10
- package/scripts/gds/memory-map.js +3 -1
- package/scripts/gds/onboarding-config.js +3 -2
- package/scripts/gds/plain-cards.js +4 -0
- package/scripts/gds/preflight.js +12 -4
- package/scripts/gds/provision-core-upgrade.js +155 -4
- package/scripts/gds/provision-repo-auth.js +15 -0
- package/scripts/gds/provision.js +1 -1
- package/scripts/gds/publish-credential-check.js +3 -0
- package/scripts/gds/push-main.js +14 -3
- package/scripts/gds/push-path-brief.js +3 -2
- package/scripts/gds/run-routine.js +18 -5
- package/scripts/gds/run-unit-tests.js +14 -3
- package/scripts/gds/session-cost.js +12 -3
- package/scripts/gds/ship-deploy-target.js +11 -6
- package/scripts/gds/ship-grade-steps.js +33 -0
- package/scripts/gds/ship-land.js +157 -11
- package/scripts/gds/ship-merge.js +3 -2
- package/scripts/gds/ship-preflight-steps.js +30 -8
- package/scripts/gds/ship.js +1 -1
- package/scripts/gds/upgrade.js +3 -3
- package/scripts/hall-preview/README.md +20 -3
- package/scripts/hall-preview/live-identity.js +1 -1
- package/scripts/hall-preview/refresh-fixtures.js +71 -11
- package/scripts/hall-preview/scrub-identity.js +296 -0
- package/src/bongos/core-update.js +23 -1
- package/src/bongos/routes/core-update.js +27 -2
- package/src/bongos/routes/security.js +3 -3
- package/src/build-info.js +4 -2
- package/src/module-api.js +1 -1
- package/tests/adr_0089_scope.mjs +70 -0
- package/tests/autobongos_cadence.mjs +74 -0
- package/tests/consumer_layout_boot.mjs +363 -31
- package/tests/core_upgrade_door.mjs +40 -6
- package/tests/core_upgrade_runner.mjs +240 -4
- package/tests/credential_presence.mjs +153 -0
- package/tests/deploy_audit_gate.mjs +5 -2
- package/tests/deploy_page_projects.mjs +63 -2
- package/tests/dev_lib.mjs +2 -1
- package/tests/dev_windows.mjs +324 -0
- package/tests/env_knobs_neutral_prefix.mjs +203 -0
- package/tests/fixtures/grade-replay/recorded-panel-grades.json +27 -0
- package/tests/government_protected_surfaces.mjs +10 -0
- package/tests/grade_correlation_audit.mjs +159 -0
- package/tests/grade_engine.mjs +249 -0
- package/tests/grade_engine_replay.mjs +99 -0
- package/tests/grader_prompt_window_bound.mjs +132 -0
- package/tests/grader_unbacked_score.mjs +194 -0
- package/tests/hall_preview_scrub.mjs +214 -0
- package/tests/integration_parked_ratchet.mjs +24 -1
- package/tests/provisioning_public_refusal.mjs +195 -0
- package/tests/publish_status_branch.mjs +128 -0
- package/tests/routine_observability_api.mjs +195 -0
- package/tests/ship_assurance.mjs +86 -3
- package/tests/ship_land_red_pr.mjs +236 -0
- package/tests/ship_preflight.mjs +67 -1
- package/tests/start_env_guard.mjs +16 -0
- package/tests/upgrade.mjs +20 -1
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);
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// tests/ship_land_red_pr.mjs — a stalled land names its red check (task 1003764, gap 2).
|
|
2
|
+
//
|
|
3
|
+
// WHY. Task 1003747's ship sat at "waiting for auto-merge + prod deploy" while a
|
|
4
|
+
// required check was red, and ended on "This is NOT a failure". The cause — CI tests
|
|
5
|
+
// the branch MERGED with main, and main had moved six commits, staling the copy
|
|
6
|
+
// registry in the merged tree only — took ~20 minutes of git archaeology to find,
|
|
7
|
+
// though it was one API call away. The laptop poll (pollCiDeploy) never looked at
|
|
8
|
+
// checks at all; the server poll named them but not the merge trap, and not how a
|
|
9
|
+
// freshness gate heals. These drive both polls with fake gh / server reads (no
|
|
10
|
+
// network, no sleeping) and pin the pure pieces they are built from.
|
|
11
|
+
//
|
|
12
|
+
// Run: node tests/ship_land_red_pr.mjs
|
|
13
|
+
|
|
14
|
+
import { strict as assert } from 'node:assert';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import {
|
|
18
|
+
FRESHNESS_TESTS, failingTestNames, landPendingLines, localMergeDriver, mergeTrapLines,
|
|
19
|
+
pollCiDeploy, pollServerPublish, redBlocksMerge, redPrLines, unitReportFor,
|
|
20
|
+
} from '../scripts/gds/ship-land.js';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const githubPush = require('../modules/lifecycle/github-push.js');
|
|
24
|
+
const { CARDS } = require('../scripts/gds/plain-cards.js');
|
|
25
|
+
|
|
26
|
+
let passed = 0;
|
|
27
|
+
let failed = 0;
|
|
28
|
+
async function test(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
30
|
+
catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Run fn with console.log captured; returns { result, out }.
|
|
34
|
+
async function capture(fn) {
|
|
35
|
+
const lines = [];
|
|
36
|
+
const orig = console.log;
|
|
37
|
+
console.log = (...a) => { lines.push(a.join(' ')); };
|
|
38
|
+
try { return { result: await fn(), out: lines.join('\n'), lines }; }
|
|
39
|
+
finally { console.log = orig; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SHA = 'a'.repeat(40);
|
|
43
|
+
// What `gh pr view --json state,mergeCommit,mergeStateStatus,headRefOid,statusCheckRollup`
|
|
44
|
+
// really returns (shape taken from a live PR on this repo): check runs, then statuses.
|
|
45
|
+
const run = (name, conclusion, status = 'COMPLETED') => ({ __typename: 'CheckRun', name, conclusion, status, workflowName: 'unit' });
|
|
46
|
+
const ctx = (context, state) => ({ __typename: 'StatusContext', context, state, targetUrl: '' });
|
|
47
|
+
const openPr = (rollup, mergeStateStatus = 'BLOCKED') => ({ state: 'OPEN', mergeCommit: null, mergeStateStatus, headRefOid: SHA, statusCheckRollup: rollup });
|
|
48
|
+
const RED_ROLLUP = [run('unit', 'FAILURE'), run('report', 'SUCCESS'), ctx('gate-author-trust', 'SUCCESS'), ctx('unit-report', 'FAILURE')];
|
|
49
|
+
const COMBINED_STATUS = { state: 'failure', statuses: [
|
|
50
|
+
{ context: 'gate-author-trust', state: 'success', description: 'Metic+ author' },
|
|
51
|
+
{ context: 'unit-report', state: 'failure', description: '1 failed: fitness' },
|
|
52
|
+
] };
|
|
53
|
+
|
|
54
|
+
// ---- localMergeDriver: the laptop's view of a red PR, in publish-status's shape ----
|
|
55
|
+
|
|
56
|
+
await test('localMergeDriver names the failing required check and asks for the tests once the report is posted', async () => {
|
|
57
|
+
const asked = [];
|
|
58
|
+
const md = localMergeDriver(openPr(RED_ROLLUP), (sha) => { asked.push(sha); return '1 failed: fitness'; });
|
|
59
|
+
assert.deepEqual(md, { reason: 'failing_check:unit', failing_tests: '1 failed: fitness' });
|
|
60
|
+
assert.deepEqual(asked, [SHA], 'the unit-report is read for the PR head sha');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await test('localMergeDriver does not ask for tests before the unit-report status exists', async () => {
|
|
64
|
+
let asked = 0;
|
|
65
|
+
const md = localMergeDriver(openPr([run('unit', 'FAILURE')]), () => { asked++; return 'x'; });
|
|
66
|
+
assert.deepEqual(md, { reason: 'failing_check:unit', failing_tests: null });
|
|
67
|
+
assert.equal(asked, 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await test('localMergeDriver: pending, expected and in-progress are waiting, not red', async () => {
|
|
71
|
+
for (const rollup of [
|
|
72
|
+
[run('unit', '', 'IN_PROGRESS')],
|
|
73
|
+
[ctx('unit-report', 'PENDING')],
|
|
74
|
+
[ctx('some-required-status', 'EXPECTED')],
|
|
75
|
+
[run('unit', 'SUCCESS'), run('lint', 'NEUTRAL'), run('docs', 'SKIPPED'), ctx('unit-report', 'SUCCESS')],
|
|
76
|
+
]) assert.equal(localMergeDriver(openPr(rollup)), null, JSON.stringify(rollup));
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await test('localMergeDriver: an UNSTABLE PR means the failing check is optional', async () => {
|
|
80
|
+
assert.equal(localMergeDriver(openPr([run('lint', 'FAILURE')], 'UNSTABLE')).reason, 'non_required_check_failing:lint');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await test('localMergeDriver: nothing to say for a merged/closed PR, or a gh that returned no rollup', async () => {
|
|
84
|
+
assert.equal(localMergeDriver(null), null);
|
|
85
|
+
assert.equal(localMergeDriver({ ...openPr(RED_ROLLUP), state: 'MERGED' }), null);
|
|
86
|
+
assert.equal(localMergeDriver({ state: 'OPEN', mergeCommit: null }), null, 'the legacy two-field read has no rollup');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await test('localMergeDriver agrees with the server\'s mergeBlockReason on a red required check', async () => {
|
|
90
|
+
const server = githubPush.mergeBlockReason({
|
|
91
|
+
merged: false, pr_state: 'open', mergeable: 'MERGEABLE', merge_state: 'BLOCKED', review_decision: null,
|
|
92
|
+
rollup_state: 'FAILURE', checks: [{ name: 'unit', conclusion: 'FAILURE', status: 'COMPLETED' }], checks_unreadable: false,
|
|
93
|
+
});
|
|
94
|
+
assert.equal(localMergeDriver(openPr([run('unit', 'FAILURE')])).reason, server);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ---- unitReportFor: the failing-test line, via the same parser the server uses ----
|
|
98
|
+
|
|
99
|
+
await test('unitReportFor reads the unit-report status for the sha', async () => {
|
|
100
|
+
const calls = [];
|
|
101
|
+
const text = unitReportFor(SHA, (args) => { calls.push(args); return COMBINED_STATUS; });
|
|
102
|
+
assert.equal(text, '1 failed: fitness');
|
|
103
|
+
assert.deepEqual(calls, [['api', `repos/{owner}/{repo}/commits/${SHA}/status`]]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await test('unitReportFor: a green report, an unreadable one, or a bad sha → null (and a bad sha never reaches gh)', async () => {
|
|
107
|
+
assert.equal(unitReportFor(SHA, () => ({ statuses: [{ context: 'unit-report', state: 'success', description: 'all unit tests passed' }] })), null);
|
|
108
|
+
assert.equal(unitReportFor(SHA, () => null), null);
|
|
109
|
+
assert.equal(unitReportFor('HEAD; rm -rf /', () => { throw new Error('must not call gh'); }), null);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ---- the merge trap + the freshness heal ----
|
|
113
|
+
|
|
114
|
+
await test('failingTestNames parses the unit-report line and drops a name the status limit cut', async () => {
|
|
115
|
+
assert.deepEqual(failingTestNames('1 failed: fitness'), ['fitness']);
|
|
116
|
+
assert.deepEqual(failingTestNames('3 failed: api_docs, fitness, security'), ['api_docs', 'fitness', 'security']);
|
|
117
|
+
assert.deepEqual(failingTestNames('40 failed: a_test, b_test, some_lo…'), ['a_test', 'b_test']);
|
|
118
|
+
assert.deepEqual(failingTestNames(null), []);
|
|
119
|
+
assert.deepEqual(failingTestNames('all unit tests passed'), []);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await test('every name in FRESHNESS_TESTS is a real test file', async () => {
|
|
123
|
+
for (const name of FRESHNESS_TESTS) {
|
|
124
|
+
assert.ok(fs.existsSync(new URL(`./${name}.mjs`, import.meta.url)), `tests/${name}.mjs does not exist`);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await test('every red PR names the trap: CI tests the MERGE, not the branch', async () => {
|
|
129
|
+
const local = mergeTrapLines({ reason: 'failing_check:unit' }, { taskId: 42, via: 'local' }).join('\n');
|
|
130
|
+
assert.match(local, /MERGED with the current main, not the branch alone/);
|
|
131
|
+
assert.match(local, /git fetch origin && git merge origin\/main/, 'the laptop can reproduce on the merge');
|
|
132
|
+
const server = mergeTrapLines({ reason: 'failing_check:unit' }, { taskId: 42, via: 'server' }).join('\n');
|
|
133
|
+
assert.match(server, /MERGED with the current main/);
|
|
134
|
+
assert.ok(!/git merge origin\/main/.test(server), 'a credential-less machine cannot merge main locally — never tell it to');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await test('a failing freshness gate gets its healing command, per push path', async () => {
|
|
138
|
+
const md = { reason: 'failing_check:unit-report', failing_tests: '1 failed: fitness' };
|
|
139
|
+
const local = mergeTrapLines(md, { taskId: 42, via: 'local' }).join('\n');
|
|
140
|
+
assert.match(local, /`fitness` is a freshness gate/);
|
|
141
|
+
assert.match(local, /stale in the MERGED tree/);
|
|
142
|
+
assert.match(local, /node scripts\/gds\/ship\.js 42/, 'names the re-ship for THIS task');
|
|
143
|
+
assert.match(local, /ship-check\.js/);
|
|
144
|
+
const server = mergeTrapLines(md, { taskId: 42, via: 'server' }).join('\n');
|
|
145
|
+
assert.match(server, /The server regenerates stale generated files/);
|
|
146
|
+
const plain = mergeTrapLines({ reason: 'failing_check:unit-report', failing_tests: '1 failed: security' }, { taskId: 42 }).join('\n');
|
|
147
|
+
assert.ok(!/freshness gate/.test(plain), 'an ordinary failing test gets no regenerate advice');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await test('redPrLines: the red line first, then the trap, then the plain-words card', async () => {
|
|
151
|
+
const lines = redPrLines({ reason: 'failing_check:unit-report', failing_tests: '1 failed: fitness' }, { taskId: 42, via: 'local' });
|
|
152
|
+
assert.match(lines[0], /the PR is RED — 1 failed: fitness/);
|
|
153
|
+
assert.match(lines[1], /MERGED with the current main/);
|
|
154
|
+
const at = lines.findIndex((l) => l.includes('[otb-plain]'));
|
|
155
|
+
assert.ok(at > 2, 'the card still follows');
|
|
156
|
+
assert.ok(lines.slice(at).join('\n').includes(CARDS.failing_check.happened));
|
|
157
|
+
assert.deepEqual(redPrLines({ reason: 'review_required' }), [], 'nothing red → nothing printed');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await test('redBlocksMerge: only a failing REQUIRED check stops the merge', async () => {
|
|
161
|
+
assert.equal(redBlocksMerge({ reason: 'failing_check:unit' }), true);
|
|
162
|
+
assert.equal(redBlocksMerge({ reason: 'failing_check_unreadable' }), true);
|
|
163
|
+
assert.equal(redBlocksMerge({ reason: 'non_required_check_failing:lint' }), false, 'GitHub still merges an UNSTABLE PR');
|
|
164
|
+
assert.equal(redBlocksMerge(null), false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await test('landPendingLines: a RED PR never gets "NOT a failure"; a green-but-slow one keeps it', async () => {
|
|
168
|
+
const red = landPendingLines(42, { red: true }).join('\n');
|
|
169
|
+
assert.ok(!/NOT a failure/.test(red), 'a failing required check does not pass by waiting');
|
|
170
|
+
assert.match(red, /RED/);
|
|
171
|
+
assert.match(red, /node scripts\/gds\/ship\.js 42/);
|
|
172
|
+
assert.match(landPendingLines(42).join('\n'), /This is NOT a failure/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ---- the two poll loops, end to end with fakes ----
|
|
176
|
+
|
|
177
|
+
await test('pollCiDeploy (laptop): a red PR is named once, the trap and heal are printed, and the banner is red', async () => {
|
|
178
|
+
const seen = { prView: 0, status: 0 };
|
|
179
|
+
const gh = (args) => {
|
|
180
|
+
if (args[0] === 'pr' && args[1] === 'view') { seen.prView++; return openPr(RED_ROLLUP); }
|
|
181
|
+
if (args[0] === 'api') { seen.status++; return COMBINED_STATUS; }
|
|
182
|
+
throw new Error(`unexpected gh call: ${args.join(' ')}`);
|
|
183
|
+
};
|
|
184
|
+
const { result, out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
|
|
185
|
+
assert.equal(result.pending, true, 'still a pending land — the check can yet pass');
|
|
186
|
+
assert.equal(seen.prView, 30, 'polled the whole window');
|
|
187
|
+
assert.equal(seen.status, 1, 'the unit-report is read once per head sha, not once per poll');
|
|
188
|
+
assert.equal(out.split('the PR is RED — 1 failed: fitness').length - 1, 1, 'named once, not thirty times');
|
|
189
|
+
assert.match(out, /MERGED with the current main/);
|
|
190
|
+
assert.match(out, /`fitness` is a freshness gate/);
|
|
191
|
+
assert.match(out, /node scripts\/gds\/ship\.js 42/);
|
|
192
|
+
assert.ok(!/This is NOT a failure/.test(out), 'the closing banner must not contradict the red line');
|
|
193
|
+
assert.ok(!out.includes(CARDS.land_pending.happened), 'nor the "nothing to do" card');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await test('pollCiDeploy: a gh that rejects the new fields falls back to the old read and still sees the merge', async () => {
|
|
197
|
+
const gh = (args) => {
|
|
198
|
+
if (args[0] === 'pr' && args.includes('state,mergeCommit')) return { state: 'MERGED', mergeCommit: { oid: 'b'.repeat(40) } };
|
|
199
|
+
if (args[0] === 'pr') return null; // unknown JSON field → gh exits non-zero → ghJson null
|
|
200
|
+
if (args[0] === 'run') return [];
|
|
201
|
+
throw new Error(`unexpected gh call: ${args.join(' ')}`);
|
|
202
|
+
};
|
|
203
|
+
const { result, out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
|
|
204
|
+
assert.match(out, /merged to main \(bbbbbbbb\)/);
|
|
205
|
+
assert.equal(result.pending, true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
await test('pollCiDeploy: a green PR that is merely slow keeps the reassuring banner', async () => {
|
|
209
|
+
const gh = (args) => (args[0] === 'pr' ? openPr([run('unit', '', 'IN_PROGRESS')]) : null);
|
|
210
|
+
const { out } = await capture(() => pollCiDeploy(42, 'task-42', SHA, { ghJson: gh, pause: () => {} }));
|
|
211
|
+
assert.ok(!/RED/.test(out));
|
|
212
|
+
assert.match(out, /This is NOT a failure/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await test('pollServerPublish (credential-less): a failing status read is said once; a red PR gets the server-path heal', async () => {
|
|
216
|
+
let n = 0;
|
|
217
|
+
const readStatus = async () => {
|
|
218
|
+
n++;
|
|
219
|
+
if (n <= 3) return { ok: false, status: 502, data: { error: { code: 'status_failed', message: 'x' } } };
|
|
220
|
+
return { ok: true, status: 200, data: {
|
|
221
|
+
pr_state: 'open', merged: false, deploy_status: 'pending', reconcile_blocked: null,
|
|
222
|
+
merge_driver: { reason: 'failing_check:unit-report', failing_tests: '1 failed: fitness' },
|
|
223
|
+
} };
|
|
224
|
+
};
|
|
225
|
+
const { result, out } = await capture(() => pollServerPublish(42, 'task-42', { readStatus, pause: () => {} }));
|
|
226
|
+
assert.equal(result.pending, true);
|
|
227
|
+
assert.equal(out.split('could not read the land status from the server (HTTP 502 status_failed)').length - 1, 1,
|
|
228
|
+
'three identical failures are reported once');
|
|
229
|
+
assert.equal(out.split('the PR is RED — 1 failed: fitness').length - 1, 1);
|
|
230
|
+
assert.match(out, /The server regenerates stale generated files/);
|
|
231
|
+
assert.ok(!/git merge origin\/main/.test(out), 'never tells a credential-less machine to merge main');
|
|
232
|
+
assert.ok(!/This is NOT a failure/.test(out));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
console.log(`\nship-land red PR (task 1003764) tests: ${passed} passed, ${failed} failed`);
|
|
236
|
+
process.exitCode = failed > 0 ? 1 : 0;
|
package/tests/ship_preflight.mjs
CHANGED
|
@@ -17,7 +17,7 @@ const {
|
|
|
17
17
|
untrackedPaths, untrackedLeftBehind, untrackedWarningLines,
|
|
18
18
|
} = require('../modules/lifecycle/ship-preflight.js');
|
|
19
19
|
// task 1001436: the preflight step itself + the ship-input resolver it is handed.
|
|
20
|
-
const { preflight, shipInputPaths } = require('../scripts/gds/ship-preflight-steps.js');
|
|
20
|
+
const { preflight, shipInputPaths, staleToolingFiles, TOOLING_PATHSPECS } = require('../scripts/gds/ship-preflight-steps.js');
|
|
21
21
|
|
|
22
22
|
const tests = [];
|
|
23
23
|
const test = (name, fn) => tests.push({ name, fn });
|
|
@@ -509,6 +509,72 @@ test('preflight (real git): nothing committed on the branch → no untracked war
|
|
|
509
509
|
});
|
|
510
510
|
});
|
|
511
511
|
|
|
512
|
+
// ---- stale ship-tooling guard (task 1004273) ----
|
|
513
|
+
// The guard named ship.js + grader.js only; two carves moved the logic into ship-*.js
|
|
514
|
+
// and grader-*.js / grader-workers/ / grade-engine.js, so a branch behind main on any
|
|
515
|
+
// of those shipped with a stale copy — the #662 shape the guard exists for.
|
|
516
|
+
|
|
517
|
+
test('stale-tooling guard: every tracked ship*.js and grader file is covered by TOOLING_PATHSPECS', () => {
|
|
518
|
+
const root = path.resolve(import.meta.dirname, '..');
|
|
519
|
+
const ls = (...spec) => execSync(`git ls-files -z -- ${spec.map((s) => `'${s}'`).join(' ')}`, { cwd: root, encoding: 'utf8' })
|
|
520
|
+
.split('\0').filter(Boolean);
|
|
521
|
+
// Chosen by an independent rule (the filename), then asked of git's own pathspec matcher.
|
|
522
|
+
const shipTooling = ls('scripts/gds', 'modules/grading').filter((f) =>
|
|
523
|
+
/^scripts\/gds\/ship[^/]*\.js$/.test(f) || /^modules\/grading\/(?:grader|grade-engine)/.test(f));
|
|
524
|
+
assert.ok(shipTooling.length >= 25, `only ${shipTooling.length} ship/grader file(s) found — the scan is reading the wrong tree`);
|
|
525
|
+
assert.ok(shipTooling.includes('scripts/gds/ship-land.js') && shipTooling.includes('modules/grading/grader-score.js'));
|
|
526
|
+
const covered = new Set(ls(...TOOLING_PATHSPECS));
|
|
527
|
+
const missed = shipTooling.filter((f) => !covered.has(f));
|
|
528
|
+
assert.deepEqual(missed, [], `ship/grade tooling the stale-tooling guard would not refuse: ${missed.join(', ')}`);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Publish the throwaway repo's main to its local bare "origin".
|
|
532
|
+
const publishMain = (run) => run(['git', 'push', '-q', 'origin', 'main'].join(' '));
|
|
533
|
+
|
|
534
|
+
test('preflight (real git): main moved a carved ship-*.js and added a new grader file → refused stale-tooling', () => {
|
|
535
|
+
withPreflightRepo(({ work, run }) => {
|
|
536
|
+
fs.mkdirSync(path.join(work, 'scripts', 'gds'), { recursive: true });
|
|
537
|
+
fs.mkdirSync(path.join(work, 'modules', 'grading'), { recursive: true });
|
|
538
|
+
fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// v1\n');
|
|
539
|
+
fs.writeFileSync(path.join(work, 'modules', 'grading', 'grader-score.js'), '// v1\n');
|
|
540
|
+
run('git add -A && git commit -q -m tooling');
|
|
541
|
+
publishMain(run);
|
|
542
|
+
run('git checkout -q -b feature');
|
|
543
|
+
fs.writeFileSync(path.join(work, 'src.txt'), 'the branch\'s own work\n');
|
|
544
|
+
run('git add -A && git commit -q -m work');
|
|
545
|
+
// main moves: one carved file changes, one file appears that this checkout never had.
|
|
546
|
+
run('git checkout -q main');
|
|
547
|
+
fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// v2\n');
|
|
548
|
+
fs.writeFileSync(path.join(work, 'modules', 'grading', 'grader-new.js'), '// new carve\n');
|
|
549
|
+
run('git add -A && git commit -q -m "main moves the tooling"');
|
|
550
|
+
publishMain(run);
|
|
551
|
+
run('git checkout -q feature');
|
|
552
|
+
assert.ok(!fs.existsSync(path.join(work, 'modules', 'grading', 'grader-new.js')), 'the new file is on main only');
|
|
553
|
+
|
|
554
|
+
const { result, out } = capturePreflight({ task_id: 4244, head_sha: null }, {});
|
|
555
|
+
assert.equal(result.ok, false);
|
|
556
|
+
assert.equal(result.reason, 'stale-tooling');
|
|
557
|
+
assert.match(out, /- scripts\/gds\/ship-land\.js/, 'a carved ship-*.js is tooling');
|
|
558
|
+
assert.match(out, /- modules\/grading\/grader-new\.js/, 'a file only main has is still matched');
|
|
559
|
+
assert.deepEqual(staleToolingFiles({ cwd: work }).sort(), ['modules/grading/grader-new.js', 'scripts/gds/ship-land.js']);
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('preflight (real git): the branch\'s OWN edit to a ship-*.js is not stale tooling', () => {
|
|
564
|
+
withPreflightRepo(({ work, run }) => {
|
|
565
|
+
fs.mkdirSync(path.join(work, 'scripts', 'gds'), { recursive: true });
|
|
566
|
+
fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// v1\n');
|
|
567
|
+
run('git add -A && git commit -q -m tooling');
|
|
568
|
+
publishMain(run);
|
|
569
|
+
run('git checkout -q -b feature');
|
|
570
|
+
fs.writeFileSync(path.join(work, 'scripts', 'gds', 'ship-land.js'), '// the fix this task ships\n');
|
|
571
|
+
run('git add -A && git commit -q -m "fix the land"');
|
|
572
|
+
const { result } = capturePreflight({ task_id: 4245, head_sha: null }, {});
|
|
573
|
+
assert.equal(result.ok, true);
|
|
574
|
+
assert.deepEqual(staleToolingFiles({ cwd: work }), []);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
512
578
|
// ---- runner ----
|
|
513
579
|
|
|
514
580
|
let pass = 0;
|
|
@@ -118,6 +118,22 @@ t('a hosted origin says the core installs with no token, and never re-offers the
|
|
|
118
118
|
assert.doesNotMatch(card, /\bprivate\b/i, 'must not call the package private');
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
// task 1004288: the card above was corrected when the core went public, but the SKILL that
|
|
122
|
+
// routes the same fresh-clone wall kept teaching the old posture — "a PRIVATE package", "404s
|
|
123
|
+
// on any machine with no NPM_TOKEN", "the expected tokenless outcome". An agent that believes
|
|
124
|
+
// that reads a real 404 (a version never published, a mistyped name) as normal and moves on.
|
|
125
|
+
t('the builder-start skill agrees with the card: the core is public, and a 404 is not normal', () => {
|
|
126
|
+
const { readFileSync } = require('node:fs');
|
|
127
|
+
const path = require('node:path');
|
|
128
|
+
const skill = readFileSync(path.join(import.meta.dirname, '..', '.claude', 'skills', 'builder-start', 'SKILL.md'), 'utf8');
|
|
129
|
+
assert.doesNotMatch(skill, /PRIVATE package|private package/i, 'must not call @bongos/core a private package (ADR 0283)');
|
|
130
|
+
assert.doesNotMatch(skill, /expected tokenless outcome/i, 'must not present a tokenless 404 as expected');
|
|
131
|
+
assert.doesNotMatch(skill, /404s on any machine with no `?NPM_TOKEN/i, 'must not tell the agent a tokenless install 404s');
|
|
132
|
+
assert.match(skill, /PUBLIC npm package/, 'names the core as public');
|
|
133
|
+
assert.match(skill, /ADR 0283/, 'cites the decision that made it public');
|
|
134
|
+
assert.match(skill, /404 on `@bongos\/core` is therefore \*\*not\*\* a normal outcome/, 'says a 404 now means something is wrong');
|
|
135
|
+
});
|
|
136
|
+
|
|
121
137
|
t('isLocalOrigin: loopback is local, everything else is remote', () => {
|
|
122
138
|
for (const o of ['http://localhost:3000', 'http://app.localhost:3000', 'http://127.0.0.1:3000', 'http://[::1]:3000']) {
|
|
123
139
|
assert.equal(isLocalOrigin(o), true, `${o} should be local`);
|
package/tests/upgrade.mjs
CHANGED
|
@@ -1543,7 +1543,7 @@ t('persistPin: a refused origin push on a WIRED private instance is loud — nam
|
|
|
1543
1543
|
assert.equal(r.refused, 'read-only-deploy-key');
|
|
1544
1544
|
// The tokenless refusal is a plain sentence, not the bare status line.
|
|
1545
1545
|
assert.match(r.error, /read-only deploy key at \/srv\/bongos\/acme\/\.git\/cloudbongos-deploy-key/, 'names the key');
|
|
1546
|
-
assert.match(r.error, /no owner GitHub token
|
|
1546
|
+
assert.match(r.error, /no owner GitHub token was available/, 'and why nothing else was tried');
|
|
1547
1547
|
assert.notEqual(r.error, 'git push failed');
|
|
1548
1548
|
const text = warnings.join('\n');
|
|
1549
1549
|
assert.match(text, /PIN NOT PUSHED/, 'shouts');
|
|
@@ -1655,6 +1655,25 @@ t('resolvePinPushAuth: the owner token is spent only when one is stored AND the
|
|
|
1655
1655
|
assert.equal(await u.resolvePinPushAuth(dir, { log: hush, instance: inst, getGithubToken: async () => { throw new Error('secret-box off'); } }, at('git@github.com:acme/widgets.git')), null);
|
|
1656
1656
|
});
|
|
1657
1657
|
|
|
1658
|
+
t('resolvePinPushAuth: on a hosted project\u2019s own box a missing row SAYS WHY, instead of falling back silently (task 1004065)', async () => {
|
|
1659
|
+
// The project's own database has no provisioning table (ADR 0281), so the lookup
|
|
1660
|
+
// answers null there — and on a checkout wired with the read-only deploy key that is
|
|
1661
|
+
// not "the origin path", it is a push that cannot succeed.
|
|
1662
|
+
const dir = { instanceDir: '/srv/bongos/acme' };
|
|
1663
|
+
const wired = { config: { status: 0, stdout: 'ssh -F /dev/null -i /srv/bongos/acme/.git/cloudbongos-deploy-key -o IdentitiesOnly=yes\n' }, remote: { status: 0, stdout: 'git@github.com:acme/widgets.git\n' } };
|
|
1664
|
+
const said = [];
|
|
1665
|
+
const r = await u.resolvePinPushAuth(dir, { log: (m) => said.push(m), getInstanceBySlug: async () => null }, pinRun(wired));
|
|
1666
|
+
assert.equal(r, null, 'still the remote path — there is no token to use');
|
|
1667
|
+
const line = said.join('\n');
|
|
1668
|
+
assert.match(line, /no provisioning row is readable from this database/, 'names what is missing');
|
|
1669
|
+
assert.match(line, /read-only deploy key at \/srv\/bongos\/acme\/\.git\/cloudbongos-deploy-key/, 'and why the fallback cannot work');
|
|
1670
|
+
assert.match(line, /pushes the pin from the hub/, 'and where the push actually happens');
|
|
1671
|
+
// An UNWIRED checkout with no row (a laptop, a public instance) is the real origin path: quiet.
|
|
1672
|
+
const quiet = [];
|
|
1673
|
+
await u.resolvePinPushAuth(dir, { log: (m) => quiet.push(m), getInstanceBySlug: async () => null }, pinRun({ remote: wired.remote }));
|
|
1674
|
+
assert.deepEqual(quiet, [], 'no noise where the fallback is correct');
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1658
1677
|
t('runUpgrade --commit-pin: on a wired private instance the pin lands via the owner token, end to end', async () => {
|
|
1659
1678
|
const dir = scratchConsumer({ pinned: '1.14.1', installed: '1.14.1' });
|
|
1660
1679
|
const src = mkdtempSync(join(tmpdir(), 'otb-upgrade-src-'));
|