Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/verify-diff-lifecycle/covered.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ file lua/claudecode/config.lua 787fa0432d1d77d7
== lua/claudecode/cwd.lua
file lua/claudecode/cwd.lua 55d26ea362173afa
== lua/claudecode/diff.lua
file lua/claudecode/diff.lua b437f6282811538e
file lua/claudecode/diff.lua ab11e592a3f2c13d
block resolve_saved 46d5f3f732711fe1
block resolve_rejected c7a620816abff9a1
block open_blocking 92067df7f9d4d4d9
block setup_blocking b45a7bfd5203a085
block setup_blocking 257189386f7b6147
block close_by_name cc40c0d2ffdf7946
block close_pending 31bc827d1065da70
block close_for_client 330cc91666bc7884
Expand Down
7 changes: 7 additions & 0 deletions lua/claudecode/diff.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1867,6 +1867,13 @@ function M._setup_blocking_diff(params, resolution_callback)
end
end

-- A raising handler of the user (autocmd callback) makes the cleanup above fail inside Neovim API calls. Neovim
-- 0.8.0 bundles a LuaJIT that then keeps pointing at the main Lua thread, while this code runs in the coroutine
-- of tools.handle_invoke. The next JIT trace exit corrupts the stack of the main thread and Neovim aborts or
-- crashes, exit 134 or 139 (LuaJIT issue #1066, fixed in September 2023). An API call that returns normally points
-- LuaJIT at the running coroutine again. Make one before the error leaves the coroutine.
vim.api.nvim_get_current_tabpage()

-- Re-throw the error for MCP compliance
error({
code = -32000,
Expand Down
59 changes: 47 additions & 12 deletions tests/integration/diff_failed_setup_guards_spec.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- Real-Neovim guards for openDiff with `diff_opts.open_in_new_tab`: when the diff setup fails after `:tabnew` and a
-- handler of the user raises while the plugin cleans up, openDiff still answers "Diff setup failed", closes the tab
-- it opened and goes back to the tab it left. Under busted's mock vim this is pending;
-- it opened and goes back to the tab it left, and Neovim keeps running. Under busted's mock vim this is pending;
-- scripts/run_integration_tests_individually.sh runs it in Neovim, including CI's oldest supported version.
if vim == nil or vim.fn.has("nvim") ~= 1 then
describe("openDiff when the setup fails in a new tab", function()
Expand Down Expand Up @@ -101,32 +101,67 @@ describe("openDiff when the setup fails in a new tab", function()
_G.claude_deferred_responses = {}
end)

after_each(function()
local function reset()
pcall(vim.api.nvim_del_augroup_by_name, group)
diff._cleanup_all_active_diffs("test cleanup")
vim.cmd("silent! tabonly!")
vim.cmd("silent! only!")
vim.cmd("silent! %bwipeout!")
end

after_each(function()
reset()
vim.fn.delete(dir, "rf")
_G.claude_deferred_responses = nil
if jit then
jit.opt.start("hotloop=56", "hotexit=10", "tryside=4") -- LuaJIT's defaults
end
end)

local function a_WinClosed_handler_raises()
wipe_the_proposal_when_shown()
raise_when_a_window_closes()
end

local function a_WinLeave_handler_raises_during_the_cleanup()
fail_the_setup_and_then_raise_when("WinLeave")
end

local function a_BufLeave_handler_raises_during_the_cleanup()
fail_the_setup_and_then_raise_when("BufLeave")
end

it("closes the new tab and goes back to the tab it left when a WinClosed handler raises", function()
closes_the_new_tab_and_goes_back(function()
wipe_the_proposal_when_shown()
raise_when_a_window_closes()
end)
closes_the_new_tab_and_goes_back(a_WinClosed_handler_raises)
end)

it("closes the new tab and goes back when a WinLeave handler raises during the cleanup", function()
closes_the_new_tab_and_goes_back(function()
fail_the_setup_and_then_raise_when("WinLeave")
end)
closes_the_new_tab_and_goes_back(a_WinLeave_handler_raises_during_the_cleanup)
end)

it("closes the new tab and goes back when a BufLeave handler raises during the cleanup", function()
closes_the_new_tab_and_goes_back(function()
fail_the_setup_and_then_raise_when("BufLeave")
end)
closes_the_new_tab_and_goes_back(a_BufLeave_handler_raises_during_the_cleanup)
end)

-- Neovim 0.8.0 bundles a LuaJIT with a bug (LuaJIT issue #1066, fixed in September 2023). When a Neovim API call
-- raises inside a coroutine because a handler it ran raised, LuaJIT keeps pointing at the main Lua thread, and the
-- next JIT trace exit corrupts the stack of that thread. Neovim then aborts (exit 134, "bad stack slot") or crashes
-- (exit 139), and plenary prints no summary. openDiff runs in a coroutine (tools.handle_invoke). The three cases
-- above hit the bug in a few of 100 runs. A JIT that compiles at once makes the passes below hit it in most runs,
-- so this case fails on Neovim 0.8.0 when diff.lua leaves the bug in place.
it("keeps Neovim running when handlers raise on repeated failed setups under an eager JIT", function()
if jit then
jit.opt.start("hotloop=1", "hotexit=1", "tryside=1")
end
local arrangements = {
a_WinClosed_handler_raises,
a_WinLeave_handler_raises_during_the_cleanup,
a_BufLeave_handler_raises_during_the_cleanup,
}
for pass = 1, 6 do
vim.api.nvim_create_augroup(group, { clear = true })
closes_the_new_tab_and_goes_back(arrangements[pass % #arrangements + 1])
reset()
end
end)
end)
Loading