@@ -21,6 +21,7 @@ const mockFileSystem = {
2121 readFile : vi . fn ( ) ,
2222 rename : vi . fn ( ) ,
2323 stat : vi . fn ( ) ,
24+ unlink : vi . fn ( ) . mockResolvedValue ( undefined ) ,
2425 writeFile : vi . fn ( ) ,
2526} ;
2627
@@ -53,19 +54,19 @@ Host coder-vscode--*
5354 UserKnownHostsFile /dev/null
5455# --- END CODER VSCODE ---` ;
5556
56- expect ( mockFileSystem . readFile ) . toBeCalledWith (
57+ expect ( mockFileSystem . readFile ) . toHaveBeenCalledWith (
5758 sshFilePath ,
5859 expect . anything ( ) ,
5960 ) ;
60- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
61+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
6162 expect . stringContaining ( sshTempFilePrefix ) ,
6263 expectedOutput ,
6364 expect . objectContaining ( {
6465 encoding : "utf-8" ,
6566 mode : 0o600 , // Default mode for new files.
6667 } ) ,
6768 ) ;
68- expect ( mockFileSystem . rename ) . toBeCalledWith (
69+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
6970 expect . stringContaining ( sshTempFilePrefix ) ,
7071 sshFilePath ,
7172 ) ;
@@ -96,19 +97,19 @@ Host coder-vscode.dev.coder.com--*
9697 UserKnownHostsFile /dev/null
9798# --- END CODER VSCODE dev.coder.com ---` ;
9899
99- expect ( mockFileSystem . readFile ) . toBeCalledWith (
100+ expect ( mockFileSystem . readFile ) . toHaveBeenCalledWith (
100101 sshFilePath ,
101102 expect . anything ( ) ,
102103 ) ;
103- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
104+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
104105 expect . stringContaining ( sshTempFilePrefix ) ,
105106 expectedOutput ,
106107 expect . objectContaining ( {
107108 encoding : "utf-8" ,
108109 mode : 0o600 , // Default mode for new files.
109110 } ) ,
110111 ) ;
111- expect ( mockFileSystem . rename ) . toBeCalledWith (
112+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
112113 expect . stringContaining ( sshTempFilePrefix ) ,
113114 sshFilePath ,
114115 ) ;
@@ -148,15 +149,15 @@ Host coder-vscode.dev.coder.com--*
148149 UserKnownHostsFile /dev/null
149150# --- END CODER VSCODE dev.coder.com ---` ;
150151
151- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
152+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
152153 expect . stringContaining ( sshTempFilePrefix ) ,
153154 expectedOutput ,
154155 {
155156 encoding : "utf-8" ,
156157 mode : 0o644 ,
157158 } ,
158159 ) ;
159- expect ( mockFileSystem . rename ) . toBeCalledWith (
160+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
160161 expect . stringContaining ( sshTempFilePrefix ) ,
161162 sshFilePath ,
162163 ) ;
@@ -222,15 +223,15 @@ Host coder-vscode.dev-updated.coder.com--*
222223Host *
223224 SetEnv TEST=1` ;
224225
225- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
226+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
226227 expect . stringContaining ( sshTempFilePrefix ) ,
227228 expectedOutput ,
228229 {
229230 encoding : "utf-8" ,
230231 mode : 0o644 ,
231232 } ,
232233 ) ;
233- expect ( mockFileSystem . rename ) . toBeCalledWith (
234+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
234235 expect . stringContaining ( sshTempFilePrefix ) ,
235236 sshFilePath ,
236237 ) ;
@@ -277,15 +278,15 @@ Host coder-vscode.dev.coder.com--*
277278 UserKnownHostsFile /dev/null
278279# --- END CODER VSCODE dev.coder.com ---` ;
279280
280- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
281+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
281282 expect . stringContaining ( sshTempFilePrefix ) ,
282283 expectedOutput ,
283284 {
284285 encoding : "utf-8" ,
285286 mode : 0o644 ,
286287 } ,
287288 ) ;
288- expect ( mockFileSystem . rename ) . toBeCalledWith (
289+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
289290 expect . stringContaining ( sshTempFilePrefix ) ,
290291 sshFilePath ,
291292 ) ;
@@ -321,15 +322,15 @@ Host coder-vscode.dev.coder.com--*
321322 UserKnownHostsFile /dev/null
322323# --- END CODER VSCODE dev.coder.com ---` ;
323324
324- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
325+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
325326 expect . stringContaining ( sshTempFilePrefix ) ,
326327 expectedOutput ,
327328 {
328329 encoding : "utf-8" ,
329330 mode : 0o644 ,
330331 } ,
331332 ) ;
332- expect ( mockFileSystem . rename ) . toBeCalledWith (
333+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
333334 expect . stringContaining ( sshTempFilePrefix ) ,
334335 sshFilePath ,
335336 ) ;
@@ -605,15 +606,15 @@ Host afterconfig
605606 LogLevel : "ERROR" ,
606607 } ) ;
607608
608- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
609+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
609610 expect . stringContaining ( sshTempFilePrefix ) ,
610611 expectedOutput ,
611612 {
612613 encoding : "utf-8" ,
613614 mode : 0o644 ,
614615 } ,
615616 ) ;
616- expect ( mockFileSystem . rename ) . toBeCalledWith (
617+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
617618 expect . stringContaining ( sshTempFilePrefix ) ,
618619 sshFilePath ,
619620 ) ;
@@ -659,19 +660,19 @@ Host coder-vscode.dev.coder.com--*
659660 loglevel DEBUG
660661# --- END CODER VSCODE dev.coder.com ---` ;
661662
662- expect ( mockFileSystem . readFile ) . toBeCalledWith (
663+ expect ( mockFileSystem . readFile ) . toHaveBeenCalledWith (
663664 sshFilePath ,
664665 expect . anything ( ) ,
665666 ) ;
666- expect ( mockFileSystem . writeFile ) . toBeCalledWith (
667+ expect ( mockFileSystem . writeFile ) . toHaveBeenCalledWith (
667668 expect . stringContaining ( sshTempFilePrefix ) ,
668669 expectedOutput ,
669670 expect . objectContaining ( {
670671 encoding : "utf-8" ,
671672 mode : 0o600 , // Default mode for new files.
672673 } ) ,
673674 ) ;
674- expect ( mockFileSystem . rename ) . toBeCalledWith (
675+ expect ( mockFileSystem . rename ) . toHaveBeenCalledWith (
675676 expect . stringContaining ( sshTempFilePrefix ) ,
676677 sshFilePath ,
677678 ) ;
@@ -689,7 +690,7 @@ it("fails if we are unable to write the temporary file", async () => {
689690
690691 await sshConfig . load ( ) ;
691692
692- expect ( mockFileSystem . readFile ) . toBeCalledWith (
693+ expect ( mockFileSystem . readFile ) . toHaveBeenCalledWith (
693694 sshFilePath ,
694695 expect . anything ( ) ,
695696 ) ;
@@ -705,28 +706,72 @@ it("fails if we are unable to write the temporary file", async () => {
705706 ) . rejects . toThrow ( / F a i l e d t o w r i t e t e m p o r a r y S S H c o n f i g f i l e .* E A C C E S / ) ;
706707} ) ;
707708
708- it ( "fails if we are unable to rename the temporary file" , async ( ) => {
709- const existentSSHConfig = `Host beforeconfig
710- HostName before.config.tld
711- User before` ;
712-
713- const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem ) ;
714- mockFileSystem . readFile . mockResolvedValueOnce ( existentSSHConfig ) ;
709+ it ( "cleans up temp file when rename fails" , async ( ) => {
710+ mockFileSystem . readFile . mockResolvedValueOnce ( "Host existing\n HostName x" ) ;
715711 mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o600 } ) ;
716712 mockFileSystem . writeFile . mockResolvedValueOnce ( "" ) ;
717- mockFileSystem . rename . mockRejectedValueOnce ( new Error ( "EACCES" ) ) ;
713+ const err = new Error ( "EXDEV" ) ;
714+ ( err as NodeJS . ErrnoException ) . code = "EXDEV" ;
715+ mockFileSystem . rename . mockRejectedValueOnce ( err ) ;
718716
717+ const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem ) ;
719718 await sshConfig . load ( ) ;
720719 await expect (
721720 sshConfig . update ( "dev.coder.com" , {
722721 Host : "coder-vscode.dev.coder.com--*" ,
723- ProxyCommand : "some-command-here " ,
722+ ProxyCommand : "cmd " ,
724723 ConnectTimeout : "0" ,
725724 StrictHostKeyChecking : "no" ,
726725 UserKnownHostsFile : "/dev/null" ,
727726 LogLevel : "ERROR" ,
728727 } ) ,
729- ) . rejects . toThrow ( / F a i l e d t o r e n a m e t e m p o r a r y S S H c o n f i g f i l e .* E A C C E S / ) ;
728+ ) . rejects . toThrow ( / F a i l e d t o r e n a m e t e m p o r a r y S S H c o n f i g f i l e / ) ;
729+ expect ( mockFileSystem . unlink ) . toHaveBeenCalledWith (
730+ expect . stringContaining ( sshTempFilePrefix ) ,
731+ ) ;
732+ } ) ;
733+
734+ describe ( "rename retry on Windows" , ( ) => {
735+ const realPlatform = process . platform ;
736+
737+ beforeEach ( ( ) => {
738+ Object . defineProperty ( process , "platform" , { value : "win32" } ) ;
739+ vi . useFakeTimers ( ) ;
740+ } ) ;
741+ afterEach ( ( ) => {
742+ vi . useRealTimers ( ) ;
743+ Object . defineProperty ( process , "platform" , { value : realPlatform } ) ;
744+ } ) ;
745+
746+ it ( "retries on transient EPERM and succeeds" , async ( ) => {
747+ mockFileSystem . readFile . mockResolvedValueOnce (
748+ "Host existing\n HostName x" ,
749+ ) ;
750+ mockFileSystem . stat . mockResolvedValueOnce ( { mode : 0o600 } ) ;
751+ mockFileSystem . writeFile . mockResolvedValueOnce ( "" ) ;
752+ const err = new Error ( "EPERM" ) ;
753+ ( err as NodeJS . ErrnoException ) . code = "EPERM" ;
754+ mockFileSystem . rename
755+ . mockRejectedValueOnce ( err )
756+ . mockResolvedValueOnce ( undefined ) ;
757+
758+ const sshConfig = new SSHConfig ( sshFilePath , mockFileSystem ) ;
759+ await sshConfig . load ( ) ;
760+ const promise = sshConfig . update ( "dev.coder.com" , {
761+ Host : "coder-vscode.dev.coder.com--*" ,
762+ ProxyCommand : "cmd" ,
763+ ConnectTimeout : "0" ,
764+ StrictHostKeyChecking : "no" ,
765+ UserKnownHostsFile : "/dev/null" ,
766+ LogLevel : "ERROR" ,
767+ } ) ;
768+
769+ await vi . advanceTimersByTimeAsync ( 100 ) ;
770+ await promise ;
771+
772+ expect ( mockFileSystem . rename ) . toHaveBeenCalledTimes ( 2 ) ;
773+ expect ( mockFileSystem . unlink ) . not . toHaveBeenCalled ( ) ;
774+ } ) ;
730775} ) ;
731776
732777describe ( "parseSshConfig" , ( ) => {
0 commit comments