@ 261105.2

261020.25 · 65210ff5dd7f2ae3e482e198038dce564ec7a2a4

fix(actions): keep timed-out server kills a failure

`server_exited` (the `qa!`-timeout follow-up in `serverlist_kill`) treated any failed liveness probe as proof the target had exited, but `utils.server_probe` also fails with `Timeout` when the peer accepted the connection and did not answer `nvim_get_chan_info` within the 200ms RPC timeout: a reachable-but-hung server was reported as "Killed N server(s)" although the `qa!` was never confirmed, hiding the failure.

Inspect both `server_probe` return values and only count channel-gone evidence as an exit: "Invalid channel" (nothing listens at the address anymore) or "ch N was closed by the peer" (the process died while the probe request was in flight). A probe timeout keeps polling inside the existing bounded window - a server that is mid-exit still gets a chance to be confirmed - and otherwise falls through to the failure report, so an unconfirmed kill is no longer reported as success.

tests: `serverlist_kill` cases with stubbed `utils.rpcexec_timed`/ `utils.server_probe` for the timeout (kill failure), peer-closed (killed) and no-listener (killed) outcomes; stubs are restored even when a case fails.

Verification: focused {serverlist} 21/0; full nightly 322/0 fails/20 notes; emmylua_check clean.

reading declarations

Changes to tests/serverlist_spec.lua. All files

tests/serverlist_spec.lua

@@ -307,6 +307,17 @@ describe("serverlist actions", function()     capture_off()     if not ok then error(err) end   end+  -- swap `utils.rpcexec_timed`/`utils.server_probe` for the duration of fn,+  -- always restoring: a failing case must not leak stubs into later specs+  ---@param rpcexec fun(addr: string, method: string, ...: any): boolean, any+  ---@param probe fun(addr: string): boolean, any+  local function with_stubs(rpcexec, probe, fn)+    local orig_rpcexec, orig_probe = utils.rpcexec_timed, utils.server_probe+    utils.rpcexec_timed, utils.server_probe = rpcexec, probe+    local ok, err = pcall(fn)+    utils.rpcexec_timed, utils.server_probe = orig_rpcexec, orig_probe+    if not ok then error(err) end+  end   it("kill terminates a live sibling, tolerates stale entries", function()     with_notes(function()       with_sibling(nil, nil, function(_, addr)@@ -340,6 +351,48 @@ describe("serverlist actions", function()       end)     end)   end)+  -- after a `qa!` timeout, only channel-gone evidence proves the exit: a+  -- probe that merely times out shows the peer is reachable but unresponsive+  it("kill treats an unresponsive probe as a failure, not an exit", function()+    with_notes(function()+      local addr = utils.__IS_WINDOWS and [[\\.\pipe\nvim-srv-test-hung]]+          or (vim.fn.tempname() .. "-nvim-srv-test-hung.sock")+      with_stubs(function() return false, "Timeout" end,+        function() return false, "Timeout" end, function()+          actions.serverlist_kill({ serverlist_entry(addr) })+          assert(noted("kill failed"), "probe timeout must remain a kill failure")+          assert(not noted("Killed"), "unconfirmed exit must not report killed")+          assert(not noted("already exited"), "timeout must not report as exited")+        end)+    end)+  end)+  it("kill counts a probe channel closed by the peer as an exit", function()+    with_notes(function()+      local addr = utils.__IS_WINDOWS and [[\\.\pipe\nvim-srv-test-died]]+          or (vim.fn.tempname() .. "-nvim-srv-test-died.sock")+      -- the peer accepted the probe and died while the request was in+      -- flight: the lost `qa!` response is confirmed as an exit+      with_stubs(function() return false, "Timeout" end,+        function() return false, "ch 7 was closed by the peer" end, function()+          actions.serverlist_kill({ serverlist_entry(addr) })+          assert(noted("Killed 1 server"), "probe-confirmed exit must count as killed")+          assert(not noted("kill failed"), "probe-confirmed exit must not fail")+        end)+    end)+  end)+  it("kill counts a probe with no listener as an exit", function()+    with_notes(function()+      local addr = utils.__IS_WINDOWS and [[\\.\pipe\nvim-srv-test-gone]]+          or (vim.fn.tempname() .. "-nvim-srv-test-gone.sock")+      -- `sockconnect` failing means nothing listens at the address anymore+      with_stubs(function() return false, "Timeout" end,+        function() return false, "Invalid channel" end, function()+          actions.serverlist_kill({ serverlist_entry(addr) })+          assert(noted("Killed 1 server"), "dead socket must count as killed")+          assert(not noted("kill failed"), "dead socket must not fail")+        end)+    end)+  end)   it("connect guards unreachable and self targets", function()     with_notes(function()       local stale = utils.__IS_WINDOWS and [[\\.\pipe\nvim-srv-test-stale]]