test(config): verify Vim/Neovim config payload parity
Expose the init_options payload builders publicly in both clients
(M.init_options/M.server_settings in Lua, nuwiki#lsp#settings() in Vim) so
they can be inspected. Add editor harnesses that dump each client's
server-bound config as deterministic key=value lines and diff against a
shared golden file: nvim == golden and vim == golden together prove the two
clients send identical config. Add a server-side test asserting the Vim flat
shape and the Neovim {nuwiki:{...}} wrapper desugar to the same WikiConfig.
Wire both harnesses into CI.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -96,3 +96,7 @@ jobs:
|
||||
run: ./development/tests/test-keymaps.sh
|
||||
- name: run Vim keymap harness
|
||||
run: ./development/tests/test-keymaps-vim.sh
|
||||
- name: run Neovim config parity harness
|
||||
run: ./development/tests/test-config.sh
|
||||
- name: run Vim config parity harness
|
||||
run: ./development/tests/test-config-vim.sh
|
||||
|
||||
@@ -91,3 +91,11 @@ function! s:settings() abort
|
||||
endif
|
||||
return l:cfg
|
||||
endfunction
|
||||
|
||||
" Public accessor for the `initialization_options` payload. Mirrors the
|
||||
" Neovim client's `require('nuwiki.lsp').init_options()` so the config
|
||||
" parity harness can inspect the exact dict each client sends to the
|
||||
" server.
|
||||
function! nuwiki#lsp#settings() abort
|
||||
return s:settings()
|
||||
endfunction
|
||||
|
||||
@@ -319,6 +319,54 @@ fn raw_wiki_parses_every_new_key() {
|
||||
assert!(w.auto_toc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vim_flat_and_neovim_wrapped_payloads_desugar_identically() {
|
||||
// Parity guard for the two client shapes:
|
||||
// * Vim (`autoload/nuwiki/lsp.vim`) → flat init_options dict.
|
||||
// * Neovim (`lua/nuwiki/lsp.lua`) → `{ nuwiki = { … } }` for
|
||||
// `workspace/didChangeConfiguration` (server_settings()).
|
||||
// Both must produce the same wikis; otherwise switching editors would
|
||||
// silently change how the server resolves links/diary/HTML paths.
|
||||
let inner = json!({
|
||||
"wiki_root": "/tmp/base",
|
||||
"file_extension": ".md",
|
||||
"syntax": "vimwiki",
|
||||
"log_level": "debug",
|
||||
"wikis": [
|
||||
{
|
||||
"name": "personal",
|
||||
"root": "/tmp/personal",
|
||||
"file_extension": ".wiki",
|
||||
"index": "home",
|
||||
"diary_rel_path": "journal",
|
||||
},
|
||||
{ "name": "work", "root": "/tmp/work" },
|
||||
],
|
||||
});
|
||||
|
||||
let flat = config_from_json(inner.clone());
|
||||
let wrapped = config_from_json(json!({ "nuwiki": inner }));
|
||||
|
||||
assert_eq!(flat.wikis.len(), wrapped.wikis.len());
|
||||
for (a, b) in flat.wikis.iter().zip(wrapped.wikis.iter()) {
|
||||
assert_eq!(a.name, b.name);
|
||||
assert_eq!(a.root, b.root);
|
||||
assert_eq!(a.file_extension, b.file_extension);
|
||||
assert_eq!(a.syntax, b.syntax);
|
||||
assert_eq!(a.index, b.index);
|
||||
assert_eq!(a.diary_rel_path, b.diary_rel_path);
|
||||
}
|
||||
|
||||
// And the desugared list matches the per-wiki values both clients send.
|
||||
assert_eq!(flat.wikis[0].name, "personal");
|
||||
assert_eq!(flat.wikis[0].root, PathBuf::from("/tmp/personal"));
|
||||
assert_eq!(flat.wikis[0].file_extension, ".wiki");
|
||||
assert_eq!(flat.wikis[0].index, "home");
|
||||
assert_eq!(flat.wikis[0].diary_rel_path, "journal");
|
||||
assert_eq!(flat.wikis[1].name, "work");
|
||||
assert_eq!(flat.wikis[1].root, PathBuf::from("/tmp/work"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_single_wiki_shape_picks_up_defaults() {
|
||||
let cfg = config_from_json(serde_json::json!({
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[default]
|
||||
wiki_root=~/vimwiki
|
||||
file_extension=.wiki
|
||||
syntax=vimwiki
|
||||
log_level=warn
|
||||
[sample]
|
||||
wiki_root=/tmp/base
|
||||
file_extension=.md
|
||||
syntax=vimwiki
|
||||
log_level=debug
|
||||
wikis[0].diary_rel_path=journal
|
||||
wikis[0].file_extension=.wiki
|
||||
wikis[0].index=home
|
||||
wikis[0].name=personal
|
||||
wikis[0].root=/tmp/personal
|
||||
wikis[1].name=work
|
||||
wikis[1].root=/tmp/work
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# development/tests/test-config-vim.sh — Vim counterpart of test-config.sh.
|
||||
#
|
||||
# Runs development/tests/test-config-vim.vim under headless Vim, which dumps
|
||||
# the Vim client's `initialization_options` payload (nuwiki#lsp#settings())
|
||||
# as `key=value` lines, then diffs against the shared golden
|
||||
# config-expected.txt. Matching the same golden the Neovim harness uses is
|
||||
# what proves Vim/Neovim config parity.
|
||||
#
|
||||
# Exit code: 0 when the payload matches the golden, 1 otherwise. Skips
|
||||
# (exit 0) when vim is not installed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
log() { printf '\033[1;34m[config-vim]\033[0m %s\n' "$*"; }
|
||||
|
||||
if ! command -v vim >/dev/null 2>&1; then
|
||||
log 'vim not installed — skipping'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP="$(mktemp -d -t nuwiki-config-vim-test-XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
GOLDEN="$REPO_ROOT/development/tests/config-expected.txt"
|
||||
OUT="$TMP/vim-config.txt"
|
||||
|
||||
VIMRC="$TMP/vimrc"
|
||||
cat > "$VIMRC" <<EOF
|
||||
set nocompatible
|
||||
let &runtimepath = '$REPO_ROOT' . ',' . &runtimepath
|
||||
filetype plugin indent on
|
||||
EOF
|
||||
|
||||
log 'running Vim config harness…'
|
||||
# `</dev/null` so ex-mode doesn't hang on stdin; `timeout` guards CI.
|
||||
NUWIKI_CONFIG_OUT="$OUT" \
|
||||
timeout 30 vim -e -s -u "$VIMRC" \
|
||||
-c "source $REPO_ROOT/development/tests/test-config-vim.vim" \
|
||||
</dev/null >"$TMP/vim.log" 2>&1 || true
|
||||
|
||||
if [[ ! -f "$OUT" ]]; then
|
||||
echo 'config harness produced no output' >&2
|
||||
echo '--- vim log ---' >&2
|
||||
cat "$TMP/vim.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if diff -u "$GOLDEN" "$OUT"; then
|
||||
log 'Vim config payload matches golden ✓'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo 'Vim config payload DIVERGED from golden (see diff above)' >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,50 @@
|
||||
" development/tests/test-config-vim.vim — Vim counterpart of
|
||||
" test-config.lua. Dumps the Vim client's `initialization_options` payload
|
||||
" (nuwiki#lsp#settings()) for the same two scenarios as the Neovim harness,
|
||||
" as byte-identical `key=value` lines, so both can be diffed against the
|
||||
" shared golden development/tests/config-expected.txt.
|
||||
"
|
||||
" "nvim == golden" and "vim == golden" together prove Vim/Neovim config
|
||||
" parity for everything the server consumes.
|
||||
|
||||
let s:out = $NUWIKI_CONFIG_OUT
|
||||
let s:lines = []
|
||||
|
||||
function! s:dump(payload) abort
|
||||
call add(s:lines, 'wiki_root=' . a:payload.wiki_root)
|
||||
call add(s:lines, 'file_extension=' . a:payload.file_extension)
|
||||
call add(s:lines, 'syntax=' . a:payload.syntax)
|
||||
call add(s:lines, 'log_level=' . a:payload.log_level)
|
||||
if has_key(a:payload, 'wikis')
|
||||
let l:i = 0
|
||||
for l:w in a:payload.wikis
|
||||
for l:k in sort(keys(l:w))
|
||||
call add(s:lines, printf('wikis[%d].%s=%s', l:i, l:k, l:w[l:k]))
|
||||
endfor
|
||||
let l:i += 1
|
||||
endfor
|
||||
endif
|
||||
endfunction
|
||||
|
||||
" Scenario 1: defaults (no globals set).
|
||||
unlet! g:nuwiki_wiki_root g:nuwiki_file_extension g:nuwiki_syntax
|
||||
unlet! g:nuwiki_log_level g:nuwiki_wikis
|
||||
call add(s:lines, '[default]')
|
||||
call s:dump(nuwiki#lsp#settings())
|
||||
|
||||
" Scenario 2: same representative config as the Neovim harness.
|
||||
let g:nuwiki_wiki_root = '/tmp/base'
|
||||
let g:nuwiki_file_extension = '.md'
|
||||
let g:nuwiki_syntax = 'vimwiki'
|
||||
let g:nuwiki_log_level = 'debug'
|
||||
let g:nuwiki_wikis = [
|
||||
\ { 'name': 'personal', 'root': '/tmp/personal',
|
||||
\ 'file_extension': '.wiki', 'index': 'home',
|
||||
\ 'diary_rel_path': 'journal' },
|
||||
\ { 'name': 'work', 'root': '/tmp/work' },
|
||||
\ ]
|
||||
call add(s:lines, '[sample]')
|
||||
call s:dump(nuwiki#lsp#settings())
|
||||
|
||||
call writefile(s:lines, s:out)
|
||||
qall!
|
||||
@@ -0,0 +1,74 @@
|
||||
-- development/tests/test-config.lua — dump the Neovim client's
|
||||
-- `initialization_options` payload for two scenarios (default config and a
|
||||
-- representative multi-wiki sample) as deterministic `key=value` lines.
|
||||
--
|
||||
-- The runner (test-config.sh) diffs the output against
|
||||
-- development/tests/config-expected.txt. The Vim harness
|
||||
-- (test-config-vim.vim) emits byte-identical lines against the SAME golden,
|
||||
-- so "nvim == golden" and "vim == golden" together prove the two clients
|
||||
-- send the same server-bound config — i.e. Vim/Neovim config parity.
|
||||
--
|
||||
-- Only the server-relevant keys are emitted (wiki_root, file_extension,
|
||||
-- syntax, log_level, wikis[*]). `mappings`/`folding` are client-side
|
||||
-- concerns the server ignores, so they're intentionally excluded.
|
||||
|
||||
local out = assert(os.getenv('NUWIKI_CONFIG_OUT'), 'NUWIKI_CONFIG_OUT unset')
|
||||
|
||||
local config = require('nuwiki.config')
|
||||
local lsp = require('nuwiki.lsp')
|
||||
|
||||
local lines = {}
|
||||
local function emit(s)
|
||||
lines[#lines + 1] = s
|
||||
end
|
||||
|
||||
local function dump(payload)
|
||||
emit('wiki_root=' .. tostring(payload.wiki_root))
|
||||
emit('file_extension=' .. tostring(payload.file_extension))
|
||||
emit('syntax=' .. tostring(payload.syntax))
|
||||
emit('log_level=' .. tostring(payload.log_level))
|
||||
if payload.wikis then
|
||||
for i, w in ipairs(payload.wikis) do
|
||||
local keys = {}
|
||||
for k in pairs(w) do
|
||||
keys[#keys + 1] = k
|
||||
end
|
||||
table.sort(keys)
|
||||
for _, k in ipairs(keys) do
|
||||
emit(string.format('wikis[%d].%s=%s', i - 1, k, tostring(w[k])))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Scenario 1: defaults (no user config, no VimL globals).
|
||||
vim.g.nuwiki_wikis = nil
|
||||
config.apply({})
|
||||
emit('[default]')
|
||||
dump(lsp.init_options())
|
||||
|
||||
-- Scenario 2: representative config exercising scalar keys + a wikis list.
|
||||
config.apply({
|
||||
wiki_root = '/tmp/base',
|
||||
file_extension = '.md',
|
||||
syntax = 'vimwiki',
|
||||
log_level = 'debug',
|
||||
wikis = {
|
||||
{
|
||||
name = 'personal',
|
||||
root = '/tmp/personal',
|
||||
file_extension = '.wiki',
|
||||
index = 'home',
|
||||
diary_rel_path = 'journal',
|
||||
},
|
||||
{ name = 'work', root = '/tmp/work' },
|
||||
},
|
||||
})
|
||||
emit('[sample]')
|
||||
dump(lsp.init_options())
|
||||
|
||||
local f = assert(io.open(out, 'w'))
|
||||
f:write(table.concat(lines, '\n') .. '\n')
|
||||
f:close()
|
||||
|
||||
vim.cmd('qa!')
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# development/tests/test-config.sh — verify the Neovim client builds the
|
||||
# exact `initialization_options` payload the server expects.
|
||||
#
|
||||
# Runs development/tests/test-config.lua under headless Neovim, which dumps
|
||||
# the server-bound config (wiki_root, file_extension, syntax, log_level,
|
||||
# wikis[*]) for a default and a sample scenario as `key=value` lines, then
|
||||
# diffs the result against the shared golden config-expected.txt.
|
||||
#
|
||||
# The Vim harness (test-config-vim.sh) diffs against the SAME golden, so a
|
||||
# green run of both proves Vim/Neovim config parity.
|
||||
#
|
||||
# Exit code: 0 when the payload matches the golden, 1 otherwise.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TMP="$(mktemp -d -t nuwiki-config-test-XXXXXX)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
log() { printf '\033[1;34m[config-test]\033[0m %s\n' "$*"; }
|
||||
|
||||
GOLDEN="$REPO_ROOT/development/tests/config-expected.txt"
|
||||
OUT="$TMP/nvim-config.txt"
|
||||
|
||||
# No LSP server needed — we only inspect the payload the client would send.
|
||||
INIT="$TMP/init.lua"
|
||||
cat > "$INIT" <<EOF
|
||||
vim.opt.runtimepath:prepend('$REPO_ROOT')
|
||||
EOF
|
||||
|
||||
log 'running Neovim config harness…'
|
||||
NUWIKI_CONFIG_OUT="$OUT" \
|
||||
timeout 30 nvim --clean -u "$INIT" --headless \
|
||||
-c "luafile $REPO_ROOT/development/tests/test-config.lua" \
|
||||
>"$TMP/nvim.log" 2>&1 || true
|
||||
|
||||
if [[ ! -f "$OUT" ]]; then
|
||||
echo 'config harness produced no output' >&2
|
||||
echo '--- nvim log ---' >&2
|
||||
cat "$TMP/nvim.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if diff -u "$GOLDEN" "$OUT"; then
|
||||
log 'Neovim config payload matches golden ✓'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo 'Neovim config payload DIVERGED from golden (see diff above)' >&2
|
||||
exit 1
|
||||
+8
-6
@@ -28,14 +28,16 @@ end
|
||||
--- `initializationOptions` payload: the server reads it once at
|
||||
--- `initialize` time via `Config::from_init_params`. This is what
|
||||
--- delivers `wiki_root`/`wikis`/HTML/diary config to Rust.
|
||||
local function init_options()
|
||||
--- Public so the config parity harness can inspect the exact payload.
|
||||
function M.init_options()
|
||||
return resolved_opts()
|
||||
end
|
||||
|
||||
--- `settings` payload: the same shape, but sent via
|
||||
--- `workspace/didChangeConfiguration` after startup. Lets the server
|
||||
--- pick up config changes without a restart.
|
||||
local function server_settings()
|
||||
--- Public so the config parity harness can inspect the exact payload.
|
||||
function M.server_settings()
|
||||
return { nuwiki = resolved_opts() }
|
||||
end
|
||||
|
||||
@@ -69,8 +71,8 @@ function M.register()
|
||||
cmd = command(),
|
||||
filetypes = { 'vimwiki' },
|
||||
root_markers = { '.git', '.nuwiki' },
|
||||
init_options = init_options(),
|
||||
settings = server_settings(),
|
||||
init_options = M.init_options(),
|
||||
settings = M.server_settings(),
|
||||
})
|
||||
vim.lsp.enable('nuwiki')
|
||||
return
|
||||
@@ -85,8 +87,8 @@ function M.register()
|
||||
name = 'nuwiki',
|
||||
cmd = command(),
|
||||
root_dir = root_dir_for(ev.file),
|
||||
init_options = init_options(),
|
||||
settings = server_settings(),
|
||||
init_options = M.init_options(),
|
||||
settings = M.server_settings(),
|
||||
})
|
||||
end,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user