@bongos/core 1.19.256 → 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 +86 -26
- package/docs/adr/0022-secrets-policy.md +16 -12
- package/docs/adr/0180-dependency-intake-and-drift-policy.md +36 -0
- package/docs/adr/README.md +1 -0
- 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/audit-gate.js +121 -0
- 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 +2 -1
- package/src/bongos/serve-internal.js +2 -1
- package/src/module-api.js +5 -1
- package/src/platform-server.js +9 -2
- package/tests/audit_gate.mjs +89 -0
- 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
|
@@ -117,17 +117,34 @@ t('materializeClaude: materializes scheduled-tasks — bodies→bongos, companio
|
|
|
117
117
|
assert.ok(res.scheduledTasks > 0, 'at least one scheduled-task file materialized');
|
|
118
118
|
const rt = join(dir, '.claude', 'scheduled-tasks');
|
|
119
119
|
assert.ok(existsSync(rt), '.claude/scheduled-tasks created in the instance');
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
// Every materialized routine's core invocation is bongosified — asserted over
|
|
121
|
+
// WHATEVER routines the tree carries (the neutral core carries redteam-weekly;
|
|
122
|
+
// the founder core also carries stale-claim-release/diagram-drift). No routine
|
|
123
|
+
// body may keep a bare `node scripts/gds/…` core invocation.
|
|
124
|
+
let sawBongosified = false;
|
|
125
|
+
for (const name of readdirSync(rt)) {
|
|
126
|
+
const skillPath = join(rt, name, 'SKILL.md');
|
|
127
|
+
if (!existsSync(skillPath)) continue;
|
|
128
|
+
const body = readFileSync(skillPath, 'utf8');
|
|
129
|
+
assert.ok(!/\bnode scripts\/gds\//.test(body), `${name}: bare relative node scripts/gds left in the body`);
|
|
130
|
+
if (body.includes('bongos exec scripts/gds/')) sawBongosified = true;
|
|
131
|
+
}
|
|
132
|
+
assert.ok(sawBongosified, 'at least one routine had its core invocation rewritten to bongos exec');
|
|
133
|
+
// Founder-specific fixtures — assert their finer behavior only when present, so
|
|
134
|
+
// this coverage stays live on the founder core without demanding those routines
|
|
135
|
+
// exist in the neutral core (which carries a different routine set).
|
|
136
|
+
const stalePath = join(rt, 'stale-claim-release', 'SKILL.md');
|
|
137
|
+
if (existsSync(stalePath)) {
|
|
138
|
+
assert.ok(readFileSync(stalePath, 'utf8').includes('bongos exec scripts/gds/pulse.js'), 'body core-invocation rewritten to bongos exec');
|
|
139
|
+
}
|
|
140
|
+
const ddCheck = join(rt, 'diagram-drift', 'check.js');
|
|
141
|
+
if (existsSync(ddCheck)) {
|
|
142
|
+
const dd = readFileSync(join(rt, 'diagram-drift', 'SKILL.md'), 'utf8');
|
|
143
|
+
assert.ok(dd.includes('node .claude/scheduled-tasks/diagram-drift/check.js'), 'companion-path step command preserved');
|
|
144
|
+
const check = readFileSync(ddCheck, 'utf8');
|
|
145
|
+
assert.ok(check.includes("require('../../hooks/_core-resolve.js')"), 'companion resolves core via _core-resolve');
|
|
146
|
+
assert.ok(!check.includes("require('../../../scripts/gds/"), 'no bare ../../../scripts/gds require left in the companion');
|
|
147
|
+
}
|
|
131
148
|
rmSync(dir, { recursive: true, force: true });
|
|
132
149
|
});
|
|
133
150
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// tests/control_manifest.mjs — the assurance-of-assurance check (task 1003131,
|
|
2
|
+
// goal 1000076). Proves the manifest passes on the real tree AND actually
|
|
3
|
+
// CATCHES the two failure modes it exists to catch: a control file that
|
|
4
|
+
// vanishes, and a control that comes unwired. The check is itself a control —
|
|
5
|
+
// so it too must be provably able to fail.
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const cm = require(path.join(ROOT, 'scripts', 'gds', 'control-manifest.js'));
|
|
15
|
+
|
|
16
|
+
test('the live tree passes: every goal-1000076 control is present and wired', () => {
|
|
17
|
+
const r = cm.checkControlsExist();
|
|
18
|
+
assert.equal(r.hardFail, false, `unexpected violations:\n ${r.violations.join('\n ')}`);
|
|
19
|
+
assert.ok(cm.CONTROLS.length >= 8, 'the manifest covers the shipped controls');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('a vanished control file is caught', () => {
|
|
23
|
+
const controls = [{ label: 'x', file: '.claude/hooks/secret-gate.js' }];
|
|
24
|
+
const gone = cm.evaluate(controls, { exists: () => false, read: () => '' });
|
|
25
|
+
assert.equal(gone.length, 1);
|
|
26
|
+
assert.match(gone[0], /control file missing/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('a present-but-UNWIRED control is caught (the settings.json un-hook case)', () => {
|
|
30
|
+
const controls = [{
|
|
31
|
+
label: 'secret-gate hook', file: '.claude/hooks/secret-gate.js',
|
|
32
|
+
wiredIn: { file: '.claude/settings.json', needle: 'secret-gate.js' },
|
|
33
|
+
}];
|
|
34
|
+
// file exists, but the settings no longer reference it
|
|
35
|
+
const unwired = cm.evaluate(controls, {
|
|
36
|
+
exists: (f) => f === '.claude/hooks/secret-gate.js' || f === '.claude/settings.json',
|
|
37
|
+
read: () => '{ "hooks": {} }',
|
|
38
|
+
});
|
|
39
|
+
assert.equal(unwired.length, 1);
|
|
40
|
+
assert.match(unwired[0], /not wired in/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a missing companion file (baseline/config) is caught even when the main file exists', () => {
|
|
44
|
+
const controls = [{
|
|
45
|
+
label: 'audit gate', file: 'scripts/gds/audit-gate.js',
|
|
46
|
+
alsoExists: ['config/audit-baseline.json'],
|
|
47
|
+
}];
|
|
48
|
+
const v = cm.evaluate(controls, { exists: (f) => f === 'scripts/gds/audit-gate.js', read: () => '' });
|
|
49
|
+
assert.equal(v.length, 1);
|
|
50
|
+
assert.match(v[0], /companion file missing/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('all-present + all-wired yields zero violations', () => {
|
|
54
|
+
const controls = [{
|
|
55
|
+
label: 'ok', file: 'a', alsoExists: ['b'],
|
|
56
|
+
wiredIn: { file: 'c', needle: 'NEEDLE' },
|
|
57
|
+
}];
|
|
58
|
+
const v = cm.evaluate(controls, { exists: () => true, read: () => 'has NEEDLE inside' });
|
|
59
|
+
assert.deepEqual(v, []);
|
|
60
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// tests/fitness_ratchets.mjs — the quality-ratchet mechanics (task 1003129,
|
|
2
|
+
// goal 1000076). Hermetic: exercises the pure metric + compare functions on
|
|
3
|
+
// synthetic inputs; the repo-wide sweep runs via checkQualityRatchets inside
|
|
4
|
+
// tests/fitness.mjs, and the knip half is a CI-only step by design.
|
|
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 rat = require(path.join(ROOT, 'scripts', 'gds', 'fitness-ratchets.js'));
|
|
14
|
+
|
|
15
|
+
test('ratchet compare: worse fails, equal passes, better nudges, unknown warns', () => {
|
|
16
|
+
const base = { a: 10, b: 5 };
|
|
17
|
+
assert.equal(rat.compareToBaselines({ a: 10, b: 5 }, base).violations.length, 0, 'equal passes');
|
|
18
|
+
const worse = rat.compareToBaselines({ a: 11, b: 5 }, base);
|
|
19
|
+
assert.equal(worse.violations.length, 1);
|
|
20
|
+
assert.match(worse.violations[0], /a regressed: 11 > baseline 10/);
|
|
21
|
+
const better = rat.compareToBaselines({ a: 9, b: 5 }, base);
|
|
22
|
+
assert.equal(better.violations.length, 0);
|
|
23
|
+
assert.match(better.warnings[0], /ratchet-down available/);
|
|
24
|
+
const unknown = rat.compareToBaselines({ c: 1 }, base);
|
|
25
|
+
assert.equal(unknown.violations.length, 0, 'uninitialized metric warns, never fails');
|
|
26
|
+
assert.match(unknown.warnings[0], /no baseline for "c"/);
|
|
27
|
+
assert.equal(rat.compareToBaselines({ a: null }, base).warnings.length, 0, 'a skipped metric (null) is silent');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('duplication: identical 30-line blocks in TWO files count; one file alone does not', () => {
|
|
31
|
+
const block = Array.from({ length: rat.WINDOW }, (_, i) => `const v${i} = compute(${i});`).join('\n');
|
|
32
|
+
const one = new Map([['a.js', block]]);
|
|
33
|
+
assert.equal(rat.duplicateWindowCount(one), 0, 'same-file windows are not cross-file duplication');
|
|
34
|
+
const two = new Map([['a.js', block], ['b.js', `// header\n${block}`]]);
|
|
35
|
+
assert.ok(rat.duplicateWindowCount(two) >= 1, 'the shared block registers');
|
|
36
|
+
const clean = new Map([['a.js', block], ['b.js', block.replaceAll('compute', 'derive')]]);
|
|
37
|
+
assert.equal(rat.duplicateWindowCount(clean), 0, 'a changed body is not a duplicate');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('normalizeLines drops blanks and comment lines so cosmetic edits cannot dodge the hash', () => {
|
|
41
|
+
const lines = rat.normalizeLines('a();\n\n // comment\n b();\n/* block */\n * cont\n');
|
|
42
|
+
assert.deepEqual(lines, ['a();', 'b();']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('cycle counting: a 2-node loop is one SCC; a diamond is none; self-requires ignored', () => {
|
|
46
|
+
const loop = new Map([['a.js', ['b.js']], ['b.js', ['a.js']]]);
|
|
47
|
+
assert.equal(rat.countCycles(loop), 1);
|
|
48
|
+
const diamond = new Map([['a.js', ['b.js', 'c.js']], ['b.js', ['d.js']], ['c.js', ['d.js']], ['d.js', []]]);
|
|
49
|
+
assert.equal(rat.countCycles(diamond), 0);
|
|
50
|
+
const tri = new Map([['a.js', ['b.js']], ['b.js', ['c.js']], ['c.js', ['a.js']], ['x.js', []]]);
|
|
51
|
+
assert.equal(rat.countCycles(tri), 1, 'a 3-cycle is one component, not three');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('requireGraph resolves ./x, ./x.js and ./x/index.js and skips unknown targets', () => {
|
|
55
|
+
const contents = new Map([
|
|
56
|
+
['src/a.js', "const b = require('./b'); const i = require('./lib'); const gone = require('./nope');"],
|
|
57
|
+
['src/b.js', ''],
|
|
58
|
+
['src/lib/index.js', ''],
|
|
59
|
+
]);
|
|
60
|
+
const edges = rat.requireGraph(contents);
|
|
61
|
+
assert.deepEqual(edges.get('src/a.js').sort(), ['src/b.js', 'src/lib/index.js']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('size metrics: count and excess ratchet independently', () => {
|
|
65
|
+
const files = ['big.js', 'small.js'];
|
|
66
|
+
const read = (f) => (f === 'big.js' ? 'x\n'.repeat(rat.OVERSIZE_LINES + 100) : 'x\n');
|
|
67
|
+
const m = rat.sizeMetrics(files, read);
|
|
68
|
+
assert.equal(m.oversized_file_count, 1);
|
|
69
|
+
assert.ok(m.excess_lines_total >= 100, `excess tracks growth over the threshold (got ${m.excess_lines_total})`);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('console counting only sees server paths', () => {
|
|
73
|
+
assert.equal(rat.isServerPath('src/bongos/auth.js'), true);
|
|
74
|
+
assert.equal(rat.isServerPath('modules/lifecycle/db.js'), true);
|
|
75
|
+
assert.equal(rat.isServerPath('modules/hall-ui/public/builders.js'), false, 'browser surfaces excluded');
|
|
76
|
+
assert.equal(rat.isServerPath('scripts/gds/ship.js'), false, 'CLI stdout is legitimate UX');
|
|
77
|
+
assert.equal(rat.isServerPath('modules/security/tests/floor.mjs'), false);
|
|
78
|
+
const n = rat.consoleCallCount(new Map([
|
|
79
|
+
['src/x.js', 'console.log(1); console.error(2);'],
|
|
80
|
+
['scripts/gds/y.js', 'console.log(1);'],
|
|
81
|
+
]));
|
|
82
|
+
assert.equal(n, 2);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('the committed baseline file parses and covers every hermetic metric', () => {
|
|
86
|
+
const b = require(path.join(ROOT, 'config', 'fitness-baselines.json'));
|
|
87
|
+
for (const key of ['oversized_file_count', 'excess_lines_total', 'duplicate_window_count',
|
|
88
|
+
'require_cycle_count', 'console_call_count', 'doc_stale_links', 'doc_broken_anchors', 'doc_duplicate_prose']) {
|
|
89
|
+
assert.ok(Number.isInteger(b.metrics[key]), `baseline missing ${key}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
package/tests/logger.mjs
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// tests/logger.mjs — the core structured logger (task 1003130, goal 1000076).
|
|
2
|
+
// Proves the three done-when properties without a live server: request-id
|
|
3
|
+
// correlation via AsyncLocalStorage, secret redaction on the logged shapes,
|
|
4
|
+
// and the middleware's adopt-or-mint + x-request-id echo. Hermetic — captures
|
|
5
|
+
// pino's stdout stream, no network.
|
|
6
|
+
import assert from 'node:assert/strict';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const log = require(path.join(ROOT, 'src', 'bongos', 'logger.js'));
|
|
15
|
+
const pino = require(path.join(ROOT, 'node_modules', 'pino'));
|
|
16
|
+
const { Writable } = require('node:stream');
|
|
17
|
+
|
|
18
|
+
// pino writes through sonic-boom directly to the fd, so we can't intercept the
|
|
19
|
+
// singleton's stdout. Instead build a logger with the IDENTICAL exported config
|
|
20
|
+
// onto a capture stream — same redact paths + same reqId mixin (the mixin closes
|
|
21
|
+
// over the module's ALS, so runWithRequestId still correlates).
|
|
22
|
+
function capture(fn) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
const stream = new Writable({ write(chunk, _enc, cb) { lines.push(chunk.toString()); cb(); } });
|
|
25
|
+
const testLogger = pino(log.pinoOptions, stream);
|
|
26
|
+
fn(testLogger);
|
|
27
|
+
return lines.filter((l) => l.trim().startsWith('{')).map((l) => JSON.parse(l));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('every line emitted inside runWithRequestId carries the same reqId', () => {
|
|
31
|
+
const out = capture((l) => {
|
|
32
|
+
log.runWithRequestId('req-abc', () => {
|
|
33
|
+
l.info('first');
|
|
34
|
+
l.warn('second');
|
|
35
|
+
assert.equal(log.currentRequestId(), 'req-abc');
|
|
36
|
+
});
|
|
37
|
+
l.info('outside'); // no context → no reqId
|
|
38
|
+
});
|
|
39
|
+
const inside = out.filter((l) => l.msg === 'first' || l.msg === 'second');
|
|
40
|
+
assert.equal(inside.length, 2);
|
|
41
|
+
assert.ok(inside.every((l) => l.reqId === 'req-abc'), 'correlated lines share the id');
|
|
42
|
+
const outside = out.find((l) => l.msg === 'outside');
|
|
43
|
+
assert.equal(outside.reqId, undefined, 'a line with no request context carries no reqId');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('a secret in a logged header/field is redacted, not emitted', () => {
|
|
47
|
+
const out = capture((l) => {
|
|
48
|
+
l.error({
|
|
49
|
+
req: { headers: { authorization: 'Bearer sk-ant-verysecretvalue012345', cookie: 'gds_session=abc' } },
|
|
50
|
+
token: 'ghp_shouldnotappear0000000000000000000000',
|
|
51
|
+
note: 'this stays',
|
|
52
|
+
}, 'boom');
|
|
53
|
+
});
|
|
54
|
+
const line = out.find((l) => l.msg === 'boom');
|
|
55
|
+
const blob = JSON.stringify(line);
|
|
56
|
+
assert.ok(!blob.includes('sk-ant-verysecretvalue'), 'authorization header redacted');
|
|
57
|
+
assert.ok(!blob.includes('ghp_shouldnotappear'), 'token field redacted');
|
|
58
|
+
assert.ok(!blob.includes('gds_session=abc'), 'cookie redacted');
|
|
59
|
+
assert.ok(blob.includes('[Redacted]'), 'redaction marker present');
|
|
60
|
+
assert.ok(blob.includes('this stays'), 'non-secret fields survive');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('REDACT_PATHS covers the ADR 0022 header/field shapes', () => {
|
|
64
|
+
for (const p of ['req.headers.authorization', 'req.headers.cookie', 'token', '*.token', '*.password']) {
|
|
65
|
+
assert.ok(log.REDACT_PATHS.includes(p), `expected redact path ${p}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('requestIdMiddleware adopts a valid inbound id and echoes it; mints otherwise', () => {
|
|
70
|
+
const mk = (headers) => {
|
|
71
|
+
const req = { headers };
|
|
72
|
+
const res = { _h: {}, setHeader(k, v) { this._h[k.toLowerCase()] = v; } };
|
|
73
|
+
let ran = false;
|
|
74
|
+
log.requestIdMiddleware(req, res, () => { ran = true; });
|
|
75
|
+
return { req, res, ran };
|
|
76
|
+
};
|
|
77
|
+
const adopted = mk({ 'x-request-id': 'inbound-123' });
|
|
78
|
+
assert.equal(adopted.req.reqId, 'inbound-123');
|
|
79
|
+
assert.equal(adopted.res._h['x-request-id'], 'inbound-123');
|
|
80
|
+
assert.ok(adopted.ran);
|
|
81
|
+
|
|
82
|
+
const minted = mk({});
|
|
83
|
+
assert.match(minted.req.reqId, /^[0-9a-f]{16}$/, 'mints a 16-hex id when none supplied');
|
|
84
|
+
assert.equal(minted.res._h['x-request-id'], minted.req.reqId);
|
|
85
|
+
|
|
86
|
+
// an absurdly long inbound id is rejected in favor of a minted one (header-injection guard)
|
|
87
|
+
const huge = mk({ 'x-request-id': 'x'.repeat(500) });
|
|
88
|
+
assert.match(huge.req.reqId, /^[0-9a-f]{16}$/, 'over-long inbound id is not adopted');
|
|
89
|
+
});
|
|
@@ -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
|
+
});
|