heph.nvim: RPC client + buffer editing + wiki-links + journal (slice 11a)
The primary surface begins (tech-spec §8): a Neovim plugin that is a thin
client of the local hephd over its unix-socket JSON-RPC.
- node.resolve {title} → Node|null (heph-core Store + dispatch): exact,
owner-scoped, non-tombstoned alias-then-title match — the same mapping that
materializes wiki links, so follow-link jumps to the node the stored link
points at (never fuzzy search). Unit + rpc_socket integration tests.
- heph.nvim/: vim.uv unix-socket JSON-RPC client (blocking call via vim.wait,
id-demuxed, partial-line buffered, luanil so JSON null → Lua nil; isolated
Sessions for tests). Buffer-backed nodes (heph://node/<id>, acwrite;
BufReadCmd→node.get / BufWriteCmd→node.update, whole-buffer body round-trips
exactly through the CRDT). [[wiki-link]] follow on <CR>. Daily journal.
:Heph command surface + completion.
- Headless e2e (§9): a self-contained busted-style runner (tests/e2e/runner.lua)
— no external plugins, no network, deterministic CI exit codes. Specs: journal
round-trip, follow-link (+ unresolved no-op), link-two-docs/backlink.
`make -C heph.nvim test` builds hephd and runs it.
Docs: heph-nvim reference card, §14 tracker (11a done; 11b/11c/11d queued),
changelog fragment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:33:29 -07:00
|
|
|
--- 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 daemon = require("heph.daemon")
|
|
|
|
|
|
|
|
|
|
local M = {}
|
|
|
|
|
local counter = 0
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2026-06-02 09:32:32 -07:00
|
|
|
--- A fresh temp dir + short socket/db paths, WITHOUT spawning a daemon (for
|
|
|
|
|
--- tests that drive the plugin's own autostart/lifecycle). `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
|
|
|
|
|
|
heph.nvim: RPC client + buffer editing + wiki-links + journal (slice 11a)
The primary surface begins (tech-spec §8): a Neovim plugin that is a thin
client of the local hephd over its unix-socket JSON-RPC.
- node.resolve {title} → Node|null (heph-core Store + dispatch): exact,
owner-scoped, non-tombstoned alias-then-title match — the same mapping that
materializes wiki links, so follow-link jumps to the node the stored link
points at (never fuzzy search). Unit + rpc_socket integration tests.
- heph.nvim/: vim.uv unix-socket JSON-RPC client (blocking call via vim.wait,
id-demuxed, partial-line buffered, luanil so JSON null → Lua nil; isolated
Sessions for tests). Buffer-backed nodes (heph://node/<id>, acwrite;
BufReadCmd→node.get / BufWriteCmd→node.update, whole-buffer body round-trips
exactly through the CRDT). [[wiki-link]] follow on <CR>. Daily journal.
:Heph command surface + completion.
- Headless e2e (§9): a self-contained busted-style runner (tests/e2e/runner.lua)
— no external plugins, no network, deterministic CI exit codes. Specs: journal
round-trip, follow-link (+ unresolved no-op), link-two-docs/backlink.
`make -C heph.nvim test` builds hephd and runs it.
Docs: heph-nvim reference card, §14 tracker (11a done; 11b/11c/11d queued),
changelog fragment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:33:29 -07:00
|
|
|
--- Start a fresh daemon and bind the plugin's rpc to it. Returns a `ctx` with:
|
|
|
|
|
--- `dir, sock, db, daemon, exited, q` (an isolated session for assertions).
|
|
|
|
|
function M.start()
|
|
|
|
|
local dir = unique_dir()
|
|
|
|
|
local sock = dir .. "/s"
|
|
|
|
|
local db = dir .. "/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 = daemon.spawn({
|
|
|
|
|
bin = bin,
|
|
|
|
|
db = db,
|
|
|
|
|
socket = sock,
|
|
|
|
|
on_exit = function()
|
|
|
|
|
exited.done = true
|
|
|
|
|
end,
|
|
|
|
|
})
|
|
|
|
|
local ok, reason = daemon.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
|
|
|
|
|
|
|
|
|
|
--- 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)
|
2026-06-02 09:32:32 -07:00
|
|
|
rpc.set_respawn(nil) -- don't let a managed-daemon spec leak self-heal here
|
heph.nvim: RPC client + buffer editing + wiki-links + journal (slice 11a)
The primary surface begins (tech-spec §8): a Neovim plugin that is a thin
client of the local hephd over its unix-socket JSON-RPC.
- node.resolve {title} → Node|null (heph-core Store + dispatch): exact,
owner-scoped, non-tombstoned alias-then-title match — the same mapping that
materializes wiki links, so follow-link jumps to the node the stored link
points at (never fuzzy search). Unit + rpc_socket integration tests.
- heph.nvim/: vim.uv unix-socket JSON-RPC client (blocking call via vim.wait,
id-demuxed, partial-line buffered, luanil so JSON null → Lua nil; isolated
Sessions for tests). Buffer-backed nodes (heph://node/<id>, acwrite;
BufReadCmd→node.get / BufWriteCmd→node.update, whole-buffer body round-trips
exactly through the CRDT). [[wiki-link]] follow on <CR>. Daily journal.
:Heph command surface + completion.
- Headless e2e (§9): a self-contained busted-style runner (tests/e2e/runner.lua)
— no external plugins, no network, deterministic CI exit codes. Specs: journal
round-trip, follow-link (+ unresolved no-op), link-two-docs/backlink.
`make -C heph.nvim test` builds hephd and runs it.
Docs: heph-nvim reference card, §14 tracker (11a done; 11b/11c/11d queued),
changelog fragment.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:33:29 -07:00
|
|
|
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
|