mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix: make qa config apply retries idempotent
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user