1import { describe, expect, it } from "vite-plus/test";
2import {
3 fileBasename,
4 workspaceRelativeFilePath,
5 isExplicitRelativePath,
6 isUncPath,
7 isWindowsAbsolutePath,
8 isWindowsDrivePath,
9 newProjectFolderName,
10 normalizeProjectPathForComparison,
11 normalizeProjectPathForDispatch,
12 resolveWorkspaceFilePath,
13} from "./path.ts";
14
15describe("path helpers", () => {
16 it.each([
17 ["report:123", "/workspace", "/workspace/report:123"],
18 ["/workspace/report:12:3", "/other", "/workspace/report:12:3"],
19 ["src/file", "C:\\Workspace\\", "C:\\Workspace\\src\\file"],
20 ["C:/Workspace/file", "/other", "C:/Workspace/file"],
21 ["file", "\\\\host\\share\\", "\\\\host\\share\\file"],
22 ["file", "/", "/file"],
23 ["~/report:123", "/workspace", "~/report:123"],
24 ])("resolves literal path %s within %s", (path, root, expected) => {
25 expect(resolveWorkspaceFilePath(path, root)).toBe(expected);
26 });
27
28 it("detects windows drive paths", () => {
29 expect(isWindowsDrivePath("C:\\repo")).toBe(true);
30 expect(isWindowsDrivePath("D:/repo")).toBe(true);
31 expect(isWindowsDrivePath("/repo")).toBe(false);
32 });
33
34 it("detects UNC paths", () => {
35 expect(isUncPath("\\\\server\\share\\repo")).toBe(true);
36 expect(isUncPath("C:\\repo")).toBe(false);
37 });
38
39 it("detects windows absolute paths", () => {
40 expect(isWindowsAbsolutePath("C:\\repo")).toBe(true);
41 expect(isWindowsAbsolutePath("\\\\server\\share\\repo")).toBe(true);
42 expect(isWindowsAbsolutePath("./repo")).toBe(false);
43 });
44
45 it("detects explicit relative paths", () => {
46 expect(isExplicitRelativePath(".")).toBe(true);
47 expect(isExplicitRelativePath("..")).toBe(true);
48 expect(isExplicitRelativePath("./repo")).toBe(true);
49 expect(isExplicitRelativePath("..\\repo")).toBe(true);
50 expect(isExplicitRelativePath("~/repo")).toBe(false);
51 });
52
53 it("normalizes a bare Windows drive root the same as one with a trailing separator", () => {
54 // `C:`, `C:\` and `C:/` all refer to the drive root and must compare equal.
55 expect(normalizeProjectPathForDispatch("C:")).toBe("C:\\");
56 expect(normalizeProjectPathForComparison("C:")).toBe("c:\\");
57 expect(normalizeProjectPathForComparison("C:")).toBe(normalizeProjectPathForComparison("C:\\"));
58 expect(normalizeProjectPathForComparison("C:")).toBe(normalizeProjectPathForComparison("C:/"));
59 // Non-root drive paths keep their trailing separator trimmed as before.
60 expect(normalizeProjectPathForDispatch("C:\\repo\\")).toBe("C:\\repo");
61 });
62
63 it("names a new project's folder from any typed name", () => {
64 expect(newProjectFolderName("Pinball Stats")).toBe("pinball-stats");
65 expect(newProjectFolderName(" Café & Crème! ")).toBe("cafe-creme");
66 expect(newProjectFolderName("../../etc")).toBe("etc");
67 // Nothing usable left, so the server falls back to a fixed name.
68 expect(newProjectFolderName("🎱🎱")).toBe("project");
69 expect(newProjectFolderName(`${"a".repeat(63)} b`)).toBe("a".repeat(63));
70 // Windows cannot make folders with device names.
71 expect(newProjectFolderName("Con")).toBe("con-project");
72 expect(newProjectFolderName("LPT1")).toBe("lpt1-project");
73 expect(newProjectFolderName("console")).toBe("console");
74 });
75});
76
77describe("fileBasename", () => {
78 it.each([
79 ["/tmp/favicons/", "favicons"],
80 ["C:\\Users\\kelchm\\.claude\\", ".claude"],
81 ["/tmp/", "tmp"],
82 ["AGENTS.md", "AGENTS.md"],
83 ["/", "/"],
84 ])("labels %s as %s", (path, basename) => {
85 expect(fileBasename(path)).toBe(basename);
86 });
87});
88
89describe("workspaceRelativeFilePath", () => {
90 it.each([
91 ["/repo/project/src/main.ts", "/repo/project", "src/main.ts"],
92 ["/repo/project/src/main.ts", "/repo/project/", "src/main.ts"],
93 ["C:\\Users\\mike\\t3code\\apps\\web\\a.ts", "C:/Users/mike/t3code", "apps/web/a.ts"],
94 ["/C:/Users/mike/t3code/apps/web/a.ts", "C:/Users/mike/t3code", "apps/web/a.ts"],
95 ["/Repo/Project/src/main.ts", "/repo/project", null],
96 ["/tmp/case/project/probe.txt", "/tmp/case/Project", null],
97 ["//tmp/case/project/probe.txt", "//tmp/case/Project", null],
98 ["/tmp/case/Project/probe.txt", "/tmp/case/Project", "probe.txt"],
99 ["C:/USERS/mike/t3code/main.ts", "c:/users/MIKE/t3code", "main.ts"],
100 ["/C:/USERS/mike/t3code/main.ts", "/c:/users/MIKE/t3code", "main.ts"],
101 ["\\\\server\\share\\PROJECT\\main.ts", "\\\\Server\\Share\\Project", "main.ts"],
102 ["/tmp/repo/file.ts", "/", "tmp/repo/file.ts"],
103 ["C:/Users/MIKE/main.ts", "c:/", "Users/MIKE/main.ts"],
104 ["\\\\server\\SHARE\\file.ts", "\\\\Server\\Share\\", "file.ts"],
105 ["/tmp/repo/file.ts ", "/tmp/repo", "file.ts "],
106 ["/tmp/report.ts", "/repo/project", null],
107 ["/repo/project-two/a.ts", "/repo/project", null],
108 ["/repo/project/a.ts", undefined, null],
109 ])("relates %s to %s", (path, workspaceRoot, relativePath) => {
110 expect(workspaceRelativeFilePath(path, workspaceRoot)).toBe(relativePath);
111 });
112});