Skip to content

Commit f455a8e

Browse files
committed
test: cover apply diff multi-root paths
1 parent a6ce187 commit f455a8e

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import fs from "fs/promises"
2+
3+
import type { MockedFunction } from "vitest"
4+
5+
import { fileExistsAtPath } from "../../../utils/fs"
6+
import { getWorkspaceReadablePath, resolvePathInWorkspace } from "../../../utils/pathUtils"
7+
import type { ToolUse } from "../../../shared/tools"
8+
import { applyDiffTool } from "../ApplyDiffTool"
9+
10+
vi.mock("fs/promises", () => ({
11+
default: {
12+
readFile: vi.fn(),
13+
},
14+
}))
15+
16+
vi.mock("../../../utils/fs", () => ({
17+
fileExistsAtPath: vi.fn(),
18+
}))
19+
20+
vi.mock("../../../utils/pathUtils", async () => {
21+
const actual = await vi.importActual<typeof import("../../../utils/pathUtils")>("../../../utils/pathUtils")
22+
return {
23+
...actual,
24+
resolvePathInWorkspace: vi.fn(),
25+
getWorkspaceReadablePath: vi.fn(),
26+
}
27+
})
28+
29+
vi.mock("../../prompts/responses", () => ({
30+
formatResponse: {
31+
createPrettyPatch: vi.fn(() => "--- patch ---"),
32+
rooIgnoreError: vi.fn((filePath: string) => `Access denied: ${filePath}`),
33+
},
34+
}))
35+
36+
vi.mock("../../diff/stats", () => ({
37+
sanitizeUnifiedDiff: vi.fn((diff: string) => diff),
38+
computeDiffStats: vi.fn(() => ({ additions: 1, deletions: 1 })),
39+
}))
40+
41+
vi.mock("../../../shared/experiments", () => ({
42+
EXPERIMENT_IDS: {
43+
PREVENT_FOCUS_DISRUPTION: "prevent-focus-disruption",
44+
},
45+
experiments: {
46+
isEnabled: vi.fn().mockReturnValue(false),
47+
},
48+
}))
49+
50+
describe("applyDiffTool", () => {
51+
const cwd = "/workspace/primary"
52+
const relPath = "secondary/file.ts"
53+
const absolutePath = "/workspace/secondary/file.ts"
54+
55+
const mockedReadFile = fs.readFile as MockedFunction<typeof fs.readFile>
56+
const mockedFileExistsAtPath = fileExistsAtPath as MockedFunction<typeof fileExistsAtPath>
57+
const mockedResolvePathInWorkspace = resolvePathInWorkspace as MockedFunction<typeof resolvePathInWorkspace>
58+
const mockedGetWorkspaceReadablePath = getWorkspaceReadablePath as MockedFunction<typeof getWorkspaceReadablePath>
59+
60+
let task: any
61+
let askApproval: ReturnType<typeof vi.fn>
62+
let pushToolResult: ReturnType<typeof vi.fn>
63+
let handleError: ReturnType<typeof vi.fn>
64+
65+
beforeEach(() => {
66+
vi.clearAllMocks()
67+
68+
mockedReadFile.mockResolvedValue("old\n")
69+
mockedFileExistsAtPath.mockResolvedValue(true)
70+
mockedResolvePathInWorkspace.mockResolvedValue(absolutePath)
71+
mockedGetWorkspaceReadablePath.mockReturnValue("secondary/file.ts")
72+
73+
task = {
74+
cwd,
75+
taskId: "task-123",
76+
api: {
77+
getModel: vi.fn().mockReturnValue({ id: "claude-3" }),
78+
},
79+
consecutiveMistakeCount: 0,
80+
consecutiveMistakeCountForApplyDiff: new Map(),
81+
didEditFile: false,
82+
providerRef: {
83+
deref: vi.fn().mockReturnValue({
84+
getState: vi.fn().mockResolvedValue({
85+
diagnosticsEnabled: true,
86+
writeDelayMs: 10,
87+
experiments: {},
88+
}),
89+
}),
90+
},
91+
rooIgnoreController: {
92+
validateAccess: vi.fn().mockReturnValue(true),
93+
},
94+
rooProtectedController: {
95+
isWriteProtected: vi.fn().mockReturnValue(false),
96+
},
97+
diffStrategy: {
98+
applyDiff: vi.fn().mockResolvedValue({ success: true, content: "new\n" }),
99+
},
100+
diffViewProvider: {
101+
editType: undefined,
102+
originalContent: "",
103+
open: vi.fn().mockResolvedValue(undefined),
104+
update: vi.fn().mockResolvedValue(undefined),
105+
scrollToFirstDiff: vi.fn(),
106+
saveChanges: vi.fn().mockResolvedValue(undefined),
107+
revertChanges: vi.fn().mockResolvedValue(undefined),
108+
reset: vi.fn().mockResolvedValue(undefined),
109+
pushToolWriteResult: vi.fn().mockResolvedValue("Tool result"),
110+
},
111+
fileContextTracker: {
112+
trackFileContext: vi.fn().mockResolvedValue(undefined),
113+
},
114+
recordToolError: vi.fn(),
115+
recordToolUsage: vi.fn(),
116+
processQueuedMessages: vi.fn(),
117+
say: vi.fn().mockResolvedValue(undefined),
118+
sayAndCreateMissingParamError: vi.fn().mockResolvedValue("Missing param"),
119+
}
120+
121+
askApproval = vi.fn().mockResolvedValue(true)
122+
pushToolResult = vi.fn()
123+
handleError = vi.fn()
124+
})
125+
126+
it("uses the resolved path for access checks and the workspace-readable path in approval payloads", async () => {
127+
await applyDiffTool.execute({ path: relPath, diff: "@@ -1 +1 @@\n-old\n+new\n" }, task, {
128+
askApproval,
129+
pushToolResult,
130+
handleError,
131+
})
132+
133+
expect(mockedResolvePathInWorkspace).toHaveBeenCalledWith(cwd, relPath)
134+
expect(task.rooIgnoreController.validateAccess).toHaveBeenCalledWith(absolutePath)
135+
expect(task.rooProtectedController.isWriteProtected).toHaveBeenCalledWith(absolutePath)
136+
expect(task.diffViewProvider.open).toHaveBeenCalledWith(absolutePath)
137+
expect(askApproval).toHaveBeenCalledWith(
138+
"tool",
139+
expect.stringContaining('"path":"secondary/file.ts"'),
140+
undefined,
141+
false,
142+
)
143+
expect(task.fileContextTracker.trackFileContext).toHaveBeenCalledWith(relPath, "roo_edited")
144+
expect(pushToolResult).toHaveBeenCalledWith(expect.stringContaining("Tool result"))
145+
})
146+
147+
it("uses the resolved workspace path in partial payloads after the path stabilizes", async () => {
148+
const block: ToolUse<"apply_diff"> = {
149+
type: "tool_use",
150+
name: "apply_diff",
151+
params: { path: relPath, diff: "@@ -1 +1 @@\n-old\n+new\n" },
152+
partial: true,
153+
}
154+
155+
task.ask = vi.fn().mockResolvedValue(undefined)
156+
157+
await applyDiffTool.handlePartial(task, block)
158+
await applyDiffTool.handlePartial(task, block)
159+
160+
expect(task.ask).toHaveBeenCalledWith(
161+
"tool",
162+
expect.stringContaining('"path":"secondary/file.ts"'),
163+
true,
164+
undefined,
165+
)
166+
})
167+
})

0 commit comments

Comments
 (0)