test: update suites locked to the old direct process.env wiring for the adapter (E00-S04-T04)

This commit is contained in:
implementer
2026-08-30 04:17:22 +00:00
parent 20173a8241
commit 3214807c9d
4 changed files with 79 additions and 44 deletions
+2 -2
View File
@@ -157,7 +157,7 @@ function assertReadinessGate(src) {
* after migrations finish. * after migrations finish.
*/ */
function assertReadyAfterRun(src) { function assertReadyAfterRun(src) {
const dbUrlIndex = src.indexOf('const databaseUrl = process.env.DATABASE_URL'); const dbUrlIndex = src.indexOf('const databaseUrl = config.databaseUrl');
assert.ok(dbUrlIndex !== -1, 'the server must read DATABASE_URL for the startup migration path'); assert.ok(dbUrlIndex !== -1, 'the server must read DATABASE_URL for the startup migration path');
const elseStart = src.indexOf('} else {', dbUrlIndex); const elseStart = src.indexOf('} else {', dbUrlIndex);
assert.ok(elseStart !== -1, 'the DATABASE_URL-configured startup path must exist (else branch)'); assert.ok(elseStart !== -1, 'the DATABASE_URL-configured startup path must exist (else branch)');
@@ -191,7 +191,7 @@ function assertReadyAfterRun(src) {
* non-container path and the E00-S02-T03 health endpoint working). * non-container path and the E00-S02-T03 health endpoint working).
*/ */
function assertNoDatabaseUrlPath(src) { function assertNoDatabaseUrlPath(src) {
const startup = src.slice(src.indexOf('const databaseUrl = process.env.DATABASE_URL')); const startup = src.slice(src.indexOf('const databaseUrl = config.databaseUrl'));
assert.match( assert.match(
startup, startup,
/if \(databaseUrl === undefined\) \{/, /if \(databaseUrl === undefined\) \{/,
+17 -10
View File
@@ -159,8 +159,8 @@ function assertServerSource(src) {
// package's scrubber before it reaches stdout/stderr. // package's scrubber before it reaches stdout/stderr.
assert.match( assert.match(
src, src,
/import \{ assertValidConfig \} from '@personal-blog\/config'/, /import \{ loadConfigFromEnv \} from '@personal-blog\/config'/,
'the server must import the startup validation entry point from the config package', 'the server must import the environment adapter (loadConfigFromEnv) from the config package',
); );
assert.match( assert.match(
src, src,
@@ -188,12 +188,13 @@ function assertServerSource(src) {
'error lines must be written to stderr', 'error lines must be written to stderr',
); );
// The wiring — the server keeps its validated config, creates the logger // The wiring — the server keeps its validated config (loaded through the
// with it and logs the resolved configuration redacted. // environment adapter, E00-S04-T04), creates the logger with it and logs
// the resolved configuration redacted.
assert.match( assert.match(
src, src,
/const config = assertValidConfig\(\{/, /const config = loadConfigFromEnv\(\);/,
'the server must keep its validated configuration (const config = assertValidConfig(...))', 'the server must keep its validated configuration (const config = loadConfigFromEnv(), E00-S04-T04)',
); );
assert.match( assert.match(
src, src,
@@ -215,13 +216,19 @@ function assertServerSource(src) {
/console\.(log|error)\(/, /console\.(log|error)\(/,
'the server must not write log output with bare console.log/console.error (they would bypass the redaction)', '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 assert.doesNotMatch(
// required setting still fails fast (E00-S04-T02) before any log output. src,
const validationIndex = src.indexOf('assertValidConfig({'); /process\.env\.[A-Z_]+/,
'the server must not read process.env directly (all settings flow through the config adapter, E00-S04-T04)',
);
// The startup configuration loads through the adapter (which validates it)
// before the logger is created, so a missing required setting still fails
// fast (E00-S04-T02) before any log output.
const validationIndex = src.indexOf('loadConfigFromEnv(');
const loggerIndex = src.indexOf('createLogger(config)'); const loggerIndex = src.indexOf('createLogger(config)');
assert.ok( assert.ok(
validationIndex !== -1 && loggerIndex !== -1 && validationIndex < loggerIndex, 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)', 'the startup configuration must load (and validate) before the logger is created (a missing required setting is still a startup error)',
); );
} }
+45 -30
View File
@@ -6,17 +6,20 @@
* - "missing required setting gives a field-specific startup error" → the * - "missing required setting gives a field-specific startup error" → the
* `packages/config` package exposes the startup validation entry point * `packages/config` package exposes the startup validation entry point
* (`assertValidConfig`, building on the E00-S04-T01 TypeBox/Ajv schema) * (`assertValidConfig`, building on the E00-S04-T01 TypeBox/Ajv schema)
* and the committed `apps/server/src/index.ts` calls it before the server * and the environment adapter (`loadConfigFromEnv`, E00-S04-T04 — the
* config package's single owner of `process.env` reads) validates the
* mapped environment through it; the committed `apps/server/src/index.ts`
* loads its startup configuration through the adapter before the server
* binds, so a deployment missing a required setting (the admin-session * binds, so a deployment missing a required setting (the admin-session
* secret `EPPP_SESSION_SECRET` — the schema's required field, * secret `EPPP_SESSION_SECRET` — the schema's required field,
* Security-and-Operations §32/§26) fails fast at startup instead of * Security-and-Operations §32/§26) fails fast at startup instead of
* booting with an invalid configuration. Locked in statically (mutation * booting with an invalid configuration. Locked in statically (mutation
* probes prove non-vacuity: dropping the startup validation call, * probes prove non-vacuity: dropping the adapter call, moving it after
* moving it after the bind, or dropping the compose/Dockerfile support * the bind, or dropping the compose/Dockerfile support all fail) and
* all fail) and behaviorally by the deterministic probes (the issue's * behaviorally by the deterministic probes (the issue's test plan: "start
* test plan: "start with a missing required field and confirm the error * with a missing required field and confirm the error names it" — booting
* names it" — booting the committed server without `EPPP_SESSION_SECRET` * the committed server without `EPPP_SESSION_SECRET` exits non-zero with
* exits non-zero with the error naming the missing field). * the error naming the missing field).
* - "the error names the missing field" → a missing required setting throws * - "the error names the missing field" → a missing required setting throws
* `MissingRequiredSettingError` whose message and `missingField` name the * `MissingRequiredSettingError` whose message and `missingField` name the
* missing field (e.g. `"missing required setting: sessionSecret"`); other * missing field (e.g. `"missing required setting: sessionSecret"`); other
@@ -149,33 +152,35 @@ function assertStartupErrorSource(src) {
} }
/** /**
* Asserts the committed server validates the required settings at startup: * Asserts the committed server loads its startup configuration through the
* it imports `assertValidConfig` from `@personal-blog/config` and calls it * environment adapter (E00-S04-T04) before it binds: it imports
* with the parsed startup configuration (including the required * `loadConfigFromEnv` from `@personal-blog/config` and calls it (the adapter
* `EPPP_SESSION_SECRET`) BEFORE the server binds — so a missing required * validates the mapped environment with `assertValidConfig`, including the
* setting is a startup error, never a silently-booted invalid configuration. * required `EPPP_SESSION_SECRET`) BEFORE `server.listen` — so a missing
* required setting is a startup error, never a silently-booted invalid
* configuration, and the server itself never reads `process.env` directly.
*/ */
function assertServerStartupValidation(src) { function assertServerStartupValidation(src) {
assert.match( assert.match(
src, src,
/import \{ assertValidConfig \} from '@personal-blog\/config'/, /import \{ loadConfigFromEnv \} from '@personal-blog\/config'/,
'the server must import the startup validation entry point from the config package', 'the server must import the environment adapter (loadConfigFromEnv) from the config package',
); );
assert.match( assert.match(
src, src,
/assertValidConfig\(\{/, /const config = loadConfigFromEnv\(\);/,
'the server must call assertValidConfig with its startup configuration', 'the server must load its startup configuration through the environment adapter (const config = loadConfigFromEnv())',
); );
assert.match( assert.doesNotMatch(
src, src,
/sessionSecret: process\.env\.EPPP_SESSION_SECRET/, /process\.env\.[A-Z_]+/,
'the server must feed the required admin-session secret (EPPP_SESSION_SECRET) into the startup validation', 'the server must not read process.env directly (all settings flow through the config adapter, E00-S04-T04)',
); );
const callIndex = src.indexOf('assertValidConfig({'); const callIndex = src.indexOf('loadConfigFromEnv(');
const listenIndex = src.indexOf('server.listen('); const listenIndex = src.indexOf('server.listen(');
assert.ok( assert.ok(
callIndex !== -1 && listenIndex !== -1 && callIndex < listenIndex, callIndex !== -1 && listenIndex !== -1 && callIndex < listenIndex,
'the startup validation must run before the server binds (server.listen) so a missing required setting is a startup error', 'the startup configuration must load through the adapter before the server binds (server.listen) so a missing required setting is a startup error',
); );
} }
@@ -274,25 +279,35 @@ test('the config-startup-error criterion is enforced in CI', () => {
// Mutation probes — the static assertions are non-vacuous // Mutation probes — the static assertions are non-vacuous
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
test('dropping the startup validation call fails the server wiring assertion (mutation probe)', () => { test('dropping the adapter call fails the server wiring assertion (mutation probe)', () => {
const src = read(SERVER_SRC); const src = read(SERVER_SRC);
const withoutCall = src.replace('assertValidConfig({\n', 'assertValidConfigx({\n'); const withoutCall = src.replace('const config = loadConfigFromEnv();', 'const configX = loadConfigFromEnv();');
assert.notEqual(withoutCall, src, 'the mutation must actually replace the assertValidConfig call'); assert.notEqual(withoutCall, src, 'the mutation must actually break the loadConfigFromEnv wiring');
assert.throws(() => assertServerStartupValidation(withoutCall), /must call assertValidConfig/); assert.throws(() => assertServerStartupValidation(withoutCall), /must load its startup configuration/);
}); });
test('moving the startup validation after the server binds fails the order assertion (mutation probe)', () => { test('moving the adapter call after the server binds fails the order assertion (mutation probe)', () => {
const src = read(SERVER_SRC); const src = read(SERVER_SRC);
const moved = src const moved = src
.replace(/assertValidConfig\(\{\n host: '0\.0\.0\.0',\n port: PORT,\n databaseUrl: process\.env\.DATABASE_URL,\n sessionSecret: process\.env\.EPPP_SESSION_SECRET,\n\}\);\n/, '') .replace('const config = loadConfigFromEnv();\n', '')
.replace( .replace(
'server.listen(PORT, () => {', 'server.listen(config.port, () => {',
'server.listen(PORT, () => {\n assertValidConfig({ sessionSecret: process.env.EPPP_SESSION_SECRET });', 'server.listen(config.port, () => {\n const config = loadConfigFromEnv();',
); );
assert.notEqual(moved, src, 'the mutation must actually move the validation call after the bind'); assert.notEqual(moved, src, 'the mutation must actually move the adapter call after the bind');
assert.throws(() => assertServerStartupValidation(moved), /before the server binds/); assert.throws(() => assertServerStartupValidation(moved), /before the server binds/);
}); });
test('the server reading process.env directly fails the no-direct-read assertion (mutation probe)', () => {
const src = read(SERVER_SRC);
const directRead = src.replace(
'const config = loadConfigFromEnv();',
'const config = loadConfigFromEnv();\nconst PORT = process.env.PORT;',
);
assert.notEqual(directRead, src, 'the mutation must actually add a direct process.env read');
assert.throws(() => assertServerStartupValidation(directRead), /must not read process\.env/);
});
test('an error message that does not name the missing field fails the naming assertion (mutation probe)', () => { test('an error message that does not name the missing field fails the naming assertion (mutation probe)', () => {
const src = read(STARTUP_SRC); const src = read(STARTUP_SRC);
const noField = src.replace(/missing required setting: \$\{missingField\}/g, 'missing required setting'); const noField = src.replace(/missing required setting: \$\{missingField\}/g, 'missing required setting');
+15 -2
View File
@@ -73,8 +73,8 @@ function assertHealthEndpointSource(src) {
); );
assert.match( assert.match(
src, src,
/3000/, /server\.listen\(config\.port/,
'the server must default to the application port 3000 (Dockerfile EXPOSE / compose :3000)', 'the server must bind the port from the validated configuration (config.port, E00-S04-T04)',
); );
} }
@@ -189,6 +189,19 @@ test('the endpoint reports a healthy application (committed health payload is {"
); );
}); });
test('the application port default (3000) lives in the config adapter (E00-S04-T04)', () => {
// The server binds `config.port` (asserted above); the port default (3000,
// matching the Dockerfile EXPOSE / compose :3000) and the PORT override
// live in the config package's environment adapter — the single owner of
// process.env reads.
const envSrc = read('packages/config/src/env.ts');
assert.match(
envSrc,
/3000/,
'the config adapter must default the application port to 3000 (Dockerfile EXPOSE / compose :3000)',
);
});
test('an HTTP smoke test against the booted server succeeds for GET /health (200 + healthy body)', async (t) => { test('an HTTP smoke test against the booted server succeeds for GET /health (200 + healthy body)', async (t) => {
if (!tsExecMode()) { if (!tsExecMode()) {
t.skip( t.skip(