From 721ee2a03867f8342ff619b986c76cad918f4e3d Mon Sep 17 00:00:00 2001 From: Syed Raheemuddin Date: Wed, 26 Aug 2026 05:38:05 +0530 Subject: [PATCH] refactor(local-corpus): implement dynamic root resolution and LRU cache (#11491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validated in a combined sub-batch worktree off release/v3.8.51 tip. Also removed an unused variable (idx2) from the new test — pushed to this branch. - Focused test: local-corpus-lru-cache.test.ts — 4/4 pass, part of sub-batch's 165/165 node:test run - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity — all OK - Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff (after the idx2 fix) Thanks for the LRU cache and path-traversal guard — preventing an unconfigured workspace path from indexing system root is a real safety improvement. --- src/lib/localCorpus/configured.ts | 71 +++++++++++---- tests/unit/local-corpus-lru-cache.test.ts | 105 ++++++++++++++++++++++ 2 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 tests/unit/local-corpus-lru-cache.test.ts diff --git a/src/lib/localCorpus/configured.ts b/src/lib/localCorpus/configured.ts index 7220243778..427ca14841 100644 --- a/src/lib/localCorpus/configured.ts +++ b/src/lib/localCorpus/configured.ts @@ -1,40 +1,75 @@ +import path from "path"; import { getLocalCorpusRoot } from "../db/localCorpus"; import { getDefaultLocalCorpusStatus, LocalCorpusIndex } from "./index"; -let sharedRoot: string | null = null; -let sharedIndex: LocalCorpusIndex | null = null; +const indexCache = new Map(); export function resetLocalCorpusIndex(): void { - sharedRoot = null; - sharedIndex = null; + indexCache.clear(); } -function getConfiguredIndex(): LocalCorpusIndex { - const rootPath = getLocalCorpusRoot(); - if (!rootPath) { - throw new Error("Local corpus is not configured. Set a root in Settings > Context Sources"); +function getConfiguredIndex(dynamicRoot?: string): LocalCorpusIndex { + const dbRoot = getLocalCorpusRoot(); + const finalRoot = dynamicRoot || dbRoot; + if (!finalRoot) { + throw new Error( + "Local corpus is not configured. Pass absoluteRootPath or set a root in Settings > Context Sources" + ); } - if (!sharedIndex || sharedRoot !== rootPath) { - sharedRoot = rootPath; - sharedIndex = new LocalCorpusIndex(rootPath); + + const boundingBox = dbRoot ? path.resolve(dbRoot) : process.cwd(); + if (dynamicRoot) { + const resolvedDynamic = path.resolve(dynamicRoot); + const relative = path.relative(boundingBox, resolvedDynamic); + const isInside = relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); + if (!isInside) { + throw new Error( + `Path traversal forbidden: requested path ${resolvedDynamic} is outside allowed boundary ${boundingBox}` + ); + } } - return sharedIndex; + + const resolvedRoot = path.resolve(finalRoot); + const existing = indexCache.get(resolvedRoot); + if (existing) { + indexCache.delete(resolvedRoot); + indexCache.set(resolvedRoot, existing); + return existing; + } + + const maxCacheSize = Math.max( + 1, + parseInt(process.env.OMNIROUTE_CORPUS_CACHE_SIZE || "5", 10) || 5 + ); + if (indexCache.size >= maxCacheSize) { + const firstKey = indexCache.keys().next().value; + if (firstKey) { + indexCache.delete(firstKey); + } + } + + const newIndex = new LocalCorpusIndex(resolvedRoot); + indexCache.set(resolvedRoot, newIndex); + return newIndex; } -export function getConfiguredLocalCorpusStatus() { - return getLocalCorpusRoot() ? getConfiguredIndex().getStatus() : getDefaultLocalCorpusStatus(); +export function getConfiguredLocalCorpusStatus(dynamicRoot?: string) { + const root = dynamicRoot || getLocalCorpusRoot(); + return root ? getConfiguredIndex(dynamicRoot).getStatus() : getDefaultLocalCorpusStatus(); } export async function searchConfiguredLocalCorpus( query: string, - options: { limit?: number; refresh?: boolean } = {} + options: { limit?: number; refresh?: boolean; absoluteRootPath?: string; rootPath?: string } = {} ) { - return getConfiguredIndex().search(query, options); + const root = options.absoluteRootPath || options.rootPath; + return getConfiguredIndex(root).search(query, options); } export async function readConfiguredLocalCorpus( relativePath: string, - options: { startLine?: number; endLine?: number } = {} + options: { startLine?: number; endLine?: number; absoluteRootPath?: string; rootPath?: string } = {} ) { - return getConfiguredIndex().read(relativePath, options); + const root = options.absoluteRootPath || options.rootPath; + return getConfiguredIndex(root).read(relativePath, options); } diff --git a/tests/unit/local-corpus-lru-cache.test.ts b/tests/unit/local-corpus-lru-cache.test.ts new file mode 100644 index 0000000000..1fc9598358 --- /dev/null +++ b/tests/unit/local-corpus-lru-cache.test.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; + +import { setLocalCorpusRoot } from "../../src/lib/db/localCorpus"; +import { + getConfiguredLocalCorpusStatus, + readConfiguredLocalCorpus, + resetLocalCorpusIndex, + searchConfiguredLocalCorpus, +} from "../../src/lib/localCorpus/configured"; + +test.beforeEach(() => { + resetLocalCorpusIndex(); +}); + +test("dynamic root path traversal outside bounding box throws error", async () => { + const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "omni-corpus-base-")); + const subFolder = path.join(tmpBase, "allowed-sub"); + fs.mkdirSync(subFolder, { recursive: true }); + + setLocalCorpusRoot(subFolder); + + const outsideFolder = fs.mkdtempSync(path.join(os.tmpdir(), "omni-corpus-outside-")); + + assert.throws( + () => getConfiguredLocalCorpusStatus(outsideFolder), + /Path traversal forbidden/ + ); + + fs.rmSync(tmpBase, { recursive: true, force: true }); + fs.rmSync(outsideFolder, { recursive: true, force: true }); +}); + +test("path traversal check rejects sibling directory with matching string prefix", async () => { + const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "omni-corpus-prefix")); + const siblingFolder = tmpBase + "-sibling"; + fs.mkdirSync(siblingFolder, { recursive: true }); + + setLocalCorpusRoot(tmpBase); + + assert.throws( + () => getConfiguredLocalCorpusStatus(siblingFolder), + /Path traversal forbidden/ + ); + + fs.rmSync(tmpBase, { recursive: true, force: true }); + fs.rmSync(siblingFolder, { recursive: true, force: true }); +}); + +test("search and read configured local corpus support dynamic root within bounds", async () => { + const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "omni-corpus-valid-")); + const sampleFile = path.join(tmpBase, "test.txt"); + fs.writeFileSync(sampleFile, "Line 1: searchable text\nLine 2: content"); + + setLocalCorpusRoot(tmpBase); + + const status = getConfiguredLocalCorpusStatus(tmpBase); + assert.equal(typeof status.indexedBytes, "number"); + + const searchResults = await searchConfiguredLocalCorpus("searchable", { + absoluteRootPath: tmpBase, + }); + assert.ok(Array.isArray(searchResults.results)); + + const readResult = await readConfiguredLocalCorpus("test.txt", { + absoluteRootPath: tmpBase, + }); + assert.ok(readResult.content.includes("searchable")); + + fs.rmSync(tmpBase, { recursive: true, force: true }); +}); + +test("LRU cache respects access order and OMNIROUTE_CORPUS_CACHE_SIZE", async () => { + const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "omni-corpus-lru-root-")); + setLocalCorpusRoot(tmpRoot); + + process.env.OMNIROUTE_CORPUS_CACHE_SIZE = "2"; + + const dir1 = path.join(tmpRoot, "dir1"); + const dir2 = path.join(tmpRoot, "dir2"); + const dir3 = path.join(tmpRoot, "dir3"); + + fs.mkdirSync(dir1, { recursive: true }); + fs.mkdirSync(dir2, { recursive: true }); + fs.mkdirSync(dir3, { recursive: true }); + + const idx1 = getConfiguredLocalCorpusStatus(dir1); + getConfiguredLocalCorpusStatus(dir2); + + // Access dir1 again so its access order is updated (making dir2 the least recently used) + getConfiguredLocalCorpusStatus(dir1); + + // Add dir3, which causes cache capacity (2) to be exceeded -> evicts dir2 + getConfiguredLocalCorpusStatus(dir3); + + // Accessing dir1 again should return the existing cached index instance + const idx1Again = getConfiguredLocalCorpusStatus(dir1); + assert.equal(idx1.indexedBytes, idx1Again.indexedBytes); + + delete process.env.OMNIROUTE_CORPUS_CACHE_SIZE; + fs.rmSync(tmpRoot, { recursive: true, force: true }); +});