generated from eblume/project-template
heph.nvim: rip out auto-spawn — connect-only plugin
All checks were successful
Build / validate (pull_request) Successful in 10m44s
All checks were successful
Build / validate (pull_request) Successful in 10m44s
The daemon is now an OS service (`heph daemon`); the plugin no longer spawns or supervises one. Removes the managed-daemon machinery entirely. - delete lua/heph/daemon.lua (spawn/ensure/stop_spawned/self-heal) - init.lua: connect-only; probe `health` once and guide to `heph daemon start` - rpc.lua: drop set_respawn + respawn-on-drop; a dropped connection just reconnects once (e.g. after `heph daemon restart`), never spawns - config.lua: drop autostart/bin/db; stable socket fallback (data-dir, matches hephd::default_socket_path), keep $HEPH_SOCKET for dev isolation - tests: spawn/wait_ready move into the e2e harness (test infra); rework managed_daemon_spec into a connect-only spec (connect / clean-fail / reconnect) 16 nvim e2e specs pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0cfe627055
commit
cdd4d9f62a
6 changed files with 114 additions and 308 deletions
|
|
@ -1,108 +1,47 @@
|
|||
-- The plugin-managed daemon lifecycle (tech-spec §8): plug-and-play autostart,
|
||||
-- self-heal on a dropped connection, and connect-only when autostart is off.
|
||||
-- The plugin is connect-only (tech-spec §8, [[design]] §4): it never spawns a
|
||||
-- daemon — it connects to one run as an OS service (`heph daemon start`). These
|
||||
-- specs cover connecting to a running daemon, a clean failure when none is
|
||||
-- running, and reconnecting after the daemon is restarted.
|
||||
|
||||
local h = require("e2e.helpers")
|
||||
|
||||
describe("managed daemon", function()
|
||||
local t
|
||||
before_each(function()
|
||||
t = h.tmp() -- temp paths; no daemon spawned by the harness
|
||||
describe("connect-only daemon", function()
|
||||
it("connects to a running daemon and works", function()
|
||||
local ctx = h.start() -- harness starts a real daemon; binds the plugin to it
|
||||
require("heph").setup({ socket = ctx.sock, keymaps = false })
|
||||
assert.is_truthy(require("heph.rpc").call("health", {}))
|
||||
h.stop(ctx)
|
||||
end)
|
||||
after_each(function()
|
||||
pcall(function()
|
||||
require("heph.daemon").stop_spawned()
|
||||
end)
|
||||
|
||||
it("fails cleanly when no daemon is running (never spawns one)", function()
|
||||
local t = h.tmp() -- temp socket path with nothing serving it
|
||||
require("heph.rpc").setup(t.sock)
|
||||
-- A call must fail loudly (connection error), not hang or spawn a daemon.
|
||||
local ok = pcall(require("heph.rpc").call, "health", {})
|
||||
assert.is_false(ok, "expected a connection failure with no daemon running")
|
||||
pcall(function()
|
||||
require("heph.rpc").close()
|
||||
end)
|
||||
require("heph.rpc").set_respawn(nil)
|
||||
t.rm()
|
||||
end)
|
||||
|
||||
it("autostart spawns a local daemon and connects plug-and-play", function()
|
||||
require("heph").setup({
|
||||
socket = t.sock,
|
||||
db = t.db,
|
||||
bin = h.hephd_bin(),
|
||||
autostart = true,
|
||||
keymaps = false,
|
||||
})
|
||||
assert.is_true(require("heph.daemon").is_managed())
|
||||
-- A real call works because the plugin brought the daemon up itself.
|
||||
assert.is_truthy(require("heph.rpc").call("health", {}))
|
||||
end)
|
||||
|
||||
it("self-heals: respawns and reconnects when the daemon dies", function()
|
||||
require("heph").setup({
|
||||
socket = t.sock,
|
||||
db = t.db,
|
||||
bin = h.hephd_bin(),
|
||||
autostart = true,
|
||||
keymaps = false,
|
||||
})
|
||||
it("reconnects after the daemon is restarted under it", function()
|
||||
local ctx = h.start()
|
||||
require("heph").setup({ socket = ctx.sock, keymaps = false })
|
||||
require("heph.rpc").call("health", {})
|
||||
|
||||
-- Kill the managed daemon out from under the plugin.
|
||||
local m = require("heph.daemon")._managed
|
||||
m.handle:kill("sigterm")
|
||||
-- Kill the daemon, then start a fresh one on the SAME socket (as
|
||||
-- `heph daemon restart` would). The next call should reconnect.
|
||||
ctx.daemon.handle:kill("sigterm")
|
||||
vim.wait(2000, function()
|
||||
return m.exited.done
|
||||
return ctx.exited.done
|
||||
end, 20)
|
||||
pcall(function()
|
||||
vim.uv.fs_unlink(ctx.sock)
|
||||
end)
|
||||
|
||||
-- The next call transparently respawns the daemon and succeeds.
|
||||
local ctx2 = h.start_on(ctx.dir, ctx.sock, ctx.db)
|
||||
assert.is_truthy(require("heph.rpc").call("health", {}))
|
||||
assert.is_true(require("heph.daemon").is_managed())
|
||||
end)
|
||||
|
||||
it("does not deadlock on a stale socket left by a crash (regression)", function()
|
||||
-- Bring up a managed daemon, then HARD-kill it so no cleanup runs — leaving
|
||||
-- a stale socket file with no listener (the second-launch crash scenario:
|
||||
-- wait_ready ran the rpc probe inside a vim.wait predicate, nesting vim.wait
|
||||
-- and freezing Neovim).
|
||||
require("heph").setup({
|
||||
socket = t.sock,
|
||||
db = t.db,
|
||||
bin = h.hephd_bin(),
|
||||
autostart = true,
|
||||
keymaps = false,
|
||||
})
|
||||
require("heph.rpc").call("health", {})
|
||||
local m = require("heph.daemon")._managed
|
||||
m.handle:kill("sigkill")
|
||||
vim.wait(2000, function()
|
||||
return m.exited.done
|
||||
end, 20)
|
||||
assert.is_truthy(vim.uv.fs_stat(t.sock), "precondition: a stale socket is present")
|
||||
|
||||
-- Probing the stale socket must RETURN promptly (not deadlock). The fix
|
||||
-- returns in ~200ms; the bug froze here indefinitely.
|
||||
local start = vim.uv.hrtime()
|
||||
local ready = require("heph.daemon").wait_ready(t.sock, 200)
|
||||
local elapsed_ms = (vim.uv.hrtime() - start) / 1e6
|
||||
assert.is_false(ready)
|
||||
assert.is_true(elapsed_ms < 2000, "wait_ready took " .. math.floor(elapsed_ms) .. "ms — possible deadlock")
|
||||
|
||||
-- A fresh autostart recovers despite the stale socket still being there.
|
||||
require("heph").setup({
|
||||
socket = t.sock,
|
||||
db = t.db,
|
||||
bin = h.hephd_bin(),
|
||||
autostart = true,
|
||||
keymaps = false,
|
||||
})
|
||||
assert.is_truthy(require("heph.rpc").call("health", {}))
|
||||
assert.is_true(require("heph.daemon").is_managed())
|
||||
end)
|
||||
|
||||
it("connect-only (autostart=false) errors when no daemon is running", function()
|
||||
require("heph").setup({
|
||||
socket = t.sock,
|
||||
autostart = false,
|
||||
keymaps = false,
|
||||
})
|
||||
assert.is_false(require("heph.daemon").is_managed())
|
||||
-- No daemon, no autostart, no self-heal → a call fails loudly.
|
||||
local ok = pcall(require("heph.rpc").call, "health", {})
|
||||
assert.is_false(ok, "expected connect-only to fail with no daemon running")
|
||||
h.stop(ctx2)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue