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
@@ -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]]