Skip to content

Commit 4a676fb

Browse files
committed
feat: update read_file tool tests to handle user feedback and approval states
1 parent 7e10f89 commit 4a676fb

File tree

3 files changed

+237
-9
lines changed

3 files changed

+237
-9
lines changed

src/core/__tests__/read-file-maxReadFileLine.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ describe("read_file tool with maxReadFileLine setting", () => {
115115
validateAccess: jest.fn().mockReturnValue(true),
116116
}
117117
mockCline.say = jest.fn().mockResolvedValue(undefined)
118-
mockCline.ask = jest.fn().mockResolvedValue(true)
118+
mockCline.ask = jest.fn().mockResolvedValue({ response: "yesButtonClicked" })
119119
mockCline.presentAssistantMessage = jest.fn()
120120
mockCline.getFileContextTracker = jest.fn().mockReturnValue({
121121
trackFileContext: jest.fn().mockResolvedValue(undefined),
122122
})
123123
mockCline.recordToolUsage = jest.fn().mockReturnValue(undefined)
124124
mockCline.recordToolError = jest.fn().mockReturnValue(undefined)
125+
mockCline.didRejectTool = false
125126
// Reset tool result
126127
toolResult = undefined
127128
})
@@ -257,6 +258,7 @@ describe("read_file tool with maxReadFileLine setting", () => {
257258
it("should return an empty content with source code definitions", async () => {
258259
// Setup - for maxReadFileLine = 0, the implementation won't call readLines
259260
mockedParseSourceCodeDefinitionsForFile.mockResolvedValue(sourceCodeDef)
261+
mockedCountFileLines.mockResolvedValue(5)
260262

261263
// Execute - skip addLineNumbers check as it's not called for maxReadFileLine=0
262264
const result = await executeReadFileTool(
@@ -286,6 +288,7 @@ describe("read_file tool with maxReadFileLine setting", () => {
286288
// Setup
287289
mockedIsBinaryFile.mockResolvedValue(true)
288290
mockedParseSourceCodeDefinitionsForFile.mockResolvedValue("")
291+
mockedCountFileLines.mockResolvedValue(0)
289292

290293
// Execute
291294
const result = await executeReadFileTool({}, { maxReadFileLine: 0 })

src/core/__tests__/read-file-tool.test.ts

Lines changed: 225 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe("read_file tool functionality", () => {
4343
validateAccess: jest.fn().mockReturnValue(true),
4444
},
4545
say: jest.fn().mockResolvedValue(undefined),
46-
ask: jest.fn().mockResolvedValue(true),
46+
ask: jest.fn().mockResolvedValue({ response: "yesButtonClicked" }),
4747
presentAssistantMessage: jest.fn(),
4848
getFileContextTracker: jest.fn().mockReturnValue({
4949
trackFileContext: jest.fn().mockResolvedValue(undefined),
@@ -52,21 +52,36 @@ describe("read_file tool functionality", () => {
5252
recordToolError: jest.fn(),
5353
sayAndCreateMissingParamError: jest.fn().mockResolvedValue("Missing required parameter"),
5454
consecutiveMistakeCount: 0,
55+
didRejectTool: false,
5556
}
5657

5758
beforeEach(() => {
5859
jest.clearAllMocks()
5960
mockCline.consecutiveMistakeCount = 0
61+
mockCline.didRejectTool = false
62+
63+
// Setup default mocks
6064
;(extractTextFromFile as jest.Mock).mockImplementation(() => Promise.resolve("Test content"))
6165
;(readLines as jest.Mock).mockImplementation(() => Promise.resolve("Test content"))
62-
;(addLineNumbers as jest.Mock).mockImplementation((text, startLine = 1) => {
66+
;(addLineNumbers as jest.Mock).mockImplementation((text: string) => {
67+
if (!text) return ""
6368
return text
6469
.split("\n")
65-
.map((line: string, i: number) => `${startLine + i} | ${line}`)
70+
.map((line: string, i: number) => `${i + 1} | ${line}`)
6671
.join("\n")
6772
})
6873
;(isBinaryFile as jest.Mock).mockResolvedValue(false)
6974
;(parseSourceCodeDefinitionsForFile as jest.Mock).mockResolvedValue("")
75+
mockCline.ask.mockResolvedValue({
76+
response: "yesButtonClicked",
77+
text: "Looks good!",
78+
images: ["image1.png"],
79+
})
80+
81+
// Reset mock implementations
82+
;(extractTextFromFile as jest.Mock).mockClear()
83+
;(readLines as jest.Mock).mockClear()
84+
;(addLineNumbers as jest.Mock).mockClear()
7085
})
7186

7287
describe("Args Parameter Format", () => {
@@ -473,6 +488,203 @@ describe("read_file tool functionality", () => {
473488
`<files>\n<file><path>src/code.ts</path><error>Error reading file: Parser error</error></file>\n</files>`,
474489
)
475490
})
491+
492+
describe("User Feedback Handling", () => {
493+
it("should handle approval with feedback", async () => {
494+
const toolUse: ReadFileToolUse = {
495+
type: "tool_use",
496+
name: "read_file",
497+
params: {
498+
args: `<file><path>src/app.ts</path></file>`,
499+
},
500+
partial: false,
501+
}
502+
503+
const { readFileTool } = require("../tools/readFileTool")
504+
505+
;(countFileLines as jest.Mock).mockResolvedValue(10)
506+
;(readLines as jest.Mock).mockResolvedValue("Test content")
507+
;(addLineNumbers as jest.Mock).mockReturnValue("1 | Test content")
508+
mockCline.ask.mockResolvedValueOnce({
509+
response: "yesButtonClicked",
510+
text: "Looks good!",
511+
images: ["image1.png"],
512+
})
513+
514+
let result: string | undefined
515+
await readFileTool(
516+
mockCline,
517+
toolUse,
518+
mockCline.ask,
519+
jest.fn(),
520+
(r: string) => {
521+
result = r
522+
},
523+
(param: string, value: string) => value,
524+
)
525+
526+
const expectedXml = `<files>\n<file><path>src/app.ts</path>\n<content lines="1-10">\n1 | Test content</content>\n</file>\n</files>`
527+
expect(result).toBe(
528+
`The tool was approved with feedback:\n<feedback>\nLooks good!\n</feedback>\n${expectedXml}`,
529+
)
530+
expect(mockCline.say).toHaveBeenCalledWith("user_feedback", "Looks good!", ["image1.png"])
531+
})
532+
533+
it("should handle denial with feedback", async () => {
534+
const toolUse: ReadFileToolUse = {
535+
type: "tool_use",
536+
name: "read_file",
537+
params: {
538+
args: `<file><path>src/app.ts</path></file>`,
539+
},
540+
partial: false,
541+
}
542+
543+
const { readFileTool } = require("../tools/readFileTool")
544+
545+
mockCline.ask.mockResolvedValueOnce({
546+
response: "noButtonClicked",
547+
text: "Not allowed",
548+
images: ["image2.png"],
549+
})
550+
551+
let result: string | undefined
552+
await readFileTool(
553+
mockCline,
554+
toolUse,
555+
mockCline.ask,
556+
jest.fn(),
557+
(r: string) => {
558+
result = r
559+
},
560+
(param: string, value: string) => value,
561+
)
562+
563+
const expectedXml = `<files>\n<file><path>src/app.ts</path><status>Denied by user</status></file>\n</files>`
564+
expect(result).toBe(
565+
`The user denied this operation and provided the following feedback:\n<feedback>\nNot allowed\n</feedback>\n${expectedXml}`,
566+
)
567+
expect(mockCline.say).toHaveBeenCalledWith("user_feedback", "Not allowed", ["image2.png"])
568+
expect(mockCline.didRejectTool).toBe(true)
569+
})
570+
})
571+
572+
describe("Invalid Line Range Validation", () => {
573+
it("should handle end line less than start line", async () => {
574+
const toolUse: ReadFileToolUse = {
575+
type: "tool_use",
576+
name: "read_file",
577+
params: {
578+
args: `<file><path>src/app.ts</path><line_range>10-5</line_range></file>`,
579+
},
580+
partial: false,
581+
}
582+
583+
const { readFileTool } = require("../tools/readFileTool")
584+
585+
;(countFileLines as jest.Mock).mockResolvedValue(10)
586+
;(readLines as jest.Mock).mockRejectedValue(
587+
new Error("Invalid line range: end line cannot be less than start line"),
588+
)
589+
mockCline.ask.mockResolvedValueOnce({ response: "yesButtonClicked" })
590+
591+
let result: string | undefined
592+
await readFileTool(
593+
mockCline,
594+
toolUse,
595+
mockCline.ask,
596+
jest.fn(),
597+
(r: string) => {
598+
result = r
599+
},
600+
(param: string, value: string) => value,
601+
)
602+
603+
expect(result).toBe(
604+
`<files>\n<file><path>src/app.ts</path><error>Error reading file: Invalid line range: end line cannot be less than start line</error></file>\n</files>`,
605+
)
606+
})
607+
608+
it("should handle NaN line values", async () => {
609+
const toolUse: ReadFileToolUse = {
610+
type: "tool_use",
611+
name: "read_file",
612+
params: {
613+
args: `<file><path>src/app.ts</path><line_range>abc-def</line_range></file>`,
614+
},
615+
partial: false,
616+
}
617+
618+
const { readFileTool } = require("../tools/readFileTool")
619+
620+
;(countFileLines as jest.Mock).mockResolvedValue(10)
621+
;(readLines as jest.Mock).mockRejectedValue(new Error("Invalid line range values"))
622+
mockCline.ask.mockResolvedValueOnce({ response: "yesButtonClicked" })
623+
624+
let result: string | undefined
625+
await readFileTool(
626+
mockCline,
627+
toolUse,
628+
mockCline.ask,
629+
jest.fn(),
630+
(r: string) => {
631+
result = r
632+
},
633+
(param: string, value: string) => value,
634+
)
635+
636+
expect(result).toBe(
637+
`<files>\n<file><path>src/app.ts</path><error>Error reading file: Invalid line range values</error></file>\n</files>`,
638+
)
639+
})
640+
})
641+
642+
describe("Multiple File States", () => {
643+
it("should handle mixed approval states", async () => {
644+
const toolUse: ReadFileToolUse = {
645+
type: "tool_use",
646+
name: "read_file",
647+
params: {
648+
args: `<file><path>src/app.ts</path></file><file><path>src/blocked.ts</path></file>`,
649+
},
650+
partial: false,
651+
}
652+
653+
const { readFileTool } = require("../tools/readFileTool")
654+
655+
;(countFileLines as jest.Mock).mockResolvedValue(10)
656+
657+
// First file approved
658+
mockCline.ask.mockResolvedValueOnce({
659+
response: "yesButtonClicked",
660+
text: "First approved",
661+
})
662+
663+
// Second file denied
664+
mockCline.ask.mockResolvedValueOnce({
665+
response: "noButtonClicked",
666+
text: "Second denied",
667+
})
668+
669+
let result: string | undefined
670+
await readFileTool(
671+
mockCline,
672+
toolUse,
673+
mockCline.ask,
674+
jest.fn(),
675+
(r: string) => {
676+
result = r
677+
},
678+
(param: string, value: string) => value,
679+
)
680+
681+
const expectedXml = `<files>\n<file><path>src/app.ts</path>\n<content lines="1-10">\n1 | Test content</content>\n</file>\n<file><path>src/blocked.ts</path><status>Denied by user</status></file>\n</files>`
682+
expect(result).toBe(
683+
`The user denied this operation and provided the following feedback:\n<feedback>\nSecond denied\n</feedback>\n${expectedXml}`,
684+
)
685+
expect(mockCline.didRejectTool).toBe(true)
686+
})
687+
})
476688
})
477689

478690
describe("Performance Edge Cases", () => {
@@ -490,6 +702,12 @@ describe("read_file tool functionality", () => {
490702

491703
;(countFileLines as jest.Mock).mockResolvedValue(1000000)
492704
;(readLines as jest.Mock).mockResolvedValue("First 500 lines")
705+
;(addLineNumbers as jest.Mock).mockReturnValue("1 | First 500 lines")
706+
mockCline.ask.mockResolvedValueOnce({
707+
response: "yesButtonClicked",
708+
text: "Looks good!",
709+
images: ["image1.png"],
710+
})
493711

494712
let result: string | undefined
495713
await readFileTool(
@@ -503,7 +721,10 @@ describe("read_file tool functionality", () => {
503721
(param: string, value: string) => value,
504722
)
505723

506-
expect(result).toContain("<notice>Showing only 500 of 1000000 total lines")
724+
const expectedXml = `<files>\n<file><path>huge.log</path>\n<content lines="1-500">\n1 | First 500 lines</content>\n<notice>Showing only 500 of 1000000 total lines. Use line_range if you need to read more lines</notice>\n</file>\n</files>`
725+
expect(result).toBe(
726+
`The tool was approved with feedback:\n<feedback>\nLooks good!\n</feedback>\n${expectedXml}`,
727+
)
507728
expect(extractTextFromFile).not.toHaveBeenCalled()
508729
expect(readLines).toHaveBeenCalled()
509730
})

src/core/__tests__/read-file-xml.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ describe("read_file tool XML output structure", () => {
111111
validateAccess: jest.fn().mockReturnValue(true),
112112
}
113113
mockCline.say = jest.fn().mockResolvedValue(undefined)
114-
mockCline.ask = jest.fn().mockResolvedValue(true)
114+
mockCline.ask = jest.fn().mockResolvedValue({ response: "yesButtonClicked" })
115115
mockCline.presentAssistantMessage = jest.fn()
116116
mockCline.sayAndCreateMissingParamError = jest.fn().mockResolvedValue("Missing required parameter")
117117
// Add mock for getFileContextTracker method
@@ -120,6 +120,7 @@ describe("read_file tool XML output structure", () => {
120120
})
121121
mockCline.recordToolUsage = jest.fn().mockReturnValue(undefined)
122122
mockCline.recordToolError = jest.fn().mockReturnValue(undefined)
123+
mockCline.didRejectTool = false
123124

124125
// Reset tool result
125126
toolResult = undefined
@@ -611,8 +612,11 @@ describe("read_file tool XML output structure", () => {
611612
return Promise.reject(new Error("File not found"))
612613
})
613614

614-
// Mock approval to always succeed since RooIgnore handles access
615-
mockCline.ask = jest.fn().mockResolvedValue(true)
615+
// Mock approval for both files
616+
mockCline.ask = jest
617+
.fn()
618+
.mockResolvedValueOnce({ response: "yesButtonClicked" }) // First file approved
619+
.mockResolvedValueOnce({ response: "noButtonClicked" }) // Second file denied
616620

617621
// Execute - Skip the default validateAccess mock
618622
const { readFileTool } = require("../tools/readFileTool")
@@ -654,7 +658,7 @@ describe("read_file tool XML output structure", () => {
654658

655659
// Verify result
656660
expect(result).toBe(
657-
`<files>\n<file><path>${invalidPath}</path><error>${formatResponse.rooIgnoreError(invalidPath)}</error></file>\n<file><path>${validPath}</path>\n<content lines="1-1">\n${numberedContent}</content>\n</file>\n</files>`,
661+
`<files>\n<file><path>${validPath}</path>\n<content lines="1-1">\n${numberedContent}</content>\n</file>\n<file><path>${invalidPath}</path><error>${formatResponse.rooIgnoreError(invalidPath)}</error></file>\n</files>`,
658662
)
659663
})
660664

0 commit comments

Comments
 (0)