-- 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. 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 end) after_each(function() pcall(function() require("heph.daemon").stop_spawned() end) 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, }) require("heph.rpc").call("health", {}) -- Kill the managed daemon out from under the plugin. local m = require("heph.daemon")._managed m.handle:kill("sigterm") vim.wait(2000, function() return m.exited.done end, 20) -- The next call transparently respawns the daemon and succeeds. 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") end) end)