Files
ignis/packages/shim/src/fs/sync-mutations.test.js

246 lines
6.9 KiB
JavaScript

import { describe, it, expect, vi, afterEach } from "vitest";
import { createFsSync } from "./sync.js";
import { resolvePath, registerPathResolver, _reset } from "./transforms.js";
import { isRecentLocalOp } from "./echo-guard.js";
function makeDeps() {
const store = new Map();
const metadataCache = {
has: (p) => store.has(p),
get: (p) => (store.has(p) ? store.get(p) : null),
set: (p, m) => store.set(p, m),
delete: (p) => store.delete(p),
rename: (a, b) => {
if (store.has(a)) {
store.set(b, store.get(a));
store.delete(a);
}
},
toStat: (p) =>
store.has(p)
? {
type: store.get(p).type,
isDirectory: () => store.get(p).type === "directory",
isFile: () => store.get(p).type === "file",
}
: null,
readdir: () => [],
};
const contentCache = {
get: () => null,
set: vi.fn(),
delete: vi.fn(),
invalidate: vi.fn(),
};
const transport = {
mkdir: vi.fn(async () => {}),
rmdir: vi.fn(async () => {}),
rm: vi.fn(async () => {}),
rename: vi.fn(async () => {}),
copyFile: vi.fn(async () => {}),
appendFile: vi.fn(async () => {}),
utimes: vi.fn(async () => {}),
stat: vi.fn(async () => ({ type: "file", size: 1 })),
readFileSync: vi.fn(() => {
throw new Error("transport.readFileSync should not be called");
}),
};
return { metadataCache, contentCache, transport, store };
}
describe("sync fs mutations", () => {
it("lstatSync mirrors statSync", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
deps.store.set(resolvePath("dir"), { type: "directory" });
expect(fs.lstatSync("dir").isDirectory()).toBe(true);
});
it("mkdirSync updates the cache and fires the transport", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
fs.mkdirSync("newdir", { recursive: true });
expect(deps.store.get("newdir")).toEqual({ type: "directory" });
expect(deps.transport.mkdir).toHaveBeenCalledWith("newdir", true);
});
it("rmSync deletes from the cache and fires the transport", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
const key = resolvePath("gone.md");
deps.store.set(key, { type: "file" });
fs.rmSync("gone.md", { recursive: true });
expect(deps.store.has(key)).toBe(false);
expect(deps.transport.rm).toHaveBeenCalled();
});
it("renameSync moves cache metadata and fires the transport", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
const from = resolvePath("a.md");
const to = resolvePath("b.md");
deps.store.set(from, { type: "file", size: 2 });
fs.renameSync("a.md", "b.md");
expect(deps.store.has(from)).toBe(false);
expect(deps.store.get(to)).toEqual({ type: "file", size: 2 });
expect(deps.transport.rename).toHaveBeenCalled();
});
it("copyFileSync optimistically mirrors source metadata and fires the transport", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
const srcKey = resolvePath("src.md");
const destKey = resolvePath("dest.md");
deps.store.set(srcKey, { type: "file", size: 9 });
fs.copyFileSync("src.md", "dest.md");
expect(deps.store.get(destKey)).toEqual({ type: "file", size: 9 });
expect(deps.transport.copyFile).toHaveBeenCalled();
});
it("utimesSync sets mtime and fires the transport", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
const key = resolvePath("note.md");
deps.store.set(key, { type: "file", mtime: 0 });
fs.utimesSync("note.md", 111, 222);
expect(deps.store.get(key).mtime).toBe(222);
expect(deps.transport.utimes).toHaveBeenCalled();
});
});
describe("directory mutations honor path resolvers", () => {
afterEach(() => _reset());
it("mkdirSync uses the resolved path for cache, echo-guard, and transport", () => {
registerPathResolver(
(p) => p === "logical/dir",
() => "physical/dir",
);
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
fs.mkdirSync("logical/dir", { recursive: true });
expect(deps.store.get("physical/dir")).toEqual({ type: "directory" });
expect(deps.store.has("logical/dir")).toBe(false);
expect(deps.transport.mkdir).toHaveBeenCalledWith("physical/dir", true);
expect(isRecentLocalOp("physical/dir")).toBe(true);
expect(isRecentLocalOp("logical/dir")).toBe(false);
});
it("rmdirSync uses the resolved path for cache, echo-guard, and transport", () => {
registerPathResolver(
(p) => p === "logical/dir",
() => "physical/dir",
);
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
deps.store.set("physical/dir", { type: "directory" });
fs.rmdirSync("logical/dir");
expect(deps.store.has("physical/dir")).toBe(false);
expect(deps.transport.rmdir).toHaveBeenCalledWith("physical/dir");
expect(isRecentLocalOp("physical/dir")).toBe(true);
});
});
describe("readFileSync existence", () => {
afterEach(() => _reset());
it("answers ENOENT from the cache for a missing non-redirected path, no transport", () => {
const deps = makeDeps();
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
// Leading slash: normalize strips it, so resolved !== the raw argument.
expect(() => fs.readFileSync("/.obsidian/backlink.json", "utf8")).toThrow(
/ENOENT/,
);
expect(deps.transport.readFileSync).not.toHaveBeenCalled();
});
it("falls back to the original path for a redirected miss", () => {
registerPathResolver(
(p) => p === ".obsidian/workspace.json",
() => ".obsidian/workspace.Work.json",
);
const deps = makeDeps();
deps.transport.readFileSync = vi.fn((p) => {
if (p === ".obsidian/workspace.Work.json") {
const e = new Error("ENOENT");
e.code = "ENOENT";
throw e;
}
return "BASE";
});
const fs = createFsSync(
deps.metadataCache,
deps.contentCache,
deps.transport,
);
// Returns the base content after the redirect target 404s: the fallback fired.
expect(fs.readFileSync("/.obsidian/workspace.json", "utf8")).toBe("BASE");
expect(deps.transport.readFileSync).toHaveBeenCalledWith(
".obsidian/workspace.Work.json",
"utf8",
);
});
});