mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
fix: tighten watcher exhaustion handling
This commit is contained in:
@@ -9,9 +9,9 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vites
|
||||
|
||||
type WatchIgnoredFn = (watchPath: string, stats?: { isDirectory?: () => boolean }) => boolean;
|
||||
|
||||
const { createdWatchers, watchMock } = vi.hoisted(() => {
|
||||
type WatchEvent = "add" | "change" | "unlink" | "unlinkDir";
|
||||
type WatchCallback = () => void;
|
||||
const { createdWatchers, memoryLoggerWarn, watchMock } = vi.hoisted(() => {
|
||||
type WatchEvent = "add" | "change" | "unlink" | "unlinkDir" | "error";
|
||||
type WatchCallback = (value?: unknown) => void;
|
||||
function createMockWatcher() {
|
||||
const handlers = new Map<WatchEvent, WatchCallback[]>();
|
||||
const watcher = {
|
||||
@@ -20,9 +20,9 @@ const { createdWatchers, watchMock } = vi.hoisted(() => {
|
||||
return watcher;
|
||||
}),
|
||||
close: vi.fn(async () => undefined),
|
||||
emit: (event: WatchEvent) => {
|
||||
emit: (event: WatchEvent, value?: unknown) => {
|
||||
for (const callback of handlers.get(event) ?? []) {
|
||||
callback();
|
||||
callback(value);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -31,6 +31,7 @@ const { createdWatchers, watchMock } = vi.hoisted(() => {
|
||||
const watchers: Array<ReturnType<typeof createMockWatcher>> = [];
|
||||
const result = {
|
||||
createdWatchers: watchers,
|
||||
memoryLoggerWarn: vi.fn(),
|
||||
watchMock: vi.fn(() => {
|
||||
const watcher = createMockWatcher();
|
||||
watchers.push(watcher);
|
||||
@@ -42,6 +43,18 @@ const { createdWatchers, watchMock } = vi.hoisted(() => {
|
||||
return result;
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/memory-core-host-engine-foundation", async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import("openclaw/plugin-sdk/memory-core-host-engine-foundation")>();
|
||||
return {
|
||||
...actual,
|
||||
createSubsystemLogger: (subsystem: string) => ({
|
||||
...actual.createSubsystemLogger(subsystem),
|
||||
warn: memoryLoggerWarn,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./sqlite-vec.js", () => ({
|
||||
loadSqliteVecExtension: async () => ({ ok: false, error: "sqlite-vec disabled in tests" }),
|
||||
}));
|
||||
@@ -246,4 +259,16 @@ describe("memory watcher config", () => {
|
||||
expect(syncSpy).toHaveBeenCalledWith({ reason: "watch" });
|
||||
},
|
||||
);
|
||||
|
||||
it("attaches a logging non-throwing watcher error listener", async () => {
|
||||
await setupWatcherWorkspace({ name: "notes.md", contents: "hello" });
|
||||
const cfg = createWatcherConfig();
|
||||
|
||||
await expectWatcherManager(cfg);
|
||||
|
||||
const watcher = createdWatchers[0];
|
||||
expect(watcher?.on).toHaveBeenCalledWith("error", expect.any(Function));
|
||||
expect(() => watcher?.emit("error", new Error("watcher error: ENOSPC"))).not.toThrow();
|
||||
expect(memoryLoggerWarn).toHaveBeenCalledWith("memory watcher error: watcher error: ENOSPC");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,8 +308,7 @@ describe("isTransientFileWatchError", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for watcher-related ENOSPC messages", () => {
|
||||
expect(isTransientFileWatchError(new Error("watcher error: ENOSPC"))).toBe(true);
|
||||
it("returns true for watcher-related no-space messages", () => {
|
||||
expect(isTransientFileWatchError(new Error("file watcher: no space left on device"))).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -318,8 +317,10 @@ describe("isTransientFileWatchError", () => {
|
||||
it("returns false for generic code-less watcher messages", () => {
|
||||
expect(isTransientFileWatchError(new Error("file watcher failed"))).toBe(false);
|
||||
expect(isTransientFileWatchError(new Error("watcher error: boom"))).toBe(false);
|
||||
expect(isTransientFileWatchError(new Error("watcher error: ENOSPC"))).toBe(false);
|
||||
expect(isTransientUnhandledRejectionError(new Error("file watcher failed"))).toBe(false);
|
||||
expect(isTransientUnhandledRejectionError(new Error("watcher error: boom"))).toBe(false);
|
||||
expect(isTransientUnhandledRejectionError(new Error("watcher error: ENOSPC"))).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for ENOSPC with cause chain containing watch indicator", () => {
|
||||
|
||||
@@ -405,8 +405,7 @@ export function isTransientFileWatchError(err: unknown): boolean {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
((message.includes("no space left on device") || message.includes("enosp")) &&
|
||||
hasFileWatchSignal(message)) ||
|
||||
(message.includes("no space left on device") && hasFileWatchSignal(message)) ||
|
||||
hasFileWatchExhaustionSignal(message)
|
||||
) {
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user