fix: resequence existing workbench trace migration

This commit is contained in:
lyon
2026-06-20 20:37:08 +08:00
parent 971922be47
commit 183cf653f4
4 changed files with 132 additions and 22 deletions
+35 -5
View File
@@ -299,11 +299,7 @@ async function applyMigration(report, sql, env, options) {
} 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
});
addBlocker(report, "runtime_blocker", blockerScope(classified.blocker), classified.summary, migrationErrorEvidence(classified));
} finally {
await closeClient();
}
@@ -586,8 +582,10 @@ function blockerScope(blocker) {
function classifyMigrationError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "UNKNOWN";
const details = migrationErrorDetails(error);
if (errorCode === "HWLAB_PG_DRIVER_MISSING") {
return {
...details,
blocker: RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
errorCode,
summary: "Postgres driver is unavailable for DEV runtime migration apply."
@@ -595,6 +593,7 @@ function classifyMigrationError(error) {
}
if (isMigrationSslError(error)) {
return {
...details,
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
errorCode,
summary: "DEV runtime migration reached Postgres, but SSL negotiation or SSL mode compatibility blocked apply."
@@ -602,6 +601,7 @@ function classifyMigrationError(error) {
}
if (["28P01", "28000", "42501"].includes(errorCode)) {
return {
...details,
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
errorCode,
summary: "DEV runtime migration reached Postgres but auth or authorization blocked apply."
@@ -609,18 +609,48 @@ function classifyMigrationError(error) {
}
if (["42P01", "42703", "3F000"].includes(errorCode)) {
return {
...details,
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
errorCode,
summary: "DEV runtime migration apply was blocked by schema/search_path state."
};
}
return {
...details,
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
errorCode,
summary: "DEV runtime migration apply could not complete."
};
}
function migrationErrorEvidence(classified) {
return {
blocker: classified.blocker,
errorCode: classified.errorCode,
...(classified.errorConstraint ? { errorConstraint: classified.errorConstraint } : {}),
...(classified.errorTable ? { errorTable: classified.errorTable } : {}),
...(classified.errorMessage ? { errorMessage: classified.errorMessage } : {}),
...(classified.errorDetail ? { errorDetail: classified.errorDetail } : {}),
secretValuesPrinted: false
};
}
function migrationErrorDetails(error) {
return {
errorMessage: safeDiagnosticText(error?.message, 500),
errorDetail: safeDiagnosticText(error?.detail, 500),
errorConstraint: safeDiagnosticText(error?.constraint, 160),
errorTable: safeDiagnosticText(error?.table, 160)
};
}
function safeDiagnosticText(value, maxLength) {
if (typeof value !== "string") return null;
const redacted = redactFailureText(value).trim();
if (!redacted) return null;
return redacted.length > maxLength ? `${redacted.slice(0, maxLength)}...` : redacted;
}
function isMigrationSslError(error) {
const errorCode = typeof error?.code === "string" ? error.code : "";
if (errorCode.startsWith("ERR_SSL") || errorCode.startsWith("ERR_TLS")) {
+35 -1
View File
@@ -314,6 +314,37 @@ test("apply mode records ledger and verifies durable runtime readiness", async (
assert.equal(JSON.stringify(report).includes("redacted@example.invalid"), false);
});
test("apply mode reports sanitized migration diagnostics", async () => {
const migrationApplyError = new Error("duplicate key blocked postgresql://hwlab:secret-password@db.example.test:5432/hwlab");
migrationApplyError.code = "23505";
migrationApplyError.constraint = "idx_workbench_trace_events_trace_source_event";
migrationApplyError.table = "workbench_trace_events";
migrationApplyError.detail = "Key (trace_id, source_event_id)=(trc_1, password=secret) already exists.";
const queryClient = createFakeQueryClient({ migrationReady: false, migrationApplyError });
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, "blocked");
assert.equal(report.actions.migrationApplied, false);
assert.equal(report.blockers[0].evidence.errorCode, "23505");
assert.equal(report.blockers[0].evidence.errorConstraint, "idx_workbench_trace_events_trace_source_event");
assert.equal(report.blockers[0].evidence.errorTable, "workbench_trace_events");
assert.match(report.blockers[0].evidence.errorMessage, /\[redacted-postgres-url\]/u);
assert.match(report.blockers[0].evidence.errorDetail, /password=\[redacted\]/u);
assert.equal(JSON.stringify(report.blockers[0].evidence).includes("secret-password"), false);
assert.ok(queryClient.calls.some((call) => call.sql === "ROLLBACK"));
});
test("live dry-run closes pg pool after readiness verification", async () => {
const backingClient = createFakeQueryClient({ migrationReady: true });
const pools = [];
@@ -388,7 +419,7 @@ test("apply mode uses a dedicated pg client transaction", async () => {
assert.ok(pools[1].calls.some((call) => call.type === "end"));
});
function createFakeQueryClient({ migrationReady, countErrorCode = null }) {
function createFakeQueryClient({ migrationReady, countErrorCode = null, migrationApplyError = null }) {
const state = {
migrationReady,
calls: []
@@ -402,6 +433,9 @@ function createFakeQueryClient({ migrationReady, countErrorCode = null }) {
if (sql === "BEGIN" || sql === "COMMIT" || sql === "ROLLBACK") {
return { rows: [] };
}
if (migrationApplyError && sql.includes("CREATE TABLE IF NOT EXISTS projects")) {
throw migrationApplyError;
}
if (sql.includes("INSERT INTO hwlab_schema_migrations")) {
state.migrationReady = true;
return { rows: [] };