fix: harden DEV runtime migration transactions

This commit is contained in:
Code Queue Review
2026-05-23 00:14:13 +00:00
parent 63d8f7b5a6
commit 852ec3dbc7
2 changed files with 115 additions and 6 deletions
+32 -6
View File
@@ -250,7 +250,14 @@ async function verifyRuntimeReadiness(report, env, options) {
queryClient: options.queryClient,
pgModuleLoader: options.pgModuleLoader
});
const runtime = await runtimeStore.readiness();
let runtime;
try {
runtime = await runtimeStore.readiness();
} finally {
if (!options.queryClient && typeof runtimeStore.pool?.end === "function") {
await runtimeStore.pool.end();
}
}
report.runtime = summarizeRuntime(runtime);
report.actions.readinessVerified = runtime.ready === true;
report.gates = gatesFromRuntime(runtime);
@@ -277,11 +284,9 @@ async function applyMigration(report, sql, env, options) {
if (options.queryClient) {
client = options.queryClient;
} else {
const pool = await createPgPool(env, options);
client = pool;
closeClient = async () => {
await pool.end();
};
const migrationClient = await createMigrationClient(env, options);
client = migrationClient.client;
closeClient = migrationClient.close;
}
await client.query("BEGIN");
await client.query(sql);
@@ -300,6 +305,27 @@ async function applyMigration(report, sql, env, options) {
}
}
async function createMigrationClient(env, options) {
const pool = await createPgPool(env, options);
let client;
try {
client = await pool.connect();
} catch (error) {
await pool.end();
throw error;
}
return {
client,
close: async () => {
try {
client.release();
} finally {
await pool.end();
}
}
};
}
async function createPgPool(env, options) {
let pg;
try {
@@ -104,6 +104,55 @@ test("apply mode records ledger and verifies durable runtime readiness", async (
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: "require"
},
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("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: "require"
},
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 }) {
const state = {
migrationReady,
@@ -145,6 +194,40 @@ function createFakeQueryClient({ migrationReady }) {
};
}
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) => ({