@ 261105.2

261019.1 · 348df165f1773dd08c5d710add3fd0db6d421e6a

fix(buffers): assertion failed when buflist is nil

Problem: the `contents` callback calls `utils.CTX()` without `includeBuflist`, so if the context is recreated between picker setup (which passes `includeBuflist=true`) and the async `contents` callback, `buflist` is nil and the assertion crashes. This happens with `reload` actions such as `actions.buf_del` when the picker is closed while the reload is still in flight.

Solution: pass `{ includeBuflist = true }` explicitly, ensuring the context always has `buflist` populated when accessed inside the callback.

Add a regression test that opens the buffer picker, captures its reload command, closes the picker (clearing the context), and then runs the stale reload. Without the fix this reproduces the exact assertion failure from #2736.

Closes #2736

reading declarations

Changes to tests/buffers_spec.lua. All files

tests/buffers_spec.lua

new file mode 100644@@ -0,0 +1,57 @@+---@diagnostic disable: unused-local, unused-function, unused+local MiniTest = require("mini.test")+local helpers = require("fzf-lua.test.helpers")+local child = helpers.new_child_neovim()+local eq = helpers.expect.equality+local new_set = MiniTest.new_set+local exec_lua = child.lua++local T = helpers.new_set_with_child(child)++T["buffers"] = new_set({ n_retry = not helpers.IS_LINUX() and 5 or nil })++-- Regression for #2736: `actions.buf_del` uses `reload=true`, which runs the+-- contents callback asynchronously via a separate RPC process. If the picker+-- is closed (or its context is otherwise re-created without `buflist`) before+-- that reload is processed, the old code asserted on `ctx.buflist`.+T["buffers"]["reload after picker close"] = function()+  exec_lua([[+    for i = 1, 3 do+      vim.cmd.edit(vim.fn.tempname())+      vim.api.nvim_buf_set_lines(0, 0, -1, false, { "buffer " .. i })+    end+    vim.cmd.edit(vim.fn.tempname())+  ]])++  child.FzfLua.buffers({ previewer = false })+  child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == true end)+  child.wait_until(function() return child.lua_get([[_G._fzf_load_called]]) == true end)+  local reload_cmd = exec_lua([[return FzfLua.config.__resume_data.contents]])+  eq(type(reload_cmd), "string")++  -- closing the picker clears the global context+  child.type_keys("<c-c>")+  child.wait_until(function() return child.lua_get([[_G._fzf_lua_on_create]]) == vim.NIL end)+  child.wait_until(function() return exec_lua([[return FzfLua.utils.__CTX() == nil]]) end)++  -- deliver the pending reload, as if it was still in flight when the picker+  -- was closed+  local job = exec_lua([[+    local cmd = ...+    _G._fzf_lua_reload_exit = nil+    return vim.fn.jobstart(cmd, {+      env = { FZF_LUA_SERVER = vim.v.servername },+      on_exit = function(_, code) _G._fzf_lua_reload_exit = code end,+    })+  ]], { reload_cmd })+  eq(job > 0, true, { fail_reason = "jobstart failed: " .. tostring(job) })+  child.wait_until(function()+    return exec_lua([[return _G._fzf_lua_reload_exit ~= nil]]) == true+  end)+  eq(exec_lua([[return _G._fzf_lua_reload_exit]]), 0)++  local msgs = child.cmd_capture("messages")+  eq(msgs:match("assertion failed") == nil, true, msgs)+end++return T