mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-05-13 19:11:15 +00:00
- Add chokidar ^4.0.0 as dependency (root package.json, runtime-package.json) - Mark chokidar as external in vite.config.ts (not bundled, loaded at runtime) - Implement CLIWatchAdapter.beginWatch() with chokidar: - ignoreInitial: true (startup files handled by mirror scan) - awaitWriteFinish to prevent partial-write events - Excludes dotfiles and .livesync/ directory at watcher level - Maps add/change/unlink/addDir/unlinkDir to IStorageEventWatchHandlers - Fatal error handler: logs clearly and releases watcher resources - Add close() to CLIWatchAdapter, StorageEventManagerCLI for clean shutdown - Register onUnload hook in CLIServiceModules to close watcher on shutdown
127 lines
4.9 KiB
TypeScript
127 lines
4.9 KiB
TypeScript
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
import type { IStorageEventWatchHandlers } from "@lib/managers/adapters";
|
|
import type { NodeFile } from "../adapters/NodeTypes";
|
|
|
|
// ── chokidar mock ──────────────────────────────────────────────────────────────
|
|
// Must be hoisted before imports that pull in chokidar.
|
|
|
|
const mockWatcher = {
|
|
on: vi.fn().mockReturnThis(),
|
|
once: vi.fn((event: string, cb: () => void) => {
|
|
if (event === "ready") cb();
|
|
return mockWatcher;
|
|
}),
|
|
close: vi.fn(() => Promise.resolve()),
|
|
};
|
|
|
|
vi.mock("chokidar", () => ({
|
|
watch: vi.fn(() => mockWatcher),
|
|
}));
|
|
|
|
import * as chokidar from "chokidar";
|
|
import { CLIStorageEventManagerAdapter } from "./CLIStorageEventManagerAdapter";
|
|
|
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
function makeHandlers(): IStorageEventWatchHandlers {
|
|
return {
|
|
onCreate: vi.fn(),
|
|
onChange: vi.fn(),
|
|
onDelete: vi.fn(),
|
|
onRename: vi.fn(),
|
|
} as any;
|
|
}
|
|
|
|
// ── tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
describe("CLIStorageEventManagerAdapter", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Restore the default once() behaviour (ready fires synchronously).
|
|
mockWatcher.once.mockImplementation((event: string, cb: () => void) => {
|
|
if (event === "ready") cb();
|
|
return mockWatcher;
|
|
});
|
|
});
|
|
|
|
it("beginWatch is no-op when watchEnabled=false", async () => {
|
|
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, false);
|
|
const handlers = makeHandlers();
|
|
|
|
await adapter.watch.beginWatch(handlers);
|
|
|
|
expect(chokidar.watch).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("beginWatch calls chokidar.watch when watchEnabled=true", async () => {
|
|
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true);
|
|
const handlers = makeHandlers();
|
|
|
|
await adapter.watch.beginWatch(handlers);
|
|
|
|
expect(chokidar.watch).toHaveBeenCalledTimes(1);
|
|
expect(chokidar.watch).toHaveBeenCalledWith(
|
|
"/base",
|
|
expect.objectContaining({ ignoreInitial: true })
|
|
);
|
|
});
|
|
|
|
it("add event produces NodeFile with correct relative path via onCreate", async () => {
|
|
const basePath = "/vault/base";
|
|
const adapter = new CLIStorageEventManagerAdapter(basePath, undefined, true);
|
|
const handlers = makeHandlers();
|
|
|
|
await adapter.watch.beginWatch(handlers);
|
|
|
|
// Find the callback registered for the "add" event.
|
|
const addCall = mockWatcher.on.mock.calls.find(([event]) => event === "add");
|
|
expect(addCall).toBeDefined();
|
|
const addCallback = addCall![1] as (filePath: string, stats: any) => void;
|
|
|
|
const fakeStats = { ctimeMs: 1000, mtimeMs: 2000, size: 42 };
|
|
addCallback(`${basePath}/subdir/note.md`, fakeStats);
|
|
|
|
expect(handlers.onCreate).toHaveBeenCalledTimes(1);
|
|
const created = (handlers.onCreate as ReturnType<typeof vi.fn>).mock.calls[0][0] as NodeFile;
|
|
expect(created.path).toBe("subdir/note.md");
|
|
expect(created.stat?.size).toBe(42);
|
|
});
|
|
|
|
it("close() calls watcher.close()", async () => {
|
|
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true);
|
|
const handlers = makeHandlers();
|
|
|
|
await adapter.watch.beginWatch(handlers);
|
|
await adapter.close();
|
|
|
|
expect(mockWatcher.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("close() is safe when no watcher was started", async () => {
|
|
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, false);
|
|
|
|
// Should not throw.
|
|
await expect(adapter.close()).resolves.toBeUndefined();
|
|
expect(mockWatcher.close).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("error event triggers process.exit(1)", async () => {
|
|
const adapter = new CLIStorageEventManagerAdapter("/base", undefined, true);
|
|
const handlers = makeHandlers();
|
|
|
|
await adapter.watch.beginWatch(handlers);
|
|
|
|
const processExitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
|
|
const errorCall = mockWatcher.on.mock.calls.find(([event]) => event === "error");
|
|
expect(errorCall).toBeDefined();
|
|
const errorCallback = errorCall![1] as (err: Error) => void;
|
|
|
|
errorCallback(new Error("disk failure"));
|
|
|
|
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
|
|
processExitSpy.mockRestore();
|
|
});
|
|
});
|