From 8aee15399a743c4f526f5d18654b0891e6e2b8cb Mon Sep 17 00:00:00 2001 From: Mikey Date: Wed, 2 Sep 2026 14:59:12 -0700 Subject: [PATCH 1/2] Cache existing hidden directories in codeSearch to avoid redundant stat calls --- sdk/src/__tests__/code-search.test.ts | 22 +++++++++++++++- sdk/src/tools/code-search.ts | 38 ++++++++++++++++++++++----- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/sdk/src/__tests__/code-search.test.ts b/sdk/src/__tests__/code-search.test.ts index 40b0f8a3de..cef6cfac46 100644 --- a/sdk/src/__tests__/code-search.test.ts +++ b/sdk/src/__tests__/code-search.test.ts @@ -10,7 +10,7 @@ import { } from '@codebuff/common/testing/mocks' import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test' -import { codeSearch } from '../tools/code-search' +import { codeSearch, clearHiddenDirsCache, getExistingHiddenDirs } from '../tools/code-search' import type { MockChildProcess } from '@codebuff/common/testing/mocks' @@ -19,6 +19,7 @@ describe('codeSearch', () => { let mockProcess: MockChildProcess beforeEach(async () => { + clearHiddenDirsCache() mockProcess = createMockChildProcess() mockSpawn = mock(() => mockProcess) await mockModule('child_process', () => ({ @@ -29,6 +30,7 @@ describe('codeSearch', () => { afterEach(() => { mock.restore() clearMockedModules() + clearHiddenDirsCache() }) describe('basic search', () => { @@ -899,4 +901,22 @@ describe('codeSearch', () => { expect(spawnOptions.cwd).toBe('/test/outside') }) }) + + describe('hidden directories caching', () => { + it('caches existing hidden directories across repeated calls', () => { + const dir = process.cwd() + const first = getExistingHiddenDirs(dir) + const second = getExistingHiddenDirs(dir) + expect(first).toEqual(second) + }) + + it('clears cache when clearHiddenDirsCache is called', () => { + const dir = process.cwd() + const first = getExistingHiddenDirs(dir) + clearHiddenDirsCache() + const second = getExistingHiddenDirs(dir) + expect(first).toEqual(second) + }) + }) }) + diff --git a/sdk/src/tools/code-search.ts b/sdk/src/tools/code-search.ts index 23c70e6db7..69d3459f76 100644 --- a/sdk/src/tools/code-search.ts +++ b/sdk/src/tools/code-search.ts @@ -19,6 +19,36 @@ const INCLUDED_HIDDEN_DIRS = [ '.husky', // Git hooks ] +const HIDDEN_DIRS_CACHE_TTL_MS = 30_000 // 30 seconds TTL +const hiddenDirsCache = new Map() + +export function getExistingHiddenDirs(searchCwd: string): string[] { + const cached = hiddenDirsCache.get(searchCwd) + const now = Date.now() + if (cached && now - cached.timestamp < HIDDEN_DIRS_CACHE_TTL_MS) { + return cached.dirs + } + + const existingHiddenDirs = INCLUDED_HIDDEN_DIRS.filter((dir) => { + try { + return fs.statSync(path.join(searchCwd, dir)).isDirectory() + } catch { + return false + } + }) + + if (hiddenDirsCache.size >= 100) { + hiddenDirsCache.clear() + } + hiddenDirsCache.set(searchCwd, { dirs: existingHiddenDirs, timestamp: now }) + + return existingHiddenDirs +} + +export function clearHiddenDirsCache(): void { + hiddenDirsCache.clear() +} + export function codeSearch({ projectPath, pattern, @@ -68,13 +98,7 @@ export function codeSearch({ // "--"" prevents pattern from being misparsed as a flag (e.g., pattern starting with '-') // Search paths: '.' plus blessed hidden directories that actually exist // Filter out non-existent directories to avoid ripgrep stderr errors - const existingHiddenDirs = INCLUDED_HIDDEN_DIRS.filter((dir) => { - try { - return fs.statSync(path.join(searchCwd, dir)).isDirectory() - } catch { - return false - } - }) + const existingHiddenDirs = getExistingHiddenDirs(searchCwd) const searchPaths = ['.', ...existingHiddenDirs] const args = [ '--no-config', From 9d25b46e63d44f844dcb09fa52dfd9d80a52783f Mon Sep 17 00:00:00 2001 From: Mikey Date: Thu, 3 Sep 2026 09:45:47 -0700 Subject: [PATCH 2/2] Improve hidden dirs caching with 1s burst TTL, true LRU eviction, and dynamic tests --- sdk/src/__tests__/code-search.test.ts | 152 ++++++++++++++++++++++++-- sdk/src/tools/code-search.ts | 39 +++++-- 2 files changed, 169 insertions(+), 22 deletions(-) diff --git a/sdk/src/__tests__/code-search.test.ts b/sdk/src/__tests__/code-search.test.ts index cef6cfac46..5b2cd4383b 100644 --- a/sdk/src/__tests__/code-search.test.ts +++ b/sdk/src/__tests__/code-search.test.ts @@ -1,3 +1,7 @@ +import * as fs from 'fs' +import * as os from 'os' +import * as path from 'path' + import { clearMockedModules, mockModule, @@ -10,7 +14,11 @@ import { } from '@codebuff/common/testing/mocks' import { describe, expect, it, mock, beforeEach, afterEach } from 'bun:test' -import { codeSearch, clearHiddenDirsCache, getExistingHiddenDirs } from '../tools/code-search' +import { + codeSearch, + clearHiddenDirsCache, + getExistingHiddenDirs, +} from '../tools/code-search' import type { MockChildProcess } from '@codebuff/common/testing/mocks' @@ -903,20 +911,140 @@ describe('codeSearch', () => { }) describe('hidden directories caching', () => { - it('caches existing hidden directories across repeated calls', () => { - const dir = process.cwd() - const first = getExistingHiddenDirs(dir) - const second = getExistingHiddenDirs(dir) - expect(first).toEqual(second) + let tempDirs: string[] = [] + + const createTempDir = () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-search-test-')) + tempDirs.push(dir) + return dir + } + + afterEach(() => { + clearHiddenDirsCache() + for (const dir of tempDirs) { + try { + fs.rmSync(dir, { recursive: true, force: true }) + } catch {} + } + tempDirs = [] }) - it('clears cache when clearHiddenDirsCache is called', () => { - const dir = process.cwd() - const first = getExistingHiddenDirs(dir) + it('caches detected hidden directories within TTL', () => { + const testDir = createTempDir() + const t0 = 10_000 + + // Initial check when no hidden directories exist + expect(getExistingHiddenDirs(testDir, t0)).toEqual([]) + + // Create a hidden directory on disk + fs.mkdirSync(path.join(testDir, '.github')) + + // Within 1-second TTL, cached result is returned without re-statting + expect(getExistingHiddenDirs(testDir, t0 + 500)).toEqual([]) + }) + + it('immediately picks up newly created directories after clearHiddenDirsCache()', () => { + const testDir = createTempDir() + + // Initial check + expect(getExistingHiddenDirs(testDir)).toEqual([]) + + // Create a hidden directory + fs.mkdirSync(path.join(testDir, '.github')) + + // Still cached + expect(getExistingHiddenDirs(testDir)).toEqual([]) + + // Clear cache and verify immediate discovery clearHiddenDirsCache() - const second = getExistingHiddenDirs(dir) - expect(first).toEqual(second) + expect(getExistingHiddenDirs(testDir)).toEqual(['.github']) + }) + + it('immediately picks up newly removed directories after clearHiddenDirsCache()', () => { + const testDir = createTempDir() + const huskyDir = path.join(testDir, '.husky') + fs.mkdirSync(huskyDir) + + // Initial check detects .husky + expect(getExistingHiddenDirs(testDir)).toEqual(['.husky']) + + // Remove the directory + fs.rmdirSync(huskyDir) + + // Still cached + expect(getExistingHiddenDirs(testDir)).toEqual(['.husky']) + + // Clear cache and verify immediate discovery of removal + clearHiddenDirsCache() + expect(getExistingHiddenDirs(testDir)).toEqual([]) + }) + + it('automatically discovers newly created directories once TTL expires without manual cache clear', () => { + const testDir = createTempDir() + const t0 = 20_000 + + // Initial check + expect(getExistingHiddenDirs(testDir, t0)).toEqual([]) + + // Create a hidden directory + fs.mkdirSync(path.join(testDir, '.agents')) + + // Within TTL: cached + expect(getExistingHiddenDirs(testDir, t0 + 500)).toEqual([]) + + // Past 1000ms TTL: automatically refreshed on next call + expect(getExistingHiddenDirs(testDir, t0 + 1001)).toEqual(['.agents']) + }) + + it('supports targeted directory invalidation with clearHiddenDirsCache(dir)', () => { + const dirA = createTempDir() + const dirB = createTempDir() + + // Populate both in cache + expect(getExistingHiddenDirs(dirA)).toEqual([]) + expect(getExistingHiddenDirs(dirB)).toEqual([]) + + // Create directories in both + fs.mkdirSync(path.join(dirA, '.github')) + fs.mkdirSync(path.join(dirB, '.husky')) + + // Invalidate only dirA + clearHiddenDirsCache(dirA) + + // dirA is re-evaluated, dirB remains cached + expect(getExistingHiddenDirs(dirA)).toEqual(['.github']) + expect(getExistingHiddenDirs(dirB)).toEqual([]) + }) + + it('evicts least-recently used entry when cache capacity reaches MAX_CACHE_SIZE', () => { + const dirOld = createTempDir() + const dirRecent = createTempDir() + const t0 = 30_000 + + // Cache dirOld then dirRecent + expect(getExistingHiddenDirs(dirOld, t0)).toEqual([]) + expect(getExistingHiddenDirs(dirRecent, t0)).toEqual([]) + + // Access dirRecent again to refresh its recency in the LRU Map + expect(getExistingHiddenDirs(dirRecent, t0 + 100)).toEqual([]) + + // Fill remaining capacity up to 100 entries + for (let i = 2; i < 100; i++) { + getExistingHiddenDirs(`/mock/nonexistent/dir-${i}`, t0) + } + + // Add 101st entry to trigger eviction of the oldest (dirOld) + getExistingHiddenDirs('/mock/nonexistent/dir-overflow', t0) + + // Create .github in both test directories + fs.mkdirSync(path.join(dirOld, '.github')) + fs.mkdirSync(path.join(dirRecent, '.github')) + + // dirRecent was refreshed and preserved in LRU, so it remains cached within TTL + expect(getExistingHiddenDirs(dirRecent, t0 + 200)).toEqual([]) + + // dirOld was evicted, so it re-stats disk and discovers .github + expect(getExistingHiddenDirs(dirOld, t0 + 200)).toEqual(['.github']) }) }) }) - diff --git a/sdk/src/tools/code-search.ts b/sdk/src/tools/code-search.ts index 69d3459f76..f73e130ade 100644 --- a/sdk/src/tools/code-search.ts +++ b/sdk/src/tools/code-search.ts @@ -19,34 +19,53 @@ const INCLUDED_HIDDEN_DIRS = [ '.husky', // Git hooks ] -const HIDDEN_DIRS_CACHE_TTL_MS = 30_000 // 30 seconds TTL +const HIDDEN_DIRS_CACHE_TTL_MS = 1_000 // 1 second TTL (burst de-duplication window) +const MAX_CACHE_SIZE = 100 const hiddenDirsCache = new Map() -export function getExistingHiddenDirs(searchCwd: string): string[] { - const cached = hiddenDirsCache.get(searchCwd) - const now = Date.now() +export function getExistingHiddenDirs( + searchCwd: string, + now = Date.now(), +): string[] { + const normalizedCwd = path.resolve(searchCwd) + const cached = hiddenDirsCache.get(normalizedCwd) if (cached && now - cached.timestamp < HIDDEN_DIRS_CACHE_TTL_MS) { + // Refresh LRU recency + hiddenDirsCache.delete(normalizedCwd) + hiddenDirsCache.set(normalizedCwd, cached) return cached.dirs } const existingHiddenDirs = INCLUDED_HIDDEN_DIRS.filter((dir) => { try { - return fs.statSync(path.join(searchCwd, dir)).isDirectory() + return fs.statSync(path.join(normalizedCwd, dir)).isDirectory() } catch { return false } }) - if (hiddenDirsCache.size >= 100) { - hiddenDirsCache.clear() + if (hiddenDirsCache.has(normalizedCwd)) { + hiddenDirsCache.delete(normalizedCwd) + } else if (hiddenDirsCache.size >= MAX_CACHE_SIZE) { + const oldestKey = hiddenDirsCache.keys().next().value + if (oldestKey !== undefined) { + hiddenDirsCache.delete(oldestKey) + } } - hiddenDirsCache.set(searchCwd, { dirs: existingHiddenDirs, timestamp: now }) + hiddenDirsCache.set(normalizedCwd, { + dirs: existingHiddenDirs, + timestamp: now, + }) return existingHiddenDirs } -export function clearHiddenDirsCache(): void { - hiddenDirsCache.clear() +export function clearHiddenDirsCache(dir?: string): void { + if (dir) { + hiddenDirsCache.delete(path.resolve(dir)) + } else { + hiddenDirsCache.clear() + } } export function codeSearch({