1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 isLocalEditorFileHref,
5 resolveInlineCodeFileLinkMeta,
6 resolveMarkdownFileLinkMeta,
7 rewriteMarkdownFileUriHref,
8} from "./markdown-links";
9import {
10 markdownFileCopyPaths,
11 markdownFileEditorTarget,
12 resolveMarkdownFileHostPath,
13} from "./markdownFileHostPath";
14
15describe("isLocalEditorFileHref", () => {
16 it("accepts absolute file deep links of known editors", () => {
17 for (const href of [
18 "cursor://file/Users/me/repo/file.md:186",
19 "vscode://file/Users/me/repo/file.md:3:7",
20 "vscode-insiders://file/Users/me/repo/file.md",
21 "VSCODE://file/C:/Users/me/repo/file.md:3",
22 ]) {
23 expect(isLocalEditorFileHref(href), href).toBe(true);
24 }
25 });
26
27 it("rejects other hosts, userinfo, relative paths, and unknown schemes", () => {
28 for (const href of [
29 "cursor://anysphere.cursor-deeplink/prompt?text=hi",
30 "vscode://user@file/Users/me/repo/file.md",
31 "vscode://file:8080/Users/me/repo/file.md",
32 "vscode://vscode-remote/ssh-remote+example.com/home/user/project",
33 "cursor://file",
34 "cursor:file/Users/me/repo/file.md",
35 "foo://file/Users/me/repo/file.md",
36 "zed://file/Users/me/repo/file.md",
37 "javascript:alert(1)",
38 "./file.md",
39 "",
40 ]) {
41 expect(isLocalEditorFileHref(href), href).toBe(false);
42 }
43 });
44});
45
46describe("rewriteMarkdownFileUriHref", () => {
47 it("normalizes file uri hrefs for windows drive paths", () => {
48 expect(
49 rewriteMarkdownFileUriHref(
50 "file:///D:/Programme/t3code/apps/web/src/components/chat/OpenInPicker.tsx#L69",
51 ),
52 ).toBe("D:/Programme/t3code/apps/web/src/components/chat/OpenInPicker.tsx#L69");
53 });
54});
55
56describe("relative links inside a rendered host file", () => {
57 it("anchor to the file's directory while workspace membership follows cwd", () => {
58 const meta = resolveMarkdownFileLinkMeta("appendix.md", "/repo", "/tmp/report");
59 expect(meta).toMatchObject({
60 filePath: "/tmp/report/appendix.md",
61 workspaceRelativePath: null,
62 });
63 const inline = resolveInlineCodeFileLinkMeta("Makefile:12", "/repo", "/tmp/report");
64 expect(inline).toMatchObject({ filePath: "/tmp/report/Makefile", line: 12 });
65 expect(resolveMarkdownFileLinkMeta("src/main.ts", "/repo", "/repo/docs")).toMatchObject({
66 filePath: "/repo/docs/src/main.ts",
67 workspaceRelativePath: "docs/src/main.ts",
68 });
69 });
70});
71
72describe("resolveInlineCodeFileLinkMeta", () => {
73 it("links bare filenames only when a line suffix marks them as file references", () => {
74 expect(resolveInlineCodeFileLinkMeta("script.ts:10", "/Users/julius/project")).toMatchObject({
75 targetPath: "/Users/julius/project/script.ts:10",
76 line: 10,
77 });
78 expect(resolveInlineCodeFileLinkMeta("AGENTS.md", "/Users/julius/project")).toBeNull();
79 });
80
81 it("opens a line range at its first line and keeps the range for the label", () => {
82 expect(resolveInlineCodeFileLinkMeta("src/http.ts:42-64", "/repo")).toMatchObject({
83 targetPath: "/repo/src/http.ts:42",
84 line: 42,
85 endLine: 64,
86 });
87 });
88
89 it("does not treat arbitrary name:digits shapes as files", () => {
90 expect(resolveInlineCodeFileLinkMeta("error:1", "/Users/julius/project")).toBeNull();
91 expect(resolveInlineCodeFileLinkMeta("TODO:12", "/Users/julius/project")).toBeNull();
92 expect(resolveInlineCodeFileLinkMeta("exit:0", "/Users/julius/project")).toBeNull();
93 expect(resolveInlineCodeFileLinkMeta("port:3000", "/Users/julius/project")).toBeNull();
94 expect(resolveInlineCodeFileLinkMeta("port:3000-3010", "/Users/julius/project")).toBeNull();
95 expect(
96 resolveInlineCodeFileLinkMeta("localhost:3000-3010", "/Users/julius/project"),
97 ).toBeNull();
98 expect(resolveInlineCodeFileLinkMeta("http:80", "/Users/julius/project")).toBeNull();
99 });
100
101 it("ignores hosts, ports, and versions", () => {
102 expect(resolveInlineCodeFileLinkMeta("127.0.0.1:3000", "/Users/julius/project")).toBeNull();
103 expect(resolveInlineCodeFileLinkMeta("localhost:3000", "/Users/julius/project")).toBeNull();
104 expect(
105 resolveInlineCodeFileLinkMeta("example.com/index.html", "/Users/julius/project"),
106 ).toBeNull();
107 expect(resolveInlineCodeFileLinkMeta("example.com:8080", "/Users/julius/project")).toBeNull();
108 expect(resolveInlineCodeFileLinkMeta("10.0.0.1:80:1", "/Users/julius/project")).toBeNull();
109 expect(
110 resolveInlineCodeFileLinkMeta("localhost/index.html", "/Users/julius/project"),
111 ).toBeNull();
112 expect(
113 resolveInlineCodeFileLinkMeta("example.uk/index.html", "/Users/julius/project"),
114 ).toBeNull();
115 });
116});
117
118describe("chat file chip paths", () => {
119 const cwd = String.raw`C:\Users\alice\project`;
120
121 it("opens and copies the indexed file for a bare filename, preserving its line and column", async () => {
122 const meta = resolveMarkdownFileLinkMeta("user.ts:261:7", cwd);
123 if (!meta) throw new Error("Expected a file link");
124 const hostPath = await resolveMarkdownFileHostPath({
125 meta,
126 cwd,
127 findWorkspaceMatch: async () => "src/discord/events/user.ts",
128 });
129
130 expect(markdownFileEditorTarget(meta, hostPath)).toBe(
131 String.raw`C:\Users\alice\project\src\discord\events\user.ts:261:7`,
132 );
133 expect(markdownFileCopyPaths(meta, hostPath, cwd)).toEqual({
134 fullPath: String.raw`C:\Users\alice\project\src\discord\events\user.ts:261:7`,
135 relativePath: "src/discord/events/user.ts:261:7",
136 });
137 });
138});