hephaestus/heph.nvim/tests/e2e/helpers.lua
Erich Blume cdd4d9f62a
All checks were successful
Build / validate (pull_request) Successful in 10m44s
heph.nvim: rip out auto-spawn — connect-only plugin
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>
2026-06-02 21:21:28 -07:00

192 lines
5.4 KiB
Lua

--- E2e harness (tech-spec §9): spin up a real `hephd` in local mode against a
--- temp DB + socket, point the plugin at it, and tear it down deterministically.
--- Step builders (create doc/task, open, edit, save) are reusable across specs.
local rpc = require("heph.rpc")
local uv = vim.uv or vim.loop
local M = {}
local counter = 0
--- Spawn a `local`-mode hephd against `db` listening on `socket` (test infra —
--- the plugin itself is connect-only; the daemon is normally an OS service).
local function spawn(opts)
local args = { "--mode", "local", "--db", opts.db, "--socket", opts.socket }
local handle, pid = uv.spawn(opts.bin, {
args = args,
stdio = { nil, nil, opts.stderr },
}, function(code, signal)
if opts.on_exit then
opts.on_exit(code, signal)
end
end)
if not handle then
error("heph-test: failed to spawn hephd (bin=" .. tostring(opts.bin) .. ")")
end
return { handle = handle, pid = pid }
end
--- Wait until `socket` exists and answers `health`. Plain Lua loop — never a
--- `vim.wait` predicate (the rpc round-trip uses `vim.wait`; nesting deadlocks).
local function wait_ready(socket, timeout)
timeout = timeout or 5000
local deadline = uv.hrtime() + timeout * 1e6
while uv.hrtime() < deadline do
if uv.fs_stat(socket) ~= nil then
local session = rpc.new_session(socket)
local ok = pcall(function()
session:call("health", vim.empty_dict(), { timeout = 200 })
end)
session:close()
if ok then
return true
end
end
vim.wait(50)
end
return false, "daemon not ready at " .. socket
end
M.wait_ready = wait_ready
local function repo_root()
-- ":p" makes this absolute regardless of how the runner was launched.
local here = vim.fn.fnamemodify(debug.getinfo(1, "S").source:sub(2), ":p")
return vim.fn.fnamemodify(here, ":h:h:h:h") -- .../heph.nvim/tests/e2e -> repo root
end
--- The hephd binary to drive: `$HEPHD_BIN` or the workspace debug build.
function M.hephd_bin()
local env = vim.env.HEPHD_BIN
if env and #env > 0 then
return env
end
return repo_root() .. "/target/debug/hephd"
end
-- A short unique temp dir. unix socket paths are capped near 104 bytes
-- (`sun_path`), so we stay under a short base, never `tempname()`.
local function unique_dir()
counter = counter + 1
local base = vim.env.HEPH_TEST_TMP
base = (base and #base > 0) and base:gsub("/+$", "") or "/tmp"
local dir = string.format("%s/h%d-%d", base, vim.fn.getpid(), counter)
vim.fn.mkdir(dir, "p")
return dir
end
--- A fresh temp dir + short socket/db paths, WITHOUT spawning a daemon (for
--- tests of the no-daemon-running case). `rm` removes it.
function M.tmp()
local dir = unique_dir()
return { dir = dir, sock = dir .. "/s", db = dir .. "/db", rm = function()
pcall(function()
vim.fn.delete(dir, "rf")
end)
end }
end
--- Start a daemon on explicit paths and bind the plugin's rpc to it. Returns a
--- `ctx` with `dir, sock, db, daemon, exited, q` (an isolated assert session).
function M.start_on(dir, sock, db)
assert(#sock < 104, "socket path too long for sun_path: " .. sock)
local bin = M.hephd_bin()
assert(
vim.fn.executable(bin) == 1,
"hephd not built/executable: " .. bin .. " (run: cargo build -p hephd)"
)
local exited = { done = false }
local d = spawn({
bin = bin,
db = db,
socket = sock,
on_exit = function()
exited.done = true
end,
})
local ok, reason = wait_ready(sock, 5000)
assert(ok, "daemon not ready: " .. tostring(reason))
rpc.setup(sock) -- the plugin's default session, used by buffers/commands
return {
dir = dir,
sock = sock,
db = db,
daemon = d,
exited = exited,
q = rpc.new_session(sock), -- isolated session for independent assertions
}
end
--- Start a fresh daemon on a new temp dir and bind the plugin's rpc to it.
function M.start()
local dir = unique_dir()
return M.start_on(dir, dir .. "/s", dir .. "/db")
end
--- Tear down: close sessions, delete heph:// buffers, reap the daemon, rm temp.
function M.stop(ctx)
if not ctx then
return
end
pcall(function()
ctx.q:close()
end)
pcall(function()
rpc.close()
end)
for _, b in ipairs(vim.api.nvim_list_bufs()) do
if vim.api.nvim_buf_get_name(b):match("^heph://") then
pcall(vim.api.nvim_buf_delete, b, { force = true })
end
end
local h = ctx.daemon and ctx.daemon.handle
if h then
if not ctx.exited.done then
pcall(function()
h:kill("sigterm")
end)
vim.wait(2000, function()
return ctx.exited.done
end, 20)
end
pcall(function()
if not h:is_closing() then
h:close()
end
end)
end
pcall(function()
vim.fn.delete(ctx.dir, "rf")
end)
end
-- --- step builders (drive the plugin's default session) ---
function M.create_doc(title, body)
return rpc.call("node.create", { kind = "doc", title = title, body = body or "" })
end
function M.create_task(opts)
return rpc.call("task.create", opts or {})
end
--- Open node `id` in a buffer; returns the buffer handle.
function M.open(id)
require("heph.node").open(id)
return vim.api.nvim_get_current_buf()
end
function M.set_lines(buf, lines)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
end
--- Save a heph:// buffer (fires BufWriteCmd → node.update).
function M.save(buf)
vim.api.nvim_buf_call(buf, function()
vim.cmd("write")
end)
end
return M