From 317b0bb31de753671f815874b015837227553201 Mon Sep 17 00:00:00 2001 From: myuron Date: Sat, 17 Oct 2026 13:11:15 +0900 Subject: [PATCH 1/4] feat(lock): resolve relative path inputs from flake.lock Co-Authored-By: Claude Opus 5.5 --- nix/lib/sources.nix | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/nix/lib/sources.nix b/nix/lib/sources.nix index 8bbe957..b8952e9 100644 --- a/nix/lib/sources.nix +++ b/nix/lib/sources.nix @@ -1,5 +1,6 @@ # Reads lockDir/flake.lock as nothing more than a pin DB and returns a function -# from inputName to a store path via builtins.fetchTree. +# from inputName to a store path via builtins.fetchTree (or, for a relative `path:` node, +# builtins.path). # It is never evaluated as a flake, so evaluation is fully pure (narHash is already fixed). { lockDir }: let @@ -11,11 +12,20 @@ let nodeName = rootInputs.${inputName} or (throw "nvimx: input '${inputName}' not found in flake.lock"); locked = lock.nodes.${nodeName}.locked; in -builtins.fetchTree { - inherit (locked) type narHash; - ${if locked ? owner then "owner" else null} = locked.owner or null; - ${if locked ? repo then "repo" else null} = locked.repo or null; - ${if locked ? url then "url" else null} = locked.url or null; - ${if locked ? rev then "rev" else null} = locked.rev or null; - ${if locked ? ref then "ref" else null} = locked.ref or null; -} +if locked.type == "path" && !(locked ? narHash) && builtins.substring 0 1 locked.path != "/" then + # A relative path: input, which nix locks as `parent = []` with no narHash: resolve it the way + # nix does, against the lock flake's own directory. builtins.path copies just that directory, + # so the plugin's src does not change with every unrelated file in lockDir's source tree. + builtins.path { + path = lockDir + "/${locked.path}"; + name = "source"; + } +else + builtins.fetchTree { + inherit (locked) type narHash; + ${if locked ? owner then "owner" else null} = locked.owner or null; + ${if locked ? repo then "repo" else null} = locked.repo or null; + ${if locked ? url then "url" else null} = locked.url or null; + ${if locked ? rev then "rev" else null} = locked.rev or null; + ${if locked ? ref then "ref" else null} = locked.ref or null; + } From 270ab71045751d1b2d7cb97c9e41379c2edc58e1 Mon Sep 17 00:00:00 2001 From: myuron Date: Sat, 17 Oct 2026 13:14:51 +0900 Subject: [PATCH 2/4] test(ci): add e2e-offline check covering lock, build and launch Co-Authored-By: Claude Opus 5.5 --- tests/checks/default.nix | 1 + tests/checks/e2e-offline.nix | 92 +++++++++++++ tests/checks/lock-app.nix | 8 +- tests/checks/scripts/e2e-offline.sh | 125 ++++++++++++++++++ tests/e2e-offline-test.lua | 48 +++++++ tests/fixtures/e2e-offline/init.lua | 17 +++ .../e2e-offline/nvimx-lock/flake.lock | 66 +++++++++ .../fixtures/e2e-offline/nvimx-lock/flake.nix | 22 +++ .../e2e-offline/nvimx-lock/plugins.json | 72 ++++++++++ .../plugins/alpha.nvim/plugin/alpha.lua | 1 + .../plugins/beta.nvim/plugin/beta.lua | 3 + .../plugins/gamma.nvim/lua/gamma/init.lua | 1 + 12 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 tests/checks/e2e-offline.nix create mode 100644 tests/checks/scripts/e2e-offline.sh create mode 100644 tests/e2e-offline-test.lua create mode 100644 tests/fixtures/e2e-offline/init.lua create mode 100644 tests/fixtures/e2e-offline/nvimx-lock/flake.lock create mode 100644 tests/fixtures/e2e-offline/nvimx-lock/flake.nix create mode 100644 tests/fixtures/e2e-offline/nvimx-lock/plugins.json create mode 100644 tests/fixtures/e2e-offline/plugins/alpha.nvim/plugin/alpha.lua create mode 100644 tests/fixtures/e2e-offline/plugins/beta.nvim/plugin/beta.lua create mode 100644 tests/fixtures/e2e-offline/plugins/gamma.nvim/lua/gamma/init.lua diff --git a/tests/checks/default.nix b/tests/checks/default.nix index 9bdfba8..7d749d9 100644 --- a/tests/checks/default.nix +++ b/tests/checks/default.nix @@ -55,4 +55,5 @@ pkgs.lib.genAttrs [ "dev-plugins" "dev-patterns" "extractor-dev-opts" + "e2e-offline" ] (name: import (./. + "/${name}.nix") checkArgs) diff --git a/tests/checks/e2e-offline.nix b/tests/checks/e2e-offline.nix new file mode 100644 index 0000000..30f7b8f --- /dev/null +++ b/tests/checks/e2e-offline.nix @@ -0,0 +1,92 @@ +# The whole loop a user runs, with no network (#30): config -> extract -> resolve -> genflake +# -> real `nix flake lock` -> makeEnv build -> the wrapped neovim launching. "Offline" means +# build-time: evaluating the flake still fetches its own pinned inputs, as every check does +# (lazy-nvim is one of them). No other check builds an env AND launches it with a user config, +# or runs a real nix: checks.lock-app stands a stub in for it. +# +# The env is built from tests/fixtures/e2e-offline/nvimx-lock, whose flake.lock holds relative +# `path:` nodes (nix/lib/sources.nix reads them) instead of github ones, so the three vendored +# plugins need no fetch. The pipeline never writes path sources, so plugins.json / flake.nix are +# real pipeline output (step 1 re-derives them) and only flake.lock is written by hand: +# F=tests/fixtures/e2e-offline +# jq --slurpfile p $F/nvimx-lock/plugins.json ' +# ($p[0].plugins | to_entries | map({key: .value.inputName, value: {flake: false, +# locked: {path: ("../plugins/" + .key), type: "path"}, +# original: {path: ("../plugins/" + .key), type: "path"}, parent: []}}) | from_entries) as $pn +# | {nodes: ($pn + {"lazy-nvim": .nodes["lazy-nvim"], +# root: {inputs: ((($pn|keys) + ["lazy-nvim"]) | map({key: ., value: .}) | from_entries)}}), +# root: "root", version: 7}' flake.lock > $F/nvimx-lock/flake.lock +# Its lazy-nvim node is nvimx's own, so bumping the lazy-nvim input means regenerating it +# (the eval-time failure below says so): otherwise evaluation would fetch a second lazy.nvim. +{ + pkgs, + nvimxLib, + lazy-nvim, + ... +}: +let + inherit (pkgs) lib; + fx = ../fixtures/e2e-offline; + env = nvimxLib.makeEnv { + package = pkgs.neovim-unwrapped; + lockDir = fx + "/nvimx-lock"; + }; + readJson = p: builtins.fromJSON (builtins.readFile p); + fixtureLock = readJson (fx + "/nvimx-lock/flake.lock"); + ownLock = readJson ../../flake.lock; + pluginsDb = readJson (fx + "/nvimx-lock/plugins.json"); + # The tie between the fixture and what the check launches is a plain readFile of source files + # (never IFD), forced into the derivation through `failures` as in checks.genflake-golden, so + # that `nix eval ...aarch64-darwin...drvPath` catches it too. + pluginNodes = lib.mapAttrsToList (_: p: p.inputName) pluginsDb.plugins; + nonLazyNodes = lib.filterAttrs ( + n: _: n != "root" && n != pluginsDb.lazyNvim.inputName + ) fixtureLock.nodes; + failures = + lib.optional + ( + (fixtureLock.nodes.${pluginsDb.lazyNvim.inputName}.locked or null) + != ownLock.nodes.${pluginsDb.lazyNvim.inputName}.locked + ) + "the fixture's lazy-nvim node differs from nvimx's own flake.lock: regenerate it (see the header of tests/checks/e2e-offline.nix), or evaluation fetches another lazy.nvim" + ++ + lib.optional + ( + lib.sort (a: b: a < b) (builtins.attrNames fixtureLock.nodes.root.inputs) + != lib.sort (a: b: a < b) (pluginNodes ++ [ pluginsDb.lazyNvim.inputName ]) + ) + "the fixture flake.lock root.inputs must be the plugins.json inputNames plus lazy-nvim, no stale or missing node" + ++ + lib.optional + ( + !lib.all ( + n: n.locked.type == "path" && !(n.locked ? narHash) && !(lib.hasPrefix "/" n.locked.path) + ) (builtins.attrValues nonLazyNodes) + ) + "every plugin node of the fixture flake.lock must be a relative path: node, or the check needs network"; + # Any git call is a failure: the shim logs it and exits 1. + gitShim = pkgs.writeShellScriptBin "git" '' + echo "git $*" >> "$GIT_LOG" + exit 1 + ''; +in +assert failures == [ ] || throw (lib.concatStringsSep "\n" failures); +pkgs.runCommand "e2e-offline" { + nativeBuildInputs = [ + pkgs.neovim-unwrapped + pkgs.jq + pkgs.git + pkgs.nix + pkgs.nixfmt + nvimxLib.lockApp + ]; + lazy = lazy-nvim; + lua = ../../lua/nvimx; + inherit fx gitShim; + inherit (env) farm wrapped; + ownLock = ../../flake.lock; + probe = ../e2e-offline-test.lua; + # Step 3 runs a real nix on a private store. Switch to `if pkgs.stdenv.isLinux then "1" else ""` + # only if the first darwin CI run shows that store cannot be used there. + runLockLoop = "1"; +} "source ${./scripts/e2e-offline.sh}" diff --git a/tests/checks/lock-app.nix b/tests/checks/lock-app.nix index f4b16b8..7600858 100644 --- a/tests/checks/lock-app.nix +++ b/tests/checks/lock-app.nix @@ -1,13 +1,13 @@ # The lock app itself, end to end (#71): what nix/lib/lock-app.nix hands its `nix` calls, what -# the EXIT trap prints, and everything after them -- no other check gets past those calls, -# since recursive nix cannot run in a build. `nix` comes from the caller's PATH, so a stub +# the EXIT trap prints, and everything after them. `nix` comes from the caller's PATH, so a stub # stands in: it answers the seed lookup (`eval`, #32) with nvimx's own lazy.nvim (not recorded # in argv, so it is not counted as a call), records argv and cwd, fails on the NVIMX_STUB_FAIL_AT-th # call (default 1) when NVIMX_STUB_FAIL is set (#81: the later call sites need a failure of their # own), can corrupt the update snapshot (NVIMX_STUB_CORRUPT_BEFORE) or wait for a pipe reader to # be gone before failing (NVIMX_STUB_WAIT_FOR), and otherwise writes a fixture -# flake.lock (the convergence pass reads it right after). That the real nix accepts `path:.` -# stays a manual check. +# flake.lock (the convergence pass reads it right after). The stub is for what a real nix cannot +# be made to do here (failure exit codes, signals); real-nix behaviour -- accepting `path:.`, +# convergence against a real lock, stale-node removal -- is checks.e2e-offline step 3. { pkgs, nvimxLib, diff --git a/tests/checks/scripts/e2e-offline.sh b/tests/checks/scripts/e2e-offline.sh new file mode 100644 index 0000000..392c195 --- /dev/null +++ b/tests/checks/scripts/e2e-offline.sh @@ -0,0 +1,125 @@ +# shellcheck shell=bash disable=SC2154 +# Body of checks.e2e-offline, sourced by the runCommand in ../e2e-offline.nix. +# $fx $lazy $lua $farm $wrapped $gitShim $ownLock $probe $runLockLoop come from that derivation's env. +export HOME=$TMPDIR +# nix refuses a store root with a symlink anywhere in its parents, and darwin's /tmp is one, +# so every path step 3 hands to nix (store, remotes, configs) lives under the resolved TMPDIR. +base=$(realpath "$TMPDIR") + +# 1. Fixture integrity (extract -> resolve -> genflake). Steps 2 and 3 are only meaningful if +# the committed lock is what the pipeline makes of this init.lua today. The per-stage goldens +# (extractor-snapshot, resolve-golden, genflake-golden) use other fixtures and launch nothing. +# No --lock / --prev: that is a first pass, so the pin stays unfrozen and nothing goes online. +sb=$base/sandbox +mkdir -p "$sb"/config "$sb"/data/nvim/lazy "$sb"/state "$sb"/cache gen +ln -s "$fx" "$sb"/config/nvim +ln -s "$lazy" "$sb"/data/nvim/lazy/lazy.nvim +# timeout + closed stdin: a config that never reaches setup would otherwise hang the build (#3). +env \ + XDG_CONFIG_HOME="$sb"/config \ + XDG_DATA_HOME="$sb"/data \ + XDG_STATE_HOME="$sb"/state \ + XDG_CACHE_HOME="$sb"/cache \ + NVIMX_LAZY_SEED="$lazy" \ + NVIMX_OUT="$sb"/raw-spec.json \ + timeout 120 nvim --headless --cmd "luafile $lua/extract.lua" "$git_log" +coreutils_bin=$(dirname "$(command -v timeout)") +# Closed stdin and a timeout, as for the extractor above: the guard against a hung headless nvim. +# `|| rc=$?` so that the failure message below is reached under errexit. +rc=0 +env \ + PATH="$gitShim/bin:$coreutils_bin" \ + GIT_LOG="$git_log" \ + FARM="$farm" \ + PROBE_OK="$base"/probe-ok \ + XDG_CONFIG_HOME="$x"/config \ + XDG_DATA_HOME="$x"/data \ + XDG_STATE_HOME="$x"/state \ + XDG_CACHE_HOME="$x"/cache \ + timeout 120 "$wrapped"/bin/nvim --headless -c "luafile $probe" &2 + exit 1 +fi +if [ -s "$git_log" ]; then + echo "git was called at runtime:" >&2 + cat "$git_log" >&2 + exit 1 +fi + +# 3. A real `nix flake lock` through nvimx-lock, on file:// remotes and a private store (the build +# sandbox has no daemon). lock-app only proves this against a stub nix. +if [ -n "$runLockLoop" ]; then + cp -r "$fx" "$base"/src + chmod -R u+w "$base"/src + for d in "$base"/src/plugins/*; do + r=$base/remotes/nvimx-e2e/$(basename "$d") + mkdir -p "$r" + cp -r "$d"/. "$r"/ + git -C "$r" init -q -b main + git -C "$r" add -A + git -C "$r" -c user.name=nvimx -c user.email=nvimx@example.com commit -q -m init + done + mkdir -p "$base"/cfg "$base"/out + # url_format is the one string that tells the two halves apart (see the fixture's init.lua). + sed "s#https://github.com/%s.git#file://$base/remotes/%s#" "$fx"/init.lua >"$base"/cfg/init.lua + grep -q "file://$base/remotes" "$base"/cfg/init.lua + # Seeded with nvimx's own lazy-nvim node: nix keeps a locked node whose `original` still + # matches and never fetches it, so the synthetic github input stays network-free. + jq '{nodes: {"lazy-nvim": .nodes["lazy-nvim"], root: {inputs: {"lazy-nvim": "lazy-nvim"}}}, root: "root", version: 7}' \ + "$ownLock" >"$base"/out/flake.lock + export NIX_CONFIG="experimental-features = nix-command flakes +store = $base/store +flake-registry =" + lock() { nvimx-lock --config "$base"/cfg --out "$base"/out 2>"$base"/lock.log; } + + # 3a. The first run freezes the pin: resolvedRef, flake.lock and flake.nix agree on one rev. + lock + grep -q '^nvimx-lock: freezing pinned plugins$' "$base"/lock.log + rev=$(git -C "$base"/remotes/nvimx-e2e/alpha.nvim rev-parse HEAD) + [ "$(jq -r '.plugins["alpha.nvim"].resolvedRef' "$base"/out/plugins.json)" = "$rev" ] + [ "$(jq -r '.nodes["alpha-nvim"].locked.rev' "$base"/out/flake.lock)" = "$rev" ] + grep -q "?rev=$rev" "$base"/out/flake.nix + + # 3b. Same plugin graph as the committed lock; only where the sources live differs. + proj='{plugins: (.plugins | map_values(del(.source, .resolvedRef))), lazyNvim: (.lazyNvim | del(.source)), localPlugins}' + [ "$(jq -S "$proj" "$base"/out/plugins.json)" = "$(jq -S "$proj" "$fx"/nvimx-lock/plugins.json)" ] + + # 3c. A second run converges: byte-identical output, and nothing left to freeze. + cp -r "$base"/out "$base"/out1 + lock + diff -r "$base"/out1 "$base"/out + if grep -q 'freezing' "$base"/lock.log; then + echo "the second run froze something again" >&2 + exit 1 + fi + + # 3d. Dropping a spec drops its node (and its dependency's) from flake.lock, via real nix. + sed -i '/nvimx-e2e\/beta.nvim/d' "$base"/cfg/init.lua + lock + for n in beta-nvim gamma-nvim; do + jq -e --arg n "$n" '(.nodes | has($n) | not) and (.nodes.root.inputs | has($n) | not)' "$base"/out/flake.lock >/dev/null + jq -e --arg n "$n" '[.plugins[].inputName] | index($n) | not' "$base"/out/plugins.json >/dev/null + if grep -q "$n" "$base"/out/flake.nix; then + echo "$n is still in flake.nix" >&2 + exit 1 + fi + done + [ "$(jq -S '.nodes["alpha-nvim"]' "$base"/out/flake.lock)" = "$(jq -S '.nodes["alpha-nvim"]' "$base"/out1/flake.lock)" ] +fi +touch "$out" diff --git a/tests/e2e-offline-test.lua b/tests/e2e-offline-test.lua new file mode 100644 index 0000000..cd868be --- /dev/null +++ b/tests/e2e-offline-test.lua @@ -0,0 +1,48 @@ +-- Driver of checks.e2e-offline step 2, run inside the wrapped nvim as +-- `nvim --headless -c "luafile "` with the user's init.lua already executed. The farm +-- is read from $FARM and the success marker is written to $PROBE_OK: an error in a -c command +-- alone does not make headless nvim exit non-zero, so the shell checks the marker too. +local function run() + local farm = assert(os.getenv("FARM"), "FARM unset") + assert(vim.v.errmsg == "", "startup raised: " .. vim.v.errmsg) + assert(vim.g.nvimx_e2e_alpha == true, "the startup plugin was not sourced") + + local plugins = require("lazy.core.config").plugins + local names = vim.tbl_keys(plugins) + table.sort(names) + assert( + vim.deep_equal(names, { "alpha.nvim", "beta.nvim", "gamma.nvim", "lazy.nvim" }), + "unexpected plugin set: " .. table.concat(names, ",") + ) + + -- beta is lazy (cmd): not loaded until :BetaHi, which also pulls gamma in as a dependency. + assert(not plugins["beta.nvim"]._.loaded, "beta.nvim loaded before :BetaHi") + vim.cmd("BetaHi") + assert(vim.g.nvimx_e2e_beta == 42, "BetaHi did not reach gamma through the dependency") + + local data = vim.fn.stdpath("data") + for name, p in pairs(plugins) do + assert(p._.loaded, name .. " is not loaded") + local real = vim.uv.fs_realpath(p.dir) + assert(real and vim.startswith(real, "/nix/store/"), name .. " is not in the store: " .. tostring(real)) + if name == "lazy.nvim" then + -- lazy's own entry is not a spec: its dir is the bootstrap symlink into the farm. + assert(real == vim.uv.fs_realpath(farm .. "/lazy.nvim"), "lazy.nvim does not come from the farm") + else + assert(p._.is_local, name .. " is not local") + assert(p.dir == farm .. "/" .. name, name .. " dir is " .. tostring(p.dir)) + assert(not vim.uv.fs_stat(data .. "/lazy/" .. name), name .. " was cloned") + end + end +end + +local ok, err = pcall(run) +if ok then + local f = assert(io.open(assert(os.getenv("PROBE_OK")), "w")) + f:write("ok\n") + f:close() + vim.cmd("qa!") +else + io.stderr:write("e2e-offline probe failed: " .. tostring(err) .. "\n") + vim.cmd("cquit 1") +end diff --git a/tests/fixtures/e2e-offline/init.lua b/tests/fixtures/e2e-offline/init.lua new file mode 100644 index 0000000..646019e --- /dev/null +++ b/tests/fixtures/e2e-offline/init.lua @@ -0,0 +1,17 @@ +-- Standard lazy bootstrap with its clone guard: under nvimx stdpath("data")/lazy/lazy.nvim is a +-- symlink into the farm, so a clone here would mean the farm was not wired up (checks.e2e-offline +-- puts a git shim on PATH that fails the check if it is ever called). +local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" +if not (vim.uv or vim.loop).fs_stat(lazypath) then + local lazyrepo = "https://github.com/folke/lazy.nvim.git" + vim.fn.system({ "git", "clone", "--filter=blob:none", lazyrepo, lazypath }) +end +vim.opt.rtp:prepend(lazypath) + +-- nvimx-e2e is a fake owner and is never fetched. url_format is lazy's default, spelled out so +-- that checks.e2e-offline can swap exactly this string for file:// remotes. Keep each top-level +-- spec on one line: the check deletes beta with a line-based sed. +require("lazy").setup({ + { "nvimx-e2e/alpha.nvim", lazy = false, pin = true }, + { "nvimx-e2e/beta.nvim", cmd = "BetaHi", dependencies = { "nvimx-e2e/gamma.nvim" } }, +}, { git = { url_format = "https://github.com/%s.git" } }) diff --git a/tests/fixtures/e2e-offline/nvimx-lock/flake.lock b/tests/fixtures/e2e-offline/nvimx-lock/flake.lock new file mode 100644 index 0000000..f041f6e --- /dev/null +++ b/tests/fixtures/e2e-offline/nvimx-lock/flake.lock @@ -0,0 +1,66 @@ +{ + "nodes": { + "alpha-nvim": { + "flake": false, + "locked": { + "path": "../plugins/alpha.nvim", + "type": "path" + }, + "original": { + "path": "../plugins/alpha.nvim", + "type": "path" + }, + "parent": [] + }, + "beta-nvim": { + "flake": false, + "locked": { + "path": "../plugins/beta.nvim", + "type": "path" + }, + "original": { + "path": "../plugins/beta.nvim", + "type": "path" + }, + "parent": [] + }, + "gamma-nvim": { + "flake": false, + "locked": { + "path": "../plugins/gamma.nvim", + "type": "path" + }, + "original": { + "path": "../plugins/gamma.nvim", + "type": "path" + }, + "parent": [] + }, + "lazy-nvim": { + "flake": false, + "locked": { + "lastModified": 1765971162, + "narHash": "sha256-5A4kducPwKb5fKX4oSUFvo898P0dqfsqqLxFaXBsbQY=", + "owner": "folke", + "repo": "lazy.nvim", + "rev": "306a05526ada86a7b30af95c5cc81ffba93fef97", + "type": "github" + }, + "original": { + "owner": "folke", + "repo": "lazy.nvim", + "type": "github" + } + }, + "root": { + "inputs": { + "alpha-nvim": "alpha-nvim", + "beta-nvim": "beta-nvim", + "gamma-nvim": "gamma-nvim", + "lazy-nvim": "lazy-nvim" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/tests/fixtures/e2e-offline/nvimx-lock/flake.nix b/tests/fixtures/e2e-offline/nvimx-lock/flake.nix new file mode 100644 index 0000000..a36ffe7 --- /dev/null +++ b/tests/fixtures/e2e-offline/nvimx-lock/flake.nix @@ -0,0 +1,22 @@ +# This file is generated by nvimx. Do not edit by hand. +{ + inputs = { + lazy-nvim = { + url = "github:folke/lazy.nvim"; + flake = false; + }; + alpha-nvim = { + url = "github:nvimx-e2e/alpha.nvim"; + flake = false; + }; + beta-nvim = { + url = "github:nvimx-e2e/beta.nvim"; + flake = false; + }; + gamma-nvim = { + url = "github:nvimx-e2e/gamma.nvim"; + flake = false; + }; + }; + outputs = _: { }; +} diff --git a/tests/fixtures/e2e-offline/nvimx-lock/plugins.json b/tests/fixtures/e2e-offline/nvimx-lock/plugins.json new file mode 100644 index 0000000..5137950 --- /dev/null +++ b/tests/fixtures/e2e-offline/nvimx-lock/plugins.json @@ -0,0 +1,72 @@ +{ + "lazyNvim": { + "inputName": "lazy-nvim", + "source": { + "owner": "folke", + "repo": "lazy.nvim", + "type": "github" + }, + "synthetic": true + }, + "localPlugins": {}, + "plugins": { + "alpha.nvim": { + "branch": null, + "build": { + "kind": "none" + }, + "commit": null, + "dependencies": [], + "inputName": "alpha-nvim", + "pin": true, + "resolvedRef": null, + "source": { + "owner": "nvimx-e2e", + "repo": "alpha.nvim", + "type": "github" + }, + "tag": null, + "version": null + }, + "beta.nvim": { + "branch": null, + "build": { + "kind": "none" + }, + "commit": null, + "dependencies": [ + "gamma.nvim" + ], + "inputName": "beta-nvim", + "pin": null, + "resolvedRef": null, + "source": { + "owner": "nvimx-e2e", + "repo": "beta.nvim", + "type": "github" + }, + "tag": null, + "version": null + }, + "gamma.nvim": { + "branch": null, + "build": { + "kind": "none" + }, + "commit": null, + "dependencies": [], + "inputName": "gamma-nvim", + "pin": null, + "resolvedRef": null, + "source": { + "owner": "nvimx-e2e", + "repo": "gamma.nvim", + "type": "github" + }, + "tag": null, + "version": null + } + }, + "schemaVersion": 1, + "warnings": [] +} diff --git a/tests/fixtures/e2e-offline/plugins/alpha.nvim/plugin/alpha.lua b/tests/fixtures/e2e-offline/plugins/alpha.nvim/plugin/alpha.lua new file mode 100644 index 0000000..47162ae --- /dev/null +++ b/tests/fixtures/e2e-offline/plugins/alpha.nvim/plugin/alpha.lua @@ -0,0 +1 @@ +vim.g.nvimx_e2e_alpha = true diff --git a/tests/fixtures/e2e-offline/plugins/beta.nvim/plugin/beta.lua b/tests/fixtures/e2e-offline/plugins/beta.nvim/plugin/beta.lua new file mode 100644 index 0000000..c20b7b1 --- /dev/null +++ b/tests/fixtures/e2e-offline/plugins/beta.nvim/plugin/beta.lua @@ -0,0 +1,3 @@ +vim.api.nvim_create_user_command("BetaHi", function() + vim.g.nvimx_e2e_beta = require("gamma").value +end, {}) diff --git a/tests/fixtures/e2e-offline/plugins/gamma.nvim/lua/gamma/init.lua b/tests/fixtures/e2e-offline/plugins/gamma.nvim/lua/gamma/init.lua new file mode 100644 index 0000000..df2d7d6 --- /dev/null +++ b/tests/fixtures/e2e-offline/plugins/gamma.nvim/lua/gamma/init.lua @@ -0,0 +1 @@ +return { value = 42 } From 0db59d3aed42dae65e7703da092ef17804c2de16 Mon Sep 17 00:00:00 2001 From: myuron Date: Sat, 17 Oct 2026 13:14:51 +0900 Subject: [PATCH 3/4] test(ci): build packages.demo in flake check Co-Authored-By: Claude Opus 5.5 --- tests/checks/default.nix | 1 + tests/checks/demo.nix | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 tests/checks/demo.nix diff --git a/tests/checks/default.nix b/tests/checks/default.nix index 7d749d9..591e7f4 100644 --- a/tests/checks/default.nix +++ b/tests/checks/default.nix @@ -56,4 +56,5 @@ pkgs.lib.genAttrs [ "dev-patterns" "extractor-dev-opts" "e2e-offline" + "demo" ] (name: import (./. + "/${name}.nix") checkArgs) diff --git a/tests/checks/demo.nix b/tests/checks/demo.nix new file mode 100644 index 0000000..6eab671 --- /dev/null +++ b/tests/checks/demo.nix @@ -0,0 +1,4 @@ +# `nix flake check` evaluates packages.* but never builds them; demo is the dogfooding artifact +# (nvimx's own env around tests/fixtures/basic-config), so build it here, where CI already runs. +{ pkgs, self, ... }: +self.packages.${pkgs.stdenv.hostPlatform.system}.demo From 200af39dfff42121600415955c6cfe6da945045f Mon Sep 17 00:00:00 2001 From: myuron Date: Sat, 17 Oct 2026 13:42:09 +0900 Subject: [PATCH 4/4] test(ci): serve lazy.nvim from a file remote in e2e-offline step 3 Co-Authored-By: Claude Opus 5.5 --- tests/checks/e2e-offline.nix | 1 - tests/checks/scripts/e2e-offline.sh | 26 +++++++++++++++++++------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/checks/e2e-offline.nix b/tests/checks/e2e-offline.nix index 30f7b8f..6b86418 100644 --- a/tests/checks/e2e-offline.nix +++ b/tests/checks/e2e-offline.nix @@ -84,7 +84,6 @@ pkgs.runCommand "e2e-offline" { lua = ../../lua/nvimx; inherit fx gitShim; inherit (env) farm wrapped; - ownLock = ../../flake.lock; probe = ../e2e-offline-test.lua; # Step 3 runs a real nix on a private store. Switch to `if pkgs.stdenv.isLinux then "1" else ""` # only if the first darwin CI run shows that store cannot be used there. diff --git a/tests/checks/scripts/e2e-offline.sh b/tests/checks/scripts/e2e-offline.sh index 392c195..bf336ce 100644 --- a/tests/checks/scripts/e2e-offline.sh +++ b/tests/checks/scripts/e2e-offline.sh @@ -1,6 +1,6 @@ # shellcheck shell=bash disable=SC2154 # Body of checks.e2e-offline, sourced by the runCommand in ../e2e-offline.nix. -# $fx $lazy $lua $farm $wrapped $gitShim $ownLock $probe $runLockLoop come from that derivation's env. +# $fx $lazy $lua $farm $wrapped $gitShim $probe $runLockLoop come from that derivation's env. export HOME=$TMPDIR # nix refuses a store root with a symlink anywhere in its parents, and darwin's /tmp is one, # so every path step 3 hands to nix (store, remotes, configs) lives under the resolved TMPDIR. @@ -79,14 +79,26 @@ if [ -n "$runLockLoop" ]; then # url_format is the one string that tells the two halves apart (see the fixture's init.lua). sed "s#https://github.com/%s.git#file://$base/remotes/%s#" "$fx"/init.lua >"$base"/cfg/init.lua grep -q "file://$base/remotes" "$base"/cfg/init.lua - # Seeded with nvimx's own lazy-nvim node: nix keeps a locked node whose `original` still - # matches and never fetches it, so the synthetic github input stays network-free. - jq '{nodes: {"lazy-nvim": .nodes["lazy-nvim"], root: {inputs: {"lazy-nvim": "lazy-nvim"}}}, root: "root", version: 7}' \ - "$ownLock" >"$base"/out/flake.lock + # lazy.nvim is a file:// remote too (spec added to this copy of init.lua only): the lock's + # `nix eval` seed lookup (#32) fetches the seeded github node, which has no network here. The + # copy must stay byte-identical to $lazy: nix eval on the private store returns a logical + # /nix/store/...-source path that only exists because it equals the $lazy build input. + r=$base/remotes/folke/lazy.nvim + mkdir -p "$r" + cp -r "$lazy"/. "$r"/ + chmod -R u+w "$r" + git -C "$r" init -q -b main + git -C "$r" add -A + git -C "$r" -c user.name=nvimx -c user.email=nvimx@example.com commit -q -m init + sed -i 's#^require("lazy").setup({$#&\n { "folke/lazy.nvim" },#' "$base"/cfg/init.lua + grep -q 'folke/lazy.nvim" }' "$base"/cfg/init.lua export NIX_CONFIG="experimental-features = nix-command flakes store = $base/store flake-registry =" - lock() { nvimx-lock --config "$base"/cfg --out "$base"/out 2>"$base"/lock.log; } + lock() { nvimx-lock --config "$base"/cfg --out "$base"/out 2>"$base"/lock.log || { + cat "$base"/lock.log >&2 + return 1 + }; } # 3a. The first run freezes the pin: resolvedRef, flake.lock and flake.nix agree on one rev. lock @@ -97,7 +109,7 @@ flake-registry =" grep -q "?rev=$rev" "$base"/out/flake.nix # 3b. Same plugin graph as the committed lock; only where the sources live differs. - proj='{plugins: (.plugins | map_values(del(.source, .resolvedRef))), lazyNvim: (.lazyNvim | del(.source)), localPlugins}' + proj='{plugins: (.plugins | map_values(del(.source, .resolvedRef))), lazyNvim: .lazyNvim.inputName, localPlugins}' [ "$(jq -S "$proj" "$base"/out/plugins.json)" = "$(jq -S "$proj" "$fx"/nvimx-lock/plugins.json)" ] # 3c. A second run converges: byte-identical output, and nothing left to freeze.