mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
246 lines
6.9 KiB
JavaScript
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",
|
|
);
|
|
});
|
|
});
|