summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoranj-s <[email protected]>2025-06-09 08:07:24 -0700
committerGitHub <[email protected]>2025-06-09 08:07:24 -0700
commitc55a1d90120ff8b567e9059466819c4920dc560e (patch)
treecc574fa0bbaa5f4bfe31609f49f2bb4d21471f6b
parenta2fee6bdd3927a2a08520ab87dabc8fecdb6075b (diff)
Add support for sorting files search by recency threshold followed by lexicographic sorting (#867)
-rw-r--r--packages/core/src/tools/glob.test.ts121
-rw-r--r--packages/core/src/tools/glob.ts53
2 files changed, 167 insertions, 7 deletions
diff --git a/packages/core/src/tools/glob.test.ts b/packages/core/src/tools/glob.test.ts
index f975fc08..90fe1a2e 100644
--- a/packages/core/src/tools/glob.test.ts
+++ b/packages/core/src/tools/glob.test.ts
@@ -4,11 +4,17 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { GlobTool, GlobToolParams } from './glob.js';
+import {
+ GlobTool,
+ GlobToolParams,
+ sortFileEntries,
+ GlobFileEntry,
+} from './glob.js';
import { partListUnionToString } from '../core/geminiRequest.js';
// import { ToolResult } from './tools.js'; // ToolResult is implicitly used by execute
import path from 'path';
import fs from 'fs/promises';
+import { Stats } from 'fs';
import os from 'os';
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; // Removed vi
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
@@ -263,3 +269,116 @@ describe('GlobTool', () => {
});
});
});
+
+describe('sortFileEntries', () => {
+ const nowTimestamp = new Date('2024-01-15T12:00:00.000Z').getTime();
+ const oneDayInMs = 24 * 60 * 60 * 1000;
+
+ const createFileEntry = (path: string, mtimeDate: Date): GlobFileEntry => ({
+ path,
+ stats: { mtime: mtimeDate } as Stats,
+ });
+
+ it('should sort a mix of recent and older files correctly', () => {
+ const recentTime1 = new Date(nowTimestamp - 1 * 60 * 60 * 1000); // 1 hour ago
+ const recentTime2 = new Date(nowTimestamp - 2 * 60 * 60 * 1000); // 2 hours ago
+ const olderTime1 = new Date(
+ nowTimestamp - (oneDayInMs + 1 * 60 * 60 * 1000),
+ ); // 25 hours ago
+ const olderTime2 = new Date(
+ nowTimestamp - (oneDayInMs + 2 * 60 * 60 * 1000),
+ ); // 26 hours ago
+
+ const entries: GlobFileEntry[] = [
+ createFileEntry('older_zebra.txt', olderTime2),
+ createFileEntry('recent_alpha.txt', recentTime1),
+ createFileEntry('older_apple.txt', olderTime1),
+ createFileEntry('recent_beta.txt', recentTime2),
+ createFileEntry('older_banana.txt', olderTime1), // Same mtime as apple
+ ];
+
+ const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
+ const sortedPaths = sorted.map((e) => e.path);
+
+ expect(sortedPaths).toEqual([
+ 'recent_alpha.txt', // Recent, newest
+ 'recent_beta.txt', // Recent, older
+ 'older_apple.txt', // Older, alphabetical
+ 'older_banana.txt', // Older, alphabetical
+ 'older_zebra.txt', // Older, alphabetical
+ ]);
+ });
+
+ it('should sort only recent files by mtime descending', () => {
+ const recentTime1 = new Date(nowTimestamp - 1000); // Newest
+ const recentTime2 = new Date(nowTimestamp - 2000);
+ const recentTime3 = new Date(nowTimestamp - 3000); // Oldest recent
+
+ const entries: GlobFileEntry[] = [
+ createFileEntry('c.txt', recentTime2),
+ createFileEntry('a.txt', recentTime3),
+ createFileEntry('b.txt', recentTime1),
+ ];
+ const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
+ expect(sorted.map((e) => e.path)).toEqual(['b.txt', 'c.txt', 'a.txt']);
+ });
+
+ it('should sort only older files alphabetically by path', () => {
+ const olderTime = new Date(nowTimestamp - 2 * oneDayInMs); // All equally old
+ const entries: GlobFileEntry[] = [
+ createFileEntry('zebra.txt', olderTime),
+ createFileEntry('apple.txt', olderTime),
+ createFileEntry('banana.txt', olderTime),
+ ];
+ const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
+ expect(sorted.map((e) => e.path)).toEqual([
+ 'apple.txt',
+ 'banana.txt',
+ 'zebra.txt',
+ ]);
+ });
+
+ it('should handle an empty array', () => {
+ const entries: GlobFileEntry[] = [];
+ const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
+ expect(sorted).toEqual([]);
+ });
+
+ it('should correctly sort files when mtimes are identical for older files', () => {
+ const olderTime = new Date(nowTimestamp - 2 * oneDayInMs);
+ const entries: GlobFileEntry[] = [
+ createFileEntry('b.txt', olderTime),
+ createFileEntry('a.txt', olderTime),
+ ];
+ const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
+ expect(sorted.map((e) => e.path)).toEqual(['a.txt', 'b.txt']);
+ });
+
+ it('should correctly sort files when mtimes are identical for recent files (maintaining mtime sort)', () => {
+ const recentTime = new Date(nowTimestamp - 1000);
+ const entries: GlobFileEntry[] = [
+ createFileEntry('b.txt', recentTime),
+ createFileEntry('a.txt', recentTime),
+ ];
+ const sorted = sortFileEntries(entries, nowTimestamp, oneDayInMs);
+ expect(sorted.map((e) => e.path)).toContain('a.txt');
+ expect(sorted.map((e) => e.path)).toContain('b.txt');
+ expect(sorted.length).toBe(2);
+ });
+
+ it('should use recencyThresholdMs parameter correctly', () => {
+ const justOverThreshold = new Date(nowTimestamp - (1000 + 1)); // Barely older
+ const justUnderThreshold = new Date(nowTimestamp - (1000 - 1)); // Barely recent
+ const customThresholdMs = 1000; // 1 second
+
+ const entries: GlobFileEntry[] = [
+ createFileEntry('older_file.txt', justOverThreshold),
+ createFileEntry('recent_file.txt', justUnderThreshold),
+ ];
+ const sorted = sortFileEntries(entries, nowTimestamp, customThresholdMs);
+ expect(sorted.map((e) => e.path)).toEqual([
+ 'recent_file.txt',
+ 'older_file.txt',
+ ]);
+ });
+});
diff --git a/packages/core/src/tools/glob.ts b/packages/core/src/tools/glob.ts
index d4b479eb..6acb2a2b 100644
--- a/packages/core/src/tools/glob.ts
+++ b/packages/core/src/tools/glob.ts
@@ -12,6 +12,42 @@ import { BaseTool, ToolResult } from './tools.js';
import { shortenPath, makeRelative } from '../utils/paths.js';
import { Config } from '../config/config.js';
+// Type definition for file entries returned by fast-glob with stats: true
+export interface GlobFileEntry {
+ path: string;
+ stats?: fs.Stats;
+}
+
+/**
+ * Sorts file entries based on recency and then alphabetically.
+ * Recent files (modified within recencyThresholdMs) are listed first, newest to oldest.
+ * Older files are listed after recent ones, sorted alphabetically by path.
+ */
+export function sortFileEntries(
+ entries: GlobFileEntry[],
+ nowTimestamp: number,
+ recencyThresholdMs: number,
+): GlobFileEntry[] {
+ const sortedEntries = [...entries];
+ sortedEntries.sort((a, b) => {
+ const mtimeA = a.stats?.mtime?.getTime() ?? 0;
+ const mtimeB = b.stats?.mtime?.getTime() ?? 0;
+ const aIsRecent = nowTimestamp - mtimeA < recencyThresholdMs;
+ const bIsRecent = nowTimestamp - mtimeB < recencyThresholdMs;
+
+ if (aIsRecent && bIsRecent) {
+ return mtimeB - mtimeA;
+ } else if (aIsRecent) {
+ return -1;
+ } else if (bIsRecent) {
+ return 1;
+ } else {
+ return a.path.localeCompare(b.path);
+ }
+ });
+ return sortedEntries;
+}
+
/**
* Parameters for the GlobTool
*/
@@ -232,13 +268,18 @@ export class GlobTool extends BaseTool<GlobToolParams, ToolResult> {
};
}
- filteredEntries.sort((a, b) => {
- const mtimeA = a.stats?.mtime?.getTime() ?? 0;
- const mtimeB = b.stats?.mtime?.getTime() ?? 0;
- return mtimeB - mtimeA;
- });
+ // Set filtering such that we first show the most recent files
+ const oneDayInMs = 24 * 60 * 60 * 1000;
+ const nowTimestamp = new Date().getTime();
+
+ // Sort the filtered entries using the new helper function
+ const sortedEntries = sortFileEntries(
+ filteredEntries as GlobFileEntry[], // Cast because fast-glob's Entry type is generic
+ nowTimestamp,
+ oneDayInMs,
+ );
- const sortedAbsolutePaths = filteredEntries.map((entry) => entry.path);
+ const sortedAbsolutePaths = sortedEntries.map((entry) => entry.path);
const fileListDescription = sortedAbsolutePaths.join('\n');
const fileCount = sortedAbsolutePaths.length;