@bongos/core 1.19.257 → 1.19.258
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 +70 -25
- package/docs/adr/0022-secrets-policy.md +16 -12
- package/docs/file-map.md +1 -0
- package/docs/handoff-template.md +1 -1
- package/docs/recipes/README.md +1 -1
- package/docs/recipes/redteam-patrol.md +48 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/control-manifest.js +123 -0
- package/scripts/gds/fitness-ratchets.js +328 -0
- package/scripts/gds/fitness.js +4 -4
- package/scripts/gds/gen-session-index.js +8 -1
- package/scripts/gds/redteam-patrol.js +308 -0
- package/src/bongos/logger.js +118 -0
- package/src/bongos/middleware/audit.js +4 -3
- package/src/bongos/middleware/error-envelope.js +5 -4
- package/src/bongos/module-scope-map.js +1 -0
- package/src/bongos/serve-internal.js +2 -1
- package/src/module-api.js +3 -1
- package/src/platform-server.js +9 -2
- package/tests/auth_resolve_failure.mjs +1 -1
- package/tests/claude_materialize.mjs +28 -11
- package/tests/control_manifest.mjs +60 -0
- package/tests/fitness_ratchets.mjs +91 -0
- package/tests/logger.mjs +89 -0
- package/tests/publish_reconciler.mjs +142 -0
- package/tests/redteam_patrol.mjs +67 -0
|
@@ -1547,6 +1547,148 @@ test('1002723 a shipped task appears in the dispositions line as shipped', async
|
|
|
1547
1547
|
});
|
|
1548
1548
|
|
|
1549
1549
|
// Drain the microtask queue, then report + exit with the right code.
|
|
1550
|
+
|
|
1551
|
+
// ===========================================================================
|
|
1552
|
+
// task 1001427 — PHANTOM SHIP: `shipped` must mean the code is on main.
|
|
1553
|
+
//
|
|
1554
|
+
// The task described two vectors that left a task status=shipped with its PR
|
|
1555
|
+
// still open:
|
|
1556
|
+
// (1) the reconciler flipping confirmed->shipped on mergeIfGreen alone,
|
|
1557
|
+
// without confirming the commit is an ancestor of origin/main;
|
|
1558
|
+
// (2) under GRADER_BYPASS_ENABLED, shipped_at stamped at CONFIRM time,
|
|
1559
|
+
// before any merge.
|
|
1560
|
+
//
|
|
1561
|
+
// Both were closed by later work — the per-task land proof (1313 / 1547 /
|
|
1562
|
+
// 1002615 / 1002724) and ADR 0120's move of the payout and the shipped_at stamp
|
|
1563
|
+
// to the land. Neither was pinned by a test keyed on THIS invariant, though, so
|
|
1564
|
+
// nothing would catch a regression that reopened either one. These do.
|
|
1565
|
+
//
|
|
1566
|
+
// The invariant, stated once: THE SWEEP CALLS db.shipTask ONLY WHEN THE LAND IS
|
|
1567
|
+
// PROVEN. Not when a merge was attempted, not when a merge was reported, not
|
|
1568
|
+
// when a PR looks green — proven.
|
|
1569
|
+
// ===========================================================================
|
|
1570
|
+
|
|
1571
|
+
// EVERY assertion below reads deps._calls.ship, not out.reconciled.
|
|
1572
|
+
//
|
|
1573
|
+
// That is deliberate and was learned the hard way while writing these: a first
|
|
1574
|
+
// draft used a shipTask stub that THREW on any call, on the theory that a
|
|
1575
|
+
// phantom ship should be impossible to miss. It is exactly backwards — the sweep
|
|
1576
|
+
// isolates per-row failures on purpose (one bad row must not stop the rest), so
|
|
1577
|
+
// the throw was swallowed, counted as an error, and `reconciled` stayed 0. The
|
|
1578
|
+
// test passed while a phantom ship happened. `reconciled` is a summary of what
|
|
1579
|
+
// the sweep BELIEVES it did; the recorded call is what it actually did to the
|
|
1580
|
+
// ledger, and only the second one can answer this question.
|
|
1581
|
+
|
|
1582
|
+
test('vector 1: a green merge is NOT a land — mergeIfGreen alone never ships', async () => {
|
|
1583
|
+
// The exact 1409/1410 shape: the server merges the PR itself (auto-merge
|
|
1584
|
+
// stall), but the status still reports not-landed and no ancestry proof
|
|
1585
|
+
// exists. A merge ATTEMPT is not evidence the commit reached main.
|
|
1586
|
+
const deps = mkDeps({
|
|
1587
|
+
rows: [{ task_id: 4001, builder_id: 7, worktree_name: 'wt-4001', published_head_sha: NOT_ON_MAIN }],
|
|
1588
|
+
statusByBranch: { 'claude/wt-4001': PENDING },
|
|
1589
|
+
mergeByBranch: { 'claude/wt-4001': { merged_now: true, number: 900 } },
|
|
1590
|
+
ancestryBySha: { [NOT_ON_MAIN]: false },
|
|
1591
|
+
});
|
|
1592
|
+
await reconciler.runSweep(deps);
|
|
1593
|
+
assert.deepEqual(deps._calls.ship, [],
|
|
1594
|
+
'a merged-now PR with no ancestry proof must never reach db.shipTask');
|
|
1595
|
+
assert.ok(deps._calls.merge.includes('claude/wt-4001'),
|
|
1596
|
+
'the sweep still nudges the merge — it just does not treat the nudge as evidence');
|
|
1597
|
+
});
|
|
1598
|
+
|
|
1599
|
+
test('vector 1: the SAME row ships once the tip is provably on main', async () => {
|
|
1600
|
+
// The control for the test above. Identical inputs except the ancestry answer,
|
|
1601
|
+
// so the assertion isolates exactly the proof — not some unrelated refusal.
|
|
1602
|
+
const deps = mkDeps({
|
|
1603
|
+
rows: [{ task_id: 4002, builder_id: 7, worktree_name: 'wt-4002', published_head_sha: ON_MAIN }],
|
|
1604
|
+
statusByBranch: { 'claude/wt-4002': PENDING },
|
|
1605
|
+
mergeByBranch: { 'claude/wt-4002': { merged_now: true, number: 901 } },
|
|
1606
|
+
ancestryBySha: { [ON_MAIN]: true },
|
|
1607
|
+
});
|
|
1608
|
+
const out = await reconciler.runSweep(deps);
|
|
1609
|
+
assert.equal(out.reconciled, 1);
|
|
1610
|
+
assert.deepEqual(deps._calls.ship, [{ taskId: 4002, builderId: 7 }]);
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
test('vector 1: an OPEN PR with no proof strands rather than ships', async () => {
|
|
1614
|
+
// "shipped while the PR was still open" was the reported symptom. A row the
|
|
1615
|
+
// sweep cannot prove must leave the ledger alone and be counted unprovable.
|
|
1616
|
+
const deps = mkDeps({
|
|
1617
|
+
rows: [{ task_id: 4003, builder_id: 7, worktree_name: 'wt-4003', published_head_sha: NOT_ON_MAIN }],
|
|
1618
|
+
statusByBranch: { 'claude/wt-4003': PENDING },
|
|
1619
|
+
ancestryBySha: { [NOT_ON_MAIN]: false },
|
|
1620
|
+
});
|
|
1621
|
+
const out = await reconciler.runSweep(deps);
|
|
1622
|
+
assert.deepEqual(deps._calls.ship, [], 'an open PR with no proof must never reach db.shipTask');
|
|
1623
|
+
assert.ok((out.unprovable ?? 0) >= 1, 'an unprovable row must be COUNTED, not silently skipped (blocker 47)');
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
test('vector 1: a transient proof error never resolves to "landed"', async () => {
|
|
1627
|
+
// The compare API throwing is indistinguishable from "no proof yet", and the
|
|
1628
|
+
// fail direction must be to wait. An exception that fell through to a ship
|
|
1629
|
+
// would be the phantom-ship bug wearing an error handler.
|
|
1630
|
+
const deps = mkDeps({
|
|
1631
|
+
rows: [{ task_id: 4004, builder_id: 7, worktree_name: 'wt-4004', published_head_sha: ON_MAIN }],
|
|
1632
|
+
statusByBranch: { 'claude/wt-4004': PENDING },
|
|
1633
|
+
ancestryBySha: { [ON_MAIN]: new Error('compare API 502') },
|
|
1634
|
+
});
|
|
1635
|
+
await reconciler.runSweep(deps);
|
|
1636
|
+
assert.deepEqual(deps._calls.ship, [],
|
|
1637
|
+
'a throwing proof is indistinguishable from no proof — it must wait, never ship');
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
test('vector 2: shipped_at is stamped ONLY by shipTask, and shipTask requires confirmed', async () => {
|
|
1641
|
+
// ADR 0120 moved the payout and the stamp to the land. The structural claim —
|
|
1642
|
+
// that no other write path can stamp shipped_at on a task, and that the one
|
|
1643
|
+
// that can refuses anything not already confirmed — is what closes the
|
|
1644
|
+
// grader-bypass vector, so it is asserted against the SOURCE rather than
|
|
1645
|
+
// re-argued in prose.
|
|
1646
|
+
const fs = await import('node:fs');
|
|
1647
|
+
const url = await import('node:url');
|
|
1648
|
+
const path = await import('node:path');
|
|
1649
|
+
const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
1650
|
+
const dbSrc = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/db.js'), 'utf8');
|
|
1651
|
+
|
|
1652
|
+
// Every statement that writes shipped_at, ignoring SELECT projections and comments.
|
|
1653
|
+
const writes = [...dbSrc.matchAll(/shipped_at\s*=\s*now\(\)/g)];
|
|
1654
|
+
assert.equal(writes.length, 1,
|
|
1655
|
+
`shipped_at is written in ${writes.length} places — a second stamp site is how the grader-bypass vector reopens`);
|
|
1656
|
+
|
|
1657
|
+
// …and that one site is guarded on the confirmed state.
|
|
1658
|
+
const stampIdx = dbSrc.indexOf('shipped_at = now()');
|
|
1659
|
+
const window = dbSrc.slice(stampIdx, stampIdx + 500);
|
|
1660
|
+
assert.match(window, /WHERE id = \$1 AND status = 'confirmed'/,
|
|
1661
|
+
'the stamp must refuse any task not already confirmed — that is what stops a confirm-time ship');
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
test('vector 2: the grader-bypass confirm path lands at CONFIRMED, never shipped', async () => {
|
|
1665
|
+
// The bypass relaxes WHO may confirm and whether a panel ran. It must not
|
|
1666
|
+
// change WHERE the task lands: the route calls confirmTask, and only the
|
|
1667
|
+
// proven-land path may call shipTask.
|
|
1668
|
+
const fs = await import('node:fs');
|
|
1669
|
+
const url = await import('node:url');
|
|
1670
|
+
const path = await import('node:path');
|
|
1671
|
+
const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
1672
|
+
const routeSrc = fs.readFileSync(path.join(ROOT, 'modules/lifecycle/routes/tasks.js'), 'utf8');
|
|
1673
|
+
|
|
1674
|
+
// Anchor on the ROUTE, not on `graderBypassEnabled()`. That identifier appears
|
|
1675
|
+
// three times — the function definition, a rank-gate relaxation, and the
|
|
1676
|
+
// handler's log line — so an indexOf anchor measures whichever comes first and
|
|
1677
|
+
// then reads the wrong window. Slice the confirm handler itself: from its
|
|
1678
|
+
// registration to the next route registration.
|
|
1679
|
+
const confirmIdx = routeSrc.indexOf("router.post('/tasks/:id/confirm'");
|
|
1680
|
+
assert.ok(confirmIdx > -1, 'the confirm route must exist — update this test if it moved');
|
|
1681
|
+
const afterIdx = routeSrc.indexOf('router.', confirmIdx + 20);
|
|
1682
|
+
const handler = routeSrc.slice(confirmIdx, afterIdx > -1 ? afterIdx : confirmIdx + 3000);
|
|
1683
|
+
|
|
1684
|
+
assert.match(handler, /db\.confirmTask\(/,
|
|
1685
|
+
'the confirm route must land the task at CONFIRMED via confirmTask');
|
|
1686
|
+
assert.ok(!/db\.shipTask\(/.test(handler),
|
|
1687
|
+
'a shipTask call on the confirm path IS the grader-bypass phantom-ship vector, reopened');
|
|
1688
|
+
assert.ok(!/shipped_at/.test(handler),
|
|
1689
|
+
'the confirm path must never touch shipped_at — that stamp belongs to the proven land');
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1550
1692
|
setTimeout(() => {
|
|
1551
1693
|
console.log(`\npublish reconciler (task 1134 + 1915) tests: ${passed} passed, ${failed} failed`);
|
|
1552
1694
|
process.exit(failed > 0 ? 1 : 0);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// tests/redteam_patrol.mjs — the patrol's pure helpers (task 1003128, goal 1000076).
|
|
2
|
+
// Hermetic: parsing, severity mapping, pin detection, report assembly. The live
|
|
3
|
+
// scans (gitleaks/npm/network) and the --canary end-to-end pipe are exercised by
|
|
4
|
+
// hand + the weekly cron, never in the DB-free suite.
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { createRequire } from 'node:module';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const p = require(path.join(ROOT, 'scripts', 'gds', 'redteam-patrol.js'));
|
|
14
|
+
|
|
15
|
+
test('parseGitleaksReport maps rows to high-severity findings; junk → []', () => {
|
|
16
|
+
const rows = JSON.stringify([{ RuleID: 'aws-key', File: 'x.js', Commit: 'abcdef1234567890' }]);
|
|
17
|
+
const f = p.parseGitleaksReport(rows);
|
|
18
|
+
assert.equal(f.length, 1);
|
|
19
|
+
assert.equal(f[0].severity, 'high');
|
|
20
|
+
assert.match(f[0].item, /aws-key in x\.js @ abcdef12/);
|
|
21
|
+
assert.deepEqual(p.parseGitleaksReport('not json'), []);
|
|
22
|
+
assert.deepEqual(p.parseGitleaksReport('{}'), []);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('severity mapping: npm "moderate" → "medium"; unknowns floor to low; order respected', () => {
|
|
26
|
+
assert.equal(p.mapSeverity('moderate'), 'medium');
|
|
27
|
+
assert.equal(p.mapSeverity('HIGH'), 'high');
|
|
28
|
+
assert.equal(p.mapSeverity('weird'), 'low');
|
|
29
|
+
assert.equal(p.maxSeverity([{ severity: 'low' }, { severity: 'high' }, { severity: 'medium' }]), 'high');
|
|
30
|
+
assert.equal(p.maxSeverity([{ severity: 'low' }]), 'low');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('unpinnedUses flags tags/branches, accepts 40-hex SHAs, skips local + docker refs', () => {
|
|
34
|
+
const yaml = [
|
|
35
|
+
' - uses: actions/checkout@v4',
|
|
36
|
+
' - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0',
|
|
37
|
+
' - uses: ./.github/actions/local',
|
|
38
|
+
' uses: actions/cache@main',
|
|
39
|
+
].join('\n');
|
|
40
|
+
const bad = p.unpinnedUses(yaml);
|
|
41
|
+
assert.ok(bad.includes('actions/checkout@v4'));
|
|
42
|
+
assert.ok(bad.includes('actions/cache@main'));
|
|
43
|
+
assert.ok(!bad.some((r) => r.includes('setup-node')), 'a 40-hex SHA pin is accepted');
|
|
44
|
+
assert.ok(!bad.some((r) => r.includes('local')), 'local ./ ref skipped');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('ageDays computes against a fixed now; bad dates → null', () => {
|
|
48
|
+
const now = Date.parse('2026-08-19T00:00:00Z');
|
|
49
|
+
assert.equal(Math.round(p.ageDays('2026-08-12T00:00:00Z', now)), 7);
|
|
50
|
+
assert.equal(p.ageDays('not-a-date', now), null);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('lockedVersion reads the npm v3 lock shape', () => {
|
|
54
|
+
const lock = { packages: { 'node_modules/undici': { version: '6.27.0' } } };
|
|
55
|
+
assert.equal(p.lockedVersion(lock, 'undici'), '6.27.0');
|
|
56
|
+
assert.equal(p.lockedVersion(lock, 'absent'), undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('buildClassReport caps the item list, counts the remainder, and takes max severity', () => {
|
|
60
|
+
const findings = Array.from({ length: 13 }, (_, i) => ({ severity: i === 0 ? 'high' : 'low', item: `f${i}` }));
|
|
61
|
+
const r = p.buildClassReport('dependency-review', findings, ['a note']);
|
|
62
|
+
assert.equal(r.severity, 'high');
|
|
63
|
+
assert.equal(r.target, 'redteam-patrol/dependency-review');
|
|
64
|
+
assert.match(r.title, /13 finding/);
|
|
65
|
+
assert.match(r.repro_steps_md, /…and 3 more/);
|
|
66
|
+
assert.match(r.repro_steps_md, /Notes:/);
|
|
67
|
+
});
|