mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
280 lines
9.4 KiB
TypeScript
280 lines
9.4 KiB
TypeScript
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
// Mock node:os so tilde-expansion is deterministic across platforms.
|
|
// Every other binding passes through, so `tmpdir()` reaches the real module
|
|
// for the filesystem-backed `entryExists` fixtures below.
|
|
vi.mock("node:os", async () => {
|
|
const actual = await vi.importActual<typeof import("node:os")>("node:os");
|
|
const homedir = vi.fn(() => "/mock/home");
|
|
return {
|
|
...actual,
|
|
homedir,
|
|
default: { ...actual, homedir },
|
|
};
|
|
});
|
|
|
|
// Mock node:fs so realpathSync (used by canonicalizePath) is controllable.
|
|
// Default implementation is identity — lexical assertions are unaffected.
|
|
// Every other fs binding passes through to the real module, so filesystem-
|
|
// backed helpers (lstatSync, mkdtempSync, symlinkSync, …) stay usable here.
|
|
const realpathSync = vi.hoisted(() =>
|
|
vi.fn<(path: string) => string>((p) => p),
|
|
);
|
|
vi.mock("node:fs", async () => {
|
|
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
|
|
return {
|
|
...actual,
|
|
realpathSync,
|
|
default: { ...actual, realpathSync },
|
|
};
|
|
});
|
|
|
|
import { posixPathFlavor, win32PathFlavor } from "#src/path/path-flavor";
|
|
import { PathNormalizer } from "#src/path-normalizer";
|
|
import { createTmpFixture } from "#test/helpers/tmp-fixture";
|
|
|
|
describe("PathNormalizer", () => {
|
|
beforeEach(() => {
|
|
realpathSync.mockReset();
|
|
realpathSync.mockImplementation((p: string) => p);
|
|
});
|
|
|
|
describe("posix flavor", () => {
|
|
const normalizer = new PathNormalizer(posixPathFlavor, "/projects/my-app");
|
|
|
|
test("forPath builds an AccessPath resolved against the baked cwd", () => {
|
|
const ap = normalizer.forPath("src/foo.ts");
|
|
expect(ap.value()).toBe("/projects/my-app/src/foo.ts");
|
|
expect(ap.matchValues()).toEqual([
|
|
"/projects/my-app/src/foo.ts",
|
|
"src/foo.ts",
|
|
]);
|
|
});
|
|
|
|
test("forPath honors an explicit resolveBase", () => {
|
|
const ap = normalizer.forPath("foo.ts", {
|
|
resolveBase: "/projects/my-app/sub",
|
|
});
|
|
expect(ap.value()).toBe("/projects/my-app/sub/foo.ts");
|
|
});
|
|
|
|
test("forLiteral builds a literal-only AccessPath", () => {
|
|
const ap = normalizer.forLiteral("foo.ts");
|
|
expect(ap.matchValues()).toEqual(["foo.ts"]);
|
|
expect(ap.boundaryValue()).toBe("");
|
|
});
|
|
|
|
test("isAbsolute uses posix rules", () => {
|
|
expect(normalizer.isAbsolute("/etc/hosts")).toBe(true);
|
|
expect(normalizer.isAbsolute("rel/path")).toBe(false);
|
|
});
|
|
|
|
test("resolveBase resolves an offset against the baked cwd", () => {
|
|
expect(normalizer.resolveBase("sub")).toBe("/projects/my-app/sub");
|
|
expect(normalizer.resolveBase("/abs")).toBe("/abs");
|
|
});
|
|
|
|
test("joinBase joins an offset with a relative target", () => {
|
|
expect(normalizer.joinBase("sub", "nested")).toBe("sub/nested");
|
|
});
|
|
|
|
test("isWithinDirectory decides containment", () => {
|
|
expect(normalizer.isWithinDirectory("/a/b/c", "/a/b")).toBe(true);
|
|
expect(normalizer.isWithinDirectory("/a/x", "/a/b")).toBe(false);
|
|
});
|
|
|
|
|
|
test("comparableValue returns the lexical absolute form (no FS)", () => {
|
|
expect(normalizer.comparableValue("src/foo.ts")).toBe(
|
|
"/projects/my-app/src/foo.ts",
|
|
);
|
|
expect(normalizer.comparableValue("/etc/hosts")).toBe("/etc/hosts");
|
|
});
|
|
|
|
test("forBashToken delegates to forPath on posix", () => {
|
|
const ap = normalizer.forBashToken("src/foo.ts");
|
|
expect(ap.value()).toBe("/projects/my-app/src/foo.ts");
|
|
});
|
|
|
|
|
|
test("interpretBashCdTarget maps an absolute target to absolute (posix)", () => {
|
|
expect(normalizer.interpretBashCdTarget("/etc")).toEqual({
|
|
kind: "absolute",
|
|
value: "/etc",
|
|
});
|
|
});
|
|
|
|
test("interpretBashCdTarget maps a relative target to relative (posix)", () => {
|
|
expect(normalizer.interpretBashCdTarget("sub")).toEqual({
|
|
kind: "relative",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("win32 flavor", () => {
|
|
const normalizer = new PathNormalizer(win32PathFlavor, "C:\\Projects\\App");
|
|
|
|
test("forPath builds a case-folded AccessPath with win32 rules", () => {
|
|
const ap = normalizer.forPath("src\\foo.ts");
|
|
expect(ap.value()).toBe("c:\\projects\\app\\src\\foo.ts");
|
|
expect(ap.matchValues()).toEqual([
|
|
"c:\\projects\\app\\src\\foo.ts",
|
|
"src\\foo.ts",
|
|
]);
|
|
});
|
|
|
|
test("isAbsolute uses win32 rules", () => {
|
|
expect(normalizer.isAbsolute("C:\\Users\\foo")).toBe(true);
|
|
expect(normalizer.isAbsolute("rel\\path")).toBe(false);
|
|
});
|
|
|
|
test("resolveBase resolves an offset against the baked cwd", () => {
|
|
expect(normalizer.resolveBase("sub")).toBe("C:\\Projects\\App\\sub");
|
|
});
|
|
|
|
test("joinBase joins an offset with a relative target", () => {
|
|
expect(normalizer.joinBase("sub", "nested")).toBe("sub\\nested");
|
|
});
|
|
|
|
test("isWithinDirectory folds case", () => {
|
|
expect(
|
|
normalizer.isWithinDirectory(
|
|
"c:\\users\\foo\\dir\\sub",
|
|
"C:\\Users\\Foo\\dir",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
|
|
test("comparableValue case-folds the lexical absolute form", () => {
|
|
expect(normalizer.comparableValue("src\\foo.ts")).toBe(
|
|
"c:\\projects\\app\\src\\foo.ts",
|
|
);
|
|
});
|
|
|
|
test("forBashToken preserves a POSIX device path as a safe device", () => {
|
|
const ap = normalizer.forBashToken("/dev/null");
|
|
expect(ap.value()).toBe("/dev/null");
|
|
expect(ap.boundaryValue()).toBe("/dev/null");
|
|
});
|
|
|
|
test("forBashToken preserves all four safe device paths", () => {
|
|
for (const device of [
|
|
"/dev/null",
|
|
"/dev/stdin",
|
|
"/dev/stdout",
|
|
"/dev/stderr",
|
|
]) {
|
|
expect(normalizer.forBashToken(device).boundaryValue()).toBe(device);
|
|
}
|
|
});
|
|
|
|
test("forBashToken translates an MSYS drive mount to a Windows path", () => {
|
|
const ap = normalizer.forBashToken("/c/Other/x");
|
|
expect(ap.value()).toBe("c:\\other\\x");
|
|
});
|
|
|
|
test("forBashToken translates an in-cwd drive mount", () => {
|
|
const ap = normalizer.forBashToken("/c/projects/app/inside.txt");
|
|
expect(ap.value()).toBe("c:\\projects\\app\\inside.txt");
|
|
});
|
|
|
|
test("forBashToken keeps a non-mount POSIX absolute as a literal", () => {
|
|
const ap = normalizer.forBashToken("/tmp/foo");
|
|
// Matched and displayed as typed: the matcher folds separators on both
|
|
// the rule and the value, so a /tmp/* rule resolves without an alias.
|
|
expect(ap.value()).toBe("/tmp/foo");
|
|
expect(ap.boundaryValue()).toBe("");
|
|
expect(ap.matchValues()).toEqual(["/tmp/foo"]);
|
|
});
|
|
|
|
test("interpretBashCdTarget translates a drive-mount target to absolute", () => {
|
|
expect(normalizer.interpretBashCdTarget("/c/Other")).toEqual({
|
|
kind: "absolute",
|
|
value: "C:\\Other",
|
|
});
|
|
});
|
|
|
|
test("interpretBashCdTarget maps a non-mount POSIX absolute to unknown", () => {
|
|
expect(normalizer.interpretBashCdTarget("/tmp")).toEqual({
|
|
kind: "unknown",
|
|
});
|
|
});
|
|
|
|
test("interpretBashCdTarget maps a native drive path to absolute", () => {
|
|
expect(normalizer.interpretBashCdTarget("C:\\Other")).toEqual({
|
|
kind: "absolute",
|
|
value: "C:\\Other",
|
|
});
|
|
});
|
|
|
|
test("interpretBashCdTarget maps a relative target to relative (win32)", () => {
|
|
expect(normalizer.interpretBashCdTarget("sub")).toEqual({
|
|
kind: "relative",
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
|
|
describe("entryExists", () => {
|
|
// Real filesystem: the probe's whole purpose is to consult fs state, so
|
|
// these use actual temp files rather than the mocked realpathSync above.
|
|
const tmp = createTmpFixture();
|
|
let root: string;
|
|
let normalizer: PathNormalizer;
|
|
|
|
beforeEach(() => {
|
|
root = tmp.dir("pi-perm-exists-");
|
|
normalizer = new PathNormalizer(posixPathFlavor, root);
|
|
});
|
|
|
|
afterEach(() => {
|
|
tmp.cleanup();
|
|
});
|
|
|
|
test("existing regular file → true", () => {
|
|
const file = tmp.file(root, "secret.txt", "contents");
|
|
expect(normalizer.entryExists(file)).toBe(true);
|
|
});
|
|
|
|
test("existing directory → true", () => {
|
|
const dir = tmp.subdir(root, "nested");
|
|
expect(normalizer.entryExists(dir)).toBe(true);
|
|
});
|
|
|
|
test("symlink to an existing target → true", () => {
|
|
const target = tmp.file(root, "target.txt");
|
|
const link = tmp.symlink(root, "link", target);
|
|
expect(normalizer.entryExists(link)).toBe(true);
|
|
});
|
|
|
|
test("dangling symlink → true (lstat: the link itself is an entry)", () => {
|
|
const link = tmp.symlink(root, "dangling", join(root, "gone.txt"));
|
|
expect(normalizer.entryExists(link)).toBe(true);
|
|
});
|
|
|
|
test("nonexistent path → false", () => {
|
|
expect(normalizer.entryExists(join(root, "missing.txt"))).toBe(false);
|
|
});
|
|
|
|
test("path under a nonexistent parent → false (ENOTDIR/ENOENT)", () => {
|
|
expect(normalizer.entryExists(join(root, "no-dir", "file.txt"))).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
test("empty path → false", () => {
|
|
expect(normalizer.entryExists("")).toBe(false);
|
|
});
|
|
|
|
test("answers from the filesystem, not the flavor — win32 normalizer agrees", () => {
|
|
const file = tmp.file(root, "flavor-independent.txt");
|
|
const win32Normalizer = new PathNormalizer(win32PathFlavor, root);
|
|
expect(win32Normalizer.entryExists(file)).toBe(true);
|
|
expect(win32Normalizer.entryExists(join(root, "nope.txt"))).toBe(false);
|
|
});
|
|
});
|
|
});
|