2025-05-18 00:04:32 -07:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-08-26 00:04:53 +02:00
import type { GlobToolParams , GlobPath } from './glob.js' ;
import { GlobTool , sortFileEntries } from './glob.js' ;
2025-05-20 13:02:41 -07:00
import { partListUnionToString } from '../core/geminiRequest.js' ;
2025-08-25 22:11:27 +02:00
import path from 'node:path' ;
2026-01-27 13:17:40 -08:00
import { isSubpath } from '../utils/paths.js' ;
2025-08-25 22:11:27 +02:00
import fs from 'node:fs/promises' ;
import os from 'node:os' ;
2025-08-21 14:40:18 -07:00
import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
2025-06-03 21:40:46 -07:00
import { FileDiscoveryService } from '../services/fileDiscoveryService.js' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-07-31 05:38:20 +09:00
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js' ;
2025-08-21 14:40:18 -07:00
import { ToolErrorType } from './tool-error.js' ;
import * as glob from 'glob' ;
2026-01-04 14:59:35 -05:00
import { createMockMessageBus } from '../test-utils/mock-message-bus.js' ;
2026-01-27 13:17:40 -08:00
import { DEFAULT_FILE_FILTERING_OPTIONS } from '../config/constants.js' ;
2025-08-21 14:40:18 -07:00
vi . mock ( 'glob' , { spy : true } ) ;
2025-05-18 00:04:32 -07:00
describe ( 'GlobTool' , ( ) = > {
let tempRootDir : string ; // This will be the rootDirectory for the GlobTool instance
let globTool : GlobTool ;
const abortSignal = new AbortController ( ) . signal ;
2026-01-27 13:17:40 -08:00
let mockConfig : Config ;
2025-06-03 21:40:46 -07:00
2025-05-18 00:04:32 -07:00
beforeEach ( async ( ) = > {
// Create a unique root directory for each test run
tempRootDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'glob-tool-root-' ) ) ;
2025-09-10 09:54:50 -07:00
await fs . writeFile ( path . join ( tempRootDir , '.git' ) , '' ) ; // Fake git repo
2026-01-27 13:17:40 -08:00
const rootDir = tempRootDir ;
const workspaceContext = createMockWorkspaceContext ( rootDir ) ;
const fileDiscovery = new FileDiscoveryService ( rootDir ) ;
const mockStorage = {
getProjectTempDir : vi.fn ( ) . mockReturnValue ( '/tmp/project' ) ,
} ;
mockConfig = {
getTargetDir : ( ) = > rootDir ,
getWorkspaceContext : ( ) = > workspaceContext ,
getFileService : ( ) = > fileDiscovery ,
getFileFilteringOptions : ( ) = > DEFAULT_FILE_FILTERING_OPTIONS ,
getFileExclusions : ( ) = > ( { getGlobExcludes : ( ) = > [ ] } ) ,
storage : mockStorage ,
isPathAllowed ( this : Config , absolutePath : string ) : boolean {
const workspaceContext = this . getWorkspaceContext ( ) ;
if ( workspaceContext . isPathWithinWorkspace ( absolutePath ) ) {
return true ;
}
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return isSubpath ( path . resolve ( projectTempDir ) , absolutePath ) ;
} ,
validatePathAccess ( this : Config , absolutePath : string ) : string | null {
if ( this . isPathAllowed ( absolutePath ) ) {
return null ;
}
const workspaceDirs = this . getWorkspaceContext ( ) . getDirectories ( ) ;
const projectTempDir = this . storage . getProjectTempDir ( ) ;
return ` Path not in workspace: Attempted path " ${ absolutePath } " resolves outside the allowed workspace directories: ${ workspaceDirs . join ( ', ' ) } or the project temp directory: ${ projectTempDir } ` ;
} ,
} as unknown as Config ;
2026-01-04 14:59:35 -05:00
globTool = new GlobTool ( mockConfig , createMockMessageBus ( ) ) ;
2025-05-18 00:04:32 -07:00
// Create some test files and directories within this root
// Top-level files
await fs . writeFile ( path . join ( tempRootDir , 'fileA.txt' ) , 'contentA' ) ;
await fs . writeFile ( path . join ( tempRootDir , 'FileB.TXT' ) , 'contentB' ) ; // Different case for testing
// Subdirectory and files within it
await fs . mkdir ( path . join ( tempRootDir , 'sub' ) ) ;
await fs . writeFile ( path . join ( tempRootDir , 'sub' , 'fileC.md' ) , 'contentC' ) ;
await fs . writeFile ( path . join ( tempRootDir , 'sub' , 'FileD.MD' ) , 'contentD' ) ; // Different case
// Deeper subdirectory
await fs . mkdir ( path . join ( tempRootDir , 'sub' , 'deep' ) ) ;
await fs . writeFile (
path . join ( tempRootDir , 'sub' , 'deep' , 'fileE.log' ) ,
'contentE' ,
) ;
// Files for mtime sorting test
await fs . writeFile ( path . join ( tempRootDir , 'older.sortme' ) , 'older_content' ) ;
// Ensure a noticeable difference in modification time
await new Promise ( ( resolve ) = > setTimeout ( resolve , 50 ) ) ;
await fs . writeFile ( path . join ( tempRootDir , 'newer.sortme' ) , 'newer_content' ) ;
} ) ;
afterEach ( async ( ) = > {
// Clean up the temporary root directory
await fs . rm ( tempRootDir , { recursive : true , force : true } ) ;
2026-01-27 13:17:40 -08:00
vi . resetAllMocks ( ) ;
2025-05-18 00:04:32 -07:00
} ) ;
describe ( 'execute' , ( ) = > {
it ( 'should find files matching a simple pattern in the root' , async ( ) = > {
const params : GlobToolParams = { pattern : '*.txt' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain ( 'Found 2 file(s)' ) ;
expect ( result . llmContent ) . toContain ( path . join ( tempRootDir , 'fileA.txt' ) ) ;
expect ( result . llmContent ) . toContain ( path . join ( tempRootDir , 'FileB.TXT' ) ) ;
expect ( result . returnDisplay ) . toBe ( 'Found 2 matching file(s)' ) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should find files case-sensitively when case_sensitive is true' , async ( ) = > {
const params : GlobToolParams = { pattern : '*.txt' , case_sensitive : true } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain ( path . join ( tempRootDir , 'fileA.txt' ) ) ;
expect ( result . llmContent ) . not . toContain (
path . join ( tempRootDir , 'FileB.TXT' ) ,
) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should find files case-insensitively by default (pattern: *.TXT)' , async ( ) = > {
const params : GlobToolParams = { pattern : '*.TXT' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
2026-01-19 20:07:28 -08:00
2025-08-07 10:05:37 -07:00
const result = await invocation . execute ( abortSignal ) ;
2026-01-19 20:07:28 -08:00
expect ( result . llmContent ) . toContain ( 'fileA.txt' ) ;
expect ( result . llmContent ) . toContain ( 'FileB.TXT' ) ;
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should find files case-insensitively when case_sensitive is false (pattern: *.TXT)' , async ( ) = > {
const params : GlobToolParams = {
pattern : '*.TXT' ,
case_sensitive : false ,
} ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain ( 'Found 2 file(s)' ) ;
expect ( result . llmContent ) . toContain ( path . join ( tempRootDir , 'fileA.txt' ) ) ;
expect ( result . llmContent ) . toContain ( path . join ( tempRootDir , 'FileB.TXT' ) ) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should find files using a pattern that includes a subdirectory' , async ( ) = > {
const params : GlobToolParams = { pattern : 'sub/*.md' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain ( 'Found 2 file(s)' ) ;
expect ( result . llmContent ) . toContain (
path . join ( tempRootDir , 'sub' , 'fileC.md' ) ,
) ;
expect ( result . llmContent ) . toContain (
path . join ( tempRootDir , 'sub' , 'FileD.MD' ) ,
) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should find files in a specified relative path (relative to rootDir)' , async ( ) = > {
2025-11-06 15:03:52 -08:00
const params : GlobToolParams = { pattern : '*.md' , dir_path : 'sub' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain ( 'Found 2 file(s)' ) ;
expect ( result . llmContent ) . toContain (
path . join ( tempRootDir , 'sub' , 'fileC.md' ) ,
) ;
expect ( result . llmContent ) . toContain (
path . join ( tempRootDir , 'sub' , 'FileD.MD' ) ,
) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should find files using a deep globstar pattern (e.g., **/*.log)' , async ( ) = > {
const params : GlobToolParams = { pattern : '**/*.log' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain (
path . join ( tempRootDir , 'sub' , 'deep' , 'fileE.log' ) ,
) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
it ( 'should return "No files found" message when pattern matches nothing' , async ( ) = > {
const params : GlobToolParams = { pattern : '*.nonexistent' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-18 00:04:32 -07:00
expect ( result . llmContent ) . toContain (
'No files found matching pattern "*.nonexistent"' ,
) ;
expect ( result . returnDisplay ) . toBe ( 'No files found' ) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
2025-08-18 16:39:05 -07:00
it ( 'should find files with special characters in the name' , async ( ) = > {
await fs . writeFile ( path . join ( tempRootDir , 'file[1].txt' ) , 'content' ) ;
const params : GlobToolParams = { pattern : 'file[1].txt' } ;
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain (
path . join ( tempRootDir , 'file[1].txt' ) ,
) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-08-18 16:39:05 -07:00
it ( 'should find files with special characters like [] and () in the path' , async ( ) = > {
const filePath = path . join (
tempRootDir ,
'src/app/[test]/(dashboard)/testing/components/code.tsx' ,
) ;
await fs . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
await fs . writeFile ( filePath , 'content' ) ;
const params : GlobToolParams = {
pattern : 'src/app/[test]/(dashboard)/testing/components/code.tsx' ,
} ;
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain ( filePath ) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-08-18 16:39:05 -07:00
2025-05-18 00:04:32 -07:00
it ( 'should correctly sort files by modification time (newest first)' , async ( ) = > {
const params : GlobToolParams = { pattern : '*.sortme' } ;
2025-08-07 10:05:37 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-05-20 13:02:41 -07:00
const llmContent = partListUnionToString ( result . llmContent ) ;
2026-01-27 13:17:40 -08:00
const newerIndex = llmContent . indexOf ( 'newer.sortme' ) ;
const olderIndex = llmContent . indexOf ( 'older.sortme' ) ;
expect ( newerIndex ) . toBeLessThan ( olderIndex ) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-08-21 14:40:18 -07:00
it ( 'should return a PATH_NOT_IN_WORKSPACE error if path is outside workspace' , async ( ) = > {
2026-01-27 13:17:40 -08:00
const params : GlobToolParams = { pattern : '*' , dir_path : '/etc' } ;
expect ( ( ) = > globTool . build ( params ) ) . toThrow ( /Path not in workspace/ ) ;
} ) ;
2025-08-21 14:40:18 -07:00
it ( 'should return a GLOB_EXECUTION_ERROR on glob failure' , async ( ) = > {
vi . mocked ( glob . glob ) . mockRejectedValue ( new Error ( 'Glob failed' ) ) ;
2026-01-27 13:17:40 -08:00
const params : GlobToolParams = { pattern : '*' } ;
2025-08-21 14:40:18 -07:00
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
expect ( result . error ? . type ) . toBe ( ToolErrorType . GLOB_EXECUTION_ERROR ) ;
2026-01-19 20:07:28 -08:00
} , 30000 ) ;
2025-05-18 00:04:32 -07:00
} ) ;
describe ( 'validateToolParams' , ( ) = > {
2026-01-27 13:17:40 -08:00
it ( 'should return null for valid parameters' , ( ) = > {
const params : GlobToolParams = { pattern : '*.txt' } ;
expect ( globTool . validateToolParams ( params ) ) . toBeNull ( ) ;
2025-05-18 00:04:32 -07:00
} ) ;
2025-07-31 05:38:20 +09:00
2026-01-27 13:17:40 -08:00
it ( 'should return null for valid parameters with dir_path' , ( ) = > {
const params : GlobToolParams = { pattern : '*.txt' , dir_path : 'sub' } ;
expect ( globTool . validateToolParams ( params ) ) . toBeNull ( ) ;
} ) ;
it ( 'should return null for valid parameters with absolute dir_path within workspace' , async ( ) = > {
const params : GlobToolParams = {
pattern : '*.txt' ,
dir_path : tempRootDir ,
} ;
expect ( globTool . validateToolParams ( params ) ) . toBeNull ( ) ;
} ) ;
it ( 'should return error if pattern is missing' , ( ) = > {
const params = { } as unknown as GlobToolParams ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
"params must have required property 'pattern'" ,
) ;
} ) ;
it ( 'should return error if pattern is an empty string' , ( ) = > {
const params : GlobToolParams = { pattern : '' } ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
"The 'pattern' parameter cannot be empty" ,
) ;
} ) ;
it ( 'should return error if pattern is only whitespace' , ( ) = > {
const params : GlobToolParams = { pattern : ' ' } ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
"The 'pattern' parameter cannot be empty" ,
) ;
} ) ;
it ( 'should return error if dir_path is not a string' , ( ) = > {
const params = {
pattern : '*' ,
dir_path : 123 ,
} as unknown as GlobToolParams ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
'params/dir_path must be string' ,
) ;
} ) ;
it ( 'should return error if case_sensitive is not a boolean' , ( ) = > {
const params = {
pattern : '*' ,
case_sensitive : 'true' ,
} as unknown as GlobToolParams ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
'params/case_sensitive must be boolean' ,
) ;
} ) ;
2025-07-31 05:38:20 +09:00
2026-01-27 13:17:40 -08:00
it ( 'should return error if search path resolves outside workspace' , ( ) = > {
const params : GlobToolParams = { pattern : '*' , dir_path : '../' } ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
2025-07-31 05:38:20 +09:00
'resolves outside the allowed workspace directories' ,
) ;
} ) ;
2026-01-27 13:17:40 -08:00
it ( 'should return error if specified search path does not exist' , ( ) = > {
const params : GlobToolParams = {
pattern : '*' ,
dir_path : 'non-existent' ,
} ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
'Search path does not exist' ,
) ;
} ) ;
it ( 'should return error if specified search path is not a directory' , async ( ) = > {
await fs . writeFile ( path . join ( tempRootDir , 'not-a-dir' ) , 'content' ) ;
const params : GlobToolParams = { pattern : '*' , dir_path : 'not-a-dir' } ;
expect ( globTool . validateToolParams ( params ) ) . toContain (
'Search path is not a directory' ,
) ;
} ) ;
} ) ;
describe ( 'workspace boundary validation' , ( ) = > {
it ( 'should validate search paths are within workspace boundaries' , ( ) = > {
expect ( globTool . validateToolParams ( { pattern : '*' } ) ) . toBeNull ( ) ;
expect (
globTool . validateToolParams ( { pattern : '*' , dir_path : '.' } ) ,
) . toBeNull ( ) ;
expect (
globTool . validateToolParams ( { pattern : '*' , dir_path : tempRootDir } ) ,
) . toBeNull ( ) ;
expect (
globTool . validateToolParams ( { pattern : '*' , dir_path : '..' } ) ,
) . toContain ( 'resolves outside the allowed workspace directories' ) ;
expect (
globTool . validateToolParams ( { pattern : '*' , dir_path : '/' } ) ,
) . toContain ( 'resolves outside the allowed workspace directories' ) ;
} ) ;
2025-07-31 05:38:20 +09:00
2026-01-27 13:17:40 -08:00
it ( 'should provide clear error messages when path is outside workspace' , ( ) = > {
const result = globTool . validateToolParams ( {
pattern : '*' ,
dir_path : '/tmp/outside' ,
} ) ;
expect ( result ) . toContain (
2025-07-31 05:38:20 +09:00
'resolves outside the allowed workspace directories' ,
) ;
} ) ;
it ( 'should work with paths in workspace subdirectories' , async ( ) = > {
2026-01-27 13:17:40 -08:00
const subDir = path . join ( tempRootDir , 'allowed-sub' ) ;
await fs . mkdir ( subDir ) ;
expect (
globTool . validateToolParams ( { pattern : '*' , dir_path : 'allowed-sub' } ) ,
) . toBeNull ( ) ;
2025-07-31 05:38:20 +09:00
} ) ;
} ) ;
2025-09-10 09:54:50 -07:00
describe ( 'ignore file handling' , ( ) = > {
2026-01-27 13:17:40 -08:00
it ( 'should respect .gitignore files by default' , async ( ) = > {
await fs . writeFile (
path . join ( tempRootDir , '.gitignore' ) ,
'ignored_test.txt' ,
) ;
await fs . writeFile ( path . join ( tempRootDir , 'ignored_test.txt' ) , 'content' ) ;
await fs . writeFile ( path . join ( tempRootDir , 'visible_test.txt' ) , 'content' ) ;
2025-10-28 20:02:55 +05:30
2026-01-27 13:17:40 -08:00
const params : GlobToolParams = { pattern : '*_test.txt' } ;
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
2025-10-28 20:02:55 +05:30
2026-01-27 13:17:40 -08:00
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain ( 'visible_test.txt' ) ;
expect ( result . llmContent ) . not . toContain ( 'ignored_test.txt' ) ;
} , 30000 ) ;
2025-10-28 20:02:55 +05:30
2026-01-27 13:17:40 -08:00
it ( 'should respect .geminiignore files by default' , async ( ) = > {
await fs . writeFile (
path . join ( tempRootDir , '.geminiignore' ) ,
'gemini-ignored_test.txt' ,
) ;
await fs . writeFile (
path . join ( tempRootDir , 'gemini-ignored_test.txt' ) ,
'content' ,
) ;
await fs . writeFile ( path . join ( tempRootDir , 'visible_test.txt' ) , 'content' ) ;
const params : GlobToolParams = { pattern : 'visible_test.txt' } ;
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain ( 'visible_test.txt' ) ;
expect ( result . llmContent ) . not . toContain ( 'gemini-ignored_test.txt' ) ;
} , 30000 ) ;
it ( 'should not respect .gitignore when respect_git_ignore is false' , async ( ) = > {
await fs . writeFile (
path . join ( tempRootDir , '.gitignore' ) ,
'ignored_test.txt' ,
) ;
await fs . writeFile ( path . join ( tempRootDir , 'ignored_test.txt' ) , 'content' ) ;
const params : GlobToolParams = {
pattern : 'ignored_test.txt' ,
respect_git_ignore : false ,
} ;
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain ( 'ignored_test.txt' ) ;
} , 30000 ) ;
it ( 'should not respect .geminiignore when respect_gemini_ignore is false' , async ( ) = > {
await fs . writeFile (
path . join ( tempRootDir , '.geminiignore' ) ,
'gemini-ignored_test.txt' ,
) ;
await fs . writeFile (
path . join ( tempRootDir , 'gemini-ignored_test.txt' ) ,
'content' ,
) ;
const params : GlobToolParams = {
pattern : 'gemini-ignored_test.txt' ,
respect_gemini_ignore : false ,
} ;
const invocation = globTool . build ( params ) ;
const result = await invocation . execute ( abortSignal ) ;
expect ( result . llmContent ) . toContain ( 'Found 1 file(s)' ) ;
expect ( result . llmContent ) . toContain ( 'gemini-ignored_test.txt' ) ;
} , 30000 ) ;
2025-09-10 09:54:50 -07:00
} ) ;
2025-05-18 00:04:32 -07:00
} ) ;
2025-06-09 08:07:24 -07:00
describe ( 'sortFileEntries' , ( ) = > {
2026-01-27 13:17:40 -08:00
const now = 1000000 ;
const threshold = 10000 ;
it ( 'should sort a mix of recent and older files correctly' , ( ) = > {
const entries : GlobPath [ ] = [
{ fullpath : ( ) = > 'older-b.txt' , mtimeMs : now - 20000 } ,
{ fullpath : ( ) = > 'recent-b.txt' , mtimeMs : now - 1000 } ,
{ fullpath : ( ) = > 'recent-a.txt' , mtimeMs : now - 500 } ,
{ fullpath : ( ) = > 'older-a.txt' , mtimeMs : now - 30000 } ,
] ;
const sorted = sortFileEntries ( entries , now , threshold ) ;
expect ( sorted . map ( ( e ) = > e . fullpath ( ) ) ) . toEqual ( [
'recent-a.txt' , // Recent, newest first
'recent-b.txt' ,
'older-a.txt' , // Older, alphabetical
'older-b.txt' ,
] ) ;
} ) ;
2025-06-09 08:07:24 -07:00
2026-01-27 13:17:40 -08:00
it ( 'should sort only recent files by mtime descending' , ( ) = > {
const entries : GlobPath [ ] = [
{ fullpath : ( ) = > 'a.txt' , mtimeMs : now - 2000 } ,
{ fullpath : ( ) = > 'b.txt' , mtimeMs : now - 1000 } ,
] ;
const sorted = sortFileEntries ( entries , now , threshold ) ;
expect ( sorted . map ( ( e ) = > e . fullpath ( ) ) ) . toEqual ( [ 'b.txt' , 'a.txt' ] ) ;
2025-06-09 08:07:24 -07:00
} ) ;
2026-01-27 13:17:40 -08:00
it ( 'should sort only older files alphabetically' , ( ) = > {
const entries : GlobPath [ ] = [
{ fullpath : ( ) = > 'b.txt' , mtimeMs : now - 20000 } ,
{ fullpath : ( ) = > 'a.txt' , mtimeMs : now - 30000 } ,
] ;
const sorted = sortFileEntries ( entries , now , threshold ) ;
expect ( sorted . map ( ( e ) = > e . fullpath ( ) ) ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
} ) ;
it ( 'should handle an empty array' , ( ) = > {
expect ( sortFileEntries ( [ ] , now , threshold ) ) . toEqual ( [ ] ) ;
} ) ;
it ( 'should correctly sort files when mtimeMs is missing' , ( ) = > {
const entries : GlobPath [ ] = [
{ fullpath : ( ) = > 'b.txt' } ,
{ fullpath : ( ) = > 'a.txt' } ,
] ;
const sorted = sortFileEntries ( entries , now , threshold ) ;
expect ( sorted . map ( ( e ) = > e . fullpath ( ) ) ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
} ) ;
it ( 'should use recencyThresholdMs parameter' , ( ) = > {
const customThreshold = 5000 ;
const entries : GlobPath [ ] = [
{ fullpath : ( ) = > 'old.txt' , mtimeMs : now - 8000 } ,
{ fullpath : ( ) = > 'new.txt' , mtimeMs : now - 3000 } ,
] ;
const sorted = sortFileEntries ( entries , now , customThreshold ) ;
expect ( sorted . map ( ( e ) = > e . fullpath ( ) ) ) . toEqual ( [ 'new.txt' , 'old.txt' ] ) ;
} ) ;
2025-06-09 08:07:24 -07:00
} ) ;