fix(hashline-edit): scope formatter cache by directory

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-03-24 20:30:16 +09:00
parent cea8769a7f
commit 6da4d2dae0
2 changed files with 132 additions and 5 deletions

View File

@@ -0,0 +1,117 @@
import { beforeEach, describe, expect, it, mock } from "bun:test"
import { clearFormatterCache, resolveFormatters, type FormatterClient } from "./formatter-trigger"
function createDirectoryAwareClient(
resolveConfig: (directory: string) => Promise<Record<string, unknown> | undefined>,
): FormatterClient {
return {
config: {
get: mock(async ({ query }: { query?: { directory?: string } } = {}) => ({
data: await resolveConfig(query?.directory ?? ""),
})),
},
}
}
describe("resolveFormatters cache behavior", () => {
beforeEach(() => {
clearFormatterCache()
})
it("caches formatter resolution per directory", async () => {
//#given
const client = createDirectoryAwareClient(async (directory) => {
if (directory === "/project-a") {
return {
formatter: {
prettier: {
command: ["prettier", "--write", "$FILE"],
extensions: [".ts"],
},
},
}
}
return {
formatter: {
biome: {
command: ["biome", "format", "$FILE"],
extensions: [".ts"],
},
},
}
})
//#when
const firstProjectAResult = await resolveFormatters(client, "/project-a")
const projectBResult = await resolveFormatters(client, "/project-b")
const secondProjectAResult = await resolveFormatters(client, "/project-a")
//#then
expect(client.config.get).toHaveBeenCalledTimes(2)
expect(firstProjectAResult.get(".ts")?.[0]?.command).toEqual(["prettier", "--write", "$FILE"])
expect(projectBResult.get(".ts")?.[0]?.command).toEqual(["biome", "format", "$FILE"])
expect(secondProjectAResult).toBe(firstProjectAResult)
})
it("does not cache transient config fetch failures", async () => {
//#given
const get = mock(async () => ({
data: {
formatter: {
prettier: {
command: ["prettier", "--write", "$FILE"],
extensions: [".ts"],
},
},
},
}))
get.mockImplementationOnce(async () => {
throw new Error("network error")
})
const client: FormatterClient = {
config: { get },
}
//#when
const firstResult = await resolveFormatters(client, "/project-a")
const secondResult = await resolveFormatters(client, "/project-a")
//#then
expect(get).toHaveBeenCalledTimes(2)
expect(firstResult.size).toBe(0)
expect(secondResult.get(".ts")?.[0]?.command).toEqual(["prettier", "--write", "$FILE"])
})
it("does not cache missing config data", async () => {
//#given
let callCount = 0
const client = createDirectoryAwareClient(async () => {
callCount += 1
if (callCount === 1) {
return undefined
}
return {
formatter: {
prettier: {
command: ["prettier", "--write", "$FILE"],
extensions: [".ts"],
},
},
}
})
//#when
const firstResult = await resolveFormatters(client, "/project-a")
const secondResult = await resolveFormatters(client, "/project-a")
//#then
expect(client.config.get).toHaveBeenCalledTimes(2)
expect(firstResult.size).toBe(0)
expect(secondResult.get(".ts")?.[0]?.command).toEqual(["prettier", "--write", "$FILE"])
})
})

View File

@@ -25,15 +25,24 @@ export interface FormatterClient {
}
}
let cachedFormatters: Map<string, Array<{ command: string[]; environment: Record<string, string> }>> | null = null
type FormatterDefinition = { command: string[]; environment: Record<string, string> }
type FormatterMap = Map<string, FormatterDefinition[]>
const cachedFormattersByDirectory = new Map<string, FormatterMap>()
function getFormatterCacheKey(directory: string): string {
return path.resolve(directory)
}
export async function resolveFormatters(
client: FormatterClient,
directory: string,
): Promise<Map<string, Array<{ command: string[]; environment: Record<string, string> }>>> {
): Promise<FormatterMap> {
const cacheKey = getFormatterCacheKey(directory)
const cachedFormatters = cachedFormattersByDirectory.get(cacheKey)
if (cachedFormatters) return cachedFormatters
const result = new Map<string, Array<{ command: string[]; environment: Record<string, string> }>>()
const result = new Map<string, FormatterDefinition[]>()
try {
const response = await client.config.get({ query: { directory } })
@@ -68,11 +77,12 @@ export async function resolveFormatters(
result.set(normalizedExt, existing)
}
}
cachedFormattersByDirectory.set(cacheKey, result)
} catch (error) {
log("[formatter-trigger] Failed to fetch formatter config", { error })
}
cachedFormatters = result
return result
}
@@ -118,5 +128,5 @@ export async function runFormattersForFile(
}
export function clearFormatterCache(): void {
cachedFormatters = null
cachedFormattersByDirectory.clear()
}