@@ -9,10 +9,12 @@ vi.mock('child_process', () => ({
99// Mock fs/promises
1010vi . mock ( 'fs/promises' , ( ) => ( {
1111 access : vi . fn ( ) ,
12+ readdir : vi . fn ( ) ,
13+ readFile : vi . fn ( ) ,
1214} ) ) ;
1315
1416import { execFile } from 'child_process' ;
15- import { access } from 'fs/promises' ;
17+ import { access , readdir , readFile } from 'fs/promises' ;
1618
1719function createMockLogger ( ) {
1820 return {
@@ -164,4 +166,181 @@ describe('CliResolver', () => {
164166 it ( 'should be disposable' , ( ) => {
165167 expect ( ( ) => resolver . dispose ( ) ) . not . toThrow ( ) ;
166168 } ) ;
169+
170+ describe ( 'vscode-codeql distribution discovery' , ( ) => {
171+ const storagePath = '/mock/globalStorage/github.vscode-codeql' ;
172+ const binaryName = process . platform === 'win32' ? 'codeql.exe' : 'codeql' ;
173+
174+ beforeEach ( ( ) => {
175+ const originalEnv = process . env . CODEQL_PATH ;
176+ delete process . env . CODEQL_PATH ;
177+
178+ // Make `which codeql` fail
179+ vi . mocked ( execFile ) . mockImplementation (
180+ ( _cmd : any , _args : any , callback : any ) => {
181+ if ( String ( _cmd ) === 'which' || String ( _cmd ) === 'where' ) {
182+ callback ( new Error ( 'not found' ) , '' , '' ) ;
183+ } else {
184+ // codeql --version for validateBinary
185+ callback ( null , 'CodeQL CLI 2.24.2\n' , '' ) ;
186+ }
187+ return { } as any ;
188+ } ,
189+ ) ;
190+
191+ // All known filesystem locations fail
192+ vi . mocked ( access ) . mockRejectedValue ( new Error ( 'ENOENT' ) ) ;
193+
194+ return ( ) => {
195+ if ( originalEnv === undefined ) {
196+ delete process . env . CODEQL_PATH ;
197+ } else {
198+ process . env . CODEQL_PATH = originalEnv ;
199+ }
200+ } ;
201+ } ) ;
202+
203+ it ( 'should resolve from distribution.json hint' , async ( ) => {
204+ resolver = new CliResolver ( logger , storagePath ) ;
205+
206+ // distribution.json exists with folderIndex=3
207+ vi . mocked ( readFile ) . mockResolvedValueOnce (
208+ JSON . stringify ( { folderIndex : 3 , release : { name : 'v2.24.2' } } ) ,
209+ ) ;
210+
211+ // The binary at distribution3/codeql/codeql is valid
212+ const expectedPath = `${ storagePath } /distribution3/codeql/${ binaryName } ` ;
213+ vi . mocked ( access ) . mockImplementation ( ( path : any ) => {
214+ if ( String ( path ) === expectedPath ) return Promise . resolve ( undefined as any ) ;
215+ return Promise . reject ( new Error ( 'ENOENT' ) ) ;
216+ } ) ;
217+
218+ const result = await resolver . resolve ( ) ;
219+ expect ( result ) . toBe ( expectedPath ) ;
220+ expect ( logger . info ) . toHaveBeenCalledWith (
221+ expect . stringContaining ( 'vscode-codeql distribution' ) ,
222+ ) ;
223+ } ) ;
224+
225+ it ( 'should fall back to directory scan when distribution.json is missing' , async ( ) => {
226+ resolver = new CliResolver ( logger , storagePath ) ;
227+
228+ // distribution.json read fails
229+ vi . mocked ( readFile ) . mockRejectedValueOnce ( new Error ( 'ENOENT' ) ) ;
230+
231+ // Directory listing returns distribution directories
232+ vi . mocked ( readdir ) . mockResolvedValueOnce ( [
233+ { name : 'distribution1' , isDirectory : ( ) => true } ,
234+ { name : 'distribution3' , isDirectory : ( ) => true } ,
235+ { name : 'distribution2' , isDirectory : ( ) => true } ,
236+ { name : 'queries' , isDirectory : ( ) => true } ,
237+ { name : 'distribution.json' , isDirectory : ( ) => false } ,
238+ ] as any ) ;
239+
240+ // Only distribution3 has a valid binary
241+ const expectedPath = `${ storagePath } /distribution3/codeql/${ binaryName } ` ;
242+ vi . mocked ( access ) . mockImplementation ( ( path : any ) => {
243+ if ( String ( path ) === expectedPath ) return Promise . resolve ( undefined as any ) ;
244+ return Promise . reject ( new Error ( 'ENOENT' ) ) ;
245+ } ) ;
246+
247+ const result = await resolver . resolve ( ) ;
248+ expect ( result ) . toBe ( expectedPath ) ;
249+ } ) ;
250+
251+ it ( 'should scan directories sorted by descending number' , async ( ) => {
252+ resolver = new CliResolver ( logger , storagePath ) ;
253+
254+ vi . mocked ( readFile ) . mockRejectedValueOnce ( new Error ( 'ENOENT' ) ) ;
255+
256+ vi . mocked ( readdir ) . mockResolvedValueOnce ( [
257+ { name : 'distribution1' , isDirectory : ( ) => true } ,
258+ { name : 'distribution10' , isDirectory : ( ) => true } ,
259+ { name : 'distribution2' , isDirectory : ( ) => true } ,
260+ ] as any ) ;
261+
262+ // All binaries are valid — should pick distribution10 (highest number)
263+ vi . mocked ( access ) . mockResolvedValue ( undefined as any ) ;
264+
265+ const result = await resolver . resolve ( ) ;
266+ expect ( result ) . toBe ( `${ storagePath } /distribution10/codeql/${ binaryName } ` ) ;
267+ } ) ;
268+
269+ it ( 'should return undefined when no storage path is provided' , async ( ) => {
270+ resolver = new CliResolver ( logger ) ; // no storage path
271+
272+ vi . mocked ( execFile ) . mockImplementation (
273+ ( _cmd : any , _args : any , callback : any ) => {
274+ callback ( new Error ( 'not found' ) , '' , '' ) ;
275+ return { } as any ;
276+ } ,
277+ ) ;
278+
279+ const result = await resolver . resolve ( ) ;
280+ expect ( result ) . toBeUndefined ( ) ;
281+ } ) ;
282+
283+ it ( 'should skip distribution directories without a valid binary' , async ( ) => {
284+ resolver = new CliResolver ( logger , storagePath ) ;
285+
286+ vi . mocked ( readFile ) . mockRejectedValueOnce ( new Error ( 'ENOENT' ) ) ;
287+
288+ vi . mocked ( readdir ) . mockResolvedValueOnce ( [
289+ { name : 'distribution3' , isDirectory : ( ) => true } ,
290+ { name : 'distribution2' , isDirectory : ( ) => true } ,
291+ { name : 'distribution1' , isDirectory : ( ) => true } ,
292+ ] as any ) ;
293+
294+ const expectedPath = `${ storagePath } /distribution1/codeql/${ binaryName } ` ;
295+ vi . mocked ( access ) . mockImplementation ( ( path : any ) => {
296+ // Only distribution1 has the binary
297+ if ( String ( path ) === expectedPath ) return Promise . resolve ( undefined as any ) ;
298+ return Promise . reject ( new Error ( 'ENOENT' ) ) ;
299+ } ) ;
300+
301+ const result = await resolver . resolve ( ) ;
302+ expect ( result ) . toBe ( expectedPath ) ;
303+ } ) ;
304+
305+ it ( 'should handle distribution.json with invalid JSON gracefully' , async ( ) => {
306+ resolver = new CliResolver ( logger , storagePath ) ;
307+
308+ // Return non-JSON content
309+ vi . mocked ( readFile ) . mockResolvedValueOnce ( 'not-valid-json' ) ;
310+
311+ vi . mocked ( readdir ) . mockResolvedValueOnce ( [
312+ { name : 'distribution1' , isDirectory : ( ) => true } ,
313+ ] as any ) ;
314+
315+ const expectedPath = `${ storagePath } /distribution1/codeql/${ binaryName } ` ;
316+ vi . mocked ( access ) . mockImplementation ( ( path : any ) => {
317+ if ( String ( path ) === expectedPath ) return Promise . resolve ( undefined as any ) ;
318+ return Promise . reject ( new Error ( 'ENOENT' ) ) ;
319+ } ) ;
320+
321+ const result = await resolver . resolve ( ) ;
322+ expect ( result ) . toBe ( expectedPath ) ;
323+ } ) ;
324+
325+ it ( 'should handle distribution.json without folderIndex property' , async ( ) => {
326+ resolver = new CliResolver ( logger , storagePath ) ;
327+
328+ vi . mocked ( readFile ) . mockResolvedValueOnce (
329+ JSON . stringify ( { release : { name : 'v2.24.2' } } ) ,
330+ ) ;
331+
332+ vi . mocked ( readdir ) . mockResolvedValueOnce ( [
333+ { name : 'distribution1' , isDirectory : ( ) => true } ,
334+ ] as any ) ;
335+
336+ const expectedPath = `${ storagePath } /distribution1/codeql/${ binaryName } ` ;
337+ vi . mocked ( access ) . mockImplementation ( ( path : any ) => {
338+ if ( String ( path ) === expectedPath ) return Promise . resolve ( undefined as any ) ;
339+ return Promise . reject ( new Error ( 'ENOENT' ) ) ;
340+ } ) ;
341+
342+ const result = await resolver . resolve ( ) ;
343+ expect ( result ) . toBe ( expectedPath ) ;
344+ } ) ;
345+ } ) ;
167346} ) ;
0 commit comments