fix: add DEV runtime migration automation
This commit is contained in:
@@ -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
|
||||
}))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user