From c02b3a14064f6d3a185782a681b9af0cf0fdb278 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Sun, 26 Apr 2026 10:03:24 +0000 Subject: [PATCH] test(core): add headless regression coverage --- .github/workflows/ci.yml | 9 +++ AGENTS.md | 10 +++- Makefile | 11 +++- lua/glaze/init.lua | 17 ++++-- tests/run.lua | 117 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 tests/run.lua diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b8de1a..7720110 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,12 @@ jobs: uses: lunarmodules/luacheck@v1 with: args: lua/ + + - name: Install Neovim + run: | + sudo add-apt-repository ppa:neovim-ppa/unstable -y + sudo apt-get update + sudo apt-get install -y neovim + + - name: Run headless tests + run: make test diff --git a/AGENTS.md b/AGENTS.md index 48bb5d0..4dfad02 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,15 @@ Plugins register binaries via `require("glaze").register(name, url, opts)`. Regi ## Testing -**No automated tests exist.** Manual testing workflow: +Automated checks: + +```bash +make format # apply StyLua +make lint # run luacheck +make test # headless Neovim regression tests +``` + +Manual testing workflow: ```lua -- In Neovim: diff --git a/Makefile b/Makefile index 03d9b8e..3a76ea1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,16 @@ -.PHONY: demo clean +.PHONY: demo clean format lint test PLUGIN_PATH := $(shell pwd) +format: + stylua . + +lint: + luacheck lua/ + +test: + nvim --headless -u NONE -c "lua dofile('tests/run.lua')" -c qa + demo: @echo "Recording demo.gif..." PLUGIN_PATH=$(PLUGIN_PATH) vhs docs/demo.tape diff --git a/lua/glaze/init.lua b/lua/glaze/init.lua index 1dde7a9..e37b3a1 100644 --- a/lua/glaze/init.lua +++ b/lua/glaze/init.lua @@ -91,6 +91,15 @@ M._auto_install_queue = {} ---@type number? Timer for batched auto-install M._auto_install_timer = nil +---@param name string +---@param fn function +---@param opts table +local function ensure_user_command(name, fn, opts) + if vim.fn.exists(":" .. name) ~= 2 then + vim.api.nvim_create_user_command(name, fn, opts) + end +end + ---@param opts? GlazeConfig function M.setup(opts) M.config = vim.tbl_deep_extend("force", M.config, opts or {}) @@ -102,11 +111,11 @@ function M.setup(opts) end -- Create commands - vim.api.nvim_create_user_command("Glaze", function() + ensure_user_command("Glaze", function() require("glaze.view").open() end, { desc = "Open Glaze UI" }) - vim.api.nvim_create_user_command("GlazeUpdate", function(cmd) + ensure_user_command("GlazeUpdate", function(cmd) if cmd.args and cmd.args ~= "" then require("glaze.runner").update({ cmd.args }) else @@ -120,7 +129,7 @@ function M.setup(opts) end, }) - vim.api.nvim_create_user_command("GlazeInstall", function(cmd) + ensure_user_command("GlazeInstall", function(cmd) if cmd.args and cmd.args ~= "" then require("glaze.runner").install({ cmd.args }) else @@ -134,7 +143,7 @@ function M.setup(opts) end, }) - vim.api.nvim_create_user_command("GlazeCheck", function() + ensure_user_command("GlazeCheck", function() require("glaze.checker").check() end, { desc = "Check for binary updates" }) diff --git a/tests/run.lua b/tests/run.lua new file mode 100644 index 0000000..36ad265 --- /dev/null +++ b/tests/run.lua @@ -0,0 +1,117 @@ +package.path = table.concat({ + "./lua/?.lua", + "./lua/?/init.lua", + package.path, +}, ";") + +local function assert_eq(actual, expected, message) + if actual ~= expected then + error((message or "assertion failed") .. ": expected " .. vim.inspect(expected) .. ", got " .. vim.inspect(actual)) + end +end + +local function assert_truthy(value, message) + if not value then + error(message or "expected truthy value") + end +end + +local results = {} +local function test(name, fn) + table.insert(results, { name = name, fn = fn }) +end + +local function reset_glaze() + for _, module in ipairs({ + "glaze", + "glaze.init", + "glaze.runner", + "glaze.checker", + "glaze.view", + "glaze.float", + "glaze.text", + "glaze.colors", + "glaze.health", + }) do + package.loaded[module] = nil + end + + for _, command in ipairs({ "Glaze", "GlazeUpdate", "GlazeInstall", "GlazeCheck" }) do + pcall(vim.api.nvim_del_user_command, command) + end + + return require("glaze") +end + +test("setup is idempotent and preserves updated config", function() + local glaze = reset_glaze() + + glaze.setup({ auto_check = { enabled = false }, concurrency = 2 }) + glaze.setup({ auto_check = { enabled = false }, concurrency = 7 }) + + assert_eq(glaze.config.concurrency, 7, "second setup should update config") + assert_truthy(vim.fn.exists(":Glaze") == 2, "Glaze command should exist") + assert_truthy(vim.fn.exists(":GlazeUpdate") == 2, "GlazeUpdate command should exist") + assert_truthy(vim.fn.exists(":GlazeInstall") == 2, "GlazeInstall command should exist") + assert_truthy(vim.fn.exists(":GlazeCheck") == 2, "GlazeCheck command should exist") +end) + +test("register batches auto-install requests into one install call", function() + local glaze = reset_glaze() + local runner = require("glaze.runner") + local original_install = runner.install + local install_calls = {} + + runner.install = function(names, opts) + table.insert(install_calls, { + names = vim.deepcopy(names), + opts = vim.deepcopy(opts), + }) + end + + glaze.setup({ + auto_check = { enabled = false }, + auto_install = { enabled = true, silent = true }, + }) + + glaze.is_installed = function() + return false + end + + glaze.register("freeze", "github.com/charmbracelet/freeze", { plugin = "freeze.nvim" }) + glaze.register("glow", "github.com/charmbracelet/glow", { plugin = "glow.nvim" }) + vim.wait(350) + + assert_eq(#install_calls, 1, "auto-install should batch into one runner call") + table.sort(install_calls[1].names) + assert_eq(install_calls[1].names[1], "freeze") + assert_eq(install_calls[1].names[2], "glow") + assert_eq(install_calls[1].opts.silent, true, "batched install should preserve silent option") + + runner.install = original_install +end) + +test("register merges duplicate plugin registrations without duplicates", function() + local glaze = reset_glaze() + + glaze.setup({ auto_check = { enabled = false }, auto_install = { enabled = false } }) + glaze.register("freeze", "github.com/charmbracelet/freeze", { plugin = "freeze.nvim" }) + glaze.register("freeze", "github.com/charmbracelet/freeze", { plugin = "freeze.nvim" }) + glaze.register("freeze", "github.com/charmbracelet/freeze", { plugin = "blast.nvim" }) + + local binary = glaze.binaries().freeze + assert_truthy(binary ~= nil, "binary should be registered") + table.sort(binary.plugins) + assert_eq(#binary.plugins, 2, "plugins list should stay deduplicated") + assert_eq(binary.plugins[1], "blast.nvim") + assert_eq(binary.plugins[2], "freeze.nvim") +end) + +for _, case in ipairs(results) do + local ok, err = pcall(case.fn) + if not ok then + io.stderr:write("FAIL: " .. case.name .. "\n" .. tostring(err) .. "\n") + os.exit(1) + end + print("PASS: " .. case.name) +end