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 — lexical tests are unaffected. const realpathSync = vi.hoisted(() => vi.fn<(path: string) => string>((p) => p), ); vi.mock("node:fs", () => ({ realpathSync, default: { realpathSync }, })); import { AccessPath } from "#src/access-intent/access-path"; import { posixPathFlavor, win32PathFlavor } from "#src/path/path-flavor"; describe("AccessPath.forPath", () => { const cwd = "/projects/my-app"; beforeEach(() => { realpathSync.mockReset(); realpathSync.mockImplementation((p: string) => p); }); describe("matchValues()", () => { test("adds the symlink-resolved alias alongside the typed path", () => { // /tmp -> /private/tmp (the macOS symlink from the bug report, #418). realpathSync.mockImplementation((p: string) => p.startsWith("/tmp") ? `/private${p}` : p, ); expect( AccessPath.forPath("/tmp/x", { cwd, flavor: posixPathFlavor, }).matchValues(), ).toEqual(["/tmp/x", "/private/tmp/x"]); }); test("deduplicates when the canonical form equals the lexical form", () => { expect( AccessPath.forPath("/etc/hosts", { cwd, flavor: posixPathFlavor, }).matchValues(), ).toEqual(["/etc/hosts"]); }); test("keeps the relative aliases for an in-cwd token without duplicating", () => { expect( AccessPath.forPath("src/foo.ts", { cwd, flavor: posixPathFlavor, }).matchValues(), ).toEqual(["/projects/my-app/src/foo.ts", "src/foo.ts"]); }); test("includes only the lexical aliases when canonical is empty", () => { // Force canonicalizePath to return the original (no-op symlink resolution // effectively means canonical === lexical, handled by dedup). expect( AccessPath.forPath("/etc/hosts", { cwd, flavor: posixPathFlavor, }).matchValues(), ).not.toHaveLength(0); }); test("resolves a relative token against an explicit resolveBase", () => { // The cd-folded effective base differs from cwd (the bash-path case). expect( AccessPath.forPath("foo.ts", { cwd, resolveBase: "/projects/my-app/sub", flavor: posixPathFlavor, }).matchValues(), ).toEqual(["/projects/my-app/sub/foo.ts", "sub/foo.ts", "foo.ts"]); }); test("adds the canonical alias resolved against resolveBase", () => { realpathSync.mockImplementation((p: string) => p === "/projects/my-app/sub/foo.ts" ? "/real/foo.ts" : p, ); expect( AccessPath.forPath("foo.ts", { cwd, resolveBase: "/projects/my-app/sub", flavor: posixPathFlavor, }).matchValues(), ).toEqual([ "/projects/my-app/sub/foo.ts", "sub/foo.ts", "foo.ts", "/real/foo.ts", ]); }); }); describe("platform option", () => { test("win32: builds lexical/match/boundary values with win32 rules", () => { const ap = AccessPath.forPath("src\\foo.ts", { cwd: "C:\\Projects\\App", flavor: win32PathFlavor, }); expect(ap.value()).toBe("c:\\projects\\app\\src\\foo.ts"); expect(ap.boundaryValue()).toBe("c:\\projects\\app\\src\\foo.ts"); expect(ap.matchValues()).toEqual([ "c:\\projects\\app\\src\\foo.ts", "src\\foo.ts", ]); }); test("win32: lowercases the symlink-resolved boundary value", () => { realpathSync.mockImplementation((p: string) => p === "c:\\projects\\app\\link" ? "C:\\Real\\App" : p, ); expect( AccessPath.forPath("link", { cwd: "C:\\Projects\\App", flavor: win32PathFlavor, }).boundaryValue(), ).toBe("c:\\real\\app"); }); }); describe("boundaryValue()", () => { test("returns the canonical (symlink-resolved) form", () => { realpathSync.mockImplementation((p: string) => p.startsWith("/tmp") ? `/private${p}` : p, ); expect( AccessPath.forPath("/tmp/x", { cwd, flavor: posixPathFlavor, }).boundaryValue(), ).toBe("/private/tmp/x"); }); test("returns the lexical form when path has no symlinks", () => { expect( AccessPath.forPath("/etc/hosts", { cwd, flavor: posixPathFlavor, }).boundaryValue(), ).toBe("/etc/hosts"); }); test("returns empty string for empty input", () => { expect( AccessPath.forPath("", { cwd, flavor: posixPathFlavor, }).boundaryValue(), ).toBe(""); }); }); describe("value()", () => { test("returns the lexical (as-typed, normalized) form", () => { realpathSync.mockImplementation((p: string) => p.startsWith("/tmp") ? `/private${p}` : p, ); // Even when the path resolves to a different canonical, value() stays lexical. expect( AccessPath.forPath("/tmp/x", { cwd, flavor: posixPathFlavor }).value(), ).toBe("/tmp/x"); }); test("normalizes the path against cwd", () => { // A relative path becomes an absolute lexical value. expect( AccessPath.forPath("src/foo.ts", { cwd, flavor: posixPathFlavor, }).value(), ).toBe("/projects/my-app/src/foo.ts"); }); test("normalizes a relative path against an explicit resolveBase", () => { expect( AccessPath.forPath("foo.ts", { cwd, resolveBase: "/projects/my-app/sub", flavor: posixPathFlavor, }).value(), ).toBe("/projects/my-app/sub/foo.ts"); }); test("returns empty string for empty input", () => { expect( AccessPath.forPath("", { cwd, flavor: posixPathFlavor }).value(), ).toBe(""); }); }); }); describe("resolvedAlias()", () => { const cwd = "/projects/my-app"; beforeEach(() => { realpathSync.mockReset(); realpathSync.mockImplementation((p: string) => p); }); test("returns the canonical form when a symlink resolves elsewhere", () => { realpathSync.mockImplementation((p: string) => p === "/projects/my-app/demo-symlink-passwd" ? "/etc/passwd" : p, ); expect( AccessPath.forPath("demo-symlink-passwd", { cwd, flavor: posixPathFlavor, }).resolvedAlias(), ).toBe("/etc/passwd"); }); test("returns undefined when the path has no symlinks (canonical equals lexical)", () => { expect( AccessPath.forPath("/etc/hosts", { cwd, flavor: posixPathFlavor, }).resolvedAlias(), ).toBeUndefined(); }); test("returns undefined for a literal-only path (no canonical)", () => { expect(AccessPath.forLiteral("foo.ts").resolvedAlias()).toBeUndefined(); }); test("returns undefined for empty input", () => { expect( AccessPath.forPath("", { cwd, flavor: posixPathFlavor }).resolvedAlias(), ).toBeUndefined(); }); test("win32: returns the lowercased canonical form for a real symlink target", () => { realpathSync.mockImplementation((p: string) => p === "c:\\projects\\app\\link" ? "C:\\Real\\App" : p, ); expect( AccessPath.forPath("link", { cwd: "C:\\Projects\\App", flavor: win32PathFlavor, }).resolvedAlias(), ).toBe("c:\\real\\app"); }); test("win32: returns undefined for a case-only difference (both forms lowercased)", () => { expect( AccessPath.forPath("src\\foo.ts", { cwd: "C:\\Projects\\App", flavor: win32PathFlavor, }).resolvedAlias(), ).toBeUndefined(); }); }); describe("AccessPath.forDevice", () => { test("lexical, boundary, and match values are all the device path", () => { const ap = AccessPath.forDevice("/dev/null"); expect(ap.value()).toBe("/dev/null"); expect(ap.boundaryValue()).toBe("/dev/null"); expect(ap.matchValues()).toEqual(["/dev/null"]); }); test("resolvedAlias is undefined (canonical equals lexical)", () => { expect(AccessPath.forDevice("/dev/null").resolvedAlias()).toBeUndefined(); }); }); describe("AccessPath.forLiteral", () => { beforeEach(() => { realpathSync.mockReset(); realpathSync.mockImplementation((p: string) => p); }); test("matchValues() carries only the literal — no canonical, no absolute", () => { expect(AccessPath.forLiteral("foo.ts").matchValues()).toEqual(["foo.ts"]); }); test("boundaryValue() is empty (no outside-cwd notion for an unknown base)", () => { expect(AccessPath.forLiteral("foo.ts").boundaryValue()).toBe(""); }); test("value() returns the literal", () => { expect(AccessPath.forLiteral("foo.ts").value()).toBe("foo.ts"); }); test("an empty literal yields no match values", () => { expect(AccessPath.forLiteral("").matchValues()).toEqual([]); expect(AccessPath.forLiteral("").value()).toBe(""); }); });