Skip to content

Commit d0f1f73

Browse files
committed
fix: address PR review comments for MCP tools compliance
- Fix closeAllDiffTabs potential duplicate window closing by using set-based approach instead of array - Add comprehensive test coverage for openFile parameters (makeFrontmost, preview mode, line/text selection) - Update CLAUDE.md documentation to mention endLine parameter for openFile tool - Enhance JSON encoder/decoder in test suite with proper escape sequence handling - Add missing vim API mocks for complex openFile functionality testing All 325 tests now pass with complete coverage of new MCP tool features. Change-Id: I15bceb2bb44552205ea63c5ef1cb83722f7b5893 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 63066c2 commit d0f1f73

File tree

4 files changed

+126
-13
lines changed

4 files changed

+126
-13
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ The WebSocket server implements secure authentication using:
7171

7272
**MCP-Exposed Tools** (with JSON schemas):
7373

74-
- `openFile` - Opens files with optional line/text selection, preview mode, and text pattern matching
74+
- `openFile` - Opens files with optional line/text selection (startLine/endLine), preview mode, text pattern matching, and makeFrontmost flag
7575
- `getCurrentSelection` - Gets current text selection from active editor
7676
- `getLatestSelection` - Gets most recent text selection (even from inactive editors)
7777
- `getOpenEditors` - Lists currently open files with VS Code-compatible `tabs` structure

lua/claudecode/tools/close_all_diff_tabs.lua

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,38 @@ local function handler(_params) -- Prefix unused params with underscore
1919

2020
-- Get all windows
2121
local windows = vim.api.nvim_list_wins()
22-
local windows_to_close = {}
22+
local windows_to_close = {} -- Use set to avoid duplicates
2323

2424
for _, win in ipairs(windows) do
2525
local buf = vim.api.nvim_win_get_buf(win)
2626
local buftype = vim.api.nvim_buf_get_option(buf, "buftype")
2727
local diff_mode = vim.api.nvim_win_get_option(win, "diff")
28+
local should_close = false
2829

2930
-- Check if this is a diff window
3031
if diff_mode then
31-
table.insert(windows_to_close, win)
32+
should_close = true
3233
end
3334

3435
-- Also check for diff-related buffer names or types
3536
local buf_name = vim.api.nvim_buf_get_name(buf)
3637
if buf_name:match("%.diff$") or buf_name:match("diff://") then
37-
table.insert(windows_to_close, win)
38+
should_close = true
3839
end
3940

4041
-- Check for special diff buffer types
4142
if buftype == "nofile" and buf_name:match("^fugitive://") then
42-
table.insert(windows_to_close, win)
43+
should_close = true
44+
end
45+
46+
-- Add to close set only once (prevents duplicates)
47+
if should_close then
48+
windows_to_close[win] = true
4349
end
4450
end
4551

4652
-- Close the identified diff windows
47-
for _, win in ipairs(windows_to_close) do
53+
for win, _ in pairs(windows_to_close) do
4854
if vim.api.nvim_win_is_valid(win) then
4955
local success = pcall(vim.api.nvim_win_close, win, false)
5056
if success then

tests/busted_setup.lua

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,14 @@ _G.json_encode = function(data)
155155

156156
return table.concat(parts)
157157
elseif type(data) == "string" then
158-
return '"' .. data:gsub('"', '\\"') .. '"'
158+
-- Handle escape sequences properly
159+
local escaped = data
160+
:gsub("\\", "\\\\") -- Escape backslashes first
161+
:gsub('"', '\\"') -- Escape quotes
162+
:gsub("\n", "\\n") -- Escape newlines
163+
:gsub("\r", "\\r") -- Escape carriage returns
164+
:gsub("\t", "\\t") -- Escape tabs
165+
return '"' .. escaped .. '"'
159166
elseif type(data) == "boolean" then
160167
return data and "true" or "false"
161168
elseif type(data) == "number" then
@@ -197,7 +204,13 @@ _G.json_decode = function(str)
197204
end
198205
pos = pos + 1
199206
end
200-
local value = str:sub(start, pos - 1):gsub('\\"', '"')
207+
local value = str
208+
:sub(start, pos - 1)
209+
:gsub('\\"', '"') -- Unescape quotes
210+
:gsub("\\\\", "\\") -- Unescape backslashes
211+
:gsub("\\n", "\n") -- Unescape newlines
212+
:gsub("\\r", "\r") -- Unescape carriage returns
213+
:gsub("\\t", "\t") -- Unescape tabs
201214
pos = pos + 1
202215
return value
203216
elseif char == "{" then

tests/unit/tools/open_file_spec.lua

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,30 @@ describe("Tool: open_file", function()
5757
_G.vim.api.nvim_get_current_win = spy.new(function()
5858
return 1000
5959
end)
60+
_G.vim.api.nvim_get_current_buf = spy.new(function()
61+
return 1 -- Mock current buffer ID
62+
end)
63+
_G.vim.api.nvim_buf_get_name = spy.new(function(buf)
64+
return "test.txt" -- Mock buffer name
65+
end)
66+
_G.vim.api.nvim_buf_line_count = spy.new(function(buf)
67+
return 10 -- Mock line count
68+
end)
69+
_G.vim.api.nvim_buf_set_mark = spy.new(function(buf, name, line, col, opts)
70+
-- Mock mark setting
71+
end)
72+
_G.vim.api.nvim_buf_get_lines = spy.new(function(buf, start, end_line, strict)
73+
-- Mock buffer lines for search
74+
return {
75+
"local function test()",
76+
" print('hello')",
77+
" return true",
78+
"end",
79+
}
80+
end)
81+
_G.vim.api.nvim_win_set_cursor = spy.new(function(win, pos)
82+
-- Mock cursor setting
83+
end)
6084
end)
6185

6286
after_each(function()
@@ -129,10 +153,80 @@ describe("Tool: open_file", function()
129153
expect(_G.vim.cmd_history[1]).to_be("edit /Users/testuser/.config/nvim/init.lua")
130154
end)
131155

132-
-- TODO: Add tests for selection by line numbers (params.startLine, params.endLine)
133-
-- This will require mocking vim.api.nvim_win_set_cursor or similar for selection
134-
-- and potentially vim.api.nvim_buf_get_lines if text content matters for selection.
156+
it("should handle makeFrontmost=false to return detailed JSON", function()
157+
local params = { filePath = "test.txt", makeFrontmost = false }
158+
local success, result = pcall(open_file_handler, params)
159+
160+
expect(success).to_be_true()
161+
expect(result.content).to_be_table()
162+
expect(result.content[1].type).to_be("text")
163+
164+
-- makeFrontmost=false should return JSON-encoded detailed info
165+
local parsed_result = require("tests.busted_setup").json_decode(result.content[1].text)
166+
expect(parsed_result.success).to_be_true()
167+
expect(parsed_result.filePath).to_be("test.txt")
168+
end)
169+
170+
it("should handle preview mode parameter", function()
171+
local params = { filePath = "test.txt", preview = true }
172+
local success, result = pcall(open_file_handler, params)
173+
174+
expect(success).to_be_true()
175+
expect(result.content[1].text).to_be("Opened file: test.txt")
176+
-- Preview mode affects window behavior but basic functionality should work
177+
end)
178+
179+
it("should handle line selection parameters", function()
180+
-- Mock additional functions needed for line selection
181+
_G.vim.api.nvim_win_set_cursor = spy.new(function(win, pos)
182+
-- Mock cursor setting
183+
end)
184+
_G.vim.fn.setpos = spy.new(function(mark, pos)
185+
-- Mock position setting
186+
end)
187+
188+
local params = { filePath = "test.txt", startLine = 5, endLine = 10 }
189+
local success, result = pcall(open_file_handler, params)
135190

136-
-- TODO: Add tests for selection by text patterns (params.startText, params.endText)
137-
-- This will require more complex mocking of buffer content and search functions.
191+
expect(success).to_be_true()
192+
expect(result.content).to_be_table()
193+
expect(result.content[1].type).to_be("text")
194+
expect(result.content[1].text).to_be("Opened file and selected lines 5 to 10")
195+
end)
196+
197+
it("should handle text pattern selection when pattern found", function()
198+
local params = {
199+
filePath = "test.txt",
200+
startText = "function",
201+
endText = "end",
202+
selectToEndOfLine = true,
203+
}
204+
205+
local success, result = pcall(open_file_handler, params)
206+
207+
expect(success).to_be_true()
208+
expect(result.content).to_be_table()
209+
expect(result.content[1].type).to_be("text")
210+
-- Since the mock buffer contains "function" and "end", selection should work
211+
expect(result.content[1].text).to_be('Opened file and selected text from "function" to "end"')
212+
end)
213+
214+
it("should handle text pattern selection when pattern not found", function()
215+
-- Mock search to return 0 (not found)
216+
_G.vim.fn.search = spy.new(function(pattern)
217+
return 0 -- Pattern not found
218+
end)
219+
220+
local params = {
221+
filePath = "test.txt",
222+
startText = "nonexistent",
223+
}
224+
225+
local success, result = pcall(open_file_handler, params)
226+
227+
expect(success).to_be_true()
228+
expect(result.content).to_be_table()
229+
expect(result.content[1].type).to_be("text")
230+
assert_contains(result.content[1].text, "not found")
231+
end)
138232
end)

0 commit comments

Comments
 (0)