mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
246 lines
7.1 KiB
TypeScript
246 lines
7.1 KiB
TypeScript
import { join } from "node:path";
|
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
|
|
// Mock node:os so tilde-expansion is deterministic across platforms.
|
|
vi.mock("node:os", () => {
|
|
const homedir = vi.fn(() => "/mock/home");
|
|
return {
|
|
homedir,
|
|
default: { homedir },
|
|
};
|
|
});
|
|
|
|
// Mock node:fs so realpathSync (used by canonicalizePath) is controllable.
|
|
// Default implementation is identity — existing lexical tests are unaffected.
|
|
const realpathSync = vi.hoisted(() =>
|
|
vi.fn<(path: string) => string>((p) => p),
|
|
);
|
|
vi.mock("node:fs", () => ({
|
|
realpathSync,
|
|
default: { realpathSync },
|
|
}));
|
|
|
|
import {
|
|
canonicalNormalizePathForComparison,
|
|
getPathPolicyValues,
|
|
normalizePathForComparison,
|
|
normalizePathPolicyLiteral,
|
|
} from "#src/access-intent/path-normalization";
|
|
import { posixPathFlavor, win32PathFlavor } from "#src/path/path-flavor";
|
|
|
|
describe("normalizePathForComparison", () => {
|
|
const cwd = "/projects/my-app";
|
|
|
|
test("resolves absolute path unchanged", () => {
|
|
expect(
|
|
normalizePathForComparison("/usr/local/bin", cwd, posixPathFlavor),
|
|
).toBe("/usr/local/bin");
|
|
});
|
|
|
|
test("resolves relative path against cwd", () => {
|
|
expect(normalizePathForComparison("src/foo.ts", cwd, posixPathFlavor)).toBe(
|
|
"/projects/my-app/src/foo.ts",
|
|
);
|
|
});
|
|
|
|
test("expands bare ~ to homedir", () => {
|
|
expect(normalizePathForComparison("~", cwd, posixPathFlavor)).toBe(
|
|
"/mock/home",
|
|
);
|
|
});
|
|
|
|
test("expands ~/... to homedir-relative path", () => {
|
|
expect(
|
|
normalizePathForComparison("~/docs/readme.md", cwd, posixPathFlavor),
|
|
).toBe(join("/mock/home", "docs/readme.md"));
|
|
});
|
|
|
|
test("expands bare $HOME to homedir", () => {
|
|
expect(normalizePathForComparison("$HOME", cwd, posixPathFlavor)).toBe(
|
|
"/mock/home",
|
|
);
|
|
});
|
|
|
|
test("expands $HOME/... to homedir-relative path", () => {
|
|
expect(
|
|
normalizePathForComparison("$HOME/.ssh/config", cwd, posixPathFlavor),
|
|
).toBe(join("/mock/home", ".ssh/config"));
|
|
});
|
|
|
|
test("strips leading @ before resolving", () => {
|
|
expect(
|
|
normalizePathForComparison("@/usr/local/bin", cwd, posixPathFlavor),
|
|
).toBe("/usr/local/bin");
|
|
});
|
|
|
|
test("strips surrounding quotes", () => {
|
|
expect(
|
|
normalizePathForComparison("'/usr/local/bin'", cwd, posixPathFlavor),
|
|
).toBe("/usr/local/bin");
|
|
expect(
|
|
normalizePathForComparison('"/usr/local/bin"', cwd, posixPathFlavor),
|
|
).toBe("/usr/local/bin");
|
|
});
|
|
|
|
test("returns empty string for blank/whitespace-only path", () => {
|
|
expect(normalizePathForComparison("", cwd, posixPathFlavor)).toBe("");
|
|
expect(normalizePathForComparison(" ", cwd, posixPathFlavor)).toBe("");
|
|
});
|
|
|
|
// ── injected platform flavor (Windows is case-folded, win32-resolved) ────
|
|
|
|
test("win32: lowercases the resolved absolute path", () => {
|
|
expect(
|
|
normalizePathForComparison(
|
|
"C:\\Users\\Foo\\Bar.txt",
|
|
"C:\\Projects",
|
|
win32PathFlavor,
|
|
),
|
|
).toBe("c:\\users\\foo\\bar.txt");
|
|
});
|
|
|
|
test("win32: resolves a relative path against cwd with win32 rules", () => {
|
|
expect(
|
|
normalizePathForComparison(
|
|
"src\\foo.ts",
|
|
"C:\\Projects\\App",
|
|
win32PathFlavor,
|
|
),
|
|
).toBe("c:\\projects\\app\\src\\foo.ts");
|
|
});
|
|
|
|
test("posix platform leaves case untouched", () => {
|
|
expect(
|
|
normalizePathForComparison("/Projects/App/Src.ts", cwd, posixPathFlavor),
|
|
).toBe("/Projects/App/Src.ts");
|
|
});
|
|
});
|
|
|
|
describe("canonicalNormalizePathForComparison", () => {
|
|
const cwd = "/projects/my-app";
|
|
|
|
beforeEach(() => {
|
|
realpathSync.mockReset();
|
|
realpathSync.mockImplementation((p: string) => p);
|
|
});
|
|
|
|
test("returns canonical form of an existing path", () => {
|
|
realpathSync.mockImplementation((p: string) => {
|
|
if (p === "/projects/link") return "/real/projects/app";
|
|
return p;
|
|
});
|
|
expect(
|
|
canonicalNormalizePathForComparison(
|
|
"/projects/link",
|
|
cwd,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe("/real/projects/app");
|
|
});
|
|
|
|
test("returns empty string for empty input", () => {
|
|
expect(canonicalNormalizePathForComparison("", cwd, posixPathFlavor)).toBe(
|
|
"",
|
|
);
|
|
});
|
|
|
|
test("returns lexical form when no symlinks (identity realpathSync)", () => {
|
|
expect(
|
|
canonicalNormalizePathForComparison(
|
|
"/projects/my-app/src/index.ts",
|
|
cwd,
|
|
posixPathFlavor,
|
|
),
|
|
).toBe("/projects/my-app/src/index.ts");
|
|
});
|
|
|
|
test("win32: lowercases the canonical form", () => {
|
|
expect(
|
|
canonicalNormalizePathForComparison(
|
|
"C:\\Projects\\App\\Src",
|
|
"C:\\Projects\\App",
|
|
win32PathFlavor,
|
|
),
|
|
).toBe("c:\\projects\\app\\src");
|
|
});
|
|
});
|
|
|
|
describe("normalizePathPolicyLiteral", () => {
|
|
test("returns a relative token unchanged", () => {
|
|
expect(normalizePathPolicyLiteral("src/foo.ts")).toBe("src/foo.ts");
|
|
});
|
|
|
|
test("trims and strips simple wrapping quotes", () => {
|
|
expect(normalizePathPolicyLiteral(" 'src/foo.ts' ")).toBe("src/foo.ts");
|
|
expect(normalizePathPolicyLiteral('"a/b"')).toBe("a/b");
|
|
});
|
|
|
|
test("strips a leading @ prefix", () => {
|
|
expect(normalizePathPolicyLiteral("@src/foo.ts")).toBe("src/foo.ts");
|
|
});
|
|
|
|
test("expands ~ to the home directory", () => {
|
|
expect(normalizePathPolicyLiteral("~/docs/readme.md")).toBe(
|
|
join("/mock/home", "docs/readme.md"),
|
|
);
|
|
});
|
|
|
|
test("does not resolve a relative value against any cwd", () => {
|
|
expect(normalizePathPolicyLiteral("foo.ts")).toBe("foo.ts");
|
|
});
|
|
|
|
test("returns empty string for blank input", () => {
|
|
expect(normalizePathPolicyLiteral(" ")).toBe("");
|
|
});
|
|
|
|
test("preserves the surface catch-all", () => {
|
|
expect(normalizePathPolicyLiteral("*")).toBe("*");
|
|
});
|
|
});
|
|
|
|
describe("getPathPolicyValues", () => {
|
|
const cwd = "/projects/my-app";
|
|
|
|
test("returns only the literal when no base is available", () => {
|
|
expect(getPathPolicyValues("src/foo.ts", {}, posixPathFlavor)).toEqual([
|
|
"src/foo.ts",
|
|
]);
|
|
expect(getPathPolicyValues("src/foo.ts", {}, posixPathFlavor)).toEqual([
|
|
"src/foo.ts",
|
|
]);
|
|
});
|
|
|
|
test("adds absolute and project-relative aliases for a relative token", () => {
|
|
expect(getPathPolicyValues("src/foo.ts", { cwd }, posixPathFlavor)).toEqual(
|
|
["/projects/my-app/src/foo.ts", "src/foo.ts"],
|
|
);
|
|
});
|
|
|
|
test("omits the relative alias for a token outside cwd", () => {
|
|
expect(getPathPolicyValues("/etc/hosts", { cwd }, posixPathFlavor)).toEqual(
|
|
["/etc/hosts"],
|
|
);
|
|
});
|
|
|
|
test("resolves against resolveBase while aliasing relative to cwd", () => {
|
|
expect(
|
|
getPathPolicyValues(
|
|
"foo.txt",
|
|
{
|
|
cwd,
|
|
resolveBase: "/projects/my-app/nested",
|
|
},
|
|
posixPathFlavor,
|
|
),
|
|
).toEqual(["/projects/my-app/nested/foo.txt", "nested/foo.txt", "foo.txt"]);
|
|
});
|
|
|
|
test("preserves the surface catch-all", () => {
|
|
expect(getPathPolicyValues("*", { cwd }, posixPathFlavor)).toEqual(["*"]);
|
|
});
|
|
|
|
test("returns empty for blank input", () => {
|
|
expect(getPathPolicyValues(" ", { cwd }, posixPathFlavor)).toEqual([]);
|
|
});
|
|
});
|