Merge branch 'fix/perf-q06' into fix/perf-omo-in-tree
This commit is contained in:
@@ -76,7 +76,15 @@ export function isOverwriteEnabled(value: boolean | string | undefined): boolean
|
|||||||
export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
|
export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
|
||||||
const readPermissionsBySession = new Map<string, Set<string>>()
|
const readPermissionsBySession = new Map<string, Set<string>>()
|
||||||
const sessionLastAccess = new Map<string, number>()
|
const sessionLastAccess = new Map<string, number>()
|
||||||
const canonicalSessionRoot = toCanonicalPath(resolveInputPath(ctx, ctx.directory))
|
let canonicalSessionRoot: string | undefined
|
||||||
|
|
||||||
|
function getCanonicalSessionRoot(): string {
|
||||||
|
if (!canonicalSessionRoot) {
|
||||||
|
canonicalSessionRoot = toCanonicalPath(resolveInputPath(ctx, ctx.directory))
|
||||||
|
}
|
||||||
|
|
||||||
|
return canonicalSessionRoot
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": async (input, output) => {
|
"tool.execute.before": async (input, output) => {
|
||||||
@@ -86,7 +94,7 @@ export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
|
|||||||
output,
|
output,
|
||||||
readPermissionsBySession,
|
readPermissionsBySession,
|
||||||
sessionLastAccess,
|
sessionLastAccess,
|
||||||
canonicalSessionRoot,
|
getCanonicalSessionRoot,
|
||||||
maxTrackedSessions: MAX_TRACKED_SESSIONS,
|
maxTrackedSessions: MAX_TRACKED_SESSIONS,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||||
|
import { tmpdir } from "node:os"
|
||||||
|
import { join } from "node:path"
|
||||||
|
|
||||||
|
const realFs = await import("node:fs")
|
||||||
|
|
||||||
|
const existsSyncMock = mock(realFs.existsSync)
|
||||||
|
const realpathNativeMock = mock(realFs.realpathSync.native)
|
||||||
|
|
||||||
|
mock.module("fs", () => ({
|
||||||
|
...realFs,
|
||||||
|
existsSync: existsSyncMock,
|
||||||
|
realpathSync: {
|
||||||
|
...realFs.realpathSync,
|
||||||
|
native: realpathNativeMock,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { createWriteExistingFileGuardHook } = await import("./index")
|
||||||
|
|
||||||
|
describe("createWriteExistingFileGuardHook", () => {
|
||||||
|
let tempDir = ""
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// given
|
||||||
|
tempDir = mkdtempSync(join(tmpdir(), "write-existing-file-guard-lazy-"))
|
||||||
|
mkdirSync(tempDir, { recursive: true })
|
||||||
|
existsSyncMock.mockClear()
|
||||||
|
realpathNativeMock.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given hook factory #when created #then defers fs canonical path calls until first tool invocation", async () => {
|
||||||
|
// given
|
||||||
|
const existingFile = join(tempDir, "existing.txt")
|
||||||
|
writeFileSync(existingFile, "content")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const hook = createWriteExistingFileGuardHook({ directory: tempDir } as never)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(existsSyncMock).toHaveBeenCalledTimes(0)
|
||||||
|
expect(realpathNativeMock).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
|
// when
|
||||||
|
await expect(
|
||||||
|
hook["tool.execute.before"]?.(
|
||||||
|
{
|
||||||
|
tool: "write",
|
||||||
|
sessionID: "ses_lazy",
|
||||||
|
callID: "call_lazy",
|
||||||
|
} as never,
|
||||||
|
{ args: { filePath: existingFile, content: "updated" } } as never,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("File already exists. Use edit tool instead.")
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(existsSyncMock).toHaveBeenCalledTimes(3)
|
||||||
|
expect(realpathNativeMock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -90,10 +90,10 @@ export async function handleWriteExistingFileGuardToolExecuteBefore(params: {
|
|||||||
output: { args?: unknown }
|
output: { args?: unknown }
|
||||||
readPermissionsBySession: Map<string, Set<string>>
|
readPermissionsBySession: Map<string, Set<string>>
|
||||||
sessionLastAccess: Map<string, number>
|
sessionLastAccess: Map<string, number>
|
||||||
canonicalSessionRoot: string
|
getCanonicalSessionRoot: () => string
|
||||||
maxTrackedSessions: number
|
maxTrackedSessions: number
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { ctx, input, output, readPermissionsBySession, sessionLastAccess, canonicalSessionRoot, maxTrackedSessions } = params
|
const { ctx, input, output, readPermissionsBySession, sessionLastAccess, getCanonicalSessionRoot, maxTrackedSessions } = params
|
||||||
const toolName = input.tool?.toLowerCase()
|
const toolName = input.tool?.toLowerCase()
|
||||||
if (toolName !== "write" && toolName !== "read") {
|
if (toolName !== "write" && toolName !== "read") {
|
||||||
return
|
return
|
||||||
@@ -107,6 +107,7 @@ export async function handleWriteExistingFileGuardToolExecuteBefore(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = resolveInputPath(ctx, filePath)
|
const resolvedPath = resolveInputPath(ctx, filePath)
|
||||||
|
const canonicalSessionRoot = getCanonicalSessionRoot()
|
||||||
const canonicalPath = toCanonicalPath(resolvedPath)
|
const canonicalPath = toCanonicalPath(resolvedPath)
|
||||||
if (!isPathInsideDirectory(canonicalPath, canonicalSessionRoot)) {
|
if (!isPathInsideDirectory(canonicalPath, canonicalSessionRoot)) {
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user