Skip to content

Commit def718a

Browse files
test(#169): cover realPathOrNearest fallbacks and ReadFileTool handlePartial boundary
1 parent 41cfc7f commit def718a

2 files changed

Lines changed: 63 additions & 0 deletions

File tree

src/core/tools/__tests__/readFileTool.spec.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,27 @@ describe("ReadFileTool", () => {
259259
expect.objectContaining({ tool: "readFile", isOutsideWorkspace: true }),
260260
)
261261
})
262+
263+
it("resolves the boundary through symlink resolution in handlePartial", async () => {
264+
vi.mocked(isPathOutsideWorkspace).mockReturnValue(true)
265+
const mockTask = createMockTask({ allowSymlinksOutsideWorkspace: false })
266+
267+
const block = {
268+
type: "tool_use",
269+
name: "read_file",
270+
params: {},
271+
partial: true,
272+
nativeArgs: { path: "link-to-outside.txt" },
273+
} as any
274+
275+
await readFileTool.handlePartial(mockTask as any, block)
276+
277+
const toolAsk = (mockTask.ask.mock.calls as any[]).find(([type]: [string]) => type === "tool")
278+
expect(toolAsk).toBeDefined()
279+
expect(JSON.parse(toolAsk![1] as string)).toEqual(
280+
expect.objectContaining({ tool: "readFile", isOutsideWorkspace: true }),
281+
)
282+
})
262283
})
263284

264285
describe("input validation", () => {

src/utils/__tests__/pathUtils.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,48 @@ describe("isPathOutsideWorkspace", () => {
105105
}
106106
})
107107

108+
it("falls back to the lexical path when no ancestor resolves up to the root (#169)", () => {
109+
const inside = path.join(workspaceDir, "a", "b", "new-file.ts")
110+
111+
// Force realpath to report ENOENT for every segment, so the walk-up reaches the
112+
// filesystem root without resolving anything and falls back to the lexical path.
113+
// Both the target and the workspace folder resolve lexically, so containment holds.
114+
const spy = vi.spyOn(fs.realpathSync, "native").mockImplementation(() => {
115+
const err: NodeJS.ErrnoException = new Error("no entry")
116+
err.code = "ENOENT"
117+
throw err
118+
})
119+
120+
try {
121+
expect(isPathOutsideWorkspace(inside)).toBe(false)
122+
} finally {
123+
spy.mockRestore()
124+
}
125+
})
126+
127+
it("treats a path as outside when the workspace folder itself cannot be resolved (#169)", () => {
128+
const inside = path.join(workspaceDir, "file.ts")
129+
fs.writeFileSync(inside, "x")
130+
131+
// The target resolves fine, but realpath on the workspace folder throws EACCES.
132+
// A folder that can't be resolved can't prove containment, so we fail closed.
133+
const realNative = fs.realpathSync.native
134+
const spy = vi.spyOn(fs.realpathSync, "native").mockImplementation(((p: string) => {
135+
if (p === workspaceDir) {
136+
const err: NodeJS.ErrnoException = new Error("permission denied")
137+
err.code = "EACCES"
138+
throw err
139+
}
140+
return realNative(p)
141+
}) as typeof fs.realpathSync.native)
142+
143+
try {
144+
expect(isPathOutsideWorkspace(inside)).toBe(true)
145+
} finally {
146+
spy.mockRestore()
147+
}
148+
})
149+
108150
it("returns true when there are no workspace folders", () => {
109151
mockWorkspace.folders = []
110152
expect(isPathOutsideWorkspace(path.join(workspaceDir, "file.ts"))).toBe(true)

0 commit comments

Comments
 (0)