diff --git a/src/hooks/rules-injector/injector.ts b/src/hooks/rules-injector/injector.ts index 58340f0d9..dc4e9fe29 100644 --- a/src/hooks/rules-injector/injector.ts +++ b/src/hooks/rules-injector/injector.ts @@ -43,33 +43,6 @@ interface ParsedRuleEntry { const parsedRuleCache = new Map(); -function getCachedParsedRule( - filePath: string, - realPath: string -): { metadata: RuleMetadata; body: string } { - try { - const stat = statSync(filePath); - const cached = parsedRuleCache.get(realPath); - - if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) { - return { metadata: cached.metadata, body: cached.body }; - } - - const rawContent = readFileSync(filePath, "utf-8"); - const { metadata, body } = parseRuleFrontmatter(rawContent); - parsedRuleCache.set(realPath, { - mtimeMs: stat.mtimeMs, - size: stat.size, - metadata, - body, - }); - return { metadata, body }; - } catch { - const rawContent = readFileSync(filePath, "utf-8"); - return parseRuleFrontmatter(rawContent); - } -} - function resolveFilePath( workspaceDirectory: string, path: string @@ -84,6 +57,14 @@ export function createRuleInjectionProcessor(deps: { truncator: DynamicTruncator; getSessionCache: (sessionID: string) => SessionInjectedRulesCache; ruleFinderOptions?: FindRuleFilesOptions; + readFileSync?: typeof readFileSync; + statSync?: typeof statSync; + homedir?: typeof homedir; + shouldApplyRule?: typeof shouldApplyRule; + isDuplicateByRealPath?: typeof isDuplicateByRealPath; + createContentHash?: typeof createContentHash; + isDuplicateByContentHash?: typeof isDuplicateByContentHash; + saveInjectedRules?: typeof saveInjectedRules; }): { processFilePathForInjection: ( filePath: string, @@ -91,7 +72,44 @@ export function createRuleInjectionProcessor(deps: { output: ToolExecuteOutput ) => Promise; } { - const { workspaceDirectory, truncator, getSessionCache, ruleFinderOptions } = deps; + const { + workspaceDirectory, + truncator, + getSessionCache, + ruleFinderOptions, + readFileSync: readRuleFileSync = readFileSync, + statSync: statRuleSync = statSync, + homedir: getHomeDir = homedir, + shouldApplyRule: shouldApplyRuleImpl = shouldApplyRule, + isDuplicateByRealPath: isDuplicateByRealPathImpl = isDuplicateByRealPath, + createContentHash: createContentHashImpl = createContentHash, + isDuplicateByContentHash: isDuplicateByContentHashImpl = isDuplicateByContentHash, + saveInjectedRules: saveInjectedRulesImpl = saveInjectedRules, + } = deps; + + function getParsedRule(filePath: string, realPath: string): { metadata: RuleMetadata; body: string } { + try { + const stat = statRuleSync(filePath); + const cached = parsedRuleCache.get(realPath); + + if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) { + return { metadata: cached.metadata, body: cached.body }; + } + + const rawContent = readRuleFileSync(filePath, "utf-8"); + const { metadata, body } = parseRuleFrontmatter(rawContent); + parsedRuleCache.set(realPath, { + mtimeMs: stat.mtimeMs, + size: stat.size, + metadata, + body, + }); + return { metadata, body }; + } catch { + const rawContent = readRuleFileSync(filePath, "utf-8"); + return parseRuleFrontmatter(rawContent); + } + } async function processFilePathForInjection( filePath: string, @@ -103,17 +121,17 @@ export function createRuleInjectionProcessor(deps: { const projectRoot = findProjectRoot(resolved); const cache = getSessionCache(sessionID); - const home = homedir(); + const home = getHomeDir(); const ruleFileCandidates = findRuleFiles(projectRoot, home, resolved, ruleFinderOptions); const toInject: RuleToInject[] = []; let dirty = false; for (const candidate of ruleFileCandidates) { - if (isDuplicateByRealPath(candidate.realPath, cache.realPaths)) continue; + if (isDuplicateByRealPathImpl(candidate.realPath, cache.realPaths)) continue; try { - const { metadata, body } = getCachedParsedRule( + const { metadata, body } = getParsedRule( candidate.path, candidate.realPath ); @@ -122,13 +140,13 @@ export function createRuleInjectionProcessor(deps: { if (candidate.isSingleFile) { matchReason = "copilot-instructions (always apply)"; } else { - const matchResult = shouldApplyRule(metadata, resolved, projectRoot); + const matchResult = shouldApplyRuleImpl(metadata, resolved, projectRoot); if (!matchResult.applies) continue; matchReason = matchResult.reason ?? "matched"; } - const contentHash = createContentHash(body); - if (isDuplicateByContentHash(contentHash, cache.contentHashes)) continue; + const contentHash = createContentHashImpl(body); + if (isDuplicateByContentHashImpl(contentHash, cache.contentHashes)) continue; const relativePath = projectRoot ? relative(projectRoot, candidate.path) @@ -163,7 +181,7 @@ export function createRuleInjectionProcessor(deps: { } if (dirty) { - saveInjectedRules(sessionID, cache); + saveInjectedRulesImpl(sessionID, cache); } }