@@ -64,4 +64,214 @@ describe("buildRequestParts", () => {
6464 expect ( fooFiles ) . toHaveLength ( 2 )
6565 expect ( synthetic ) . toHaveLength ( 1 )
6666 } )
67+
68+ test ( "handles Windows paths correctly (simulated on macOS)" , ( ) => {
69+ const prompt : Prompt = [ { type : "file" , path : "src\\foo.ts" , content : "@src\\foo.ts" , start : 0 , end : 11 } ]
70+
71+ const result = buildRequestParts ( {
72+ prompt,
73+ context : [ ] ,
74+ images : [ ] ,
75+ text : "@src\\foo.ts" ,
76+ messageID : "msg_win_1" ,
77+ sessionID : "ses_win_1" ,
78+ sessionDirectory : "D:\\projects\\myapp" , // Windows path
79+ } )
80+
81+ // Should create valid file URLs
82+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
83+ expect ( filePart ) . toBeDefined ( )
84+ if ( filePart ?. type === "file" ) {
85+ // URL should be parseable
86+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
87+ // Should not have encoded backslashes in wrong place
88+ expect ( filePart . url ) . not . toContain ( "%5C" )
89+ // Should have normalized to forward slashes
90+ expect ( filePart . url ) . toContain ( "/src/foo.ts" )
91+ }
92+ } )
93+
94+ test ( "handles Windows absolute path with special characters" , ( ) => {
95+ const prompt : Prompt = [ { type : "file" , path : "file#name.txt" , content : "@file#name.txt" , start : 0 , end : 14 } ]
96+
97+ const result = buildRequestParts ( {
98+ prompt,
99+ context : [ ] ,
100+ images : [ ] ,
101+ text : "@file#name.txt" ,
102+ messageID : "msg_win_2" ,
103+ sessionID : "ses_win_2" ,
104+ sessionDirectory : "C:\\Users\\test\\Documents" , // Windows path
105+ } )
106+
107+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
108+ expect ( filePart ) . toBeDefined ( )
109+ if ( filePart ?. type === "file" ) {
110+ // URL should be parseable
111+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
112+ // Special chars should be encoded
113+ expect ( filePart . url ) . toContain ( "file%23name.txt" )
114+ // Should have Windows drive letter properly encoded
115+ expect ( filePart . url ) . toMatch ( / f i l e : \/ \/ \/ [ A - Z ] % 3 A / )
116+ }
117+ } )
118+
119+ test ( "handles Linux absolute paths correctly" , ( ) => {
120+ const prompt : Prompt = [ { type : "file" , path : "src/app.ts" , content : "@src/app.ts" , start : 0 , end : 10 } ]
121+
122+ const result = buildRequestParts ( {
123+ prompt,
124+ context : [ ] ,
125+ images : [ ] ,
126+ text : "@src/app.ts" ,
127+ messageID : "msg_linux_1" ,
128+ sessionID : "ses_linux_1" ,
129+ sessionDirectory : "/home/user/project" ,
130+ } )
131+
132+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
133+ expect ( filePart ) . toBeDefined ( )
134+ if ( filePart ?. type === "file" ) {
135+ // URL should be parseable
136+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
137+ // Should be a normal Unix path
138+ expect ( filePart . url ) . toBe ( "file:///home/user/project/src/app.ts" )
139+ }
140+ } )
141+
142+ test ( "handles macOS paths correctly" , ( ) => {
143+ const prompt : Prompt = [ { type : "file" , path : "README.md" , content : "@README.md" , start : 0 , end : 9 } ]
144+
145+ const result = buildRequestParts ( {
146+ prompt,
147+ context : [ ] ,
148+ images : [ ] ,
149+ text : "@README.md" ,
150+ messageID : "msg_mac_1" ,
151+ sessionID : "ses_mac_1" ,
152+ sessionDirectory : "/Users/kelvin/Projects/opencode" ,
153+ } )
154+
155+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
156+ expect ( filePart ) . toBeDefined ( )
157+ if ( filePart ?. type === "file" ) {
158+ // URL should be parseable
159+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
160+ // Should be a normal Unix path
161+ expect ( filePart . url ) . toBe ( "file:///Users/kelvin/Projects/opencode/README.md" )
162+ }
163+ } )
164+
165+ test ( "handles context files with Windows paths" , ( ) => {
166+ const prompt : Prompt = [ ]
167+
168+ const result = buildRequestParts ( {
169+ prompt,
170+ context : [
171+ { key : "ctx:1" , type : "file" , path : "src\\utils\\helper.ts" } ,
172+ { key : "ctx:2" , type : "file" , path : "test\\unit.test.ts" , comment : "check tests" } ,
173+ ] ,
174+ images : [ ] ,
175+ text : "test" ,
176+ messageID : "msg_win_ctx" ,
177+ sessionID : "ses_win_ctx" ,
178+ sessionDirectory : "D:\\workspace\\app" ,
179+ } )
180+
181+ const fileParts = result . requestParts . filter ( ( part ) => part . type === "file" )
182+ expect ( fileParts ) . toHaveLength ( 2 )
183+
184+ // All file URLs should be valid
185+ fileParts . forEach ( ( part ) => {
186+ if ( part . type === "file" ) {
187+ expect ( ( ) => new URL ( part . url ) ) . not . toThrow ( )
188+ expect ( part . url ) . not . toContain ( "%5C" ) // No encoded backslashes
189+ }
190+ } )
191+ } )
192+
193+ test ( "handles absolute Windows paths (user manually specifies full path)" , ( ) => {
194+ const prompt : Prompt = [
195+ { type : "file" , path : "D:\\other\\project\\file.ts" , content : "@D:\\other\\project\\file.ts" , start : 0 , end : 25 } ,
196+ ]
197+
198+ const result = buildRequestParts ( {
199+ prompt,
200+ context : [ ] ,
201+ images : [ ] ,
202+ text : "@D:\\other\\project\\file.ts" ,
203+ messageID : "msg_abs" ,
204+ sessionID : "ses_abs" ,
205+ sessionDirectory : "C:\\current\\project" ,
206+ } )
207+
208+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
209+ expect ( filePart ) . toBeDefined ( )
210+ if ( filePart ?. type === "file" ) {
211+ // Should handle absolute path that differs from sessionDirectory
212+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
213+ expect ( filePart . url ) . toContain ( "/D%3A/other/project/file.ts" )
214+ }
215+ } )
216+
217+ test ( "handles selection with query parameters on Windows" , ( ) => {
218+ const prompt : Prompt = [
219+ {
220+ type : "file" ,
221+ path : "src\\App.tsx" ,
222+ content : "@src\\App.tsx" ,
223+ start : 0 ,
224+ end : 11 ,
225+ selection : { startLine : 10 , startChar : 0 , endLine : 20 , endChar : 5 } ,
226+ } ,
227+ ]
228+
229+ const result = buildRequestParts ( {
230+ prompt,
231+ context : [ ] ,
232+ images : [ ] ,
233+ text : "@src\\App.tsx" ,
234+ messageID : "msg_sel" ,
235+ sessionID : "ses_sel" ,
236+ sessionDirectory : "C:\\project" ,
237+ } )
238+
239+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
240+ expect ( filePart ) . toBeDefined ( )
241+ if ( filePart ?. type === "file" ) {
242+ // Should have query parameters
243+ expect ( filePart . url ) . toContain ( "?start=10&end=20" )
244+ // Should be valid URL
245+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
246+ // Query params should parse correctly
247+ const url = new URL ( filePart . url )
248+ expect ( url . searchParams . get ( "start" ) ) . toBe ( "10" )
249+ expect ( url . searchParams . get ( "end" ) ) . toBe ( "20" )
250+ }
251+ } )
252+
253+ test ( "handles file paths with dots and special segments on Windows" , ( ) => {
254+ const prompt : Prompt = [
255+ { type : "file" , path : "..\\..\\shared\\util.ts" , content : "@..\\..\\shared\\util.ts" , start : 0 , end : 21 } ,
256+ ]
257+
258+ const result = buildRequestParts ( {
259+ prompt,
260+ context : [ ] ,
261+ images : [ ] ,
262+ text : "@..\\..\\shared\\util.ts" ,
263+ messageID : "msg_dots" ,
264+ sessionID : "ses_dots" ,
265+ sessionDirectory : "C:\\projects\\myapp\\src" ,
266+ } )
267+
268+ const filePart = result . requestParts . find ( ( part ) => part . type === "file" )
269+ expect ( filePart ) . toBeDefined ( )
270+ if ( filePart ?. type === "file" ) {
271+ // Should be valid URL
272+ expect ( ( ) => new URL ( filePart . url ) ) . not . toThrow ( )
273+ // Should preserve .. segments (backend normalizes)
274+ expect ( filePart . url ) . toContain ( "/.." )
275+ }
276+ } )
67277} )
0 commit comments