Skip to content

Commit fb5d9e4

Browse files
committed
removed duplicated function from export.py, added test for normalize file path util function
1 parent 91a8d6e commit fb5d9e4

2 files changed

Lines changed: 112 additions & 40 deletions

File tree

scripts/export.py

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
build_searchable_text,
3131
is_excluded_by_rules,
3232
)
33-
from utils.path_helpers import get_workspace_folder_paths as _shared_get_workspace_folder_paths # noqa: E402
33+
from utils.path_helpers import ( # noqa: E402
34+
get_workspace_folder_paths as _shared_get_workspace_folder_paths,
35+
normalize_file_path,
36+
to_epoch_ms,
37+
)
3438
from utils.tool_parser import parse_tool_call # noqa: E402
3539
from utils.workspace_path import get_cli_chats_path # noqa: E402
3640
from utils.cli_chat_reader import ( # noqa: E402
@@ -141,45 +145,6 @@ def get_global_state_dir() -> str:
141145
return os.path.join(str(Path.home()), ".cursor-chat-browser")
142146

143147

144-
def normalize_file_path(p: str) -> str:
145-
n = re.sub(r"^file:///", "", p or "")
146-
n = re.sub(r"^file://", "", n)
147-
try:
148-
from urllib.parse import unquote
149-
n = unquote(n)
150-
except Exception:
151-
pass
152-
if sys.platform == "win32":
153-
n = n.replace("/", "\\")
154-
n = re.sub(r"^\\([a-zA-Z]:)", r"\1", n)
155-
n = n.lower()
156-
return n
157-
158-
159-
def to_epoch_ms(value) -> int:
160-
"""Convert a timestamp (int, float, or ISO-8601 string) to epoch ms."""
161-
if value is None:
162-
return 0
163-
if isinstance(value, (int, float)):
164-
if value > 1e12:
165-
return int(value)
166-
if value > 0:
167-
return int(value * 1000)
168-
return 0
169-
if isinstance(value, str):
170-
try:
171-
cleaned = value.rstrip("Z") + "+00:00" if value.endswith("Z") else value
172-
dt = datetime.fromisoformat(cleaned)
173-
return int(dt.timestamp() * 1000)
174-
except Exception:
175-
pass
176-
try:
177-
return to_epoch_ms(float(value))
178-
except Exception:
179-
pass
180-
return 0
181-
182-
183148
def slug(s: str) -> str:
184149
s = re.sub(r'[<>:"/\\|?*]', "_", s or "")
185150
s = re.sub(r"\s+", "-", s)

tests/test_normalize_file_path.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""Tests for utils.path_helpers.normalize_file_path.
2+
3+
Covers the shared implementation that was previously duplicated in
4+
scripts/export.py (closes #46). All call-sites in both the web app and the
5+
CLI export script now use this single copy.
6+
7+
Edge-case matrix:
8+
- file:/// and file:// URI schemes
9+
- Percent-encoded characters: spaces (%20), colons (%3A), hashes (%23)
10+
- Windows-style drive paths (backslash and forward-slash) on all platforms
11+
- Drive-letter lowercasing on win32
12+
- Plain POSIX paths pass through unchanged
13+
- Empty / None-like input
14+
"""
15+
16+
import sys
17+
import unittest
18+
19+
from utils.path_helpers import normalize_file_path
20+
21+
22+
class TestNormalizeFilePathUriStripping(unittest.TestCase):
23+
def test_file_triple_slash_stripped(self) -> None:
24+
out = normalize_file_path("file:///home/user/project")
25+
self.assertFalse(out.startswith("file:"))
26+
self.assertIn("home", out)
27+
28+
def test_file_double_slash_stripped(self) -> None:
29+
out = normalize_file_path("file://server/share/file.txt")
30+
self.assertFalse(out.startswith("file:"))
31+
self.assertIn("share", out)
32+
33+
def test_empty_string(self) -> None:
34+
self.assertEqual(normalize_file_path(""), "")
35+
36+
37+
class TestNormalizeFilePathPercentEncoding(unittest.TestCase):
38+
def test_space_decoded(self) -> None:
39+
out = normalize_file_path("file:///C:/My%20Documents/file.txt")
40+
self.assertNotIn("%20", out)
41+
self.assertIn("My Documents" if sys.platform != "win32" else "my documents", out)
42+
43+
def test_hash_decoded(self) -> None:
44+
out = normalize_file_path("file:///C:/repo/src%23internal/mod.py")
45+
self.assertNotIn("%23", out)
46+
self.assertIn("#", out)
47+
48+
def test_percent_encoded_colon_in_uri_prefix(self) -> None:
49+
"""URI-style /d%3A/... path: %3A is decoded to ':'.
50+
51+
On win32 the backslash branch is entered (leading slash removed
52+
and path lowercased). On other platforms the leading slash prevents
53+
the Windows-drive branch, so the path is returned as decoded only.
54+
"""
55+
out = normalize_file_path("/d%3A/_Work/project")
56+
self.assertNotIn("%3A", out)
57+
if sys.platform == "win32":
58+
self.assertEqual(out, r"d:\_work\project")
59+
else:
60+
self.assertEqual(out, "/d:/_Work/project")
61+
62+
63+
class TestNormalizeFilePathWindowsDrives(unittest.TestCase):
64+
"""Paths with Windows-style drive letters are normalised on all platforms.
65+
66+
On win32 the win32 branch handles them natively. On Linux/macOS the
67+
``^[a-zA-Z]:[/\\]`` regex branch converts forward-slashes to backslashes
68+
and lowercases the path so cross-platform reads of Cursor's Windows
69+
workspaceStorage produce consistent keys.
70+
"""
71+
72+
def test_backslash_drive_path_lowercased(self) -> None:
73+
out = normalize_file_path(r"D:\Work\Boost")
74+
self.assertEqual(out, r"d:\work\boost")
75+
76+
def test_forward_slash_drive_path_converted(self) -> None:
77+
out = normalize_file_path("D:/Work/Boost")
78+
self.assertEqual(out, r"d:\work\boost")
79+
80+
def test_file_uri_with_windows_drive(self) -> None:
81+
out = normalize_file_path("file:///C:/Users/Dev/project")
82+
self.assertIn("users", out)
83+
self.assertIn("dev", out)
84+
self.assertTrue(out.startswith("c:") or out.startswith("C:"))
85+
86+
def test_mixed_case_drive_lowercased(self) -> None:
87+
out = normalize_file_path(r"E:\Mixed\Case\Path")
88+
self.assertTrue(out.startswith("e:"))
89+
self.assertEqual(out, r"e:\mixed\case\path")
90+
91+
92+
class TestNormalizeFilePathPosixPassthrough(unittest.TestCase):
93+
def test_plain_posix_path_unchanged_on_non_windows(self) -> None:
94+
if sys.platform == "win32":
95+
self.skipTest("POSIX path semantics differ on win32")
96+
out = normalize_file_path("/home/user/project")
97+
self.assertEqual(out, "/home/user/project")
98+
99+
def test_path_without_scheme_unchanged(self) -> None:
100+
if sys.platform == "win32":
101+
self.skipTest("plain relative path behaviour differs on win32")
102+
out = normalize_file_path("relative/path/file.py")
103+
self.assertEqual(out, "relative/path/file.py")
104+
105+
106+
if __name__ == "__main__":
107+
unittest.main()

0 commit comments

Comments
 (0)