fix: add DEV runtime migration automation

This commit is contained in:
Code Queue Review
2026-05-22 23:57:09 +00:00
parent 7de6edd2c4
commit 63d8f7b5a6
7 changed files with 906 additions and 2 deletions
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import {
formatDevRuntimeMigrationFailure,
runDevRuntimeMigrationCli
} from "./src/dev-runtime-migration.mjs";
try {
process.exitCode = await runDevRuntimeMigrationCli(process.argv.slice(2));
} catch (error) {
process.stdout.write(`${JSON.stringify(formatDevRuntimeMigrationFailure(error), null, 2)}\n`);
process.exitCode = 1;
}
+646
View File
@@ -0,0 +1,646 @@
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "../../internal/db/schema.mjs";
import {
PostgresCloudRuntimeStore,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_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";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const migrationPath = "internal/db/migrations/0001_cloud_core_skeleton.sql";
const defaultReportPath = "reports/dev-gate/dev-runtime-migration-report.json";
const issue = "pikasTech/HWLAB#164";
export async function runDevRuntimeMigrationCli(argv = process.argv.slice(2), options = {}) {
const args = parseArgs(argv);
if (args.help) {
process.stdout.write(`${usage()}\n`);
return 0;
}
const report = await buildDevRuntimeMigrationReport(args, options);
if (args.writeReport) {
await writeReport(report, args.reportPath, options.repoRoot ?? repoRoot);
}
const printable = args.pretty || args.writeReport ? report : summarizeReport(report);
process.stdout.write(`${JSON.stringify(printable, null, 2)}\n`);
if (report.safetyRefusal || (args.failOnBlocked && report.conclusion.status !== "ready")) {
return 1;
}
return 0;
}
export async function buildDevRuntimeMigrationReport(args = {}, options = {}) {
const parsed = normalizeArgs(args);
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
const root = options.repoRoot ?? repoRoot;
const sql = await readFile(path.join(root, migrationPath), "utf8");
const sourceCheck = validateMigrationSource(sql);
const db = summarizeDbEnv(env);
const report = {
issue,
mode: parsed.mode,
generatedAt: now(),
target: {
environment: ENVIRONMENT_DEV,
namespace: DEV_DB_ENV_CONTRACT.dns.namespace,
prodAllowed: false,
dbUrlEnv: "HWLAB_CLOUD_DB_URL",
dbUrlSecretRef: DEV_DB_ENV_CONTRACT.secretRefs[0],
dbEndpointAuthority: DEV_DB_ENV_CONTRACT.endpointAuthority.source
},
migration: {
id: CLOUD_CORE_MIGRATION_ID,
path: migrationPath,
sha256: sha256(sql),
schemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
ledgerTable: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
runtimeAdapter: "postgres"
},
db,
sourceCheck,
applyBoundary: buildApplyBoundary(parsed),
safety: buildSafety(parsed),
actions: {
sourceValidated: sourceCheck.ready,
liveDbReadAttempted: false,
liveDbWriteAttempted: false,
migrationApplied: false,
readinessVerified: false
},
gates: sourceOnlyGates(sourceCheck),
blockers: [],
safetyRefusal: false
};
if (!sourceCheck.ready) {
addBlocker(report, "contract_blocker", "runtime-migration-source", "Runtime migration source does not match the durable runtime schema contract.", {
missing: sourceCheck.missing
});
}
if (parsed.mode === "check" || (parsed.mode === "dry-run" && !parsed.allowLiveDbRead)) {
return finalizeReport(report);
}
if (parsed.mode === "dry-run") {
if (!parsed.confirmDev) {
addSafetyRefusal(report, "dry-run live DB verification requires --confirm-dev.");
return finalizeReport(report);
}
if (!db.urlPresent && !options.queryClient) {
addBlocker(report, "runtime_blocker", "runtime-migration-auth", "Live DB read verification requires HWLAB_CLOUD_DB_URL from the DEV Secret reference.", {
env: "HWLAB_CLOUD_DB_URL",
valueRedacted: true
});
return finalizeReport(report);
}
await verifyRuntimeReadiness(report, env, options);
return finalizeReport(report);
}
if (parsed.mode === "apply") {
if (!parsed.confirmDev || !parsed.confirmedNonProduction) {
addSafetyRefusal(report, "DEV runtime migration apply requires --confirm-dev and --confirmed-non-production.");
return finalizeReport(report);
}
if (!db.urlPresent && !options.queryClient) {
addBlocker(report, "runtime_blocker", "runtime-migration-auth", "Runtime migration apply requires HWLAB_CLOUD_DB_URL from the DEV Secret reference.", {
env: "HWLAB_CLOUD_DB_URL",
valueRedacted: true
});
return finalizeReport(report);
}
if (!sourceCheck.ready) {
addSafetyRefusal(report, "Refusing to apply because source migration validation is blocked.");
return finalizeReport(report);
}
await applyMigration(report, sql, env, options);
if (!report.safetyRefusal && report.actions.migrationApplied) {
await verifyRuntimeReadiness(report, env, options);
}
return finalizeReport(report);
}
addSafetyRefusal(report, `Unsupported runtime migration mode: ${parsed.mode}`);
return finalizeReport(report);
}
export function parseArgs(argv = []) {
const args = {
mode: "check",
allowLiveDbRead: false,
confirmDev: false,
confirmedNonProduction: false,
writeReport: false,
reportPath: defaultReportPath,
pretty: false,
failOnBlocked: false,
help: false
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--check") args.mode = "check";
else if (arg === "--dry-run") args.mode = "dry-run";
else if (arg === "--apply") args.mode = "apply";
else if (arg === "--allow-live-db-read") args.allowLiveDbRead = true;
else if (arg === "--confirm-dev") args.confirmDev = true;
else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true;
else if (arg === "--write-report") args.writeReport = true;
else if (arg === "--pretty") args.pretty = true;
else if (arg === "--fail-on-blocked") args.failOnBlocked = true;
else if (arg === "--help" || arg === "-h") args.help = true;
else if (arg === "--report") {
index += 1;
args.reportPath = requireArgValue(argv[index], "--report");
} else if (arg.startsWith("--report=")) {
args.reportPath = requireArgValue(arg.slice("--report=".length), "--report");
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
return args;
}
export function validateMigrationSource(sql) {
const missing = [];
for (const [table, columns] of Object.entries(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS)) {
const tableMatch = sql.match(new RegExp(`CREATE TABLE IF NOT EXISTS ${table}\\s*\\(([\\s\\S]*?)\\);`, "u"));
if (!tableMatch) {
missing.push(`table:${table}`);
continue;
}
for (const column of columns) {
if (!new RegExp(`\\b${column}\\b`, "u").test(tableMatch[1])) {
missing.push(`column:${table}.${column}`);
}
}
}
const ledgerMatch = sql.match(
new RegExp(`CREATE TABLE IF NOT EXISTS ${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}\\s*\\(([\\s\\S]*?)\\);`, "u")
);
if (!ledgerMatch) {
missing.push(`table:${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}`);
} else {
for (const column of CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS) {
if (!new RegExp(`\\b${column}\\b`, "u").test(ledgerMatch[1])) {
missing.push(`column:${CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE}.${column}`);
}
}
}
if (!new RegExp(`'${CLOUD_CORE_MIGRATION_ID}'`, "u").test(sql)) {
missing.push(`ledger-id:${CLOUD_CORE_MIGRATION_ID}`);
}
if (!new RegExp(`'${CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION}'`, "u").test(sql)) {
missing.push(`schema-version:${CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION}`);
}
if (!/ON CONFLICT\s*\(id\)\s*DO UPDATE/iu.test(sql)) {
missing.push("ledger-upsert:on-conflict-id");
}
return {
ready: missing.length === 0,
checked: true,
migrationPath,
requiredMigrationId: CLOUD_CORE_MIGRATION_ID,
requiredSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
requiredTables: Object.keys(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS),
requiredLedgerTable: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
missing,
connectsToDatabase: false,
fixtureEvidence: false
};
}
function normalizeArgs(args) {
return {
...parseArgs([]),
...args
};
}
async function verifyRuntimeReadiness(report, env, options) {
report.actions.liveDbReadAttempted = true;
const runtimeStore = new PostgresCloudRuntimeStore({
env,
dbUrl: env?.HWLAB_CLOUD_DB_URL,
sslMode: env?.HWLAB_CLOUD_DB_SSL_MODE,
queryClient: options.queryClient,
pgModuleLoader: options.pgModuleLoader
});
const runtime = await runtimeStore.readiness();
report.runtime = summarizeRuntime(runtime);
report.actions.readinessVerified = runtime.ready === true;
report.gates = gatesFromRuntime(runtime);
if (runtime.ready !== true) {
addBlocker(report, "runtime_blocker", blockerScope(runtime.blocker), runtime.reason ?? "Runtime durable readiness is blocked.", {
blocker: runtime.blocker ?? "runtime_durable_adapter_blocked",
adapter: runtime.adapter,
durable: Boolean(runtime.durable),
schemaReady: Boolean(runtime.schema?.ready),
migrationReady: Boolean(runtime.migration?.ready),
readinessReady: runtime.ready === true,
queryResult: runtime.connection?.queryResult ?? "unknown",
secretValuesPrinted: false
});
}
}
async function applyMigration(report, sql, env, options) {
report.actions.liveDbWriteAttempted = true;
let client;
let closeClient = async () => {};
try {
if (options.queryClient) {
client = options.queryClient;
} else {
const pool = await createPgPool(env, options);
client = pool;
closeClient = async () => {
await pool.end();
};
}
await client.query("BEGIN");
await client.query(sql);
await client.query("COMMIT");
report.actions.migrationApplied = true;
} catch (error) {
await rollbackQuietly(client);
const classified = classifyMigrationError(error);
addBlocker(report, "runtime_blocker", blockerScope(classified.blocker), classified.summary, {
blocker: classified.blocker,
errorCode: classified.errorCode,
secretValuesPrinted: false
});
} finally {
await closeClient();
}
}
async function createPgPool(env, options) {
let pg;
try {
pg = await (options.pgModuleLoader ? options.pgModuleLoader() : import("pg"));
} catch (error) {
if (error?.code === "ERR_MODULE_NOT_FOUND") {
const driverError = new Error("Postgres migration apply requires the pg package");
driverError.code = "HWLAB_PG_DRIVER_MISSING";
throw driverError;
}
throw error;
}
const Pool = pg.Pool ?? pg.default?.Pool;
if (typeof Pool !== "function") {
const driverError = new Error("Postgres migration apply could not load pg.Pool");
driverError.code = "HWLAB_PG_DRIVER_MISSING";
throw driverError;
}
return new Pool({
connectionString: env.HWLAB_CLOUD_DB_URL,
ssl: env.HWLAB_CLOUD_DB_SSL_MODE === "disable" ? false : { rejectUnauthorized: false },
connectionTimeoutMillis: normalizeTimeoutMs(env.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS)
});
}
async function rollbackQuietly(client) {
if (!client) return;
try {
await client.query("ROLLBACK");
} catch {
// The original migration failure is reported without leaking connection details.
}
}
function gatesFromRuntime(runtime = {}) {
const gates = runtime.gates ?? {};
return {
auth: normalizeGate(gates.auth),
schema: normalizeGate(gates.schema),
migration: normalizeGate(gates.migration),
readiness: normalizeGate(gates.durability)
};
}
function sourceOnlyGates(sourceCheck) {
return {
auth: notCheckedGate("source-only validation does not read DB credentials or connect to Postgres"),
schema: sourceCheck.ready
? readyGate("source migration declares required durable runtime tables and columns")
: blockedGate("runtime_durable_schema_source_blocked"),
migration: sourceCheck.ready
? readyGate("source migration declares the required ledger upsert")
: blockedGate("runtime_durable_migration_source_blocked"),
readiness: notCheckedGate("runtime readiness requires a separate authorized DEV DB read or apply")
};
}
function normalizeGate(gate = {}) {
if (gate.ready === true) return readyGate();
if (gate.status === "blocked") return blockedGate(gate.blocker ?? "runtime_durable_adapter_blocked");
return notCheckedGate();
}
function readyGate(summary = "ready") {
return {
checked: true,
ready: true,
status: "ready",
blocker: null,
summary
};
}
function blockedGate(blocker, summary = "blocked") {
return {
checked: true,
ready: false,
status: "blocked",
blocker,
summary
};
}
function notCheckedGate(summary = "not checked") {
return {
checked: false,
ready: false,
status: "not_checked",
blocker: null,
summary
};
}
function summarizeRuntime(runtime = {}) {
return {
adapter: runtime.adapter ?? "unknown",
durable: Boolean(runtime.durable),
durableRequested: Boolean(runtime.durableRequested),
ready: runtime.ready === true,
status: runtime.status ?? "unknown",
blocker: runtime.blocker ?? null,
liveRuntimeEvidence: Boolean(runtime.liveRuntimeEvidence),
fixtureEvidence: Boolean(runtime.fixtureEvidence),
connection: {
queryAttempted: Boolean(runtime.connection?.queryAttempted),
queryResult: runtime.connection?.queryResult ?? "unknown",
endpointRedacted: true,
valueRedacted: true,
errorCode: runtime.connection?.errorCode ?? null
},
schema: summarizeRuntimeSchema(runtime.schema),
migration: summarizeRuntimeMigration(runtime.migration)
};
}
function summarizeRuntimeSchema(schema = {}) {
return {
checked: Boolean(schema.checked),
ready: Boolean(schema.ready),
requiredTables: Array.isArray(schema.requiredTables) ? [...schema.requiredTables] : Object.keys(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS),
missingTables: Array.isArray(schema.missingTables) ? [...schema.missingTables] : [],
missingColumns: Array.isArray(schema.missingColumns) ? [...schema.missingColumns] : []
};
}
function summarizeRuntimeMigration(migration = {}) {
return {
checked: Boolean(migration.checked),
ready: Boolean(migration.ready),
table: migration.table ?? CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
requiredMigrationId: migration.requiredMigrationId ?? CLOUD_CORE_MIGRATION_ID,
requiredSchemaVersion: migration.requiredSchemaVersion ?? CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
appliedMigrationId: migration.appliedMigrationId ?? null,
appliedSchemaVersion: migration.appliedSchemaVersion ?? null,
missing: migration.missing !== false
};
}
function summarizeDbEnv(env = {}) {
const urlPresent = typeof env.HWLAB_CLOUD_DB_URL === "string" && env.HWLAB_CLOUD_DB_URL.trim().length > 0;
const sslMode = env.HWLAB_CLOUD_DB_SSL_MODE || DEV_DB_ENV_CONTRACT.nonSecretDefaults.HWLAB_CLOUD_DB_SSL_MODE;
return {
requiredEnv: DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => ({
name,
present: name === "HWLAB_CLOUD_DB_URL" ? urlPresent : typeof env[name] === "string" && env[name].trim().length > 0,
redacted: name === "HWLAB_CLOUD_DB_URL",
source: name === "HWLAB_CLOUD_DB_URL" ? "k8s-secret-ref" : "runtime-env"
})),
urlPresent,
urlValueRedacted: true,
sslMode,
sslModeSecret: false,
secretRef: DEV_DB_ENV_CONTRACT.secretRefs[0],
endpointAuthority: DEV_DB_ENV_CONTRACT.endpointAuthority.source,
optionalPublicDnsAliasRequired: DEV_DB_ENV_CONTRACT.dns.requiredForReadiness
};
}
function buildApplyBoundary(args) {
return {
defaultMode: "check",
mode: args.mode,
liveDbReads: args.mode === "apply" || (args.mode === "dry-run" && args.allowLiveDbRead),
liveDbWrites: args.mode === "apply",
requiresForLiveDbRead: ["--confirm-dev", "HWLAB_CLOUD_DB_URL from hwlab-cloud-api-dev-db/database-url"],
requiresForApply: ["--apply", "--confirm-dev", "--confirmed-non-production", "HWLAB_CLOUD_DB_URL from hwlab-cloud-api-dev-db/database-url"],
writeScope: args.mode === "apply"
? [
"DEV Postgres schema objects declared by internal/db/migrations/0001_cloud_core_skeleton.sql",
"DEV hwlab_schema_migrations ledger row 0001_cloud_core_skeleton"
]
: [],
noWriteScope: [
"Kubernetes Secret resources",
"PROD",
"service restarts",
"M3/M4/M5 acceptance"
],
forbidden: [
"printing DB URLs, passwords, tokens, Secret values, or kubeconfig material",
"using fixture output as DEV-LIVE runtime evidence",
"mutating PROD"
]
};
}
function buildSafety(args) {
return {
devOnly: true,
environment: ENVIRONMENT_DEV,
targetNamespace: DEV_DB_ENV_CONTRACT.dns.namespace,
prodAllowed: false,
sourceStoresSecretValues: false,
printsSecretValues: false,
dbUrlValueRedacted: true,
endpointRedacted: true,
readsKubernetesSecrets: false,
writesKubernetesSecrets: false,
liveDbWrites: args.mode === "apply",
fixtureEvidenceAllowed: false
};
}
function addBlocker(report, type, scope, summary, evidence = {}) {
report.blockers.push({
type,
scope,
status: "open",
summary,
sourceIssue: scope.includes("auth") ? "pikasTech/HWLAB#49" : issue,
evidence
});
}
function addSafetyRefusal(report, summary) {
report.safetyRefusal = true;
addBlocker(report, "safety_refusal", "runtime-migration-apply-boundary", summary, {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
});
}
function finalizeReport(report) {
const openBlockers = report.blockers.filter((blocker) => blocker.status === "open");
report.conclusion = {
status: openBlockers.length === 0 ? "ready" : "blocked",
summary: openBlockers.length === 0
? conclusionReadySummary(report)
: "DEV runtime migration automation is blocked; see separated auth/schema/migration/readiness blockers.",
blockerCount: openBlockers.length
};
return report;
}
function conclusionReadySummary(report) {
if (report.mode === "apply") {
return "DEV runtime migration applied and durable runtime readiness was verified without printing secret values.";
}
if (report.mode === "dry-run" && report.actions.liveDbReadAttempted) {
return "DEV runtime migration read-only verification passed without DB writes or secret output.";
}
return "Runtime migration source contract is ready; no DB connection or live write was attempted.";
}
function blockerScope(blocker) {
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";
if (blocker === RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING) return "runtime-migration-driver";
return "runtime-migration-readiness";
}
function classifyMigrationError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "UNKNOWN";
if (errorCode === "HWLAB_PG_DRIVER_MISSING") {
return {
blocker: RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
errorCode,
summary: "Postgres driver is unavailable for DEV runtime migration apply."
};
}
if (["28P01", "28000", "42501"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
errorCode,
summary: "DEV runtime migration reached Postgres but auth or authorization blocked apply."
};
}
if (["42P01", "42703", "3F000"].includes(errorCode)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
errorCode,
summary: "DEV runtime migration apply was blocked by schema/search_path state."
};
}
return {
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
errorCode,
summary: "DEV runtime migration apply could not complete."
};
}
async function writeReport(report, reportPath, root) {
const absolute = path.resolve(root, reportPath);
await mkdir(path.dirname(absolute), { recursive: true });
await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`);
}
function summarizeReport(report) {
return {
issue: report.issue,
mode: report.mode,
conclusion: report.conclusion,
migration: report.migration,
actions: report.actions,
gates: report.gates,
blockers: report.blockers,
safety: report.safety
};
}
function normalizeTimeoutMs(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : 1200;
}
function requireArgValue(value, flag) {
if (typeof value !== "string" || value.trim() === "") {
throw new Error(`${flag} requires a value`);
}
return value;
}
function sha256(value) {
return createHash("sha256").update(value).digest("hex");
}
function usage() {
return [
"Usage: node scripts/dev-runtime-migration.mjs [--check|--dry-run|--apply]",
"",
"Default --check validates the source migration contract only and never connects to DB.",
"--dry-run is source-only unless --allow-live-db-read --confirm-dev is provided.",
"--apply requires --confirm-dev --confirmed-non-production and HWLAB_CLOUD_DB_URL.",
"Reports never print DB URLs, passwords, tokens, Secret values, or kubeconfig material."
].join("\n");
}
export function formatDevRuntimeMigrationFailure(error) {
return {
issue,
conclusion: {
status: "blocked",
summary: "DEV runtime migration command failed before producing a report."
},
error: error instanceof Error ? error.message : String(error),
safety: {
devOnly: true,
prodAllowed: false,
secretValuesPrinted: false
}
};
}
+155
View File
@@ -0,0 +1,155 @@
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: "require"
},
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: "require"
},
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_URL: "postgresql://hwlab:redacted@example.invalid:5432/hwlab",
HWLAB_CLOUD_DB_SSL_MODE: "require"
},
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("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: "require"
},
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);
});
function createFakeQueryClient({ migrationReady }) {
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 ")) {
return { rows: [{ count: 0 }] };
}
throw new Error(`unexpected query: ${sql}`);
}
};
}
function schemaRows() {
return Object.entries(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS).flatMap(([table, columns]) =>
columns.map((column) => ({
table_name: table,
column_name: column
}))
);
}