From 492029ff7cdd8663483bc37ab4c8da9bf879effd Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 13 Feb 2026 11:44:07 +0900 Subject: [PATCH] perf(directory-injectors): skip writeFileSync when no new paths injected --- .../injector.test.ts | 114 ++++++++++++++++++ .../directory-agents-injector/injector.ts | 6 +- .../injector.test.ts | 114 ++++++++++++++++++ .../directory-readme-injector/injector.ts | 6 +- 4 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/hooks/directory-agents-injector/injector.test.ts create mode 100644 src/hooks/directory-readme-injector/injector.test.ts diff --git a/src/hooks/directory-agents-injector/injector.test.ts b/src/hooks/directory-agents-injector/injector.test.ts new file mode 100644 index 00000000..6a6b211f --- /dev/null +++ b/src/hooks/directory-agents-injector/injector.test.ts @@ -0,0 +1,114 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test" + +const readFileSyncMock = mock((_: string, __: string) => "# AGENTS") +const findAgentsMdUpMock = mock((_: { startDir: string; rootDir: string }) => [] as string[]) +const resolveFilePathMock = mock((_: string, path: string) => path) +const loadInjectedPathsMock = mock((_: string) => new Set()) +const saveInjectedPathsMock = mock((_: string, __: Set) => {}) + +mock.module("node:fs", () => ({ + readFileSync: readFileSyncMock, +})) + +mock.module("./finder", () => ({ + findAgentsMdUp: findAgentsMdUpMock, + resolveFilePath: resolveFilePathMock, +})) + +mock.module("./storage", () => ({ + loadInjectedPaths: loadInjectedPathsMock, + saveInjectedPaths: saveInjectedPathsMock, +})) + +const { processFilePathForAgentsInjection } = await import("./injector") + +describe("processFilePathForAgentsInjection", () => { + beforeEach(() => { + readFileSyncMock.mockClear() + findAgentsMdUpMock.mockClear() + resolveFilePathMock.mockClear() + loadInjectedPathsMock.mockClear() + saveInjectedPathsMock.mockClear() + }) + + it("does not save when all discovered paths are already cached", async () => { + //#given + const sessionID = "session-1" + const cachedDirectory = "/repo/src" + loadInjectedPathsMock.mockReturnValueOnce(new Set([cachedDirectory])) + findAgentsMdUpMock.mockReturnValueOnce(["/repo/src/AGENTS.md"]) + + const truncator = { + truncate: mock(async () => ({ result: "trimmed", truncated: false })), + } + + //#when + await processFilePathForAgentsInjection({ + ctx: { directory: "/repo" } as never, + truncator: truncator as never, + sessionCaches: new Map(), + filePath: "/repo/src/file.ts", + sessionID, + output: { title: "Result", output: "", metadata: {} }, + }) + + //#then + expect(saveInjectedPathsMock).not.toHaveBeenCalled() + }) + + it("saves when a new path is injected", async () => { + //#given + const sessionID = "session-2" + loadInjectedPathsMock.mockReturnValueOnce(new Set()) + findAgentsMdUpMock.mockReturnValueOnce(["/repo/src/AGENTS.md"]) + + const truncator = { + truncate: mock(async () => ({ result: "trimmed", truncated: false })), + } + + //#when + await processFilePathForAgentsInjection({ + ctx: { directory: "/repo" } as never, + truncator: truncator as never, + sessionCaches: new Map(), + filePath: "/repo/src/file.ts", + sessionID, + output: { title: "Result", output: "", metadata: {} }, + }) + + //#then + expect(saveInjectedPathsMock).toHaveBeenCalledTimes(1) + const saveCall = saveInjectedPathsMock.mock.calls[0] + expect(saveCall[0]).toBe(sessionID) + expect((saveCall[1] as Set).has("/repo/src")).toBe(true) + }) + + it("saves once when cached and new paths are mixed", async () => { + //#given + const sessionID = "session-3" + loadInjectedPathsMock.mockReturnValueOnce(new Set(["/repo/already-cached"])) + findAgentsMdUpMock.mockReturnValueOnce([ + "/repo/already-cached/AGENTS.md", + "/repo/new-dir/AGENTS.md", + ]) + + const truncator = { + truncate: mock(async () => ({ result: "trimmed", truncated: false })), + } + + //#when + await processFilePathForAgentsInjection({ + ctx: { directory: "/repo" } as never, + truncator: truncator as never, + sessionCaches: new Map(), + filePath: "/repo/new-dir/file.ts", + sessionID, + output: { title: "Result", output: "", metadata: {} }, + }) + + //#then + expect(saveInjectedPathsMock).toHaveBeenCalledTimes(1) + const saveCall = saveInjectedPathsMock.mock.calls[0] + expect((saveCall[1] as Set).has("/repo/new-dir")).toBe(true) + }) +}) diff --git a/src/hooks/directory-agents-injector/injector.ts b/src/hooks/directory-agents-injector/injector.ts index dc6ca081..28d0be94 100644 --- a/src/hooks/directory-agents-injector/injector.ts +++ b/src/hooks/directory-agents-injector/injector.ts @@ -33,6 +33,7 @@ export async function processFilePathForAgentsInjection(input: { const cache = getSessionCache(input.sessionCaches, input.sessionID); const agentsPaths = findAgentsMdUp({ startDir: dir, rootDir: input.ctx.directory }); + let dirty = false; for (const agentsPath of agentsPaths) { const agentsDir = dirname(agentsPath); if (cache.has(agentsDir)) continue; @@ -48,8 +49,11 @@ export async function processFilePathForAgentsInjection(input: { : ""; input.output.output += `\n\n[Directory Context: ${agentsPath}]\n${result}${truncationNotice}`; cache.add(agentsDir); + dirty = true; } catch {} } - saveInjectedPaths(input.sessionID, cache); + if (dirty) { + saveInjectedPaths(input.sessionID, cache); + } } diff --git a/src/hooks/directory-readme-injector/injector.test.ts b/src/hooks/directory-readme-injector/injector.test.ts new file mode 100644 index 00000000..775d8a89 --- /dev/null +++ b/src/hooks/directory-readme-injector/injector.test.ts @@ -0,0 +1,114 @@ +import { beforeEach, describe, expect, it, mock } from "bun:test" + +const readFileSyncMock = mock((_: string, __: string) => "# README") +const findReadmeMdUpMock = mock((_: { startDir: string; rootDir: string }) => [] as string[]) +const resolveFilePathMock = mock((_: string, path: string) => path) +const loadInjectedPathsMock = mock((_: string) => new Set()) +const saveInjectedPathsMock = mock((_: string, __: Set) => {}) + +mock.module("node:fs", () => ({ + readFileSync: readFileSyncMock, +})) + +mock.module("./finder", () => ({ + findReadmeMdUp: findReadmeMdUpMock, + resolveFilePath: resolveFilePathMock, +})) + +mock.module("./storage", () => ({ + loadInjectedPaths: loadInjectedPathsMock, + saveInjectedPaths: saveInjectedPathsMock, +})) + +const { processFilePathForReadmeInjection } = await import("./injector") + +describe("processFilePathForReadmeInjection", () => { + beforeEach(() => { + readFileSyncMock.mockClear() + findReadmeMdUpMock.mockClear() + resolveFilePathMock.mockClear() + loadInjectedPathsMock.mockClear() + saveInjectedPathsMock.mockClear() + }) + + it("does not save when all discovered paths are already cached", async () => { + //#given + const sessionID = "session-1" + const cachedDirectory = "/repo/src" + loadInjectedPathsMock.mockReturnValueOnce(new Set([cachedDirectory])) + findReadmeMdUpMock.mockReturnValueOnce(["/repo/src/README.md"]) + + const truncator = { + truncate: mock(async () => ({ result: "trimmed", truncated: false })), + } + + //#when + await processFilePathForReadmeInjection({ + ctx: { directory: "/repo" } as never, + truncator: truncator as never, + sessionCaches: new Map(), + filePath: "/repo/src/file.ts", + sessionID, + output: { title: "Result", output: "", metadata: {} }, + }) + + //#then + expect(saveInjectedPathsMock).not.toHaveBeenCalled() + }) + + it("saves when a new path is injected", async () => { + //#given + const sessionID = "session-2" + loadInjectedPathsMock.mockReturnValueOnce(new Set()) + findReadmeMdUpMock.mockReturnValueOnce(["/repo/src/README.md"]) + + const truncator = { + truncate: mock(async () => ({ result: "trimmed", truncated: false })), + } + + //#when + await processFilePathForReadmeInjection({ + ctx: { directory: "/repo" } as never, + truncator: truncator as never, + sessionCaches: new Map(), + filePath: "/repo/src/file.ts", + sessionID, + output: { title: "Result", output: "", metadata: {} }, + }) + + //#then + expect(saveInjectedPathsMock).toHaveBeenCalledTimes(1) + const saveCall = saveInjectedPathsMock.mock.calls[0] + expect(saveCall[0]).toBe(sessionID) + expect((saveCall[1] as Set).has("/repo/src")).toBe(true) + }) + + it("saves once when cached and new paths are mixed", async () => { + //#given + const sessionID = "session-3" + loadInjectedPathsMock.mockReturnValueOnce(new Set(["/repo/already-cached"])) + findReadmeMdUpMock.mockReturnValueOnce([ + "/repo/already-cached/README.md", + "/repo/new-dir/README.md", + ]) + + const truncator = { + truncate: mock(async () => ({ result: "trimmed", truncated: false })), + } + + //#when + await processFilePathForReadmeInjection({ + ctx: { directory: "/repo" } as never, + truncator: truncator as never, + sessionCaches: new Map(), + filePath: "/repo/new-dir/file.ts", + sessionID, + output: { title: "Result", output: "", metadata: {} }, + }) + + //#then + expect(saveInjectedPathsMock).toHaveBeenCalledTimes(1) + const saveCall = saveInjectedPathsMock.mock.calls[0] + expect((saveCall[1] as Set).has("/repo/new-dir")).toBe(true) + }) +}) diff --git a/src/hooks/directory-readme-injector/injector.ts b/src/hooks/directory-readme-injector/injector.ts index 08216584..bfeae7d4 100644 --- a/src/hooks/directory-readme-injector/injector.ts +++ b/src/hooks/directory-readme-injector/injector.ts @@ -33,6 +33,7 @@ export async function processFilePathForReadmeInjection(input: { const cache = getSessionCache(input.sessionCaches, input.sessionID); const readmePaths = findReadmeMdUp({ startDir: dir, rootDir: input.ctx.directory }); + let dirty = false; for (const readmePath of readmePaths) { const readmeDir = dirname(readmePath); if (cache.has(readmeDir)) continue; @@ -48,8 +49,11 @@ export async function processFilePathForReadmeInjection(input: { : ""; input.output.output += `\n\n[Project README: ${readmePath}]\n${result}${truncationNotice}`; cache.add(readmeDir); + dirty = true; } catch {} } - saveInjectedPaths(input.sessionID, cache); + if (dirty) { + saveInjectedPaths(input.sessionID, cache); + } }