fix: tighten watcher exhaustion handling

This commit is contained in:
Altay
2026-05-02 20:03:59 +03:00
parent 494ca27b8b
commit c9046f33e2
3 changed files with 34 additions and 9 deletions

View File

@@ -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");
});
});

View File

@@ -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", () => {

View File

@@ -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;