@bongos/core 1.21.4 → 1.21.6
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 +233 -108
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -1
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/adr/0362-the-governor-docket-is-collected-from-contributed-sources.md +34 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +37 -2
- package/docs/api-reference.md +4 -3
- package/docs/copy-inventory.md +541 -658
- package/docs/copy-registry.json +1205 -2340
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +47 -28
- package/docs/page-readings.json +1128 -1339
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/governor-docket.js +101 -0
- package/modules/autonomy/routes/autonomy.js +27 -0
- package/modules/autonomy/runner-health.js +22 -1
- package/modules/discord/board-broadcast.js +3 -1
- package/modules/discord/craft-broadcast.js +107 -0
- package/modules/discord/routes/discord.js +10 -0
- package/modules/discord/ship-broadcast.js +30 -2
- package/modules/economy/reward.js +3 -0
- package/modules/government/board.js +20 -5
- package/modules/government/docket-vocab.js +82 -0
- package/modules/government/docket.js +458 -0
- package/modules/government/routes/government.js +34 -0
- package/modules/hall-ui/public/city-draw.js +438 -0
- package/modules/hall-ui/public/city.css +110 -0
- package/modules/hall-ui/public/city.html +71 -0
- package/modules/hall-ui/public/city.js +177 -0
- package/modules/hall-ui/public/city.states.json +30 -0
- package/modules/hall-ui/public/gate.js +11 -4
- package/modules/hall-ui/public/genesis-home.js +9 -10
- package/modules/hall-ui/public/profile.css +4 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/hall-ui/public/settings-autobongos.js +19 -1
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +8 -0
- package/modules/hall-ui/public/settings.js +43 -3
- package/modules/ideas/governor-docket.js +93 -0
- package/modules/ideas/routes/blockers.js +5 -0
- package/modules/lifecycle/governor-docket.js +192 -0
- package/modules/lifecycle/routes/gate-approvals.js +78 -33
- package/modules/lifecycle/routes/lifecycle.js +3 -0
- package/modules/onboarding/routes/access-requests.js +68 -27
- package/modules/platform-identity/craft-rollup.js +72 -0
- package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
- package/modules/platform-identity/platform-identity.js +12 -6
- package/modules/platform-identity/routes/sso.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/core-upgrade.js +34 -7
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +2 -0
- package/modules/public-landing/public/assets/cosmos.css +6 -50
- package/modules/public-landing/public/projects.html +1170 -1659
- package/modules/public-landing/public/projects.probes.json +18 -18
- package/modules/public-landing/public/projects.states.json +24 -31
- package/modules/security/routes/reports.js +39 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +72 -0
- package/scripts/gds/autobongos-grade-cap.js +187 -0
- package/scripts/gds/autobongos-loop.js +4 -0
- package/scripts/gds/autobongos-run.js +154 -13
- package/scripts/gds/autobongos-verify.js +211 -14
- package/scripts/gds/provision-core-upgrade.js +14 -2
- package/scripts/gds/provision-repo.js +77 -13
- package/scripts/gds/ship-flow.js +5 -0
- package/scripts/gds/ship-preflight-steps.js +2 -1
- package/scripts/gds/ship.js +10 -0
- package/scripts/gds/update-sweep.js +24 -8
- package/scripts/gds/upgrade-outcome.js +1 -0
- package/src/bongos/auth-admission.js +95 -13
- package/src/bongos/db-kernel.js +1 -1
- package/src/bongos/module-rechecks.js +77 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/core-update.js +68 -2
- package/src/bongos/routes/modules.js +17 -0
- package/src/bongos/serve-internal.js +14 -0
- package/src/bongos/software-update.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_order_db.mjs +3 -1
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/autobongos_grade_cap.mjs +125 -0
- package/tests/autobongos_loop.mjs +230 -1
- package/tests/autobongos_verify.mjs +239 -3
- package/tests/autonomy_config_idle.mjs +272 -0
- package/tests/core_update_banner.mjs +29 -0
- package/tests/core_upgrade_door.mjs +31 -1
- package/tests/core_upgrade_runner.mjs +20 -0
- package/tests/discord_craft_broadcast.mjs +162 -0
- package/tests/genesis_home.mjs +1 -1
- package/tests/government_routes.mjs +3 -1
- package/tests/governor_docket.mjs +552 -0
- package/tests/hall_audit.mjs +10 -1
- package/tests/hall_city.mjs +369 -0
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hub_craft_rollup.mjs +350 -0
- package/tests/onboard.mjs +10 -4
- package/tests/platform_boot.mjs +1 -1
- package/tests/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/projects_hub.mjs +196 -89
- package/tests/projects_hub_app_status.mjs +3 -1
- package/tests/projects_hub_app_step.mjs +72 -48
- package/tests/projects_hub_dns_ready.mjs +10 -2
- package/tests/projects_hub_look.mjs +32 -22
- package/tests/projects_hub_module_picker.mjs +112 -62
- package/tests/projects_hub_pre_uat.mjs +48 -41
- package/tests/provision.mjs +78 -0
- package/tests/public_landing_projects.mjs +12 -6
- package/tests/settings_main_role.mjs +190 -0
- package/tests/software_update.mjs +56 -1
- package/tests/update_subscription_engine.mjs +55 -3
- package/tests/wizard_demo.mjs +37 -11
- package/tests/wizard_draft_resume.mjs +34 -22
- package/tests/wizard_front_door.mjs +173 -103
- package/tests/wizard_intent_resume.mjs +28 -25
- package/tests/wizard_physics.mjs +94 -76
- package/tests/wizard_physics_more.mjs +428 -277
- package/tests/wizard_six_screens.mjs +126 -0
|
@@ -619,9 +619,22 @@ test('a mid-ship strand is named in the run log and blocked, but not released',
|
|
|
619
619
|
}
|
|
620
620
|
});
|
|
621
621
|
|
|
622
|
-
test('a ship with
|
|
622
|
+
test('a fresh ship with nothing on main yet is pending_land: unverified, no blocker (task 1004538)', async () => {
|
|
623
|
+
// The harness ledger shipped just now — well inside the land grace window.
|
|
623
624
|
const h = harness({ probeArtifact: async () => ({ checked: true, onMain: false, sha: null, subject: null }) });
|
|
624
625
|
const row = await runner.iteration(opts, h.deps);
|
|
626
|
+
assert.equal(row.ledger_shape, 'pending_land', 'the run log row is what the next loop re-probes from');
|
|
627
|
+
assert.equal(row.verified, false);
|
|
628
|
+
assert.deepEqual(h.blockers, [], 'a slow land is not an owner problem yet');
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test('a ship with no artifact on main is recorded as unverified', async () => {
|
|
632
|
+
const h = harness({
|
|
633
|
+
probeArtifact: async () => ({ checked: true, onMain: false, sha: null, subject: null }),
|
|
634
|
+
// Shipped two days ago: past the land grace window.
|
|
635
|
+
ledger: { id: 4242, status: 'shipped', shipped_at: new Date(Date.now() - 48 * 3600_000).toISOString() },
|
|
636
|
+
});
|
|
637
|
+
const row = await runner.iteration(opts, h.deps);
|
|
625
638
|
assert.equal(row.ledger_status, 'shipped');
|
|
626
639
|
assert.equal(row.ledger_shape, 'shipped_no_artifact');
|
|
627
640
|
assert.equal(row.verified, false);
|
|
@@ -1379,3 +1392,219 @@ test('the server heartbeat carries the goal the current task came from', async (
|
|
|
1379
1392
|
assert.ok(b, 'the beat after a worked row must name its task');
|
|
1380
1393
|
assert.equal(b.working_goal_id, 1000090);
|
|
1381
1394
|
});
|
|
1395
|
+
|
|
1396
|
+
// ── the grade cap (task 1004536) ────────────────────────────────────────────────
|
|
1397
|
+
// Owner rule: a task that fails the grade twice goes to the owner. The prompt
|
|
1398
|
+
// asked for it and was ignored (task 1004512: $63.81 over three rounds), so the
|
|
1399
|
+
// supervisor reads the run's grade ledger itself.
|
|
1400
|
+
const gradeCapLib = require('../scripts/gds/autobongos-grade-cap.js');
|
|
1401
|
+
const fakeGrade = (passed, outage = false) => ({ passed, score: passed ? 8 : 5, signals: { panel_outcome: outage ? 'unavailable' : (passed ? 'pass' : 'fail') } });
|
|
1402
|
+
function ledgerHarness(grades, { ledgerStatus = 'completed' } = {}) {
|
|
1403
|
+
const file = join(mkdtempSync(join(tmpdir(), 'abgcap-')), 'run.jsonl');
|
|
1404
|
+
let handed = null;
|
|
1405
|
+
const h = harness({
|
|
1406
|
+
ledger: { id: 4242, status: ledgerStatus, updated_at: new Date().toISOString() },
|
|
1407
|
+
spawnWorker: async (a) => {
|
|
1408
|
+
handed = a.gradeLedger;
|
|
1409
|
+
// What ship.js does inside the worker: one row per graded round.
|
|
1410
|
+
for (const g of grades) gradeCapLib.recordGradeOutcome({ taskId: 4242, grade: g, env: { [gradeCapLib.LEDGER_ENV]: a.gradeLedger } });
|
|
1411
|
+
// ...and the worker IGNORING its brief, claiming a ship anyway.
|
|
1412
|
+
return { envelope: { is_error: false, result: '', structured_output: {
|
|
1413
|
+
outcome: 'shipped', task_id: 4242, what_happened: 'fixed and re-graded', undetermined_decisions: [],
|
|
1414
|
+
} }, exitCode: 0 };
|
|
1415
|
+
},
|
|
1416
|
+
});
|
|
1417
|
+
h.deps.gradeLedgerPath = () => file;
|
|
1418
|
+
return { h, file, handed: () => handed };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
test('the brief states the grade cap: one re-grade, then cannot_complete; an outage does not count', () => {
|
|
1422
|
+
const p = buildWorkerPrompt({ task: { id: 7, title: 't', description: 'd' } });
|
|
1423
|
+
assert.match(p, /at most ONE re-grade/);
|
|
1424
|
+
assert.match(p, /fails twice, STOP/);
|
|
1425
|
+
assert.match(p, /GRADER UNAVAILABLE[^.]*does not count/);
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
test('two graded FAILs in one run end it as needs_human, whatever the worker claims', async () => {
|
|
1429
|
+
const { h, file, handed } = ledgerHarness([fakeGrade(false), fakeGrade(false)]);
|
|
1430
|
+
const row = await runner.iteration(opts, h.deps);
|
|
1431
|
+
assert.equal(handed(), file, 'the supervisor picks the ledger and hands it to the worker');
|
|
1432
|
+
assert.equal(row.outcome, 'needs_human', 'the worker said "shipped"; the ledger says the grade failed twice');
|
|
1433
|
+
assert.match(row.reason, /failed 2 times/);
|
|
1434
|
+
assert.equal(row.grade_rounds, 2);
|
|
1435
|
+
assert.equal(row.grade_cap_reached, true);
|
|
1436
|
+
assert.equal(h.blockers.length, 1, 'the owner gets a blocker — the existing needs_human path');
|
|
1437
|
+
assert.throws(() => readFileSync(file), /ENOENT/, 'the per-run ledger is removed once read; its counts are in the row');
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
test('a grader outage between two rounds does not end the run', async () => {
|
|
1441
|
+
const { h } = ledgerHarness([fakeGrade(false), fakeGrade(false, true), fakeGrade(true)], { ledgerStatus: 'shipped' });
|
|
1442
|
+
const row = await runner.iteration(opts, h.deps);
|
|
1443
|
+
assert.equal(row.outcome, 'claims_shipped');
|
|
1444
|
+
assert.equal(row.grade_rounds, 2);
|
|
1445
|
+
assert.equal(row.grade_outages, 1);
|
|
1446
|
+
assert.equal(row.grade_cap_reached, false);
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// The grader's hacker finding on the first cut: the local ledger lives in the
|
|
1450
|
+
// worker's env and filesystem, so a worker can clear it. The supervisor's count
|
|
1451
|
+
// comes from the server's grade_attempts, which the worker cannot write.
|
|
1452
|
+
const attempt = (at, passed, { outage = false } = {}) => ({
|
|
1453
|
+
attempted_at: at, passed,
|
|
1454
|
+
workers: ['narc', 'quality', 'hacker', 'efficiency'].map((kind) => ({
|
|
1455
|
+
kind, verdict: outage ? null : (passed ? 'pass' : 'fail'), error: outage && kind !== 'narc' ? 'SUBAGENT_SPAWN_FAILED' : null, severities: [],
|
|
1456
|
+
})),
|
|
1457
|
+
});
|
|
1458
|
+
function serverApi(before, during) {
|
|
1459
|
+
let calls = 0;
|
|
1460
|
+
return { tasks: { getTasksId: async ({ query }) => {
|
|
1461
|
+
assert.match(query.include, /grade_attempts/);
|
|
1462
|
+
const attempts = calls++ === 0 ? before : during;
|
|
1463
|
+
return { ok: true, status: 200, data: { task: { id: 4242 }, grade: { attempts } } };
|
|
1464
|
+
} } };
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
test('a worker that clears the local ledger is still capped by the server count', async () => {
|
|
1468
|
+
const old = attempt('2026-10-01T09:00:00.000Z', false); // a PREVIOUS run's round — not this run's
|
|
1469
|
+
const h = harness({
|
|
1470
|
+
ledger: { id: 4242, status: 'completed', updated_at: new Date().toISOString() },
|
|
1471
|
+
api: serverApi([old], [old, attempt('2026-10-02T10:00:00.000Z', false), attempt('2026-10-02T10:20:00.000Z', false)]),
|
|
1472
|
+
// Records NOTHING locally — as if it ran ship.js with the ledger env cleared.
|
|
1473
|
+
spawnWorker: async () => ({ envelope: { is_error: false, result: '', structured_output: {
|
|
1474
|
+
outcome: 'shipped', task_id: 4242, what_happened: 'kept going', undetermined_decisions: [],
|
|
1475
|
+
} }, exitCode: 0 }),
|
|
1476
|
+
});
|
|
1477
|
+
h.deps.gradeLedgerPath = () => join(mkdtempSync(join(tmpdir(), 'abgcap-')), 'run.jsonl');
|
|
1478
|
+
const row = await runner.iteration(opts, h.deps);
|
|
1479
|
+
assert.equal(row.outcome, 'needs_human');
|
|
1480
|
+
assert.equal(row.grade_count_source, 'server');
|
|
1481
|
+
assert.equal(row.grade_rounds, 2, 'the round from before this run started is not counted');
|
|
1482
|
+
assert.equal(row.grade_cap_reached, true);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
test('server rounds where every gating worker errored are outages, not fails', async () => {
|
|
1486
|
+
const h = harness({
|
|
1487
|
+
ledger: { id: 4242, status: 'completed', updated_at: new Date().toISOString() },
|
|
1488
|
+
api: serverApi([], [attempt('2026-10-02T10:00:00.000Z', false), attempt('2026-10-02T10:05:00.000Z', false, { outage: true })]),
|
|
1489
|
+
});
|
|
1490
|
+
h.deps.gradeLedgerPath = () => join(mkdtempSync(join(tmpdir(), 'abgcap-')), 'run.jsonl');
|
|
1491
|
+
const row = await runner.iteration(opts, h.deps);
|
|
1492
|
+
assert.notEqual(row.outcome, 'needs_human');
|
|
1493
|
+
assert.equal(row.grade_rounds, 1);
|
|
1494
|
+
assert.equal(row.grade_outages, 1);
|
|
1495
|
+
assert.equal(row.grade_cap_reached, false);
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
test('spawnWorker hands the ledger to the worker in env and stops it once the cap is reached', async () => {
|
|
1499
|
+
const { EventEmitter } = await import('node:events');
|
|
1500
|
+
const file = join(mkdtempSync(join(tmpdir(), 'abgcap-')), 'run.jsonl');
|
|
1501
|
+
let env = null; let killed = false;
|
|
1502
|
+
const spawn = (_bin, _args, o) => {
|
|
1503
|
+
env = o.env;
|
|
1504
|
+
const child = new EventEmitter();
|
|
1505
|
+
child.stdout = new EventEmitter(); child.stderr = new EventEmitter();
|
|
1506
|
+
child.stdin = { end: () => {
|
|
1507
|
+
// The worker's ship.js records its second graded FAIL, then the worker keeps going.
|
|
1508
|
+
for (const g of [fakeGrade(false), fakeGrade(false)]) gradeCapLib.recordGradeOutcome({ taskId: 4242, grade: g, env: o.env });
|
|
1509
|
+
} };
|
|
1510
|
+
child.kill = () => { killed = true; setImmediate(() => child.emit('close', null)); };
|
|
1511
|
+
return child;
|
|
1512
|
+
};
|
|
1513
|
+
const capCheck = async () => gradeCapLib.gradeCapState(gradeCapLib.readLedger(file), 4242);
|
|
1514
|
+
const res = await runner.spawnWorker({
|
|
1515
|
+
task: aTask, model: 'm', timeoutMs: 60_000, cwd: process.cwd(), gradeLedger: file, capCheck,
|
|
1516
|
+
deps: { spawn, resolveBin: () => ({ bin: 'claude', shell: false }), sessionId: 's', capWatchMs: 5 },
|
|
1517
|
+
});
|
|
1518
|
+
assert.equal(env[gradeCapLib.LEDGER_ENV], file);
|
|
1519
|
+
assert.equal(killed, true, 'a worker past the cap is stopped, not left to keep paying for rounds');
|
|
1520
|
+
assert.equal(res.gradeCapped, true);
|
|
1521
|
+
assert.equal(res.timedOut, false);
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
// ── slow lands are re-checked every loop, across restarts (task 1004538) ──────
|
|
1525
|
+
|
|
1526
|
+
test('forever re-checks pending lands once per loop, and a failing check never stops it', async () => {
|
|
1527
|
+
let calls = 0;
|
|
1528
|
+
const rows = [];
|
|
1529
|
+
const code = await runner.forever({ goals: [], maxTasks: 1, maxLoops: 3 }, {
|
|
1530
|
+
iteration: async () => ({ event: 'hold', reason: 'test' }),
|
|
1531
|
+
reconcileLeftoverClaims: async () => ({ event: 'boot_reconcile', ok: true }),
|
|
1532
|
+
waitOrJump: async () => ({ waited: 0, jumped: false }),
|
|
1533
|
+
writeHeartbeat: () => true,
|
|
1534
|
+
postHeartbeat: async () => true,
|
|
1535
|
+
emit: () => {},
|
|
1536
|
+
record: (r) => { rows.push(r); return r; },
|
|
1537
|
+
codeDrifted: async () => false,
|
|
1538
|
+
loadedHead: async () => null,
|
|
1539
|
+
recheckLands: async () => { calls += 1; if (calls === 2) throw new Error('fetch failed'); return {}; },
|
|
1540
|
+
});
|
|
1541
|
+
assert.equal(code, 0);
|
|
1542
|
+
assert.equal(calls, 3, 'every loop, not just at boot');
|
|
1543
|
+
assert.ok(rows.some((r) => r.event === 'land_recheck_failed' && /fetch failed/.test(r.errors.join(' '))));
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
test('main() arms the land re-check for --forever', () => {
|
|
1547
|
+
const src = readFileSync(new URL('../scripts/gds/autobongos-run.js', import.meta.url), 'utf8');
|
|
1548
|
+
const line = src.split('\n').find((l) => l.includes('if (opts.forever) return forever('));
|
|
1549
|
+
assert.match(line, /recheckLands: true/, 'without this the runner never re-probes a slow land');
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
test('a pending land survives a restart: the run log names it, the ledger clocks it, the blocker closes', async () => {
|
|
1553
|
+
// A FRESH process: nothing in memory. Only the run log on disk says task 77
|
|
1554
|
+
// was pending, and only the database says blocker 500 is open for task 78.
|
|
1555
|
+
const dir = mkdtempSync(join(tmpdir(), 'autobongos-lands-'));
|
|
1556
|
+
process.env.AUTOBONGOS_LOG_FILE = join(dir, 'runs.jsonl');
|
|
1557
|
+
try {
|
|
1558
|
+
writeFileSync(process.env.AUTOBONGOS_LOG_FILE, [
|
|
1559
|
+
JSON.stringify({ event: 'worked', task_id: '77', ledger_shape: 'pending_land' }),
|
|
1560
|
+
JSON.stringify({ event: 'worked', task_id: '76', ledger_shape: 'shipped' }),
|
|
1561
|
+
'{"event":"worked","task_id":"75","ledger_sh', // the half line a kill -9 leaves
|
|
1562
|
+
].join('\n'));
|
|
1563
|
+
const recorded = [];
|
|
1564
|
+
const resolved = [];
|
|
1565
|
+
const probed = [];
|
|
1566
|
+
const api = {
|
|
1567
|
+
tasks: { getTasksId: async ({ id }) => ({ ok: true, data: { task: { id: String(id), status: 'shipped', shipped_at: new Date(Date.now() - 3600_000).toISOString() } } }) },
|
|
1568
|
+
blockers: {
|
|
1569
|
+
getBlockers: async () => ({ ok: true, data: { blockers: [{ id: '500', source: 'autobongos', source_ref: 'task-78-shipped_no_artifact', status: 'open' }] } }),
|
|
1570
|
+
postBlockersIdResolve: async ({ id }) => { resolved.push(String(id)); return { ok: true, data: {} }; },
|
|
1571
|
+
},
|
|
1572
|
+
};
|
|
1573
|
+
await runner.recheckPendingLands({
|
|
1574
|
+
api,
|
|
1575
|
+
state: runner.newRunnerState(),
|
|
1576
|
+
record: (r) => { recorded.push(r); return r; },
|
|
1577
|
+
landDeps: { probeArtifact: async (id) => { probed.push(String(id)); return { checked: true, onMain: true, sha: 'abc123def456', subject: `Merge from x/task-${id}` }; } },
|
|
1578
|
+
});
|
|
1579
|
+
assert.deepEqual(probed.sort(), ['77', '78'], 'only the pending task and the open blocker are probed');
|
|
1580
|
+
const re = recorded.find((r) => r.event === 'land_recheck');
|
|
1581
|
+
assert.equal(re.task_id, '77');
|
|
1582
|
+
assert.equal(re.ledger_shape, 'shipped', 'the land arrived, so the next pass drops it from the pending list');
|
|
1583
|
+
assert.deepEqual(resolved, ['500']);
|
|
1584
|
+
assert.ok(recorded.some((r) => r.event === 'land_blocker_resolved' && r.blocker_id === '500'));
|
|
1585
|
+
} finally { delete process.env.AUTOBONGOS_LOG_FILE; }
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
test('the blocker sweep runs at most hourly; with nothing pending, the loops between cost nothing', async () => {
|
|
1589
|
+
const dir = mkdtempSync(join(tmpdir(), 'autobongos-sweep-'));
|
|
1590
|
+
process.env.AUTOBONGOS_LOG_FILE = join(dir, 'runs.jsonl'); // empty: nothing pending
|
|
1591
|
+
try {
|
|
1592
|
+
let lists = 0;
|
|
1593
|
+
let now = 1_000_000_000_000;
|
|
1594
|
+
const deps = {
|
|
1595
|
+
state: runner.newRunnerState(),
|
|
1596
|
+
now: () => now,
|
|
1597
|
+
record: (r) => r,
|
|
1598
|
+
api: { tasks: {}, blockers: { getBlockers: async () => { lists += 1; return { ok: true, data: { blockers: [] } }; } } },
|
|
1599
|
+
landDeps: { probeArtifact: async () => notOnMainRow },
|
|
1600
|
+
};
|
|
1601
|
+
const notOnMainRow = { checked: true, onMain: false, sha: null, subject: null };
|
|
1602
|
+
await runner.recheckPendingLands(deps);
|
|
1603
|
+
now += 5 * 60 * 1000;
|
|
1604
|
+
await runner.recheckPendingLands(deps);
|
|
1605
|
+
assert.equal(lists, 1, 'a second sweep five minutes later is wasted I/O');
|
|
1606
|
+
now += runner.LAND_SWEEP_MS;
|
|
1607
|
+
await runner.recheckPendingLands(deps);
|
|
1608
|
+
assert.equal(lists, 2, 'an hour on, it sweeps again');
|
|
1609
|
+
} finally { delete process.env.AUTOBONGOS_LOG_FILE; }
|
|
1610
|
+
});
|
|
@@ -102,7 +102,9 @@ test('an unparseable updated_at still gets a shape — quiet is the wrong failur
|
|
|
102
102
|
// ── the incident this exists for ───────────────────────────────────────────────
|
|
103
103
|
|
|
104
104
|
test('shipped with nothing on main is called out, not celebrated', () => {
|
|
105
|
-
|
|
105
|
+
// Past the land grace window (task 1004538) — inside it the same facts are
|
|
106
|
+
// `pending_land`, covered below.
|
|
107
|
+
const v = classifyLedger({ task: task({ shipped_at: '2026-09-16T12:00:00.000Z' }), artifact: notOnMain, nowMs: NOW });
|
|
106
108
|
assert.equal(v.shape, 'shipped_no_artifact');
|
|
107
109
|
assert.equal(v.verified, false);
|
|
108
110
|
assert.equal(v.fileBlocker, true);
|
|
@@ -199,16 +201,44 @@ test('the artifact probe reads main, fixed-string, for the ship-commit conventio
|
|
|
199
201
|
const git = async (args) => {
|
|
200
202
|
calls.push(args);
|
|
201
203
|
if (args[0] === 'fetch') return { ok: true, stdout: '', stderr: '' };
|
|
202
|
-
return { ok: true, stdout: 'deadbeefcafe1234
|
|
204
|
+
return { ok: true, stdout: 'deadbeefcafe1234\x1ffeat: the thing (task 42)\x1f\x1e\n', stderr: '' };
|
|
203
205
|
};
|
|
204
206
|
const r = await probeArtifact(42, { git });
|
|
205
207
|
assert.deepEqual(r, { checked: true, onMain: true, sha: 'deadbeefcafe1234', subject: 'feat: the thing (task 42)' });
|
|
206
208
|
assert.deepEqual(calls[0], ['fetch', '-q', 'origin', 'main']);
|
|
207
209
|
assert.ok(calls[1].includes('-F'), 'the pattern is a fixed string, not a regex');
|
|
208
|
-
assert.ok(calls[1].includes('--grep=
|
|
210
|
+
assert.ok(calls[1].includes('--grep=42'), 'git narrows by the bare id; the exact match is made in JS');
|
|
209
211
|
assert.ok(calls[1].includes('FETCH_HEAD'), 'read what was just fetched, not a stale local ref');
|
|
210
212
|
});
|
|
211
213
|
|
|
214
|
+
// task 1004538 — the commits PR 1456 landed for task 1004513 never wrote the
|
|
215
|
+
// literal `(task 1004513)`: the merge subject names the BRANCH
|
|
216
|
+
// (`…/autobongos-1004513-978953`) and a body line says "task 1004513 makes…".
|
|
217
|
+
// The old `(task N)`-only grep therefore said "no artifact" even AFTER the land,
|
|
218
|
+
// so no grace window alone could ever have cleared that blocker.
|
|
219
|
+
test('the probe recognises a ship branch in a merge subject and a body mention', async () => {
|
|
220
|
+
const log = (rows) => async (args) => (args[0] === 'fetch'
|
|
221
|
+
? { ok: true, stdout: '', stderr: '' }
|
|
222
|
+
: { ok: true, stdout: rows.map(([h, s, b]) => `${h}\x1f${s}\x1f${b || ''}\x1e\n`).join(''), stderr: '' });
|
|
223
|
+
const merge = await probeArtifact(1004513, { git: log([['3d50a6289', 'Merge pull request #1456 from octo/autobongos-1004513-978953']]) });
|
|
224
|
+
assert.equal(merge.onMain, true);
|
|
225
|
+
assert.equal(merge.sha, '3d50a6289');
|
|
226
|
+
const manual = await probeArtifact(1004440, { git: log([['a55b7707e', 'Merge pull request #1471 from octo/task-1004440']]) });
|
|
227
|
+
assert.equal(manual.onMain, true);
|
|
228
|
+
const body = await probeArtifact(1004513, { git: log([['301df048f', 'docs(lifecycle): a note', 'Fixes the goal close.\n\nRefs (task 1004513).']]) });
|
|
229
|
+
assert.equal(body.onMain, true, 'the `(task N)` convention still counts in a body');
|
|
230
|
+
// A body that merely DISCUSSES the task is another task's commit talking about
|
|
231
|
+
// it — not an artifact. (This task's own commits mention task 1004513.)
|
|
232
|
+
const talk = await probeArtifact(1004513, { git: log([['dddd', 'fix(verify): slow lands (task 1004538)', 'task 1004513 was flagged too early']]) });
|
|
233
|
+
assert.equal(talk.onMain, false);
|
|
234
|
+
// A longer id that merely STARTS with this one is not this task.
|
|
235
|
+
const prefix = await probeArtifact(1004513, { git: log([['aaaa', 'fix: thing (task 10045139)'], ['bbbb', 'Merge from x/task-10045130']]) });
|
|
236
|
+
assert.equal(prefix.onMain, false);
|
|
237
|
+
// Nor is a bare number with no `task` before it (a PR number, a row count).
|
|
238
|
+
const bare = await probeArtifact(1004513, { git: log([['cccc', 'chore: bump 1004513 rows']]) });
|
|
239
|
+
assert.equal(bare.onMain, false);
|
|
240
|
+
});
|
|
241
|
+
|
|
212
242
|
test('a failed fetch reports `checked: false`, never `onMain: false`', async () => {
|
|
213
243
|
const git = async (args) => (args[0] === 'fetch'
|
|
214
244
|
? { ok: false, stdout: '', stderr: 'fatal: could not read from remote' }
|
|
@@ -345,3 +375,209 @@ test('a thrown client error is a read failure, not a crash', async () => {
|
|
|
345
375
|
assert.equal(out.halt, true);
|
|
346
376
|
assert.match(out.read_error, /fetch failed/);
|
|
347
377
|
});
|
|
378
|
+
|
|
379
|
+
// ── a slow land is not a missing artifact (task 1004538) ───────────────────────
|
|
380
|
+
//
|
|
381
|
+
// Task 1004513 was flagged `shipped_no_artifact` at 22:24Z and its merge landed
|
|
382
|
+
// ~11 hours later; the owner got a false alarm (blocker 1000185). A shipped task
|
|
383
|
+
// with nothing on main is first `pending_land`. The clock is the LEDGER's
|
|
384
|
+
// `shipped_at`, so a runner restart cannot reset it.
|
|
385
|
+
|
|
386
|
+
const { LAND_GRACE_MS, recheckLands, pendingLandIds } = verify;
|
|
387
|
+
const shippedAgo = (ms) => task({ shipped_at: new Date(NOW - ms).toISOString(), updated_at: new Date(NOW - ms).toISOString() });
|
|
388
|
+
|
|
389
|
+
test('(1) inside the grace window, a missing artifact is pending_land and files NO blocker', async () => {
|
|
390
|
+
assert.ok(LAND_GRACE_MS >= 12 * 3600_000, 'the land pipeline can take hours on a red main — 1004513 took ~11');
|
|
391
|
+
const v = classifyLedger({ task: shippedAgo(11 * 3600_000), artifact: notOnMain, nowMs: NOW });
|
|
392
|
+
assert.equal(v.shape, 'pending_land');
|
|
393
|
+
assert.equal(v.verified, false, 'not yet a fact');
|
|
394
|
+
assert.equal(v.fileBlocker, false);
|
|
395
|
+
assert.equal(v.releaseClaim, false);
|
|
396
|
+
assert.equal(v.halt, false);
|
|
397
|
+
|
|
398
|
+
let filed = 0;
|
|
399
|
+
const out = await verifyShip({ taskId: 1003903 }, {
|
|
400
|
+
api: fakeApi(), task: shippedAgo(60_000), nowMs: NOW,
|
|
401
|
+
probeArtifact: async () => notOnMain,
|
|
402
|
+
fileBlocker: async () => { filed += 1; return { filed: true, blockerId: 9 }; },
|
|
403
|
+
});
|
|
404
|
+
assert.equal(out.shape, 'pending_land');
|
|
405
|
+
assert.equal(filed, 0, 'no owner blocker for a land that is merely slow');
|
|
406
|
+
assert.equal(out.blocker, null);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test('(2) past the grace window, a still-missing artifact files the blocker', async () => {
|
|
410
|
+
const v = classifyLedger({ task: shippedAgo(LAND_GRACE_MS + 60_000), artifact: notOnMain, nowMs: NOW });
|
|
411
|
+
assert.equal(v.shape, 'shipped_no_artifact');
|
|
412
|
+
assert.equal(v.fileBlocker, true);
|
|
413
|
+
|
|
414
|
+
const rows = [];
|
|
415
|
+
const out = await verifyShip({ taskId: 1003903 }, {
|
|
416
|
+
api: fakeApi(), task: shippedAgo(LAND_GRACE_MS + 60_000), nowMs: NOW,
|
|
417
|
+
probeArtifact: async () => notOnMain,
|
|
418
|
+
fileBlocker: async (_a, row) => { rows.push(row); return { filed: true, blockerId: 9 }; },
|
|
419
|
+
});
|
|
420
|
+
assert.equal(out.shape, 'shipped_no_artifact');
|
|
421
|
+
assert.equal(rows.length, 1);
|
|
422
|
+
assert.equal(rows[0].source_ref, 'task-1003903-shipped_no_artifact', 'the dedupe key the resolve sweep matches on');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('the grace clock is shipped_at, and an unreadable clock does not hide a missing artifact', () => {
|
|
426
|
+
// updated_at moves on every touch; shipped_at is when the ship happened.
|
|
427
|
+
const t = task({ shipped_at: new Date(NOW - LAND_GRACE_MS - 1000).toISOString(), updated_at: new Date(NOW - 1000).toISOString() });
|
|
428
|
+
assert.equal(classifyLedger({ task: t, artifact: notOnMain, nowMs: NOW }).shape, 'shipped_no_artifact');
|
|
429
|
+
const undated = task({ shipped_at: null, updated_at: 'not a date' });
|
|
430
|
+
assert.equal(classifyLedger({ task: undated, artifact: notOnMain, nowMs: NOW }).shape, 'shipped_no_artifact',
|
|
431
|
+
'with no clock the grace cannot be measured, so this stays as strict as it was');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('a pending land that later appears on main verifies as shipped', () => {
|
|
435
|
+
assert.equal(classifyLedger({ task: shippedAgo(3600_000), artifact: onMain, nowMs: NOW }).shape, 'shipped');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ── re-probing across iterations, and across a restart ─────────────────────────
|
|
439
|
+
|
|
440
|
+
test('pendingLandIds reads the run log: the LATEST verdict per task decides', () => {
|
|
441
|
+
const rows = [
|
|
442
|
+
{ event: 'worked', task_id: '1', ledger_shape: 'pending_land' },
|
|
443
|
+
{ event: 'worked', task_id: '2', ledger_shape: 'pending_land' },
|
|
444
|
+
{ event: 'land_recheck', task_id: '2', ledger_shape: 'shipped' },
|
|
445
|
+
{ event: 'worked', task_id: '3', ledger_shape: 'shipped' },
|
|
446
|
+
{ event: 'land_recheck', task_id: '4', ledger_shape: 'pending_land' },
|
|
447
|
+
{ event: 'cadence', mode: 'full' },
|
|
448
|
+
{ event: 'worked', task_id: '5', ledger_shape: 'pending_land' },
|
|
449
|
+
{ event: 'land_recheck', task_id: '5', ledger_shape: 'shipped_no_artifact' },
|
|
450
|
+
null, 'garbage',
|
|
451
|
+
];
|
|
452
|
+
assert.deepEqual(pendingLandIds(rows).sort(), ['1', '4']);
|
|
453
|
+
assert.deepEqual(pendingLandIds([]), []);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const blockerApi = (open, resolved) => ({
|
|
457
|
+
tasks: { getTasksId: async ({ id }) => ({ ok: true, data: { task: { ...shippedAgo(3600_000), id: String(id) } } }) },
|
|
458
|
+
blockers: {
|
|
459
|
+
getBlockers: async () => ({ ok: true, data: { blockers: open } }),
|
|
460
|
+
postBlockersIdResolve: async ({ id, body }) => { resolved.push({ id: String(id), note: body.resolution_note }); return { ok: true, data: {} }; },
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test('(3) an already-filed no-artifact blocker is resolved once main carries the commit', async () => {
|
|
465
|
+
const resolved = [];
|
|
466
|
+
const open = [
|
|
467
|
+
{ id: '1000185', source: 'autobongos', source_ref: 'task-1004513-shipped_no_artifact', status: 'open' },
|
|
468
|
+
{ id: '1000186', source: 'autobongos', source_ref: 'task-1004514-shipped_no_artifact', status: 'open' },
|
|
469
|
+
{ id: '1000187', source: 'autobongos', source_ref: 'task-1004515-still_held', status: 'open' },
|
|
470
|
+
{ id: '1000188', source: 'manual', source_ref: 'task-1004516-shipped_no_artifact', status: 'open' },
|
|
471
|
+
];
|
|
472
|
+
const probed = [];
|
|
473
|
+
const out = await recheckLands({ pendingIds: [] }, {
|
|
474
|
+
api: blockerApi(open, resolved), nowMs: NOW,
|
|
475
|
+
probeArtifact: async (id) => {
|
|
476
|
+
probed.push(String(id));
|
|
477
|
+
return String(id) === '1004513'
|
|
478
|
+
? { checked: true, onMain: true, sha: '3d50a6289abc', subject: 'Merge pull request #1456 from octo/autobongos-1004513-978953' }
|
|
479
|
+
: notOnMain;
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
assert.deepEqual(resolved.map((r) => r.id), ['1000185'], 'only the autobongos no-artifact blocker whose commit is now on main');
|
|
483
|
+
assert.match(resolved[0].note, /3d50a628/);
|
|
484
|
+
assert.match(resolved[0].note, /task 1004513/);
|
|
485
|
+
assert.deepEqual(probed.sort(), ['1004513', '1004514'], 'other shapes and other sources are not this sweep’s business');
|
|
486
|
+
assert.deepEqual(out.resolved.map((r) => r.blocker_id), ['1000185']);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test('a probe that could not run never resolves a blocker', async () => {
|
|
490
|
+
const resolved = [];
|
|
491
|
+
await recheckLands({ pendingIds: [] }, {
|
|
492
|
+
api: blockerApi([{ id: '7', source: 'autobongos', source_ref: 'task-42-shipped_no_artifact' }], resolved), nowMs: NOW,
|
|
493
|
+
probeArtifact: async () => ({ checked: false, reason: 'no network' }),
|
|
494
|
+
});
|
|
495
|
+
assert.deepEqual(resolved, []);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test('recheckLands re-verifies each pending task: quiet inside the window, a blocker after it', async () => {
|
|
499
|
+
const filed = [];
|
|
500
|
+
const deps = (ageMs) => ({
|
|
501
|
+
api: {
|
|
502
|
+
tasks: { getTasksId: async ({ id }) => ({ ok: true, data: { task: { ...shippedAgo(ageMs), id: String(id) } } }) },
|
|
503
|
+
blockers: { getBlockers: async () => ({ ok: true, data: { blockers: [] } }) },
|
|
504
|
+
},
|
|
505
|
+
nowMs: NOW, probeArtifact: async () => notOnMain,
|
|
506
|
+
fileBlocker: async (_a, row) => { filed.push(row.source_ref); return { filed: true, blockerId: 11 }; },
|
|
507
|
+
});
|
|
508
|
+
const early = await recheckLands({ pendingIds: ['42'] }, deps(3600_000));
|
|
509
|
+
assert.deepEqual(early.rechecked.map((r) => r.ledger_shape), ['pending_land']);
|
|
510
|
+
assert.deepEqual(filed, []);
|
|
511
|
+
const late = await recheckLands({ pendingIds: ['42'] }, deps(LAND_GRACE_MS + 1000));
|
|
512
|
+
assert.deepEqual(late.rechecked.map((r) => r.ledger_shape), ['shipped_no_artifact']);
|
|
513
|
+
assert.deepEqual(filed, ['task-42-shipped_no_artifact']);
|
|
514
|
+
assert.equal(late.rechecked[0].blocker_id, 11);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('one pass fetches main ONCE, however many tasks and blockers it probes', async () => {
|
|
518
|
+
const calls = [];
|
|
519
|
+
const git = async (args) => {
|
|
520
|
+
calls.push(args[0]);
|
|
521
|
+
if (args[0] === 'fetch') return { ok: true, stdout: '', stderr: '' };
|
|
522
|
+
const id = args.find((a) => a.startsWith('--grep=')).slice(7);
|
|
523
|
+
return { ok: true, stdout: `abc${id}\x1fMerge from x/task-${id}\x1f\x1e\n`, stderr: '' };
|
|
524
|
+
};
|
|
525
|
+
const resolved = [];
|
|
526
|
+
const api = blockerApi([
|
|
527
|
+
{ id: '1', source: 'autobongos', source_ref: 'task-201-shipped_no_artifact' },
|
|
528
|
+
{ id: '2', source: 'autobongos', source_ref: 'task-202-shipped_no_artifact' },
|
|
529
|
+
], resolved);
|
|
530
|
+
await recheckLands({ pendingIds: ['101', '102', '201'] }, { api, git, nowMs: NOW });
|
|
531
|
+
assert.equal(calls.filter((c) => c === 'fetch').length, 1, 'one fetch per pass, not one per id');
|
|
532
|
+
assert.equal(calls.filter((c) => c === 'log').length, 4, 'one log per DISTINCT id (201 is both pending and blocked)');
|
|
533
|
+
assert.deepEqual(resolved.map((r) => r.id).sort(), ['1', '2']);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test('nothing to probe means no git at all, and a failed fetch resolves nothing', async () => {
|
|
537
|
+
let gitCalls = 0;
|
|
538
|
+
await recheckLands({ pendingIds: [], sweepBlockers: false }, {
|
|
539
|
+
api: { tasks: {}, blockers: {} }, git: async () => { gitCalls += 1; return { ok: true, stdout: '' }; }, nowMs: NOW,
|
|
540
|
+
});
|
|
541
|
+
assert.equal(gitCalls, 0);
|
|
542
|
+
|
|
543
|
+
const resolved = [];
|
|
544
|
+
const out = await recheckLands({ pendingIds: [] }, {
|
|
545
|
+
api: blockerApi([{ id: '9', source: 'autobongos', source_ref: 'task-301-shipped_no_artifact' }], resolved), nowMs: NOW,
|
|
546
|
+
git: async (args) => (args[0] === 'fetch' ? { ok: false, stderr: 'fatal: unreachable' } : { ok: true, stdout: 'x\x1fMerge from x/task-301\x1f\x1e' }),
|
|
547
|
+
});
|
|
548
|
+
assert.deepEqual(resolved, [], 'not looking is not evidence the commit landed either');
|
|
549
|
+
assert.deepEqual(out.resolved, []);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test('the blocker list pages by the route’s total, and says so when it gives up', async () => {
|
|
553
|
+
let gets = 0;
|
|
554
|
+
const paged = {
|
|
555
|
+
tasks: {},
|
|
556
|
+
blockers: {
|
|
557
|
+
getBlockers: async ({ query }) => {
|
|
558
|
+
gets += 1;
|
|
559
|
+
const rows = query.offset === 0
|
|
560
|
+
? Array.from({ length: 200 }, (_, i) => ({ id: String(i), source: 'manual', source_ref: null }))
|
|
561
|
+
: [{ id: '900', source: 'autobongos', source_ref: 'task-55-shipped_no_artifact' }];
|
|
562
|
+
return { ok: true, data: { blockers: rows, page: { limit: 200, offset: query.offset, total: 201 } } };
|
|
563
|
+
},
|
|
564
|
+
postBlockersIdResolve: async () => ({ ok: true }),
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
const probed = [];
|
|
568
|
+
await recheckLands({ pendingIds: [] }, { api: paged, nowMs: NOW, probeArtifact: async (id) => { probed.push(id); return notOnMain; } });
|
|
569
|
+
assert.equal(gets, 2, 'page two holds the 201st row');
|
|
570
|
+
assert.deepEqual(probed, ['55']);
|
|
571
|
+
|
|
572
|
+
const endless = { tasks: {}, blockers: { getBlockers: async ({ query }) => ({ ok: true, data: { blockers: Array.from({ length: 200 }, (_, i) => ({ id: String(query.offset + i), source: 'manual' })), page: { total: 1e9 } } }) } };
|
|
573
|
+
const out = await recheckLands({ pendingIds: [] }, { api: endless, nowMs: NOW, probeArtifact: async () => notOnMain });
|
|
574
|
+
assert.match(out.errors.join(' '), /stopped paging/, 'a cap that is hit must say so, not read as "looked everywhere"');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('an unreadable blocker list is reported, not thrown', async () => {
|
|
578
|
+
const out = await recheckLands({ pendingIds: [] }, {
|
|
579
|
+
api: { tasks: {}, blockers: { getBlockers: async () => { throw new Error('fetch failed'); } } }, nowMs: NOW,
|
|
580
|
+
probeArtifact: async () => notOnMain,
|
|
581
|
+
});
|
|
582
|
+
assert.match(out.errors.join(' '), /fetch failed/);
|
|
583
|
+
});
|