fix: make qa config apply retries idempotent

This commit is contained in:
Peter Steinberger
2026-04-25 12:19:43 +01:00
parent 67436918f3
commit c5fe80ad58
2 changed files with 84 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
getGatewayRetryAfterMs,
isConfigApplyNoopForSnapshot,
isConfigHashConflict,
isConfigPatchNoopForSnapshot,
} from "./suite-runtime-gateway.js";
@@ -62,4 +63,54 @@ describe("qa suite gateway helpers", () => {
),
).toBe(false);
});
it("detects full config applies that only differ by gateway-written metadata", () => {
const config = {
gateway: {
controlUi: {
allowedOrigins: ["http://127.0.0.1:5173"],
},
},
meta: {
updatedAt: "2026-04-25T10:00:00.000Z",
},
};
expect(
isConfigApplyNoopForSnapshot(
config,
JSON.stringify({
gateway: {
controlUi: {
allowedOrigins: ["http://127.0.0.1:5173"],
},
},
}),
),
).toBe(true);
});
it("keeps changed full config applies eligible for the gateway", () => {
expect(
isConfigApplyNoopForSnapshot(
{
gateway: {
controlUi: {
allowedOrigins: ["http://127.0.0.1:5173"],
},
},
meta: {
updatedAt: "2026-04-25T10:00:00.000Z",
},
},
JSON.stringify({
gateway: {
controlUi: {
allowedOrigins: ["http://127.0.0.1:5174"],
},
},
}),
),
).toBe(false);
});
});

View File

@@ -178,6 +178,32 @@ function areJsonValuesEqual(left: unknown, right: unknown): boolean {
return false;
}
function withoutQaConfigApplyVolatileFields(
config: Record<string, unknown>,
): Record<string, unknown> {
const comparable = structuredClone(config);
// config.apply updates root metadata on write. Retries should not turn a
// completed apply into a metadata-only write/restart loop.
delete comparable.meta;
return comparable;
}
function isConfigApplyNoopForSnapshot(config: Record<string, unknown>, raw: string): boolean {
let nextConfig: unknown;
try {
nextConfig = JSON.parse(raw);
} catch {
return false;
}
if (!isPlainObject(nextConfig)) {
return false;
}
return areJsonValuesEqual(
withoutQaConfigApplyVolatileFields(config),
withoutQaConfigApplyVolatileFields(nextConfig),
);
}
function isConfigPatchNoopForSnapshot(config: Record<string, unknown>, raw: string): boolean {
let patch: unknown;
try {
@@ -233,6 +259,12 @@ async function runConfigMutation(params: {
// control-plane write budget and making later capability checks flaky.
return { ok: true, noop: true };
}
if (
params.action === "config.apply" &&
isConfigApplyNoopForSnapshot(snapshot.config, params.raw)
) {
return { ok: true, noop: true };
}
try {
const result = await params.env.gateway.call(
params.action,
@@ -327,6 +359,7 @@ export {
fetchJson,
formatGatewayPrimaryErrorText,
getGatewayRetryAfterMs,
isConfigApplyNoopForSnapshot,
isConfigPatchNoopForSnapshot,
isConfigHashConflict,
isGatewayRestartRace,