fix: clarify durable postgres blocker diagnostics

This commit is contained in:
Code Queue Review
2026-05-23 02:30:15 +00:00
parent 905b3df74c
commit a7e12abea0
8 changed files with 501 additions and 24 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.
+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";
}
+192 -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,135 @@ 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;
const fakeDb = createTcpServer((socket) => socket.end());
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
const dbPort = fakeDb.address().port;
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test:password@127.0.0.1:${dbPort}/hwlab`;
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({
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));
assert.equal(JSON.stringify(payload).includes("password"), false);
assert.equal(JSON.stringify(payload).includes("127.0.0.1"), false);
assert.equal(JSON.stringify(payload).includes(String(dbPort)), false);
} 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;
}
await new Promise((resolve, reject) => {
fakeDb.close((error) => (error ? reject(error) : resolve()));
});
}
});
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 +585,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,
+102 -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,71 @@ 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"
},
dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab",
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"
},
dbUrl: "postgres://hwlab_secret:supersecret@db.example.invalid:5432/hwlab",
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("supersecret"), false);
assert.equal(JSON.stringify(readiness).includes("db.example.invalid"), false);
});
test("configured postgres runtime reports schema blocker without green readiness", async () => {
const store = createConfiguredCloudRuntimeStore({
env: {
@@ -133,6 +199,31 @@ 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"
},
dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab",
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 +521,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 +561,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] : [] };
}
+36 -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,32 @@ 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",
"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 });
@@ -74,6 +74,40 @@ 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_URL: "postgresql://hwlab:redacted@example.invalid:5432/hwlab",
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("redacted@example.invalid"), false);
});
test("apply mode records ledger and verifies durable runtime readiness", async () => {
const queryClient = createFakeQueryClient({ migrationReady: false });
const report = await buildDevRuntimeMigrationReport(