fix(gateway): hot reload auth cooldown changes

This commit is contained in:
IWhatsskill
2026-05-31 12:45:54 +01:00
committed by Peter Steinberger
parent 0a52206cee
commit 51232ff66c
2 changed files with 60 additions and 8 deletions
+4 -3
View File
@@ -97,9 +97,10 @@ const BASE_RELOAD_RULES: ReloadRule[] = [
kind: "hot",
actions: ["restart-heartbeat"],
},
// Auth cooldown readers resolve values from the active config for each auth
// failure decision, so cooldown tuning does not need a gateway restart.
{ prefix: "auth.cooldowns", kind: "none" },
// Auth cooldown readers resolve values from the active runtime config for each
// auth failure decision, so cooldown tuning needs a snapshot refresh but not
// a gateway restart.
{ prefix: "auth.cooldowns", kind: "hot" },
{
prefix: "agents.list",
kind: "hot",
+56 -5
View File
@@ -411,7 +411,7 @@ describe("buildGatewayReloadPlan", () => {
expect(plan.noopPaths).toStrictEqual([]);
});
it("treats auth cooldown changes as no-op for gateway restart planning", () => {
it("hot-reloads auth cooldown changes without a gateway restart", () => {
const changedPaths = [
"auth.cooldowns.billingBackoffHours",
"auth.cooldowns.billingBackoffHoursByProvider.anthropic",
@@ -420,9 +420,9 @@ describe("buildGatewayReloadPlan", () => {
expect(plan.restartGateway).toBe(false);
expect(plan.restartReasons).toStrictEqual([]);
expect(plan.hotReasons).toStrictEqual([]);
expect(plan.noopPaths).toEqual(changedPaths);
expect(resolveConfigReloadMetadata("auth.cooldowns.billingBackoffHours").kind).toBe("none");
expect(plan.hotReasons).toStrictEqual(changedPaths);
expect(plan.noopPaths).toStrictEqual([]);
expect(resolveConfigReloadMetadata("auth.cooldowns.billingBackoffHours").kind).toBe("hot");
});
it("restarts for gateway.auth.token changes", () => {
@@ -481,7 +481,7 @@ describe("buildGatewayReloadPlan", () => {
{
path: "auth.cooldowns.billingBackoffHours",
expectRestartGateway: false,
expectNoopPath: "auth.cooldowns.billingBackoffHours",
expectHotPath: "auth.cooldowns.billingBackoffHours",
},
{
path: "gateway.auth.token",
@@ -1353,6 +1353,57 @@ describe("startGatewayConfigReloader", () => {
await harness.reloader.stop();
});
it("keeps external auth cooldown writes on the hot reload path", async () => {
const previousConfig: OpenClawConfig = {
gateway: { reload: { debounceMs: 0 } },
auth: {
cooldowns: {
billingBackoffHours: 1,
billingBackoffHoursByProvider: { anthropic: 2 },
},
},
};
const nextConfig: OpenClawConfig = {
gateway: { reload: { debounceMs: 0 } },
auth: {
cooldowns: {
billingBackoffHours: 3,
billingBackoffHoursByProvider: { anthropic: 4 },
},
},
};
const readSnapshot = vi.fn<() => Promise<ConfigFileSnapshot>>().mockResolvedValueOnce(
makeSnapshot({
sourceConfig: nextConfig,
runtimeConfig: nextConfig,
config: nextConfig,
hash: "external-auth-cooldowns-1",
}),
);
const harness = createReloaderHarness(readSnapshot, {
initialCompareConfig: previousConfig,
});
harness.watcher.emit("change");
await vi.runOnlyPendingTimersAsync();
expect(harness.onRestart).not.toHaveBeenCalled();
const [plan, hotConfig] = getOnlyHotReloadCall(harness);
expect(plan.changedPaths).toEqual([
"auth.cooldowns.billingBackoffHours",
"auth.cooldowns.billingBackoffHoursByProvider.anthropic",
]);
expect(plan.restartGateway).toBe(false);
expect(plan.hotReasons).toEqual([
"auth.cooldowns.billingBackoffHours",
"auth.cooldowns.billingBackoffHoursByProvider.anthropic",
]);
expect(plan.noopPaths).toStrictEqual([]);
expect(hotConfig).toBe(nextConfig);
await harness.reloader.stop();
});
it("queues restart when an external plugin source write also changes plugin config", async () => {
const previousConfig: OpenClawConfig = {
gateway: { reload: { debounceMs: 0 } },