import assert from "node:assert/strict"; import test from "node:test"; import { buildDevRuntimeMigrationReport, parseArgs } from "./dev-runtime-migration.mjs"; import { CLOUD_CORE_MIGRATION_ID, CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION, CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS } from "../../internal/db/schema.mjs"; test("source check is non-secret and does not attempt live DB access", async () => { const report = await buildDevRuntimeMigrationReport(parseArgs(["--check"]), { env: { HWLAB_CLOUD_DB_URL: "postgresql://hwlab:secret-password@db.example.test:5432/hwlab", HWLAB_CLOUD_DB_SSL_MODE: "disable" }, now: () => "2026-05-22T00:00:00.000Z" }); assert.equal(report.conclusion.status, "ready"); assert.equal(report.actions.liveDbReadAttempted, false); assert.equal(report.actions.liveDbWriteAttempted, false); assert.equal(report.gates.auth.status, "not_checked"); assert.equal(report.gates.schema.status, "ready"); assert.equal(report.gates.migration.status, "ready"); assert.equal(report.gates.readiness.status, "not_checked"); assert.equal(report.safety.sourceStoresSecretValues, false); assert.equal(report.safety.printsSecretValues, false); assert.equal(JSON.stringify(report).includes("secret-password"), false); assert.equal(JSON.stringify(report).includes("db.example.test"), false); }); test("apply mode refuses missing DEV confirmation before touching query client", async () => { const queryClient = createFakeQueryClient({ migrationReady: false }); const report = await buildDevRuntimeMigrationReport(parseArgs(["--apply"]), { env: { HWLAB_CLOUD_DB_URL: "postgresql://hwlab:redacted@example.invalid:5432/hwlab", HWLAB_CLOUD_DB_SSL_MODE: "disable" }, queryClient, now: () => "2026-05-22T00:00:00.000Z" }); assert.equal(report.conclusion.status, "blocked"); assert.equal(report.safetyRefusal, true); assert.equal(report.actions.liveDbWriteAttempted, false); assert.equal(queryClient.calls.length, 0); }); test("live dry-run separates migration ledger 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: "disable" }, queryClient: createFakeQueryClient({ migrationReady: false }), 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, "ready"); assert.equal(report.gates.schema.status, "ready"); assert.equal(report.gates.migration.status, "blocked"); assert.equal(report.gates.readiness.status, "not_checked"); assert.equal(report.blockers[0].scope, "runtime-migration-ledger"); 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 fixture 28P01 as auth before schema or migration", 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("fixture auth failure"); error.code = "28P01"; throw error; } }, now: () => "2026-05-23T00: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.checked, true); assert.equal(report.gates.auth.status, "blocked"); assert.equal(report.gates.schema.checked, false); assert.equal(report.gates.schema.status, "not_checked"); assert.equal(report.gates.migration.checked, false); assert.equal(report.gates.migration.status, "not_checked"); assert.equal(report.gates.readiness.checked, false); assert.equal(report.gates.readiness.status, "not_checked"); assert.equal(report.blockers[0].scope, "runtime-migration-auth"); assert.equal(report.blockers[0].evidence.blocker, "runtime_durable_adapter_auth_blocked"); assert.equal(report.runtime.blocker, "runtime_durable_adapter_auth_blocked"); assert.equal(report.runtime.connection.queryResult, "auth_blocked"); assert.equal(report.runtime.connection.errorCode, "28P01"); assert.equal(report.safety.fixtureEvidenceAllowed, false); assert.equal(JSON.stringify(report).includes("fixture auth failure"), 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("live dry-run classifies missing runtime schema before migration ledger", 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); return { rows: [] }; } }, now: () => "2026-05-23T00: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, "ready"); assert.equal(report.gates.schema.status, "blocked"); assert.equal(report.gates.migration.status, "not_checked"); assert.equal(report.gates.readiness.status, "not_checked"); assert.equal(report.blockers[0].scope, "runtime-migration-schema"); assert.equal(report.blockers[0].evidence.blocker, "runtime_durable_adapter_schema_blocked"); assert.equal(report.runtime.schema.ready, false); assert.ok(report.runtime.schema.missingTables.includes("gateway_sessions")); }); test("live dry-run classifies durability read query blocker after schema and ledger are ready", async () => { const report = await buildDevRuntimeMigrationReport( parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]), { env: { HWLAB_CLOUD_DB_SSL_MODE: "disable" }, queryClient: createFakeQueryClient({ migrationReady: true, countErrorCode: "57014" }), now: () => "2026-05-23T00: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, "ready"); assert.equal(report.gates.schema.status, "ready"); assert.equal(report.gates.migration.status, "ready"); assert.equal(report.gates.readiness.status, "blocked"); assert.equal(report.blockers[0].scope, "runtime-migration-readiness"); assert.equal(report.blockers[0].evidence.blocker, "runtime_durable_adapter_query_blocked"); assert.equal(report.blockers[0].evidence.queryResult, "query_blocked"); assert.equal(report.runtime.migration.ready, true); assert.equal(report.runtime.connection.queryResult, "query_blocked"); }); test("apply mode records ledger and verifies durable runtime readiness", async () => { const queryClient = createFakeQueryClient({ migrationReady: false }); const report = await buildDevRuntimeMigrationReport( parseArgs(["--apply", "--confirm-dev", "--confirmed-non-production"]), { env: { HWLAB_CLOUD_DB_URL: "postgresql://hwlab:redacted@example.invalid:5432/hwlab", HWLAB_CLOUD_DB_SSL_MODE: "disable" }, queryClient, now: () => "2026-05-22T00:00:00.000Z" } ); assert.equal(report.conclusion.status, "ready"); assert.equal(report.actions.liveDbWriteAttempted, true); assert.equal(report.actions.migrationApplied, true); assert.equal(report.actions.readinessVerified, true); assert.equal(report.gates.auth.status, "ready"); assert.equal(report.gates.schema.status, "ready"); assert.equal(report.gates.migration.status, "ready"); assert.equal(report.gates.readiness.status, "ready"); assert.equal(report.runtime.migration.appliedMigrationId, CLOUD_CORE_MIGRATION_ID); assert.equal(report.runtime.migration.appliedSchemaVersion, CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION); assert.ok(queryClient.calls.some((call) => call.sql === "BEGIN")); assert.ok(queryClient.calls.some((call) => call.sql.includes("INSERT INTO hwlab_schema_migrations"))); assert.ok(queryClient.calls.some((call) => call.sql === "COMMIT")); assert.equal(JSON.stringify(report).includes("redacted@example.invalid"), false); }); test("live dry-run closes pg pool after readiness verification", async () => { const backingClient = createFakeQueryClient({ migrationReady: true }); const pools = []; 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: "disable" }, pgModuleLoader: async () => createFakePgModule({ backingClient, pools }), now: () => "2026-05-22T00:00:00.000Z" } ); assert.equal(report.conclusion.status, "ready"); assert.equal(pools.length, 1); assert.ok(pools[0].calls.some((call) => call.target === "pool" && call.sql.includes("information_schema.columns"))); assert.ok(pools[0].calls.some((call) => call.type === "end")); }); test("live dry-run normalizes pg pool ssl config before readiness verification", async () => { const backingClient = createFakeQueryClient({ migrationReady: true }); const pools = []; const report = await buildDevRuntimeMigrationReport( parseArgs(["--dry-run", "--allow-live-db-read", "--confirm-dev"]), { env: { HWLAB_CLOUD_DB_URL: "postgresql://hwlab:fixture-pass@db.example.test:5432/hwlab?sslmode=require", HWLAB_CLOUD_DB_SSL_MODE: "disable", HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS: "1600" }, pgModuleLoader: async () => createFakePgModule({ backingClient, pools }), now: () => "2026-05-22T00:00:00.000Z" } ); assert.equal(report.conclusion.status, "ready"); assert.equal(pools.length, 1); assert.equal(pools[0].config.ssl, false); assert.equal(pools[0].config.connectionTimeoutMillis, 1600); assert.equal(new URL(pools[0].config.connectionString).searchParams.get("sslmode"), null); assert.equal(JSON.stringify(report).includes("fixture-pass"), false); assert.equal(JSON.stringify(report).includes("db.example.test"), false); }); test("apply mode uses a dedicated pg client transaction", async () => { const backingClient = createFakeQueryClient({ migrationReady: false }); const pools = []; const report = await buildDevRuntimeMigrationReport( parseArgs(["--apply", "--confirm-dev", "--confirmed-non-production"]), { env: { HWLAB_CLOUD_DB_URL: "postgresql://hwlab:redacted@example.invalid:5432/hwlab", HWLAB_CLOUD_DB_SSL_MODE: "disable" }, pgModuleLoader: async () => createFakePgModule({ backingClient, pools }), now: () => "2026-05-22T00:00:00.000Z" } ); assert.equal(report.conclusion.status, "ready"); assert.equal(pools.length, 2); assert.ok(pools[0].calls.some((call) => call.type === "connect")); assert.ok(pools[0].calls.some((call) => call.target === "client" && call.sql === "BEGIN")); assert.ok(pools[0].calls.some((call) => call.target === "client" && call.sql.includes("INSERT INTO hwlab_schema_migrations"))); assert.ok(pools[0].calls.some((call) => call.target === "client" && call.sql === "COMMIT")); assert.equal(pools[0].calls.some((call) => call.target === "pool"), false); assert.ok(pools[0].calls.some((call) => call.type === "release")); assert.ok(pools[0].calls.some((call) => call.type === "end")); assert.ok(pools[1].calls.some((call) => call.target === "pool" && call.sql.includes("information_schema.columns"))); assert.ok(pools[1].calls.some((call) => call.type === "end")); }); function createFakeQueryClient({ migrationReady, countErrorCode = null }) { const state = { migrationReady, calls: [] }; return { get calls() { return state.calls; }, async query(sql, params = []) { state.calls.push({ sql, params }); if (sql === "BEGIN" || sql === "COMMIT" || sql === "ROLLBACK") { return { rows: [] }; } if (sql.includes("INSERT INTO hwlab_schema_migrations")) { state.migrationReady = true; return { rows: [] }; } if (sql.includes("information_schema.columns")) { return { rows: schemaRows() }; } if (sql.startsWith("SELECT id, schema_version FROM hwlab_schema_migrations")) { return { rows: state.migrationReady ? [ { id: CLOUD_CORE_MIGRATION_ID, schema_version: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION } ] : [] }; } if (sql.startsWith("SELECT COUNT(*)::int AS count FROM ")) { if (countErrorCode) { const error = new Error("durability count blocked"); error.code = countErrorCode; throw error; } return { rows: [{ count: 0 }] }; } throw new Error(`unexpected query: ${sql}`); } }; } function createFakePgModule({ backingClient, pools }) { return { Pool: class FakePool { constructor(config) { this.config = config; this.calls = []; pools.push(this); } async connect() { this.calls.push({ type: "connect" }); return { query: async (sql, params = []) => { this.calls.push({ target: "client", sql, params }); return backingClient.query(sql, params); }, release: () => { this.calls.push({ type: "release" }); } }; } async query(sql, params = []) { this.calls.push({ target: "pool", sql, params }); return backingClient.query(sql, params); } async end() { this.calls.push({ type: "end" }); } } }; } function schemaRows() { return Object.entries(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS).flatMap(([table, columns]) => columns.map((column) => ({ table_name: table, column_name: column })) ); }