diff --git a/.agents/skills/verify-diff-lifecycle/covered.lock b/.agents/skills/verify-diff-lifecycle/covered.lock index da15b26a9..9f4792a47 100644 --- a/.agents/skills/verify-diff-lifecycle/covered.lock +++ b/.agents/skills/verify-diff-lifecycle/covered.lock @@ -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 diff --git a/lua/claudecode/diff.lua b/lua/claudecode/diff.lua index 680fd0022..19d9b4518 100644 --- a/lua/claudecode/diff.lua +++ b/lua/claudecode/diff.lua @@ -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, diff --git a/tests/integration/diff_failed_setup_guards_spec.lua b/tests/integration/diff_failed_setup_guards_spec.lua index ff66b888b..58d4762e8 100644 --- a/tests/integration/diff_failed_setup_guards_spec.lua +++ b/tests/integration/diff_failed_setup_guards_spec.lua @@ -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() @@ -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)