mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 05:52:45 +00:00
fix: retry missing config snapshots before skip (#23343) (thanks @lbo728)
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import chokidar from "chokidar";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import type { ChannelPlugin } from "../channels/plugins/types.js";
|
||||
import type { ConfigFileSnapshot } from "../config/config.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
import {
|
||||
buildGatewayReloadPlan,
|
||||
diffConfigPaths,
|
||||
resolveGatewayReloadSettings,
|
||||
startGatewayConfigReloader,
|
||||
} from "./config-reload.js";
|
||||
|
||||
describe("diffConfigPaths", () => {
|
||||
@@ -163,3 +166,134 @@ describe("resolveGatewayReloadSettings", () => {
|
||||
expect(settings.debounceMs).toBe(300);
|
||||
});
|
||||
});
|
||||
|
||||
type WatcherHandler = () => void;
|
||||
type WatcherEvent = "add" | "change" | "unlink" | "error";
|
||||
|
||||
function createWatcherMock() {
|
||||
const handlers = new Map<WatcherEvent, WatcherHandler[]>();
|
||||
return {
|
||||
on(event: WatcherEvent, handler: WatcherHandler) {
|
||||
const existing = handlers.get(event) ?? [];
|
||||
existing.push(handler);
|
||||
handlers.set(event, existing);
|
||||
return this;
|
||||
},
|
||||
emit(event: WatcherEvent) {
|
||||
for (const handler of handlers.get(event) ?? []) {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
close: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSnapshot(partial: Partial<ConfigFileSnapshot> = {}): ConfigFileSnapshot {
|
||||
return {
|
||||
path: "/tmp/openclaw.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: {},
|
||||
resolved: {},
|
||||
valid: true,
|
||||
config: {},
|
||||
issues: [],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
...partial,
|
||||
};
|
||||
}
|
||||
|
||||
describe("startGatewayConfigReloader", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("retries missing snapshots and reloads once config file reappears", async () => {
|
||||
const watcher = createWatcherMock();
|
||||
vi.spyOn(chokidar, "watch").mockReturnValue(watcher as unknown as never);
|
||||
|
||||
const readSnapshot = vi
|
||||
.fn<() => Promise<ConfigFileSnapshot>>()
|
||||
.mockResolvedValueOnce(makeSnapshot({ exists: false, raw: null, hash: "missing-1" }))
|
||||
.mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
config: {
|
||||
gateway: { reload: { debounceMs: 0 } },
|
||||
hooks: { enabled: true },
|
||||
},
|
||||
hash: "next-1",
|
||||
}),
|
||||
);
|
||||
|
||||
const onHotReload = vi.fn(async () => {});
|
||||
const onRestart = vi.fn();
|
||||
const log = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const reloader = startGatewayConfigReloader({
|
||||
initialConfig: { gateway: { reload: { debounceMs: 0 } } },
|
||||
readSnapshot,
|
||||
onHotReload,
|
||||
onRestart,
|
||||
log,
|
||||
watchPath: "/tmp/openclaw.json",
|
||||
});
|
||||
|
||||
watcher.emit("unlink");
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await vi.advanceTimersByTimeAsync(150);
|
||||
|
||||
expect(readSnapshot).toHaveBeenCalledTimes(2);
|
||||
expect(onHotReload).toHaveBeenCalledTimes(1);
|
||||
expect(onRestart).not.toHaveBeenCalled();
|
||||
expect(log.info).toHaveBeenCalledWith("config reload retry (1/2): config file not found");
|
||||
expect(log.warn).not.toHaveBeenCalledWith("config reload skipped (config file not found)");
|
||||
|
||||
await reloader.stop();
|
||||
});
|
||||
|
||||
it("caps missing-file retries and skips reload after retry budget is exhausted", async () => {
|
||||
const watcher = createWatcherMock();
|
||||
vi.spyOn(chokidar, "watch").mockReturnValue(watcher as unknown as never);
|
||||
|
||||
const readSnapshot = vi
|
||||
.fn<() => Promise<ConfigFileSnapshot>>()
|
||||
.mockResolvedValue(makeSnapshot({ exists: false, raw: null, hash: "missing" }));
|
||||
|
||||
const onHotReload = vi.fn(async () => {});
|
||||
const onRestart = vi.fn();
|
||||
const log = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
};
|
||||
|
||||
const reloader = startGatewayConfigReloader({
|
||||
initialConfig: { gateway: { reload: { debounceMs: 0 } } },
|
||||
readSnapshot,
|
||||
onHotReload,
|
||||
onRestart,
|
||||
log,
|
||||
watchPath: "/tmp/openclaw.json",
|
||||
});
|
||||
|
||||
watcher.emit("unlink");
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(readSnapshot).toHaveBeenCalledTimes(3);
|
||||
expect(onHotReload).not.toHaveBeenCalled();
|
||||
expect(onRestart).not.toHaveBeenCalled();
|
||||
expect(log.warn).toHaveBeenCalledWith("config reload skipped (config file not found)");
|
||||
|
||||
await reloader.stop();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user