/** * Config log redaction test — locks in the [E00-S04-T03] guarantee that * secrets automatically redact from logs: the app's log output contains no * secret values. * * Acceptance criteria covered (each test fails without the committed state): * - "secrets automatically redact from logs" → `packages/config` exposes the * redaction layer (`redactConfig` — a config value with every secret * replaced by `[REDACTED]`, for logging the resolved configuration — and * `redactText` — scrubbing free-form log text of the config's secret * values), and the committed server writes ALL of its log output through * the redacting logger (`createLogger` in the server entrypoint, seeded * with the validated config). Locked in statically (mutation probes prove * non-vacuity: renaming the exports, dropping the split/join scrub, * unmasking the databaseUrl password, or reintroducing a bare * `console.log`/`console.error` all fail) and behaviorally by the * deterministic probes. * - "log output contains no secret values" → the deterministic probes * execute the issue's test plan ("log configuration and confirm secret * values are redacted") against the committed code: the compiled * `@personal-blog/config` boundary redacts the admin-session secret and * the password embedded in a `DATABASE_URL` connection string, and * booting the committed server logs its resolved configuration with * every secret value replaced by `[REDACTED]` — the booted server's * stdout/stderr contain no secret value. * * Run: `node --test tests/config-log-redaction.test.mjs` * (node:test — built into Node >= 18; no dependencies, lockfile untouched. * The deterministic probes boot the committed server, which imports * `@personal-blog/config` and `@personal-blog/database-postgres` — build those * packages first, exactly as the CI job does.) */ import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, existsSync, writeFileSync, rmSync } from 'node:fs'; import { spawn, spawnSync } from 'node:child_process'; import { once } from 'node:events'; import { createServer as createNetServer } from 'node:net'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8'); /** The committed redaction layer, package boundary and server entrypoint under test. */ const REDACT_SRC = 'packages/config/src/redact.ts'; const INDEX_SRC = 'packages/config/src/index.ts'; const SERVER_SRC = 'apps/server/src/index.ts'; /** The root test glob (root `scripts.test`, E00-S01-T12) that runs every suite. */ const ROOT_TEST_GLOB = 'tests/**/*.test.mjs'; /** The CI job that gates the log-redaction criterion on every PR. */ const CI_JOB = 'config-log-redaction'; /** A distinctive >= 32-char admin-session secret the probes must never leak. */ const SECRET = 'redact-me-0123456789abcdefghijklmnopqrstuv'; /** A connection string whose password the probes must never leak. */ const DATABASE_URL = 'postgres://redact-user:redact-password@db:5432/redact-db'; const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); // --------------------------------------------------------------------------- // Static assertions on the committed sources // --------------------------------------------------------------------------- /** * Asserts the config package exposes the redaction layer: `redactConfig` * (a config value with every secret replaced by `[REDACTED]` — the secret * fields by name and the `databaseUrl` password masked in place) and * `redactText` (free-form log text scrubbed of the config's secret values). * Fails fast on a deviation; the mutation probes below prove the assertions * are non-vacuous. */ function assertRedactionSource(src) { // The placeholder and the schema-derived secret field names. assert.match( src, /export const REDACTED = '\[REDACTED\]'/, 'the redaction module must export the [REDACTED] placeholder', ); assert.match( src, /export const SECRET_FIELD_NAMES/, 'the redaction module must export the secret field names (SECRET_FIELD_NAMES)', ); assert.match( src, /SECRET_FIELD_NAMES: readonly string\[\] = \['sessionSecret'\]/, "the secret field names must name the schema's secret field (sessionSecret, E00-S04-T01)", ); // redactConfig — a config copy with every secret replaced by the placeholder. assert.match( src, /export function redactConfig\(config: Config\): Config/, 'the redaction module must export redactConfig (a redacted copy of a config value, for logging the resolved configuration)', ); assert.match( src, /SECRET_FIELD_NAMES\.includes\(key\)/, 'redactConfig must replace the secret fields by name (SECRET_FIELD_NAMES)', ); assert.match( src, /redacted\[key\] = REDACTED/, 'redactConfig must replace secret field values with the [REDACTED] placeholder', ); assert.match( src, /redactDatabaseUrl\(value\)/, 'redactConfig must mask the databaseUrl password (redactDatabaseUrl)', ); assert.match( src, /:\$\{REDACTED\}@/, 'redactDatabaseUrl must mask the password in place (scheme://user:[REDACTED]@host)', ); // redactText — free-form log text scrubbed of the config's secret values. assert.match( src, /export function redactText\(text: string, config: Config\): string/, 'the redaction module must export redactText (scrub free-form log text of the config\'s secret values)', ); assert.match( src, /\.split\(value\)\.join\(REDACTED\)/, 'redactText must replace every occurrence of a secret value with the [REDACTED] placeholder', ); } /** * Asserts the package boundary re-exports the redaction layer. */ function assertBoundary(src) { assert.match( src, /export \{ REDACTED, SECRET_FIELD_NAMES, redactConfig, redactText \} from '\.\/redact\.js'/, 'the package boundary must re-export the redaction layer (REDACTED, SECRET_FIELD_NAMES, redactConfig, redactText)', ); } /** * Asserts the committed server logs through a redacting logger only: it * imports the redaction entry points from `@personal-blog/config`, defines * `createLogger(config)` which scrubs every joined log line with the * validated config's secret values before writing it to stdout/stderr, and * logs its resolved configuration at startup via `redactConfig` (the issue's * test plan: "log configuration and confirm secret values are redacted"). A * bare `console.log`/`console.error` would bypass the redaction and is * rejected. */ function assertServerSource(src) { // The redacting logger — every log line passes through the config // package's scrubber before it reaches stdout/stderr. assert.match( src, /import \{ assertValidConfig \} from '@personal-blog\/config'/, 'the server must import the startup validation entry point from the config package', ); assert.match( src, /import \{ redactConfig, redactText, type Config \} from '@personal-blog\/config'/, 'the server must import the redaction entry points (redactConfig, redactText) and the Config type from the config package', ); assert.match( src, /function createLogger\(config: Config\): ServerLogger/, 'the server must define the redacting logger (createLogger, seeded with the validated configuration)', ); assert.match( src, /redactText\(args\.map\(serialize\)\.join\(' '\), config\)/, 'every log line must pass through redactText (the config package\'s scrubber) before it is written', ); assert.match( src, /write\(process\.stdout, args\)/, 'log lines must be written to stdout', ); assert.match( src, /write\(process\.stderr, args\)/, 'error lines must be written to stderr', ); // The wiring — the server keeps its validated config, creates the logger // with it and logs the resolved configuration redacted. assert.match( src, /const config = assertValidConfig\(\{/, 'the server must keep its validated configuration (const config = assertValidConfig(...))', ); assert.match( src, /const logger = createLogger\(config\)/, 'the server must create the redacting logger seeded with its validated configuration', ); assert.match( src, /resolved configuration/, 'the server must log its resolved configuration at startup (the issue\'s test plan: "log configuration and confirm secret values are redacted")', ); assert.match( src, /redactConfig\(config\)/, 'the configuration log must be redacted (redactConfig) so secret values never reach the log output', ); assert.doesNotMatch( src, /console\.(log|error)\(/, 'the server must not write log output with bare console.log/console.error (they would bypass the redaction)', ); // The startup validation runs before the logger is created, so a missing // required setting still fails fast (E00-S04-T02) before any log output. const validationIndex = src.indexOf('assertValidConfig({'); const loggerIndex = src.indexOf('createLogger(config)'); assert.ok( validationIndex !== -1 && loggerIndex !== -1 && validationIndex < loggerIndex, 'the startup validation must run before the logger is created (a missing required setting is still a startup error)', ); } // --------------------------------------------------------------------------- // Criterion tests // --------------------------------------------------------------------------- test('the config package exposes the secret redaction layer (redactConfig + redactText)', () => { assert.ok(existsSync(path.join(REPO_ROOT, REDACT_SRC)), `committed ${REDACT_SRC} must exist`); assertRedactionSource(read(REDACT_SRC)); }); test('the package boundary re-exports the redaction layer', () => { assertBoundary(read(INDEX_SRC)); }); test('the server logs through the redacting logger and logs its resolved configuration redacted', () => { assertServerSource(read(SERVER_SRC)); }); test('the config-log-redaction criterion is enforced in CI', () => { // Picked up by the root test command (root `scripts.test` glob). const scripts = JSON.parse(read('package.json')).scripts ?? {}; assert.equal( scripts.test, `node --test "${ROOT_TEST_GLOB}"`, `root scripts.test must run the "${ROOT_TEST_GLOB}" glob so this suite runs with the rest`, ); // And a dedicated CI job gates it on every PR. const workflow = read('.gitea/workflows/ci.yml'); assert.ok( workflow.includes(`node --test tests/config-log-redaction.test.mjs`), `CI must run the config-log-redaction suite (job "${CI_JOB}") on every PR`, ); assert.ok( workflow.includes( 'pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build', ), 'the CI job must build the config and database-postgres packages (the probes boot the committed server which imports them)', ); }); // --------------------------------------------------------------------------- // Mutation probes — the static assertions are non-vacuous // --------------------------------------------------------------------------- test('renaming the redactText export fails the redaction assertion (mutation probe)', () => { const src = read(REDACT_SRC); const renamed = src.replace('export function redactText(', 'export function redactTextX('); assert.notEqual(renamed, src, 'the mutation must actually rename the redactText export'); assert.throws(() => assertRedactionSource(renamed), /must export redactText/); }); test('changing the [REDACTED] placeholder fails the redaction assertion (mutation probe)', () => { const src = read(REDACT_SRC); const noPlaceholder = src.replace("export const REDACTED = '[REDACTED]';", "export const REDACTED = '***';"); assert.notEqual(noPlaceholder, src, 'the mutation must actually change the placeholder'); assert.throws(() => assertRedactionSource(noPlaceholder), /\[REDACTED\] placeholder/); }); test('replacing every occurrence instead of scrubbing the whole value fails the redaction assertion (mutation probe)', () => { const src = read(REDACT_SRC); const partial = src.replace('.split(value).join(REDACTED)', '.replace(value, REDACTED)'); assert.notEqual(partial, src, 'the mutation must actually change the scrubbing'); assert.throws(() => assertRedactionSource(partial), /every occurrence/); }); test('unmasking the databaseUrl password fails the redaction assertion (mutation probe)', () => { const src = read(REDACT_SRC); const unmasked = src.replace('redactDatabaseUrl(value)', 'value'); assert.notEqual(unmasked, src, 'the mutation must actually drop the databaseUrl password masking'); assert.throws(() => assertRedactionSource(unmasked), /must mask the databaseUrl password/); }); test('dropping a redaction export from the package boundary fails the boundary assertion (mutation probe)', () => { const src = read(INDEX_SRC); const dropped = src.replace('REDACTED, SECRET_FIELD_NAMES, redactConfig, redactText', 'REDACTED, SECRET_FIELD_NAMES, redactConfig'); assert.notEqual(dropped, src, 'the mutation must actually drop the redactText export'); assert.throws(() => assertBoundary(dropped), /must re-export the redaction layer/); }); test('renaming createLogger fails the logger assertion (mutation probe)', () => { const src = read(SERVER_SRC); const renamed = src.replace('function createLogger(', 'function createLoggerX('); assert.notEqual(renamed, src, 'the mutation must actually rename the createLogger function'); assert.throws(() => assertServerSource(renamed), /must define the redacting logger/); }); test('writing a log line without redacting it fails the logger assertion (mutation probe)', () => { const src = read(SERVER_SRC); const unredacted = src.replace("redactText(args.map(serialize).join(' '), config)", "args.map(serialize).join(' ')"); assert.notEqual(unredacted, src, 'the mutation must actually drop the redactText pass-through'); assert.throws(() => assertServerSource(unredacted), /must pass through redactText/); }); test('reintroducing a bare console.log/console.error in the server fails the wiring assertion (mutation probe)', () => { const src = read(SERVER_SRC); const bareConsole = src.replaceAll('logger.log(', 'console.log(').replaceAll('logger.error(', 'console.error('); assert.notEqual(bareConsole, src, 'the mutation must actually replace the logger calls with bare console calls'); assert.throws(() => assertServerSource(bareConsole), /console\.(log|error)/); }); test('dropping the resolved-configuration log fails the wiring assertion (mutation probe)', () => { const src = read(SERVER_SRC); const noConfigLog = src.replace( "logger.log('[config] resolved configuration:', JSON.stringify(redactConfig(config)));", '', ); assert.notEqual(noConfigLog, src, 'the mutation must actually drop the resolved-configuration log'); assert.throws(() => assertServerSource(noConfigLog), /resolved configuration/); }); // --------------------------------------------------------------------------- // Deterministic behavioral probe — the compiled @personal-blog/config boundary // --------------------------------------------------------------------------- /** * How the current Node executes TypeScript sources: `default` (>= 23.6, type * stripping on by default), `strip-types-flag` (>= 22.6 via * `--experimental-strip-types`) or `null` (cannot run .ts at all). The * workspace pins engines.node to 24.x, where type stripping is stable. */ function tsExecMode() { const [major, minor] = process.versions.node.split('.').map(Number); if (major > 23 || (major === 23 && minor >= 6)) return 'default'; if (major === 22 && minor >= 6) return 'strip-types-flag'; return null; } /** True when this Node can execute the committed `.ts` server source (>= 22.6, type stripping). */ const TS_STRIPPING = tsExecMode() !== null; /** The compiled config package boundary the probes import (built by the CI job first). */ const CONFIG_DIST = existsSync(path.join(REPO_ROOT, 'packages/config', 'dist', 'index.js')); /** The compiled database-postgres package the booted server also imports. */ const DATABASE_POSTGRES_DIST = existsSync( path.join(REPO_ROOT, 'packages/database-postgres', 'dist', 'index.js'), ); /** Why the boot probes may be skipped on a clean clone without a build step. */ const BUILD_HINT = 'build the config and database-postgres packages first (pnpm --filter @personal-blog/config build && pnpm --filter @personal-blog/database-postgres build)'; /** * The boundary probe source: exercises the compiled `@personal-blog/config` * redaction boundary (`redactConfig` + `redactText`) exactly as the server's * logger consumes it — the admin-session secret is replaced by `[REDACTED]`, * the `databaseUrl` password is masked in place (and an unparseable * `databaseUrl` is replaced wholesale), free-form text is scrubbed of the * secret values, and non-secret text passes through unchanged. Written to a * temp file inside `packages/config/` so `ajv`/`@sinclair/typebox` resolve * through the package's own dependency links, then removed. */ const PROBE_SOURCE = ` import { REDACTED, redactConfig, redactText } from './dist/index.js'; const SECRET = '${SECRET}'; const URL = '${DATABASE_URL}'; const result = { placeholder: REDACTED, redactedSecret: redactConfig({ sessionSecret: SECRET }).sessionSecret, maskedUrl: redactConfig({ sessionSecret: SECRET, databaseUrl: URL }).databaseUrl, unparseableUrl: redactConfig({ sessionSecret: SECRET, databaseUrl: 'not a url' }).databaseUrl, nonSecretKept: redactConfig({ sessionSecret: SECRET, host: '0.0.0.0', port: 3000 }), scrubText: redactText('connecting with ' + SECRET + ' now', { sessionSecret: SECRET }), scrubUrl: redactText('failed at ' + URL, { sessionSecret: SECRET, databaseUrl: URL }), untouched: redactText('no secrets here', { sessionSecret: SECRET }), }; console.log('CONFIG_REDACTION_PROBE_RESULT ' + JSON.stringify(result)); `; test('the compiled boundary redacts secret values from config and text (deterministic probe)', { skip: !CONFIG_DIST ? BUILD_HINT : false }, () => { const probeFile = path.join(REPO_ROOT, 'packages/config', `.config-redaction-probe-${process.pid}.mjs`); try { writeFileSync(probeFile, PROBE_SOURCE); const run = spawnSync(process.execPath, [path.basename(probeFile)], { cwd: path.join(REPO_ROOT, 'packages/config'), encoding: 'utf8', timeout: 60_000, }); assert.equal( run.status, 0, `the probe must exit 0 (status ${run.status}):\n${(run.stderr || run.stdout || '').trim()}`, ); const match = run.stdout.match(/CONFIG_REDACTION_PROBE_RESULT (\{.*\})/); assert.ok(match, `the probe must print CONFIG_REDACTION_PROBE_RESULT:\n${run.stdout.trim()}`); const result = JSON.parse(match[1]); // The placeholder and the redacted admin-session secret. assert.equal(result.placeholder, '[REDACTED]', 'REDACTED must be the [REDACTED] placeholder'); assert.equal( result.redactedSecret, '[REDACTED]', 'redactConfig must replace the admin-session secret with [REDACTED]', ); // The databaseUrl password is masked in place; an unparseable databaseUrl // is replaced wholesale (its password cannot be isolated). assert.equal( result.maskedUrl, 'postgres://redact-user:[REDACTED]@db:5432/redact-db', `redactConfig must mask the databaseUrl password in place (got: ${JSON.stringify(result.maskedUrl)})`, ); assert.equal( result.unparseableUrl, '[REDACTED]', 'redactConfig must replace an unparseable databaseUrl wholesale (its password cannot be isolated)', ); // Non-secret fields pass through unchanged. assert.equal(result.nonSecretKept.host, '0.0.0.0', 'non-secret fields must pass through unchanged'); assert.equal(result.nonSecretKept.port, 3000, 'non-secret fields must pass through unchanged'); assert.equal(result.nonSecretKept.sessionSecret, '[REDACTED]', 'the secret field must still be redacted'); // Free-form text is scrubbed of the config's secret values. assert.equal( result.scrubText, 'connecting with [REDACTED] now', `redactText must scrub the admin-session secret from free text (got: ${JSON.stringify(result.scrubText)})`, ); assert.ok( !result.scrubText.includes(SECRET), 'redactText output must not contain the admin-session secret value', ); assert.equal( result.scrubUrl, 'failed at postgres://redact-user:[REDACTED]@db:5432/redact-db', `redactText must scrub the database password from free text (got: ${JSON.stringify(result.scrubUrl)})`, ); assert.ok( !result.scrubUrl.includes('redact-password'), 'redactText output must not contain the database password', ); assert.equal(result.untouched, 'no secrets here', 'redactText must leave non-secret text unchanged'); } finally { rmSync(probeFile, { force: true }); } }); // --------------------------------------------------------------------------- // Server-boot probes — the issue's test plan executed against the real // committed server: "log configuration and confirm secret values are // redacted" // --------------------------------------------------------------------------- /** Reserves an ephemeral TCP port, then releases it for the child to bind. */ function reservePort() { return new Promise((resolve, reject) => { const probe = createNetServer(); probe.once('error', reject); probe.listen(0, '127.0.0.1', () => { const address = probe.address(); const port = typeof address === 'object' && address !== null ? address.port : 0; probe.close(() => resolve(port)); }); }); } /** * Boots the committed server source on `port` with the given env overrides. * An inherited `DATABASE_URL` (the no-database path must be deterministic) * and `EPPP_SESSION_SECRET` (the secret must be deterministic) are stripped * unless explicitly provided. Returns `{ child, stdout, stderr }` with * closures for the captured output. */ function bootServer(port, envOverrides = {}) { const args = tsExecMode() === 'strip-types-flag' ? ['--experimental-strip-types', SERVER_SRC] : [SERVER_SRC]; const env = { ...process.env, PORT: String(port) }; delete env.DATABASE_URL; delete env.EPPP_SESSION_SECRET; Object.assign(env, envOverrides); const child = spawn(process.execPath, args, { cwd: REPO_ROOT, env, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.on('data', (chunk) => { stdout += String(chunk); }); child.stderr.on('data', (chunk) => { stderr += String(chunk); }); return { child, stdout: () => stdout, stderr: () => stderr }; } /** * Polls `GET /health` until the server answers with ANY status, the child * exits, or the deadline passes. Returns the fetch Response. */ async function waitForAnswer(port, child, stderr, deadlineMs = 10_000) { const deadline = Date.now() + deadlineMs; let lastError = ''; while (Date.now() < deadline) { if (child.exitCode !== null) { throw new Error( `the server exited before answering GET /health (code ${child.exitCode}): ${stderr().trim()}`, ); } try { return await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1_000), }); } catch (err) { lastError = err instanceof Error ? err.message : String(err); await delay(100); } } throw new Error( `GET /health did not answer within ${deadlineMs}ms (last error: ${lastError}; server stderr: ${stderr().trim()})`, ); } /** Waits until the captured log output matches `pattern` (or the deadline passes). */ async function waitForLog(readOutput, pattern, deadlineMs = 5_000) { const deadline = Date.now() + deadlineMs; while (Date.now() < deadline) { if (pattern.test(readOutput())) return true; await delay(100); } return false; } /** Kills a booted child (SIGTERM, then SIGKILL if needed) and waits for exit. */ async function stopChild(child) { if (child.exitCode !== null || child.signalCode !== null) return; child.kill('SIGTERM'); await Promise.race([once(child, 'exit'), delay(2_000)]); if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); } test('booting the server logs the resolved configuration with secret values redacted (server boot probe)', { skip: !TS_STRIPPING || !CONFIG_DIST || !DATABASE_POSTGRES_DIST ? BUILD_HINT : false }, async () => { // The issue's test plan: "log configuration and confirm secret values are // redacted". The committed server logs its resolved configuration at // startup through the redacting logger — the admin-session secret must // appear as [REDACTED], and the secret value must not appear in the log // output at all. const port = await reservePort(); const { child, stdout, stderr } = bootServer(port, { EPPP_SESSION_SECRET: SECRET }); // DATABASE_URL stripped try { const response = await waitForAnswer(port, child, stderr); assert.equal( response.status, 200, `the server must boot to GET /health 200 (got ${response.status}); server output: ${stdout().trim()} ${stderr().trim()}`, ); // The resolved configuration is logged, with the secret redacted. assert.match( stdout(), /\[config\] resolved configuration:/, `the server must log its resolved configuration at startup (got: ${stdout().trim()})`, ); assert.match( stdout(), /"sessionSecret":"\[REDACTED\]"/, `the resolved-configuration log must show the admin-session secret redacted (got: ${stdout().trim()})`, ); // No secret value in the log output (the acceptance criterion). assert.ok( !(stdout() + stderr()).includes(SECRET), `the log output must not contain the admin-session secret value (got: ${stdout().trim()} ${stderr().trim()})`, ); } finally { await stopChild(child); } }); test('the log output contains no database password when a DATABASE_URL is configured (server boot probe)', { skip: !TS_STRIPPING || !CONFIG_DIST || !DATABASE_POSTGRES_DIST ? BUILD_HINT : false }, async () => { // With a DATABASE_URL whose password must never leak, the startup // migration run cannot complete (nothing listens on the dead port), so the // app stays not-ready — and the log output (the resolved-configuration log // AND the migration-failure log) must contain the masked URL, never the // password and never the raw connection string. const port = await reservePort(); const deadPort = await reservePort(); // reserved then released: nothing listens const databaseUrl = `postgres://redact-user:redact-password@127.0.0.1:${deadPort}/redact-db`; const { child, stdout, stderr } = bootServer(port, { EPPP_SESSION_SECRET: SECRET, DATABASE_URL: databaseUrl, }); try { const response = await waitForAnswer(port, child, stderr); assert.equal( response.status, 503, `the app must stay not-ready while the migration run cannot complete (got ${response.status}); server output: ${stdout().trim()} ${stderr().trim()}`, ); // The resolved-configuration log masks the databaseUrl password in place. assert.match( stdout(), new RegExp(`postgres://redact-user:${'\\[REDACTED\\]'}@127\\.0\\.0\\.1:`), `the resolved-configuration log must show the databaseUrl password masked in place (got: ${stdout().trim()})`, ); // Wait for the migration-failure log (also written through the redacting logger). assert.ok( await waitForLog(() => stdout() + stderr(), /startup migration run failed; app stays not-ready/), `the app must log the failed startup migration run (got: ${stdout().trim()} ${stderr().trim()})`, ); const output = stdout() + stderr(); assert.ok( !output.includes('redact-password'), `the log output must not contain the database password (got: ${output.trim()})`, ); assert.ok( !output.includes('postgres://redact-user:redact-password@'), `the log output must not contain the raw connection string with its password (got: ${output.trim()})`, ); assert.ok( !output.includes(SECRET), `the log output must not contain the admin-session secret value (got: ${output.trim()})`, ); } finally { await stopChild(child); } });