@@ -2,7 +2,7 @@ import {zip, brotliCompress} from './archiver.js'
22import { fileExists , inTemporaryDirectory , mkdir , touchFile } from './fs.js'
33import { joinPath , dirname } from './path.js'
44import { exec } from './system.js'
5- import { describe , expect , test } from 'vitest'
5+ import { describe , expect , test , vi } from 'vitest'
66import StreamZip from 'node-stream-zip'
77import brotli from 'brotli'
88import fs from 'fs'
@@ -57,6 +57,51 @@ describe('zip', () => {
5757 expect ( expectedEntries . sort ( ) ) . toEqual ( archiveEntries . sort ( ) )
5858 } )
5959 } )
60+
61+ test ( 'gracefully handles files deleted during archiving' , async ( ) => {
62+ await inTemporaryDirectory ( async ( tmpDir ) => {
63+ // Given
64+ const zipPath = joinPath ( tmpDir , 'output.zip' )
65+ const outputDirectoryName = 'output'
66+ const outputDirectoryPath = joinPath ( tmpDir , outputDirectoryName )
67+ const structure = [ 'file1.js' , 'file2.js' , 'file3.js' ]
68+
69+ await createFiles ( structure , outputDirectoryPath )
70+
71+ const file2Path = joinPath ( outputDirectoryPath , 'file2.js' )
72+
73+ // Spy on readFile to simulate a file being deleted during archiving
74+ // We'll make readFile throw ENOENT for file2.js specifically
75+ const fsModule = await import ( './fs.js' )
76+ const originalReadFile = fsModule . readFile
77+ const readFileSpy = vi . spyOn ( fsModule , 'readFile' )
78+ readFileSpy . mockImplementation ( ( async ( path : string , options ?: unknown ) => {
79+ if ( path === file2Path ) {
80+ const error : NodeJS . ErrnoException = new Error ( `ENOENT: no such file or directory, open '${ path } '` )
81+ error . code = 'ENOENT'
82+ throw error
83+ }
84+ return originalReadFile ( path , options as never )
85+ } ) as typeof originalReadFile )
86+
87+ // When - should not throw even though file2.js throws ENOENT
88+ await expect (
89+ zip ( {
90+ inputDirectory : outputDirectoryPath ,
91+ outputZipPath : zipPath ,
92+ } ) ,
93+ ) . resolves . not . toThrow ( )
94+
95+ // Then - archive should contain remaining files only
96+ const archiveEntries = await readArchiveFiles ( zipPath )
97+ expect ( archiveEntries ) . toContain ( 'file1.js' )
98+ expect ( archiveEntries ) . toContain ( 'file3.js' )
99+ // file2.js should not be in archive since readFile failed
100+ expect ( archiveEntries ) . not . toContain ( 'file2.js' )
101+
102+ readFileSpy . mockRestore ( )
103+ } )
104+ } )
60105} )
61106
62107describe ( 'brotliCompress' , ( ) => {
@@ -66,7 +111,6 @@ describe('brotliCompress', () => {
66111 const brotliPath = joinPath ( tmpDir , 'output.br' )
67112 const outputDirectoryName = 'output'
68113 const outputDirectoryPath = joinPath ( tmpDir , outputDirectoryName )
69- const extractPath = joinPath ( tmpDir , 'extract' )
70114 const testContent = 'test content'
71115
72116 // Create test file
@@ -141,6 +185,48 @@ describe('brotliCompress', () => {
141185 expect ( fs . existsSync ( joinPath ( extractPath , 'test.json' ) ) ) . toBeFalsy ( )
142186 } )
143187 } )
188+
189+ test ( 'gracefully handles files deleted during compression' , async ( ) => {
190+ await inTemporaryDirectory ( async ( tmpDir ) => {
191+ // Given
192+ const brotliPath = joinPath ( tmpDir , 'output.br' )
193+ const outputDirectoryName = 'output'
194+ const outputDirectoryPath = joinPath ( tmpDir , outputDirectoryName )
195+ const structure = [ 'file1.js' , 'file2.js' , 'file3.js' ]
196+
197+ await createFiles ( structure , outputDirectoryPath )
198+
199+ const file2Path = joinPath ( outputDirectoryPath , 'file2.js' )
200+
201+ // Spy on readFile to simulate a file being deleted during compression
202+ // We'll make readFile throw ENOENT for file2.js specifically
203+ const fsModule = await import ( './fs.js' )
204+ const originalReadFile = fsModule . readFile
205+ const readFileSpy = vi . spyOn ( fsModule , 'readFile' )
206+ readFileSpy . mockImplementation ( ( async ( path : string , options ?: unknown ) => {
207+ if ( path === file2Path ) {
208+ const error : NodeJS . ErrnoException = new Error ( `ENOENT: no such file or directory, open '${ path } '` )
209+ error . code = 'ENOENT'
210+ throw error
211+ }
212+ return originalReadFile ( path , options as never )
213+ } ) as typeof originalReadFile )
214+
215+ // When - should not throw even though file2.js throws ENOENT
216+ await expect (
217+ brotliCompress ( {
218+ inputDirectory : outputDirectoryPath ,
219+ outputPath : brotliPath ,
220+ } ) ,
221+ ) . resolves . not . toThrow ( )
222+
223+ // Then - compressed file should exist and be valid
224+ const exists = await fileExists ( brotliPath )
225+ expect ( exists ) . toBeTruthy ( )
226+
227+ readFileSpy . mockRestore ( )
228+ } )
229+ } )
144230} )
145231
146232async function createFiles ( structure : string [ ] , directory : string ) {
0 commit comments