fix: clarify durable Postgres blocker diagnostics

Host commander merge after PR workflow closeout. Reviewed PR #230 against current main; D601 closeout task codex_1779506316145_1 recommended MERGE. Verification recorded: GitHub CLEAN/MERGEABLE; merge-tree clean over current main; focused PR tests node --check plus node --test internal/db/runtime-store.test.mjs internal/cloud/server.test.mjs scripts/src/dev-runtime-migration.test.mjs passed 33/33; synthetic merge including scripts/artifact-runtime-readiness-guard.test.mjs passed 40/40. This merge does not claim live DEV deployment or M3/M4/M5 acceptance; runtime durability remains blocked until deployed and separately verified.
This commit is contained in:
Lyon
2026-05-23 11:26:48 +08:00
committed by GitHub
9 changed files with 577 additions and 25 deletions
+3 -3
View File
@@ -59,10 +59,10 @@ checks:
- read readiness against the runtime tables used by audit and evidence
queries.
If DB auth, schema, migration, or read readiness is missing, `/health/live`
If DB SSL negotiation, auth, schema, migration, or read readiness is missing, `/health/live`
stays degraded with `runtime.durable: false`, `runtime.durableRequested: true`,
and a runtime blocker such as `runtime_durable_adapter_auth_blocked`,
`runtime_durable_adapter_schema_blocked`, or
and a runtime blocker such as `runtime_durable_adapter_ssl_blocked`,
`runtime_durable_adapter_auth_blocked`, `runtime_durable_adapter_schema_blocked`, or
`runtime_durable_adapter_migration_blocked`. Fixture data and env presence are
never promoted to `runtime.liveRuntimeEvidence`.
+1 -1
View File
@@ -180,7 +180,7 @@ proven through the configured adapter.
`dbLiveEvidenceObserved=true` with
`dbLiveEvidenceIsDurabilityEvidence=false` means the DB TCP/live layer is
separate from runtime persistence. If runtime durability is blocked,
`blockedLayer` names the current adapter/auth/schema/migration/durability query
`blockedLayer` names the current adapter/ssl/auth/schema/migration/durability query
layer, and `requiredEvidence` remains the durable adapter schema, migration,
and read query contract.
+6
View File
@@ -242,6 +242,12 @@ function buildRedactionSafety(connection) {
async function probeDbConnection(env, options) {
const timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS);
if (typeof options.probe === "function") {
return options.probe({
endpointSource: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
timeoutMs
});
}
const parsed = parseDbUrlContract(env?.HWLAB_CLOUD_DB_URL);
if (!parsed.ok) {
return {
+4
View File
@@ -5,6 +5,7 @@ import {
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_DURABLE_ADAPTER_UNCONFIGURED,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_STORE_KIND_POSTGRES
@@ -190,6 +191,9 @@ function classifyRuntimeDurabilityLayer(runtime = {}) {
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
return "auth";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
return "ssl";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return "schema";
}
+199 -1
View File
@@ -5,8 +5,11 @@ import test from "node:test";
import { createCloudApiServer } from "./server.mjs";
import {
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
} from "../db/runtime-store.mjs";
test("cloud api exposes /health, /health/live, and /live probes", async () => {
@@ -399,6 +402,142 @@ test("cloud api health keeps DB live evidence separate when durable adapter quer
}
});
test("cloud api health contract distinguishes durable SSL, auth, schema, migration, and query blockers", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
process.env.HWLAB_CLOUD_DB_URL = "redacted";
process.env.HWLAB_CLOUD_DB_SSL_MODE = "require";
const cases = [
{
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
blockedLayer: "ssl",
queryResult: "ssl_negotiation_blocked",
gates: {
ssl: "blocked",
auth: "not_checked",
schema: "not_checked",
migration: "not_checked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
blockedLayer: "auth",
queryResult: "auth_blocked",
gates: {
ssl: "ready",
auth: "blocked",
schema: "not_checked",
migration: "not_checked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
blockedLayer: "schema",
queryResult: "schema_blocked",
gates: {
ssl: "ready",
auth: "ready",
schema: "blocked",
migration: "not_checked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
blockedLayer: "migration",
queryResult: "migration_blocked",
gates: {
ssl: "ready",
auth: "ready",
schema: "ready",
migration: "blocked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
blockedLayer: "durability_query",
queryResult: "query_blocked",
gates: {
ssl: "ready",
auth: "ready",
schema: "ready",
migration: "ready",
durability: "blocked"
}
}
];
try {
for (const testCase of cases) {
const server = createCloudApiServer({
dbProbe: {
probe: async ({ endpointSource, timeoutMs }) => ({
attempted: true,
networkAttempted: true,
endpointSource,
probeType: "tcp-connect",
endpointRedacted: true,
valueRedacted: true,
timeoutMs,
durationMs: 0,
result: "connected",
classification: "tcp_connected",
errorCode: null,
missingEnv: []
})
},
runtimeStore: {
async readiness() {
return durableBlockedRuntime(testCase);
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
const payload = await response.json();
assert.equal(response.status, 200);
assert.equal(payload.status, "degraded");
assert.equal(payload.db.ready, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.runtime.blocker, testCase.blocker);
assert.equal(payload.runtime.connection.queryResult, testCase.queryResult);
assert.equal(payload.runtime.gates.ssl.status, testCase.gates.ssl);
assert.equal(payload.runtime.gates.auth.status, testCase.gates.auth);
assert.equal(payload.runtime.gates.schema.status, testCase.gates.schema);
assert.equal(payload.runtime.gates.migration.status, testCase.gates.migration);
assert.equal(payload.runtime.gates.durability.status, testCase.gates.durability);
assert.equal(payload.runtime.durabilityContract.blockedLayer, testCase.blockedLayer);
assert.equal(payload.readiness.durability.blockedLayer, testCase.blockedLayer);
assert.equal(payload.readiness.durability.queryResult, testCase.queryResult);
assert.equal(payload.readiness.durability.secretMaterialRead, false);
assert.ok(payload.blockerCodes.includes(testCase.blocker));
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
}
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
}
});
test("cloud api health does not treat DB env presence-only as live readiness", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
@@ -453,6 +592,65 @@ test("cloud api health does not treat DB env presence-only as live readiness", a
}
});
function durableBlockedRuntime({ blocker, blockedLayer, queryResult, gates }) {
return {
adapter: "postgres",
durable: false,
durableRequested: true,
durableCapable: false,
ready: false,
status: "blocked",
blocker,
reason: `test durable runtime blocker ${blocker}`,
liveRuntimeEvidence: false,
fixtureEvidence: false,
connection: {
queryAttempted: true,
queryResult,
endpointRedacted: true,
valueRedacted: true,
errorCode: "TEST_BLOCKER"
},
schema: {
checked: gates.schema !== "not_checked",
ready: gates.schema === "ready",
missingTables: gates.schema === "blocked" ? ["gateway_sessions"] : [],
missingColumns: gates.schema === "blocked" ? ["gateway_sessions.gateway_session_json"] : []
},
migration: {
checked: gates.migration !== "not_checked",
ready: gates.migration === "ready",
missing: gates.migration !== "ready"
},
gates: Object.fromEntries(Object.entries(gates).map(([name, status]) => [
name,
{
checked: status !== "not_checked",
ready: status === "ready",
status,
blocker: status === "blocked" ? blocker : null
}
])),
durabilityContract: {
ready: false,
status: "blocked",
requiredEvidence: "runtime_adapter_schema_migration_read_query",
dbLiveEvidenceIsDurabilityEvidence: false,
liveRuntimeEvidence: false,
adapterQueryRequired: true,
blockedLayer,
blocker,
secretMaterialRead: false
},
safety: {
secretMaterialRead: false,
valuesRedacted: true,
endpointRedacted: true
},
counts: {}
};
}
test("cloud api /v1 describes Code Agent provider blocker without leaking secret values", async () => {
const server = createCloudApiServer({
env: {
+129 -17
View File
@@ -23,6 +23,7 @@ export const RUNTIME_STORE_KIND_POSTGRES = "postgres";
export const RUNTIME_DURABLE_ADAPTER_MISSING = "runtime_durable_adapter_missing";
export const RUNTIME_DURABLE_ADAPTER_UNCONFIGURED = "runtime_durable_adapter_unconfigured";
export const RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING = "runtime_durable_adapter_driver_missing";
export const RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED = "runtime_durable_adapter_ssl_blocked";
export const RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED = "runtime_durable_adapter_auth_blocked";
export const RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED = "runtime_durable_adapter_schema_blocked";
export const RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED = "runtime_durable_adapter_migration_blocked";
@@ -629,6 +630,7 @@ export class PostgresCloudRuntimeStore {
queryResult: "schema_blocked"
},
gates: runtimeGates({
ssl: readyGate(),
auth: readyGate(),
schema: blockedGate({ checked: true, blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED }),
migration: notCheckedGate(),
@@ -643,32 +645,41 @@ export class PostgresCloudRuntimeStore {
migration = await this.readMigrationReadiness();
} catch (error) {
const classified = classifyRuntimeDbError(error);
const migrationReadBlocker = [
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
].includes(classified.blocker)
? classified.blocker
: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED;
this.lastReadiness = this.blockedSummary({
blocker: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? classified.blocker
: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
reason: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
blocker: migrationReadBlocker,
reason: migrationReadBlocker === classified.blocker
? classified.reason
: "Postgres runtime schema is present, but the durable runtime migration ledger could not be proven",
schema,
migration: blockedRuntimeMigration({ errorCode: classified.connection?.errorCode }),
connection: {
queryAttempted: true,
queryResult: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED ? "auth_blocked" : "migration_blocked",
queryResult: migrationReadBlocker === classified.blocker
? classified.connection?.queryResult
: "migration_blocked",
errorCode: classified.connection?.errorCode ?? null
},
gates: runtimeGates({
ssl: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: readyGate(),
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? notCheckedGate()
: readyGate(),
schema: readyGate(),
migration: blockedGate({
checked: true,
blocker: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? classified.blocker
: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED
blocker: migrationReadBlocker
}),
durability: blockedGate({ checked: false, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED })
durability: blockedGate({ checked: false, blocker: migrationReadBlocker })
})
});
return this.summary();
@@ -685,6 +696,7 @@ export class PostgresCloudRuntimeStore {
queryResult: "migration_blocked"
},
gates: runtimeGates({
ssl: readyGate(),
auth: readyGate(),
schema: readyGate(),
migration: blockedGate({ checked: true, blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED }),
@@ -709,9 +721,14 @@ export class PostgresCloudRuntimeStore {
errorCode: classified.connection?.errorCode ?? null
},
gates: runtimeGates({
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
ssl: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: readyGate(),
auth: classified.blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED
? blockedGate({ checked: true, blocker: classified.blocker })
: classified.blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
? notCheckedGate()
: readyGate(),
schema: readyGate(),
migration: readyGate(),
durability: blockedGate({ checked: true, blocker: classified.blocker })
@@ -740,6 +757,7 @@ export class PostgresCloudRuntimeStore {
schema,
migration,
gates: runtimeGates({
ssl: readyGate(),
auth: readyGate(),
schema: readyGate(),
migration: readyGate(),
@@ -1158,13 +1176,9 @@ export class PostgresCloudRuntimeStore {
schema: blockedSchema,
migration: blockedMigration,
gates: gates ?? runtimeGates({
auth: connection?.queryAttempted ? blockedGate({ checked: true, blocker }) : notCheckedGate(),
schema: blockedSchema.checked
? gateFromReadiness(blockedSchema.ready, { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED })
: notCheckedGate(),
migration: blockedMigration.checked
? gateFromReadiness(blockedMigration.ready, { blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED })
: notCheckedGate(),
...defaultRuntimeGates({ blocker, connection, schema: blockedSchema, migration: blockedMigration }),
schema: defaultSchemaGate({ blocker, schema: blockedSchema }),
migration: defaultMigrationGate({ blocker, migration: blockedMigration }),
durability: blockedGate({ checked: false, blocker })
}),
safety: runtimeSafety(),
@@ -1335,6 +1349,9 @@ function durabilityBlockedLayer(summary = {}) {
) {
return "adapter";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
return "ssl";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
return "auth";
}
@@ -1373,6 +1390,17 @@ function classifyRuntimeDbError(error) {
}
};
}
if (isRuntimeDbSslError(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
reason: "Postgres runtime adapter reached the DB driver but SSL negotiation or SSL mode compatibility blocked the schema query",
connection: {
queryAttempted: true,
queryResult: "ssl_negotiation_blocked",
errorCode: code
}
};
}
if (["28P01", "28000", "42501"].includes(code)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -1447,6 +1475,37 @@ function durableRuntimeReadBlockedData(method, readiness = {}) {
});
}
function isRuntimeDbSslError(error) {
const code = typeof error?.code === "string" ? error.code : "";
if (code.startsWith("ERR_SSL") || code.startsWith("ERR_TLS")) {
return true;
}
if ([
"DEPTH_ZERO_SELF_SIGNED_CERT",
"SELF_SIGNED_CERT_IN_CHAIN",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED"
].includes(code)) {
return true;
}
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
return [
"does not support ssl",
"ssl is not enabled",
"ssl negotiation",
"ssl off",
"ssl required",
"requires ssl",
"no ssl encryption",
"no encryption",
"hostssl",
"server requires ssl",
"before secure tls connection",
"tls handshake"
].some((pattern) => message.includes(pattern));
}
function summarizeRuntimeSchema(rows) {
const columnsByTable = new Map();
for (const row of rows) {
@@ -1506,12 +1565,14 @@ function blockedRuntimeMigration({ errorCode = null } = {}) {
}
function runtimeGates({
ssl = notCheckedGate(),
auth = notCheckedGate(),
schema = notCheckedGate(),
migration = notCheckedGate(),
durability = notCheckedGate()
} = {}) {
return {
ssl,
auth,
schema,
migration,
@@ -1519,6 +1580,57 @@ function runtimeGates({
};
}
function defaultRuntimeGates({ blocker, connection, schema, migration } = {}) {
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
return {
ssl: blockedGate({ checked: true, blocker }),
auth: notCheckedGate()
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
return {
ssl: readyGate(),
auth: blockedGate({ checked: true, blocker })
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return {
ssl: connection?.queryAttempted ? readyGate() : notCheckedGate(),
auth: connection?.queryAttempted ? readyGate() : notCheckedGate()
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED && schema?.checked && migration?.checked) {
return {
ssl: readyGate(),
auth: readyGate()
};
}
return {
ssl: notCheckedGate(),
auth: notCheckedGate()
};
}
function defaultSchemaGate({ blocker, schema } = {}) {
if (schema?.checked) {
return gateFromReadiness(schema.ready, { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED });
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return blockedGate({ checked: true, blocker });
}
return notCheckedGate();
}
function defaultMigrationGate({ blocker, migration } = {}) {
if (migration?.checked) {
return gateFromReadiness(migration.ready, { blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED });
}
if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) {
return blockedGate({ checked: true, blocker });
}
return notCheckedGate();
}
function readyGate() {
return {
checked: true,
+127 -1
View File
@@ -9,6 +9,7 @@ import {
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_STORE_KIND_POSTGRES,
createCloudRuntimeStore,
createConfiguredCloudRuntimeStore
@@ -69,6 +70,97 @@ test("configured postgres runtime classifies auth blocker before schema and migr
assert.equal(readiness.durabilityContract.dbLiveEvidenceIsDurabilityEvidence, false);
});
test("configured postgres runtime classifies schema driver errors separately from query failures", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
},
queryClient: {
async query(sql) {
assert.match(sql, /information_schema\.columns/u);
const error = new Error("schema missing");
error.code = "3F000";
throw error;
}
}
});
const readiness = await store.readiness();
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
assert.equal(readiness.connection.queryAttempted, true);
assert.equal(readiness.connection.queryResult, "schema_blocked");
assert.equal(readiness.gates.ssl.status, "ready");
assert.equal(readiness.gates.auth.status, "ready");
assert.equal(readiness.gates.schema.status, "blocked");
assert.equal(readiness.gates.migration.status, "not_checked");
assert.equal(readiness.durabilityContract.blockedLayer, "schema");
});
test("configured postgres runtime classifies SSL negotiation blocker separately from auth", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
},
queryClient: {
async query(sql) {
assert.match(sql, /information_schema\.columns/u);
const error = new Error("The server does not support SSL connections");
error.code = "08P01";
throw error;
}
}
});
const readiness = await store.readiness();
assert.equal(readiness.adapter, RUNTIME_STORE_KIND_POSTGRES);
assert.equal(readiness.durable, false);
assert.equal(readiness.ready, false);
assert.equal(readiness.status, "blocked");
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED);
assert.equal(readiness.connection.queryAttempted, true);
assert.equal(readiness.connection.queryResult, "ssl_negotiation_blocked");
assert.equal(readiness.connection.errorCode, "08P01");
assert.equal(readiness.gates.ssl.status, "blocked");
assert.equal(readiness.gates.auth.status, "not_checked");
assert.equal(readiness.gates.schema.status, "not_checked");
assert.equal(readiness.gates.migration.status, "not_checked");
assert.equal(readiness.durabilityContract.ready, false);
assert.equal(readiness.durabilityContract.blockedLayer, "ssl");
assert.equal(readiness.durabilityContract.secretMaterialRead, false);
assert.equal(JSON.stringify(readiness).includes("The server does not support SSL connections"), false);
});
test("configured postgres runtime classifies pg_hba no-encryption rejection as SSL before auth", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
},
sslMode: "disable",
queryClient: {
async query(sql) {
assert.match(sql, /information_schema\.columns/u);
const error = new Error('no pg_hba.conf entry for host "[redacted]", user "[redacted]", database "[redacted]", no encryption');
error.code = "28000";
throw error;
}
}
});
const readiness = await store.readiness();
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED);
assert.equal(readiness.connection.queryAttempted, true);
assert.equal(readiness.connection.queryResult, "ssl_negotiation_blocked");
assert.equal(readiness.connection.errorCode, "28000");
assert.equal(readiness.gates.ssl.status, "blocked");
assert.equal(readiness.gates.auth.status, "not_checked");
assert.equal(readiness.durabilityContract.blockedLayer, "ssl");
assert.equal(readiness.durabilityContract.dbLiveEvidenceIsDurabilityEvidence, false);
assert.equal(JSON.stringify(readiness).includes("pg_hba.conf"), false);
});
test("configured postgres runtime reports schema blocker without green readiness", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
@@ -133,6 +225,30 @@ test("configured postgres runtime keeps durable false when migration ledger is m
assert.equal(readiness.durabilityContract.dbLiveEvidenceIsDurabilityEvidence, false);
});
test("configured postgres runtime classifies migration query failures separately from generic readiness queries", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
},
queryClient: createFakePostgresClient({ migrationErrorCode: "57014" })
});
const readiness = await store.readiness();
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED);
assert.equal(readiness.schema.ready, true);
assert.equal(readiness.migration.checked, true);
assert.equal(readiness.migration.errorCode, "57014");
assert.equal(readiness.connection.queryAttempted, true);
assert.equal(readiness.connection.queryResult, "migration_blocked");
assert.equal(readiness.gates.ssl.status, "ready");
assert.equal(readiness.gates.auth.status, "ready");
assert.equal(readiness.gates.schema.status, "ready");
assert.equal(readiness.gates.migration.status, "blocked");
assert.equal(readiness.gates.durability.status, "not_checked");
assert.equal(readiness.durabilityContract.blockedLayer, "migration");
});
test("configured postgres runtime classifies durable read query blocker after schema and migration", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
@@ -430,7 +546,12 @@ test("configured postgres runtime persists and queries records through query cli
assert.equal(evidence.records[0].operationId, invoke.operationId);
});
function createFakePostgresClient({ migrationReady = true, countErrorCode = null, readErrorCode = null } = {}) {
function createFakePostgresClient({
migrationReady = true,
migrationErrorCode = null,
countErrorCode = null,
readErrorCode = null
} = {}) {
const state = {
gateway_sessions: new Map(),
box_resources: new Map(),
@@ -465,6 +586,11 @@ function createFakePostgresClient({ migrationReady = true, countErrorCode = null
return { rows: [{ count: state[table]?.size ?? 0 }] };
}
if (sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations")) {
if (migrationErrorCode) {
const error = new Error("migration query failed");
error.code = migrationErrorCode;
throw error;
}
const row = state.hwlab_schema_migrations.get(params[0]);
return { rows: row ? [row] : [] };
}
+41 -1
View File
@@ -16,7 +16,8 @@ import {
RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
} from "../../internal/db/runtime-store.mjs";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
@@ -572,6 +573,7 @@ function conclusionReadySummary(report) {
}
function blockerScope(blocker) {
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) return "runtime-migration-ssl";
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) return "runtime-migration-auth";
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) return "runtime-migration-schema";
if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) return "runtime-migration-ledger";
@@ -588,6 +590,13 @@ function classifyMigrationError(error) {
summary: "Postgres driver is unavailable for DEV runtime migration apply."
};
}
if (isMigrationSslError(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
errorCode,
summary: "DEV runtime migration reached Postgres, but SSL negotiation or SSL mode compatibility blocked apply."
};
}
if (["28P01", "28000", "42501"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -609,6 +618,37 @@ function classifyMigrationError(error) {
};
}
function isMigrationSslError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "";
if (errorCode.startsWith("ERR_SSL") || errorCode.startsWith("ERR_TLS")) {
return true;
}
if ([
"DEPTH_ZERO_SELF_SIGNED_CERT",
"SELF_SIGNED_CERT_IN_CHAIN",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED"
].includes(errorCode)) {
return true;
}
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
return [
"does not support ssl",
"ssl is not enabled",
"ssl negotiation",
"ssl off",
"ssl required",
"requires ssl",
"no ssl encryption",
"no encryption",
"hostssl",
"server requires ssl",
"before secure tls connection",
"tls handshake"
].some((pattern) => message.includes(pattern));
}
async function writeReport(report, reportPath, root) {
const absolute = path.resolve(root, reportPath);
await mkdir(path.dirname(absolute), { recursive: true });
+67 -1
View File
@@ -55,7 +55,6 @@ test("live dry-run separates migration ledger blocker from auth and schema", asy
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: {
HWLAB_CLOUD_DB_URL: "postgresql://hwlab:redacted@example.invalid:5432/hwlab",
HWLAB_CLOUD_DB_SSL_MODE: "disable"
},
queryClient: createFakeQueryClient({ migrationReady: false }),
@@ -74,6 +73,73 @@ test("live dry-run separates migration ledger blocker from auth and schema", asy
assert.equal(report.runtime.migration.missing, true);
});
test("live dry-run separates SSL negotiation blocker from auth and schema", async () => {
const report = await buildDevRuntimeMigrationReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: {
HWLAB_CLOUD_DB_SSL_MODE: "require"
},
queryClient: {
async query(sql) {
assert.match(sql, /information_schema\.columns/u);
const error = new Error("The server does not support SSL connections");
error.code = "08P01";
throw error;
}
},
now: () => "2026-05-22T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.actions.liveDbReadAttempted, true);
assert.equal(report.actions.liveDbWriteAttempted, false);
assert.equal(report.gates.auth.status, "not_checked");
assert.equal(report.gates.schema.status, "not_checked");
assert.equal(report.gates.migration.status, "not_checked");
assert.equal(report.gates.readiness.status, "not_checked");
assert.equal(report.blockers[0].scope, "runtime-migration-ssl");
assert.equal(report.blockers[0].evidence.blocker, "runtime_durable_adapter_ssl_blocked");
assert.equal(report.runtime.blocker, "runtime_durable_adapter_ssl_blocked");
assert.equal(report.runtime.connection.queryResult, "ssl_negotiation_blocked");
assert.equal(JSON.stringify(report).includes("The server does not support SSL connections"), false);
});
test("live dry-run classifies pg_hba no-encryption rejection as SSL with ssl disabled", async () => {
const report = await buildDevRuntimeMigrationReport(
parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]),
{
env: {
HWLAB_CLOUD_DB_SSL_MODE: "disable"
},
queryClient: {
async query(sql) {
assert.match(sql, /information_schema\.columns/u);
const error = new Error('no pg_hba.conf entry for host "[redacted]", user "[redacted]", database "[redacted]", no encryption');
error.code = "28000";
throw error;
}
},
now: () => "2026-05-22T00:00:00.000Z"
}
);
assert.equal(report.conclusion.status, "blocked");
assert.equal(report.actions.liveDbReadAttempted, true);
assert.equal(report.actions.liveDbWriteAttempted, false);
assert.equal(report.gates.auth.status, "not_checked");
assert.equal(report.gates.schema.status, "not_checked");
assert.equal(report.gates.migration.status, "not_checked");
assert.equal(report.gates.readiness.status, "not_checked");
assert.equal(report.blockers[0].scope, "runtime-migration-ssl");
assert.equal(report.blockers[0].evidence.blocker, "runtime_durable_adapter_ssl_blocked");
assert.equal(report.runtime.blocker, "runtime_durable_adapter_ssl_blocked");
assert.equal(report.runtime.connection.queryResult, "ssl_negotiation_blocked");
assert.equal(report.runtime.connection.errorCode, "28000");
assert.equal(JSON.stringify(report).includes("pg_hba.conf"), false);
});
test("apply mode records ledger and verifies durable runtime readiness", async () => {
const queryClient = createFakeQueryClient({ migrationReady: false });
const report = await buildDevRuntimeMigrationReport(