@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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/bongos/logger.js — the core structured logger (task 1003130, goal 1000076;
|
|
2
|
+
// docs/audits/ai-dev-redteam-baseline.md §5).
|
|
3
|
+
//
|
|
4
|
+
// The audit's logging finding: zero logging libraries, ~635 console.* calls in
|
|
5
|
+
// server paths, no request correlation, and NO redaction — a key echoed into a
|
|
6
|
+
// log line had no scrubber between it and journald. This is the fix for the
|
|
7
|
+
// CORE server path: one pino instance, request-correlation via
|
|
8
|
+
// AsyncLocalStorage, and redaction of the ADR 0022 secret-bearing header/field
|
|
9
|
+
// shapes. Modules migrate off console incrementally under the no-console
|
|
10
|
+
// ratchet (fitness-ratchets console_call_count); this module is what they
|
|
11
|
+
// migrate ONTO.
|
|
12
|
+
//
|
|
13
|
+
// Why pino: the Node structured-logging standard — JSON to stdout, low
|
|
14
|
+
// overhead, first-class redaction. It carries NO install scripts, so it is
|
|
15
|
+
// safe under the `.npmrc ignore-scripts=true` policy (ADR 0180 §2).
|
|
16
|
+
//
|
|
17
|
+
// The logger writes newline-delimited JSON to stdout; the process manager
|
|
18
|
+
// (systemd/journald) owns rotation and shipping. Level is LOG_LEVEL (default
|
|
19
|
+
// 'info'); set 'debug' to see the request-start lines.
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
24
|
+
const crypto = require('node:crypto');
|
|
25
|
+
const pino = require('pino');
|
|
26
|
+
|
|
27
|
+
// One request-scoped store holding { reqId }. A pino mixin reads it so EVERY
|
|
28
|
+
// line emitted while handling a request carries the id, without threading it
|
|
29
|
+
// through call signatures.
|
|
30
|
+
const requestContext = new AsyncLocalStorage();
|
|
31
|
+
|
|
32
|
+
// Redaction — the secret-bearing shapes from ADR 0022 that realistically reach
|
|
33
|
+
// a log line via a logged request/header/body object. pino's `redact` replaces
|
|
34
|
+
// the value with "[Redacted]"; paths are static (never built from input — the
|
|
35
|
+
// injection caveat in pino's docs). This is defense in depth behind the
|
|
36
|
+
// scrubber and the prevent hook, not the only guard.
|
|
37
|
+
const REDACT_PATHS = [
|
|
38
|
+
'req.headers.authorization',
|
|
39
|
+
'req.headers.cookie',
|
|
40
|
+
'req.headers["x-gds-session"]',
|
|
41
|
+
'req.headers["x-api-key"]',
|
|
42
|
+
'headers.authorization',
|
|
43
|
+
'headers.cookie',
|
|
44
|
+
// top-level field names (a bare path; pino's `*.x` matches one level DOWN, not the root)
|
|
45
|
+
'authorization',
|
|
46
|
+
'token',
|
|
47
|
+
'password',
|
|
48
|
+
'secret',
|
|
49
|
+
'apiKey',
|
|
50
|
+
'api_key',
|
|
51
|
+
'sessionToken',
|
|
52
|
+
'githubToken',
|
|
53
|
+
// …and the same names one level under any key
|
|
54
|
+
'*.authorization',
|
|
55
|
+
'*.token',
|
|
56
|
+
'*.password',
|
|
57
|
+
'*.secret',
|
|
58
|
+
'*.apiKey',
|
|
59
|
+
'*.api_key',
|
|
60
|
+
'*.sessionToken',
|
|
61
|
+
'*.githubToken',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
// Exported so a test can build a logger with the IDENTICAL config onto a
|
|
65
|
+
// capture stream — pino writes through sonic-boom straight to the fd, so the
|
|
66
|
+
// config, not the singleton's stdout, is what a hermetic test asserts against.
|
|
67
|
+
const pinoOptions = {
|
|
68
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
69
|
+
// A leaked secret is a compromise; a redaction false-positive in a log is
|
|
70
|
+
// cosmetic — so redaction is broad, mirroring scrubSecrets' recall-first posture.
|
|
71
|
+
redact: { paths: REDACT_PATHS, censor: '[Redacted]' },
|
|
72
|
+
base: undefined, // drop pid/hostname noise; the process manager adds host context
|
|
73
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
74
|
+
mixin() {
|
|
75
|
+
const store = requestContext.getStore();
|
|
76
|
+
return store && store.reqId ? { reqId: store.reqId } : {};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const logger = pino(pinoOptions);
|
|
81
|
+
|
|
82
|
+
// Run `fn` inside a fresh request context stamped with `reqId` (a caller-supplied
|
|
83
|
+
// inbound id, or a fresh short hex). Everything logged inside — including the
|
|
84
|
+
// error middleware — carries the same reqId.
|
|
85
|
+
function runWithRequestId(reqId, fn) {
|
|
86
|
+
return requestContext.run({ reqId: reqId || crypto.randomBytes(8).toString('hex') }, fn);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function currentRequestId() {
|
|
90
|
+
const store = requestContext.getStore();
|
|
91
|
+
return store ? store.reqId : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Express middleware: adopt an inbound correlation id (or mint one), expose it
|
|
95
|
+
// on req/res + the `x-request-id` response header, and run the rest of the
|
|
96
|
+
// request inside the AsyncLocalStorage context so every log line correlates.
|
|
97
|
+
function requestIdMiddleware(req, res, next) {
|
|
98
|
+
const inbound = req.headers['x-request-id'];
|
|
99
|
+
const reqId = (typeof inbound === 'string' && inbound.length > 0 && inbound.length <= 128)
|
|
100
|
+
? inbound
|
|
101
|
+
: crypto.randomBytes(8).toString('hex');
|
|
102
|
+
req.reqId = reqId;
|
|
103
|
+
res.setHeader('x-request-id', reqId);
|
|
104
|
+
requestContext.run({ reqId }, () => next());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// requestContext stays module-private (nothing outside needs the raw ALS).
|
|
108
|
+
// pinoOptions/REDACT_PATHS/runWithRequestId/currentRequestId are the logging
|
|
109
|
+
// toolkit — used by tests today (via dynamic require, invisible to knip's static
|
|
110
|
+
// scan) and by future non-HTTP correlation callers.
|
|
111
|
+
module.exports = {
|
|
112
|
+
logger,
|
|
113
|
+
pinoOptions,
|
|
114
|
+
requestIdMiddleware,
|
|
115
|
+
runWithRequestId,
|
|
116
|
+
currentRequestId,
|
|
117
|
+
REDACT_PATHS,
|
|
118
|
+
};
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// view useful; that information is lost at the SQL layer.
|
|
41
41
|
|
|
42
42
|
const db = require('../db');
|
|
43
|
+
const { logger } = require('../logger');
|
|
43
44
|
|
|
44
45
|
const REDACTION_TOKENS = ['token', 'password', 'key', 'secret'];
|
|
45
46
|
const REDACTED = '<redacted>';
|
|
@@ -205,7 +206,7 @@ function auditMiddleware(opts = {}) {
|
|
|
205
206
|
} catch (err) {
|
|
206
207
|
// redaction failures are themselves audit-failures: log + continue.
|
|
207
208
|
// We still attach the finish handler so a row goes in WITH an empty body.
|
|
208
|
-
|
|
209
|
+
logger.error({ err }, '[audit] redact_failed');
|
|
209
210
|
snapshotBody = { _redact_failed: true };
|
|
210
211
|
}
|
|
211
212
|
const ip = clientIp(req);
|
|
@@ -251,12 +252,12 @@ function auditMiddleware(opts = {}) {
|
|
|
251
252
|
.catch((err) => {
|
|
252
253
|
// SWALLOW. Audit failures must not surface to the caller and must
|
|
253
254
|
// not affect future requests. Console-error is the only effect.
|
|
254
|
-
|
|
255
|
+
logger.error({
|
|
255
256
|
route,
|
|
256
257
|
method,
|
|
257
258
|
status,
|
|
258
259
|
err: err && err.message ? err.message : String(err),
|
|
259
|
-
});
|
|
260
|
+
}, '[audit] insert_failed');
|
|
260
261
|
});
|
|
261
262
|
});
|
|
262
263
|
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
'use strict';
|
|
24
24
|
|
|
25
25
|
const { ApiError, isApiError, envelope, statusForCode } = require('../api-errors');
|
|
26
|
+
const { logger } = require('../logger');
|
|
26
27
|
|
|
27
28
|
// attachFail — decorate res with fail(). Mounted once, before the routers.
|
|
28
29
|
function attachFail(req, res, next) {
|
|
@@ -83,10 +84,10 @@ function errorEnvelopeHandler(err, req, res, next) {
|
|
|
83
84
|
return res.status(framework.status).json(envelope(framework.code));
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
// Anything else is an unexpected server fault. Log
|
|
87
|
-
//
|
|
88
|
-
// message/stack is never sent to the client.
|
|
89
|
-
|
|
87
|
+
// Anything else is an unexpected server fault. Log structured (auto-correlated
|
|
88
|
+
// to the request via the logger's reqId mixin, redaction applied) and answer a
|
|
89
|
+
// leak-free internal envelope — the raw message/stack is never sent to the client.
|
|
90
|
+
logger.error({ method: req && req.method, url: req && req.originalUrl, err }, '[gds] unhandled');
|
|
90
91
|
return res.status(500).json(envelope('internal_error'));
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -311,6 +311,7 @@ const MODULE_GLOBS = {
|
|
|
311
311
|
'package.json', 'package-lock.json',
|
|
312
312
|
'scripts/gds/fitness.js', 'scripts/gds/docs-entropy.js',
|
|
313
313
|
'scripts/gds/redteam-patrol.js', 'scripts/gds/audit-gate.js',
|
|
314
|
+
'scripts/gds/fitness-ratchets.js', 'scripts/gds/control-manifest.js',
|
|
314
315
|
],
|
|
315
316
|
};
|
|
316
317
|
|
|
@@ -39,6 +39,7 @@ const express = require('express');
|
|
|
39
39
|
const { buildGdsRouter } = require('./routes');
|
|
40
40
|
const { attachFail } = require('./middleware/error-envelope');
|
|
41
41
|
const metrics = require('./metrics');
|
|
42
|
+
const { logger } = require('./logger');
|
|
42
43
|
const gdsAuth = require('./auth');
|
|
43
44
|
const { buildStampedHtml } = require('../asset-version');
|
|
44
45
|
const { buildInfo } = require('../build-info');
|
|
@@ -289,7 +290,7 @@ function resolveOpenapiJson({ candidates = [], generate } = {}) {
|
|
|
289
290
|
const out = generate();
|
|
290
291
|
if (out) return out;
|
|
291
292
|
} catch (e) {
|
|
292
|
-
|
|
293
|
+
logger.error({ err: e && e.message }, '[docs] in-process OpenAPI generation failed');
|
|
293
294
|
}
|
|
294
295
|
}
|
|
295
296
|
return '';
|
package/src/module-api.js
CHANGED
|
@@ -941,8 +941,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
941
941
|
// landed since 1.19.255 with no explicit bump. run 32208475455. (task 1002620)
|
|
942
942
|
// 1.19.257 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
943
943
|
// landed since 1.19.256 with no explicit bump. run 32209295358. (task 1002620)
|
|
944
|
+
// 1.19.258 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
945
|
+
// landed since 1.19.257 with no explicit bump. run 32284962866. (task 1002620)
|
|
944
946
|
// ---------------------------------------------------------------------------
|
|
945
|
-
const CORE_VERSION = '1.19.
|
|
947
|
+
const CORE_VERSION = '1.19.258'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
946
948
|
|
|
947
949
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
948
950
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/src/platform-server.js
CHANGED
|
@@ -49,6 +49,7 @@ const { mountInternalSurfaces, isStatusHost, isBuildersHost, injectBrandingGloba
|
|
|
49
49
|
const { isApiShapedPath, apiNotFoundBody } = require('./bongos/api-path-404');
|
|
50
50
|
const moduleLoader = require('./module-loader/loader');
|
|
51
51
|
const { isModuleEnabled } = require('./modules');
|
|
52
|
+
const { logger, requestIdMiddleware } = require('./bongos/logger');
|
|
52
53
|
|
|
53
54
|
const PORT = Number(process.env.PORT) || 3000;
|
|
54
55
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
@@ -157,6 +158,12 @@ function buildPlatformApp() {
|
|
|
157
158
|
// the OAuth callbacks the GDS auth router performs.
|
|
158
159
|
app.set('trust proxy', 1);
|
|
159
160
|
|
|
161
|
+
// Request correlation FIRST (task 1003130): adopt/mint an x-request-id and run
|
|
162
|
+
// the rest of the request inside the AsyncLocalStorage context, so every
|
|
163
|
+
// structured log line — including the central error handler's — carries the
|
|
164
|
+
// same reqId. Mounted before any surface so nothing logs uncorrelated.
|
|
165
|
+
app.use(requestIdMiddleware);
|
|
166
|
+
|
|
160
167
|
// The whole internal surface — shared verbatim with server.js (ADR 0062 R51).
|
|
161
168
|
mountInternalSurfaces(app);
|
|
162
169
|
|
|
@@ -263,7 +270,7 @@ if (require.main === module) {
|
|
|
263
270
|
const app = buildPlatformApp();
|
|
264
271
|
const httpServer = http.createServer(app);
|
|
265
272
|
httpServer.listen(PORT, HOST, () => {
|
|
266
|
-
|
|
273
|
+
logger.info({ host: HOST, port: PORT }, 'Cloud Bongos platform (GDS-only, no game; ADR 0062 R51) listening');
|
|
267
274
|
startBackgroundServices();
|
|
268
275
|
});
|
|
269
276
|
|
|
@@ -288,7 +295,7 @@ if (require.main === module) {
|
|
|
288
295
|
// up with a logged error strictly dominates dying. A rejection that leaves genuinely
|
|
289
296
|
// corrupt state is not a shape this codebase produces — the DB work is transactional.
|
|
290
297
|
process.on('unhandledRejection', (reason) => {
|
|
291
|
-
|
|
298
|
+
logger.error({ reason }, '[platform] unhandled promise rejection (staying up; this is a BUG — the owning await needs its own catch)');
|
|
292
299
|
});
|
|
293
300
|
}
|
|
294
301
|
|
|
@@ -140,7 +140,7 @@ test('a process-level unhandledRejection backstop is registered and does not exi
|
|
|
140
140
|
assert.match(src, /process\.on\('unhandledRejection'/, 'the backstop must exist');
|
|
141
141
|
const handler = src.slice(src.indexOf("process.on('unhandledRejection'"));
|
|
142
142
|
const body = handler.slice(0, handler.indexOf('});') + 3);
|
|
143
|
-
assert.match(body, /console\.error/, 'it must log');
|
|
143
|
+
assert.match(body, /console\.error|logger\.error/, 'it must log (console or the structured logger — task 1003130 moved this to logger.error)');
|
|
144
144
|
assert.doesNotMatch(body, /process\.exit/,
|
|
145
145
|
'it must NOT exit — one process serves every request, so staying up dominates dying');
|
|
146
146
|
});
|
|
@@ -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
|
+
});
|