From 35ae1e76da8ae20fa1ad5b62c11ebb45cd424243 Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Fri, 26 Jun 2026 12:45:13 -0500 Subject: [PATCH 1/4] refactor: clean up nvim plugin specs --- home/dot_config/nvim/lua/config/options.lua | 1 + home/dot_config/nvim/lua/plugins/example.lua | 197 ------------------ home/dot_config/nvim/lua/plugins/opencode.lua | 3 - 3 files changed, 1 insertion(+), 200 deletions(-) delete mode 100644 home/dot_config/nvim/lua/plugins/example.lua diff --git a/home/dot_config/nvim/lua/config/options.lua b/home/dot_config/nvim/lua/config/options.lua index c45e5c6..31e385c 100644 --- a/home/dot_config/nvim/lua/config/options.lua +++ b/home/dot_config/nvim/lua/config/options.lua @@ -3,3 +3,4 @@ -- Add any additional options here vim.opt.clipboard = "unnamedplus" +vim.o.autoread = true diff --git a/home/dot_config/nvim/lua/plugins/example.lua b/home/dot_config/nvim/lua/plugins/example.lua deleted file mode 100644 index 17f53d6..0000000 --- a/home/dot_config/nvim/lua/plugins/example.lua +++ /dev/null @@ -1,197 +0,0 @@ --- since this is just an example spec, don't actually load anything here and return an empty spec --- stylua: ignore -if true then return {} end - --- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim --- --- In your plugin files, you can: --- * add extra plugins --- * disable/enabled LazyVim plugins --- * override the configuration of LazyVim plugins -return { - -- add gruvbox - { "ellisonleao/gruvbox.nvim" }, - - -- Configure LazyVim to load gruvbox - { - "LazyVim/LazyVim", - opts = { - colorscheme = "gruvbox", - }, - }, - - -- change trouble config - { - "folke/trouble.nvim", - -- opts will be merged with the parent spec - opts = { use_diagnostic_signs = true }, - }, - - -- disable trouble - { "folke/trouble.nvim", enabled = false }, - - -- override nvim-cmp and add cmp-emoji - { - "hrsh7th/nvim-cmp", - dependencies = { "hrsh7th/cmp-emoji" }, - ---@param opts cmp.ConfigSchema - opts = function(_, opts) - table.insert(opts.sources, { name = "emoji" }) - end, - }, - - -- change some telescope options and a keymap to browse plugin files - { - "nvim-telescope/telescope.nvim", - keys = { - -- add a keymap to browse plugin files - -- stylua: ignore - { - "fp", - function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end, - desc = "Find Plugin File", - }, - }, - -- change some options - opts = { - defaults = { - layout_strategy = "horizontal", - layout_config = { prompt_position = "top" }, - sorting_strategy = "ascending", - winblend = 0, - }, - }, - }, - - -- add pyright to lspconfig - { - "neovim/nvim-lspconfig", - ---@class PluginLspOpts - opts = { - ---@type lspconfig.options - servers = { - -- pyright will be automatically installed with mason and loaded with lspconfig - pyright = {}, - }, - }, - }, - - -- add tsserver and setup with typescript.nvim instead of lspconfig - { - "neovim/nvim-lspconfig", - dependencies = { - "jose-elias-alvarez/typescript.nvim", - init = function() - require("lazyvim.util").lsp.on_attach(function(_, buffer) - -- stylua: ignore - vim.keymap.set( "n", "co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" }) - vim.keymap.set("n", "cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer }) - end) - end, - }, - ---@class PluginLspOpts - opts = { - ---@type lspconfig.options - servers = { - -- tsserver will be automatically installed with mason and loaded with lspconfig - tsserver = {}, - }, - -- you can do any additional lsp server setup here - -- return true if you don't want this server to be setup with lspconfig - ---@type table - setup = { - -- example to setup with typescript.nvim - tsserver = function(_, opts) - require("typescript").setup({ server = opts }) - return true - end, - -- Specify * to use this function as a fallback for any server - -- ["*"] = function(server, opts) end, - }, - }, - }, - - -- for typescript, LazyVim also includes extra specs to properly setup lspconfig, - -- treesitter, mason and typescript.nvim. So instead of the above, you can use: - { import = "lazyvim.plugins.extras.lang.typescript" }, - - -- add more treesitter parsers - { - "nvim-treesitter/nvim-treesitter", - opts = { - ensure_installed = { - "bash", - "html", - "javascript", - "json", - "lua", - "markdown", - "markdown_inline", - "python", - "query", - "regex", - "tsx", - "typescript", - "vim", - "yaml", - }, - }, - }, - - -- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above - -- would overwrite `ensure_installed` with the new value. - -- If you'd rather extend the default config, use the code below instead: - { - "nvim-treesitter/nvim-treesitter", - opts = function(_, opts) - -- add tsx and treesitter - vim.list_extend(opts.ensure_installed, { - "tsx", - "typescript", - }) - end, - }, - - -- the opts function can also be used to change the default opts: - { - "nvim-lualine/lualine.nvim", - event = "VeryLazy", - opts = function(_, opts) - table.insert(opts.sections.lualine_x, { - function() - return "😄" - end, - }) - end, - }, - - -- or you can return new options to override all the defaults - { - "nvim-lualine/lualine.nvim", - event = "VeryLazy", - opts = function() - return { - --[[add your custom lualine config here]] - } - end, - }, - - -- use mini.starter instead of alpha - { import = "lazyvim.plugins.extras.ui.mini-starter" }, - - -- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc - { import = "lazyvim.plugins.extras.lang.json" }, - - -- add any tools you want to have installed below - { - "williamboman/mason.nvim", - opts = { - ensure_installed = { - "stylua", - "shellcheck", - "shfmt", - "flake8", - }, - }, - }, -} diff --git a/home/dot_config/nvim/lua/plugins/opencode.lua b/home/dot_config/nvim/lua/plugins/opencode.lua index 10180a9..343df9f 100644 --- a/home/dot_config/nvim/lua/plugins/opencode.lua +++ b/home/dot_config/nvim/lua/plugins/opencode.lua @@ -2,7 +2,6 @@ return { { "nickjvandyke/opencode.nvim", version = "*", - lazy = false, dependencies = { { "folke/snacks.nvim", @@ -70,8 +69,6 @@ return { server = false, }, } - - vim.o.autoread = true end, }, } From 4514025d2b6aa0b9d235fdbb7714c1340d36452c Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Fri, 26 Jun 2026 12:46:06 -0500 Subject: [PATCH 2/4] fix: quote template values and scope mise trust --- .../.chezmoitemplates/apm/mcp-server.yml.tmpl | 6 ++-- home/dot_apm/apm.yml.tmpl | 4 +-- home/dot_config/git/config.tmpl | 4 +-- home/dot_config/mise/config.toml.tmpl | 2 +- tests/template/apm-config.bats | 34 +++++++++---------- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/home/.chezmoitemplates/apm/mcp-server.yml.tmpl b/home/.chezmoitemplates/apm/mcp-server.yml.tmpl index 860e641..5b9df41 100644 --- a/home/.chezmoitemplates/apm/mcp-server.yml.tmpl +++ b/home/.chezmoitemplates/apm/mcp-server.yml.tmpl @@ -1,8 +1,8 @@ -- name: {{ index . "name" }} +- name: "{{ index . "name" }}" registry: {{ index . "registry" }} transport: {{ index . "transport" }} {{- if hasKey . "url" }} - url: {{ index . "url" }} + url: "{{ index . "url" }}" {{- end }} {{- if hasKey . "headers" }} headers: @@ -11,7 +11,7 @@ {{- end }} {{- end }} {{- if hasKey . "command" }} - command: {{ index . "command" }} + command: "{{ index . "command" }}" {{- end }} {{- if hasKey . "args" }} args: diff --git a/home/dot_apm/apm.yml.tmpl b/home/dot_apm/apm.yml.tmpl index d178edc..1b31ece 100644 --- a/home/dot_apm/apm.yml.tmpl +++ b/home/dot_apm/apm.yml.tmpl @@ -20,11 +20,11 @@ dependencies: {{- with .skills }} skills: {{- range . }} - - {{ . }} + - "{{ . }}" {{- end }} {{- end }} {{- else }} - - {{ . }} + - "{{ . }}" {{- end }} {{ end }} {{ end -}} diff --git a/home/dot_config/git/config.tmpl b/home/dot_config/git/config.tmpl index 4967678..6e9a93f 100644 --- a/home/dot_config/git/config.tmpl +++ b/home/dot_config/git/config.tmpl @@ -1,6 +1,6 @@ [user] - name = {{ .git.name }} - email = {{ .git.email }} + name = "{{ .git.name }}" + email = "{{ .git.email }}" [core] editor = code --wait pager = delta diff --git a/home/dot_config/mise/config.toml.tmpl b/home/dot_config/mise/config.toml.tmpl index 88b2267..838a67f 100644 --- a/home/dot_config/mise/config.toml.tmpl +++ b/home/dot_config/mise/config.toml.tmpl @@ -9,4 +9,4 @@ [settings] idiomatic_version_file_enable_tools = ["node", "python", "pnpm"] -trusted_config_paths = ["/"] +trusted_config_paths = ["~"] diff --git a/tests/template/apm-config.bats b/tests/template/apm-config.bats index 6318249..439579e 100644 --- a/tests/template/apm-config.bats +++ b/tests/template/apm-config.bats @@ -19,9 +19,9 @@ render_apm_template() { run render_apm_template "$PERSONAL_DATA" assert_success - assert_line ' - name: grep' + assert_line ' - name: "grep"' assert_line ' registry: false' - assert_line ' url: https://mcp.grep.app' + assert_line ' url: "https://mcp.grep.app"' refute_line --regexp 'name: figma' refute_line --regexp 'name: jira' } @@ -41,15 +41,15 @@ render_apm_template() { run render_apm_template "$PERSONAL_DATA" assert_success - assert_line --partial ' - obra/superpowers' - assert_line --partial ' - JuliusBrussee/caveman' - assert_line --partial ' - anthropics/claude-plugins-official/plugins/skill-creator' - assert_line --partial ' - schpet/linear-cli' - assert_line --partial ' - tavily-ai/skills' + assert_line --partial ' - "obra/superpowers' + assert_line --partial ' - "JuliusBrussee/caveman' + assert_line --partial ' - "anthropics/claude-plugins-official/plugins/skill-creator' + assert_line --partial ' - "schpet/linear-cli' + assert_line --partial ' - "tavily-ai/skills' assert_line --partial ' - git: JuliusBrussee/skills' assert_line ' skills:' - assert_line ' - grill-me' - assert_line ' - junior-to-senior' + assert_line ' - "grill-me"' + assert_line ' - "junior-to-senior"' refute_line --partial 'interface-kit' refute_line --partial 'loop-factory' refute_line --partial 'context-canary' @@ -59,11 +59,11 @@ render_apm_template() { run render_apm_template "$WORK_DATA" assert_success - assert_line ' - name: grep' - assert_line ' - name: figma' - assert_line ' url: https://mcp.figma.com/mcp' - assert_line ' - name: jira' - assert_line ' url: https://mcp.atlassian.com/v1/mcp' + assert_line ' - name: "grep"' + assert_line ' - name: "figma"' + assert_line ' url: "https://mcp.figma.com/mcp"' + assert_line ' - name: "jira"' + assert_line ' url: "https://mcp.atlassian.com/v1/mcp"' } @test "work APM config renders work target" { @@ -81,9 +81,9 @@ render_apm_template() { run render_apm_template "$WORK_DATA" assert_success - assert_line --partial ' - obra/superpowers' - assert_line --partial ' - JuliusBrussee/caveman' - assert_line --partial ' - anthropics/claude-plugins-official/plugins/skill-creator' + assert_line --partial ' - "obra/superpowers' + assert_line --partial ' - "JuliusBrussee/caveman' + assert_line --partial ' - "anthropics/claude-plugins-official/plugins/skill-creator' refute_line --partial ' - schpet/linear-cli' refute_line --partial ' - tavily-ai/skills' refute_line --partial 'JuliusBrussee/skills' From 242345b25e647bcd036929230e3fa93281cdd1bc Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Fri, 26 Jun 2026 12:59:45 -0500 Subject: [PATCH 3/4] fix: drop leaked nvim files from template PR --- home/dot_config/nvim/lua/config/options.lua | 1 - home/dot_config/nvim/lua/plugins/example.lua | 197 ++++++++++++++++++ home/dot_config/nvim/lua/plugins/opencode.lua | 3 + 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 home/dot_config/nvim/lua/plugins/example.lua diff --git a/home/dot_config/nvim/lua/config/options.lua b/home/dot_config/nvim/lua/config/options.lua index 31e385c..c45e5c6 100644 --- a/home/dot_config/nvim/lua/config/options.lua +++ b/home/dot_config/nvim/lua/config/options.lua @@ -3,4 +3,3 @@ -- Add any additional options here vim.opt.clipboard = "unnamedplus" -vim.o.autoread = true diff --git a/home/dot_config/nvim/lua/plugins/example.lua b/home/dot_config/nvim/lua/plugins/example.lua new file mode 100644 index 0000000..17f53d6 --- /dev/null +++ b/home/dot_config/nvim/lua/plugins/example.lua @@ -0,0 +1,197 @@ +-- since this is just an example spec, don't actually load anything here and return an empty spec +-- stylua: ignore +if true then return {} end + +-- every spec file under the "plugins" directory will be loaded automatically by lazy.nvim +-- +-- In your plugin files, you can: +-- * add extra plugins +-- * disable/enabled LazyVim plugins +-- * override the configuration of LazyVim plugins +return { + -- add gruvbox + { "ellisonleao/gruvbox.nvim" }, + + -- Configure LazyVim to load gruvbox + { + "LazyVim/LazyVim", + opts = { + colorscheme = "gruvbox", + }, + }, + + -- change trouble config + { + "folke/trouble.nvim", + -- opts will be merged with the parent spec + opts = { use_diagnostic_signs = true }, + }, + + -- disable trouble + { "folke/trouble.nvim", enabled = false }, + + -- override nvim-cmp and add cmp-emoji + { + "hrsh7th/nvim-cmp", + dependencies = { "hrsh7th/cmp-emoji" }, + ---@param opts cmp.ConfigSchema + opts = function(_, opts) + table.insert(opts.sources, { name = "emoji" }) + end, + }, + + -- change some telescope options and a keymap to browse plugin files + { + "nvim-telescope/telescope.nvim", + keys = { + -- add a keymap to browse plugin files + -- stylua: ignore + { + "fp", + function() require("telescope.builtin").find_files({ cwd = require("lazy.core.config").options.root }) end, + desc = "Find Plugin File", + }, + }, + -- change some options + opts = { + defaults = { + layout_strategy = "horizontal", + layout_config = { prompt_position = "top" }, + sorting_strategy = "ascending", + winblend = 0, + }, + }, + }, + + -- add pyright to lspconfig + { + "neovim/nvim-lspconfig", + ---@class PluginLspOpts + opts = { + ---@type lspconfig.options + servers = { + -- pyright will be automatically installed with mason and loaded with lspconfig + pyright = {}, + }, + }, + }, + + -- add tsserver and setup with typescript.nvim instead of lspconfig + { + "neovim/nvim-lspconfig", + dependencies = { + "jose-elias-alvarez/typescript.nvim", + init = function() + require("lazyvim.util").lsp.on_attach(function(_, buffer) + -- stylua: ignore + vim.keymap.set( "n", "co", "TypescriptOrganizeImports", { buffer = buffer, desc = "Organize Imports" }) + vim.keymap.set("n", "cR", "TypescriptRenameFile", { desc = "Rename File", buffer = buffer }) + end) + end, + }, + ---@class PluginLspOpts + opts = { + ---@type lspconfig.options + servers = { + -- tsserver will be automatically installed with mason and loaded with lspconfig + tsserver = {}, + }, + -- you can do any additional lsp server setup here + -- return true if you don't want this server to be setup with lspconfig + ---@type table + setup = { + -- example to setup with typescript.nvim + tsserver = function(_, opts) + require("typescript").setup({ server = opts }) + return true + end, + -- Specify * to use this function as a fallback for any server + -- ["*"] = function(server, opts) end, + }, + }, + }, + + -- for typescript, LazyVim also includes extra specs to properly setup lspconfig, + -- treesitter, mason and typescript.nvim. So instead of the above, you can use: + { import = "lazyvim.plugins.extras.lang.typescript" }, + + -- add more treesitter parsers + { + "nvim-treesitter/nvim-treesitter", + opts = { + ensure_installed = { + "bash", + "html", + "javascript", + "json", + "lua", + "markdown", + "markdown_inline", + "python", + "query", + "regex", + "tsx", + "typescript", + "vim", + "yaml", + }, + }, + }, + + -- since `vim.tbl_deep_extend`, can only merge tables and not lists, the code above + -- would overwrite `ensure_installed` with the new value. + -- If you'd rather extend the default config, use the code below instead: + { + "nvim-treesitter/nvim-treesitter", + opts = function(_, opts) + -- add tsx and treesitter + vim.list_extend(opts.ensure_installed, { + "tsx", + "typescript", + }) + end, + }, + + -- the opts function can also be used to change the default opts: + { + "nvim-lualine/lualine.nvim", + event = "VeryLazy", + opts = function(_, opts) + table.insert(opts.sections.lualine_x, { + function() + return "😄" + end, + }) + end, + }, + + -- or you can return new options to override all the defaults + { + "nvim-lualine/lualine.nvim", + event = "VeryLazy", + opts = function() + return { + --[[add your custom lualine config here]] + } + end, + }, + + -- use mini.starter instead of alpha + { import = "lazyvim.plugins.extras.ui.mini-starter" }, + + -- add jsonls and schemastore packages, and setup treesitter for json, json5 and jsonc + { import = "lazyvim.plugins.extras.lang.json" }, + + -- add any tools you want to have installed below + { + "williamboman/mason.nvim", + opts = { + ensure_installed = { + "stylua", + "shellcheck", + "shfmt", + "flake8", + }, + }, + }, +} diff --git a/home/dot_config/nvim/lua/plugins/opencode.lua b/home/dot_config/nvim/lua/plugins/opencode.lua index 343df9f..10180a9 100644 --- a/home/dot_config/nvim/lua/plugins/opencode.lua +++ b/home/dot_config/nvim/lua/plugins/opencode.lua @@ -2,6 +2,7 @@ return { { "nickjvandyke/opencode.nvim", version = "*", + lazy = false, dependencies = { { "folke/snacks.nvim", @@ -69,6 +70,8 @@ return { server = false, }, } + + vim.o.autoread = true end, }, } From 6542e63c86d05d78907baa4b71d9ff4402ebf903 Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Fri, 26 Jun 2026 13:37:24 -0500 Subject: [PATCH 4/4] test: close bats coverage and helper gaps Add refresh-apm-locks and statusline git tests, share template render helpers, align write-chezmoi-config to BATS_TEST_TMPDIR. --- tests/template/agent-instructions.bats | 10 +-- tests/template/apm-config.bats | 22 ++--- tests/template/apm-lockfile.bats | 10 +-- tests/template/chezmoi-helpers.bats | 27 ++---- tests/template/darwin-install-scripts.bats | 33 +++----- tests/template/mise-config.bats | 22 ++--- tests/test_helpers/templates.bash | 33 ++++++++ tests/unit/scripts/refresh-apm-locks.bats | 99 ++++++++++++++++++++++ tests/unit/statusline-git.bats | 46 ++++++++++ tests/unit/write-chezmoi-config.bats | 8 +- 10 files changed, 221 insertions(+), 89 deletions(-) create mode 100644 tests/test_helpers/templates.bash create mode 100644 tests/unit/scripts/refresh-apm-locks.bats create mode 100644 tests/unit/statusline-git.bats diff --git a/tests/template/agent-instructions.bats b/tests/template/agent-instructions.bats index 5548539..cf6ad50 100644 --- a/tests/template/agent-instructions.bats +++ b/tests/template/agent-instructions.bats @@ -3,20 +3,20 @@ # @brief Template rendering tests for shared agent instruction markdown. load '../test_helpers/load.bash' +load '../test_helpers/templates.bash' -SOURCE_DIR="$DOTFILES_ROOT/home" CLAUDE_TEMPLATE="$DOTFILES_ROOT/home/dot_claude/CLAUDE.md.tmpl" OPENCODE_TEMPLATE="$DOTFILES_ROOT/home/dot_config/opencode/AGENTS.md.tmpl" @test "agent instruction templates render shared markdown" { - run mise exec -- chezmoi execute-template --source "$SOURCE_DIR" <"$CLAUDE_TEMPLATE" + run render_chezmoi_template "$CLAUDE_TEMPLATE" assert_success assert_line --index 0 '# Claude Code Settings' assert_line --regexp '^### GitHub CLI$' assert_line 'Use `gh` CLI for all GitHub interactions. Never clone repositories to read code.' - run mise exec -- chezmoi execute-template --source "$SOURCE_DIR" <"$OPENCODE_TEMPLATE" + run render_chezmoi_template "$OPENCODE_TEMPLATE" assert_success assert_line --index 0 '# Claude Code Settings' @@ -36,7 +36,7 @@ OPENCODE_TEMPLATE="$DOTFILES_ROOT/home/dot_config/opencode/AGENTS.md.tmpl" } @test "agent instruction templates render Graphify guidance" { - run mise exec -- chezmoi execute-template --source "$SOURCE_DIR" <"$CLAUDE_TEMPLATE" + run render_chezmoi_template "$CLAUDE_TEMPLATE" assert_success assert_line '## Graphify' @@ -44,7 +44,7 @@ OPENCODE_TEMPLATE="$DOTFILES_ROOT/home/dot_config/opencode/AGENTS.md.tmpl" assert_line --partial 'graphify query' assert_line --partial 'GRAPH_REPORT.md' - run mise exec -- chezmoi execute-template --source "$SOURCE_DIR" <"$OPENCODE_TEMPLATE" + run render_chezmoi_template "$OPENCODE_TEMPLATE" assert_success assert_line '## Graphify' diff --git a/tests/template/apm-config.bats b/tests/template/apm-config.bats index 439579e..ee0cb8d 100644 --- a/tests/template/apm-config.bats +++ b/tests/template/apm-config.bats @@ -3,20 +3,12 @@ # @brief Template rendering tests for home/dot_apm/apm.yml.tmpl. load '../test_helpers/load.bash' +load '../test_helpers/templates.bash' -SOURCE_DIR="$DOTFILES_ROOT/home" APM_TEMPLATE="$DOTFILES_ROOT/home/dot_apm/apm.yml.tmpl" -PERSONAL_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false}' -WORK_DATA='{"chezmoi":{"os":"darwin"},"personal":false,"work":true}' - -render_apm_template() { - local data="$1" - - mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$data" <"$APM_TEMPLATE" -} @test "personal APM config renders only shared MCP servers" { - run render_apm_template "$PERSONAL_DATA" + run render_chezmoi_template "$APM_TEMPLATE" "$PERSONAL_DATA" assert_success assert_line ' - name: "grep"' @@ -27,7 +19,7 @@ render_apm_template() { } @test "personal APM config renders personal target" { - run render_apm_template "$PERSONAL_DATA" + run render_chezmoi_template "$APM_TEMPLATE" "$PERSONAL_DATA" assert_success assert_line ' - claude' @@ -38,7 +30,7 @@ render_apm_template() { } @test "personal APM config renders shared and personal APM packages" { - run render_apm_template "$PERSONAL_DATA" + run render_chezmoi_template "$APM_TEMPLATE" "$PERSONAL_DATA" assert_success assert_line --partial ' - "obra/superpowers' @@ -56,7 +48,7 @@ render_apm_template() { } @test "work APM config renders Figma and Jira MCP servers" { - run render_apm_template "$WORK_DATA" + run render_chezmoi_template "$APM_TEMPLATE" "$WORK_DATA" assert_success assert_line ' - name: "grep"' @@ -67,7 +59,7 @@ render_apm_template() { } @test "work APM config renders work target" { - run render_apm_template "$WORK_DATA" + run render_chezmoi_template "$APM_TEMPLATE" "$WORK_DATA" assert_success assert_line ' - copilot' @@ -78,7 +70,7 @@ render_apm_template() { } @test "work APM config renders shared APM packages without personal packages" { - run render_apm_template "$WORK_DATA" + run render_chezmoi_template "$APM_TEMPLATE" "$WORK_DATA" assert_success assert_line --partial ' - "obra/superpowers' diff --git a/tests/template/apm-lockfile.bats b/tests/template/apm-lockfile.bats index 6e349bf..6f07973 100644 --- a/tests/template/apm-lockfile.bats +++ b/tests/template/apm-lockfile.bats @@ -3,26 +3,22 @@ # @brief apm.lock.yaml.tmpl selects the correct per-context lockfile. load '../test_helpers/load.bash' +load '../test_helpers/templates.bash' -SOURCE_DIR="$DOTFILES_ROOT/home" TMPL="$DOTFILES_ROOT/home/dot_apm/apm.lock.yaml.tmpl" PERSONAL='{"personal":true,"work":false}' WORK='{"personal":false,"work":true}' -render() { - mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$1" <"$TMPL" -} - @test "personal context renders the personal lockfile" { personal_first="$(grep -m1 resolved_commit "$DOTFILES_ROOT/home/.chezmoitemplates/apm/apm.lock.personal.yaml")" - run render "$PERSONAL" + run render_chezmoi_template "$TMPL" "$PERSONAL" assert_success assert_line --regexp '^lockfile_version' assert_line "$personal_first" } @test "work context renders the work lockfile" { - run render "$WORK" + run render_chezmoi_template "$TMPL" "$WORK" assert_success assert_line --regexp '^lockfile_version' } diff --git a/tests/template/chezmoi-helpers.bats b/tests/template/chezmoi-helpers.bats index 96759b9..9b4a8d6 100644 --- a/tests/template/chezmoi-helpers.bats +++ b/tests/template/chezmoi-helpers.bats @@ -3,42 +3,33 @@ # @brief Template rendering tests for chezmoi helper templates. load '../test_helpers/load.bash' +load '../test_helpers/templates.bash' -SOURCE_DIR="$DOTFILES_ROOT/home" ACTIVE_GROUPS_TMPL="$DOTFILES_ROOT/home/.chezmoitemplates/lib/chezmoi/active-groups.json.tmpl" ACTIVE_GROUP_VALUES_TMPL="$DOTFILES_ROOT/home/.chezmoitemplates/lib/chezmoi/active-group-values.json.tmpl" -DARWIN_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false}' -WORK_DATA='{"chezmoi":{"os":"darwin"},"personal":false,"work":true}' SHARED_DATA='{"chezmoi":{"os":"darwin"},"personal":false,"work":false}' BOTH_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":true}' APM_VALUES_BY_GROUP='{"shared":["graphify"],"personal":["claude","agent-skills"],"work":["copilot"]}' APM_VALUES_SPARSE='{"shared":["graphify"]}' -render_helper() { - local template="$1" - local data="$2" - - mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$data" <"$template" -} - @test "active-groups returns shared and personal for personal context" { - run render_helper "$ACTIVE_GROUPS_TMPL" "$DARWIN_DATA" + run render_chezmoi_template "$ACTIVE_GROUPS_TMPL" "$DARWIN_DATA" assert_success assert_output '["shared","personal"]' } @test "active-groups returns shared and work for work context" { - run render_helper "$ACTIVE_GROUPS_TMPL" "$WORK_DATA" + run render_chezmoi_template "$ACTIVE_GROUPS_TMPL" "$WORK_DATA" assert_success assert_output '["shared","work"]' } @test "active-groups returns only shared when neither personal nor work" { - run render_helper "$ACTIVE_GROUPS_TMPL" "$SHARED_DATA" + run render_chezmoi_template "$ACTIVE_GROUPS_TMPL" "$SHARED_DATA" assert_success assert_output '["shared"]' @@ -48,7 +39,7 @@ render_helper() { local data data=$(printf '{"ctx":{"personal":true,"work":false},"valuesByGroup":%s}' "$APM_VALUES_BY_GROUP") - run render_helper "$ACTIVE_GROUP_VALUES_TMPL" "$data" + run render_chezmoi_template "$ACTIVE_GROUP_VALUES_TMPL" "$data" assert_success assert_output '["graphify","claude","agent-skills"]' @@ -58,7 +49,7 @@ render_helper() { local data data=$(printf '{"ctx":{"personal":false,"work":true},"valuesByGroup":%s}' "$APM_VALUES_BY_GROUP") - run render_helper "$ACTIVE_GROUP_VALUES_TMPL" "$data" + run render_chezmoi_template "$ACTIVE_GROUP_VALUES_TMPL" "$data" assert_success assert_output '["graphify","copilot"]' @@ -68,14 +59,14 @@ render_helper() { local data data=$(printf '{"ctx":{"personal":false,"work":false},"valuesByGroup":%s}' "$APM_VALUES_BY_GROUP") - run render_helper "$ACTIVE_GROUP_VALUES_TMPL" "$data" + run render_chezmoi_template "$ACTIVE_GROUP_VALUES_TMPL" "$data" assert_success assert_output '["graphify"]' } @test "active-groups returns shared, personal, and work when both are true" { - run render_helper "$ACTIVE_GROUPS_TMPL" "$BOTH_DATA" + run render_chezmoi_template "$ACTIVE_GROUPS_TMPL" "$BOTH_DATA" assert_success assert_output '["shared","personal","work"]' @@ -85,7 +76,7 @@ render_helper() { local data data=$(printf '{"ctx":{"personal":true,"work":false},"valuesByGroup":%s}' "$APM_VALUES_SPARSE") - run render_helper "$ACTIVE_GROUP_VALUES_TMPL" "$data" + run render_chezmoi_template "$ACTIVE_GROUP_VALUES_TMPL" "$data" assert_success assert_output '["graphify"]' diff --git a/tests/template/darwin-install-scripts.bats b/tests/template/darwin-install-scripts.bats index c1e2859..987e67b 100644 --- a/tests/template/darwin-install-scripts.bats +++ b/tests/template/darwin-install-scripts.bats @@ -3,29 +3,16 @@ # @brief Template rendering tests for Darwin chezmoi install scripts. load '../test_helpers/load.bash' +load '../test_helpers/templates.bash' -SOURCE_DIR="$DOTFILES_ROOT/home" -DARWIN_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false}' -WORK_DATA='{"chezmoi":{"os":"darwin"},"personal":false,"work":true}' EMPTY_APM_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false,"apm":{"targets":{"shared":[],"personal":[],"work":[]}}}' PACKAGE_TEMPLATE="$DOTFILES_ROOT/home/.chezmoiscripts/darwin/run_onchange_02_install-packages.sh.tmpl" UV_TOOLS_TEMPLATE="$DOTFILES_ROOT/home/.chezmoiscripts/darwin/run_onchange_03_install-uv-tools.sh.tmpl" GRAPHIFY_TEMPLATE="$DOTFILES_ROOT/home/.chezmoiscripts/darwin/run_onchange_08_install-graphify-skills.sh.tmpl" -render_template() { - mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$DARWIN_DATA" <"$1" -} - -render_template_with_data() { - local template="$1" - local data="$2" - - mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$data" <"$template" -} - @test "darwin install script templates render with bash shebang" { for template in "$DOTFILES_ROOT"/home/.chezmoiscripts/darwin/*.tmpl; do - run render_template "$template" + run render_chezmoi_template "$template" "$DARWIN_DATA" assert_success [ "${lines[0]}" = "#!/usr/bin/env bash" ] done @@ -45,13 +32,13 @@ render_template_with_data() { @test "rendered darwin install scripts are syntactically valid bash" { for template in "$DOTFILES_ROOT"/home/.chezmoiscripts/darwin/*.tmpl; do - rendered="$(render_template "$template")" + rendered="$(render_chezmoi_template "$template" "$DARWIN_DATA")" printf '%s\n' "$rendered" | bash -n done } @test "personal package template keeps personal tools and omits work apps" { - run render_template_with_data "$PACKAGE_TEMPLATE" "$DARWIN_DATA" + run render_chezmoi_template "$PACKAGE_TEMPLATE" "$DARWIN_DATA" assert_success assert_line 'brew "mas"' @@ -65,7 +52,7 @@ render_template_with_data() { } @test "work package template renders approved work apps" { - run render_template_with_data "$PACKAGE_TEMPLATE" "$WORK_DATA" + run render_chezmoi_template "$PACKAGE_TEMPLATE" "$WORK_DATA" assert_success assert_line 'brew "mas"' @@ -83,7 +70,7 @@ render_template_with_data() { } @test "personal uv tools template renders Graphify and Tavily CLI tools" { - run render_template_with_data "$UV_TOOLS_TEMPLATE" "$DARWIN_DATA" + run render_chezmoi_template "$UV_TOOLS_TEMPLATE" "$DARWIN_DATA" assert_success assert_line 'UV_TOOLS=(' @@ -93,7 +80,7 @@ render_template_with_data() { } @test "work uv tools template has no personal uv tools" { - run render_template_with_data "$UV_TOOLS_TEMPLATE" "$WORK_DATA" + run render_chezmoi_template "$UV_TOOLS_TEMPLATE" "$WORK_DATA" assert_success refute_line --partial '"graphifyy"' @@ -103,7 +90,7 @@ render_template_with_data() { } @test "personal Graphify template renders agent and Claude platforms" { - run render_template_with_data "$GRAPHIFY_TEMPLATE" "$DARWIN_DATA" + run render_chezmoi_template "$GRAPHIFY_TEMPLATE" "$DARWIN_DATA" assert_success assert_line 'GRAPHIFY_PLATFORMS=(' @@ -113,7 +100,7 @@ render_template_with_data() { } @test "work Graphify template renders no personal platforms" { - run render_template_with_data "$GRAPHIFY_TEMPLATE" "$WORK_DATA" + run render_chezmoi_template "$GRAPHIFY_TEMPLATE" "$WORK_DATA" assert_success refute_line --partial '"agents"' @@ -123,7 +110,7 @@ render_template_with_data() { } @test "empty APM target groups render no Graphify commands" { - run render_template_with_data "$GRAPHIFY_TEMPLATE" "$EMPTY_APM_DATA" + run render_chezmoi_template "$GRAPHIFY_TEMPLATE" "$EMPTY_APM_DATA" assert_success refute_line 'GRAPHIFY_PLATFORMS=(' diff --git a/tests/template/mise-config.bats b/tests/template/mise-config.bats index 3378710..9c810cc 100644 --- a/tests/template/mise-config.bats +++ b/tests/template/mise-config.bats @@ -3,20 +3,12 @@ # @brief Template rendering tests for home/dot_config/mise/config.toml.tmpl. load '../test_helpers/load.bash' +load '../test_helpers/templates.bash' -SOURCE_DIR="$DOTFILES_ROOT/home" MISE_TEMPLATE="$DOTFILES_ROOT/home/dot_config/mise/config.toml.tmpl" -PERSONAL_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false}' -WORK_DATA='{"chezmoi":{"os":"darwin"},"personal":false,"work":true}' - -render_mise_template() { - local data="$1" - - mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$data" <"$MISE_TEMPLATE" -} @test "mise config installs APM for personal hosts" { - run render_mise_template "$PERSONAL_DATA" + run render_chezmoi_template "$MISE_TEMPLATE" "$PERSONAL_DATA" assert_success assert_line '"github:microsoft/apm" = "latest"' @@ -31,35 +23,35 @@ render_mise_template() { } @test "mise config installs Linear CLI for personal hosts" { - run render_mise_template "$PERSONAL_DATA" + run render_chezmoi_template "$MISE_TEMPLATE" "$PERSONAL_DATA" assert_success assert_line '"npm:@schpet/linear-cli" = "latest"' } @test "mise config installs uv for personal hosts" { - run render_mise_template "$PERSONAL_DATA" + run render_chezmoi_template "$MISE_TEMPLATE" "$PERSONAL_DATA" assert_success assert_line '"uv" = "latest"' } @test "mise config installs APM for work hosts" { - run render_mise_template "$WORK_DATA" + run render_chezmoi_template "$MISE_TEMPLATE" "$WORK_DATA" assert_success assert_line '"github:microsoft/apm" = "latest"' } @test "mise config does not install Linear CLI for work hosts" { - run render_mise_template "$WORK_DATA" + run render_chezmoi_template "$MISE_TEMPLATE" "$WORK_DATA" assert_success refute_line '"npm:@schpet/linear-cli" = "latest"' } @test "mise config does not install uv for work hosts" { - run render_mise_template "$WORK_DATA" + run render_chezmoi_template "$MISE_TEMPLATE" "$WORK_DATA" assert_success refute_line '"uv" = "latest"' diff --git a/tests/test_helpers/templates.bash b/tests/test_helpers/templates.bash new file mode 100644 index 0000000..5280284 --- /dev/null +++ b/tests/test_helpers/templates.bash @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# @file templates.bash +# @brief Shared chezmoi template-rendering helpers for tests/template/*.bats. +# Provides the source dir, the common per-context override-data payloads, +# and a single render function so individual test files stop redefining +# their own copies. +# +# Load AFTER load.bash so DOTFILES_ROOT is already exported: +# load '../test_helpers/load.bash' +# load '../test_helpers/templates.bash' + +# Chezmoi source directory shared by every template test. +SOURCE_DIR="$DOTFILES_ROOT/home" +export SOURCE_DIR + +# Per-context override-data payloads passed to `chezmoi execute-template`. +PERSONAL_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false}' +WORK_DATA='{"chezmoi":{"os":"darwin"},"personal":false,"work":true}' +DARWIN_DATA='{"chezmoi":{"os":"darwin"},"personal":true,"work":false}' +export PERSONAL_DATA WORK_DATA DARWIN_DATA + +# @description Render a chezmoi template from SOURCE_DIR. +# @arg $1 string Absolute path to the template file. +# @arg $2 string Optional JSON override-data payload. Omit to render with defaults. +render_chezmoi_template() { + local template="$1" data="${2:-}" + + if [ -n "$data" ]; then + mise exec -- chezmoi execute-template --source "$SOURCE_DIR" --override-data "$data" <"$template" + else + mise exec -- chezmoi execute-template --source "$SOURCE_DIR" <"$template" + fi +} diff --git a/tests/unit/scripts/refresh-apm-locks.bats b/tests/unit/scripts/refresh-apm-locks.bats new file mode 100644 index 0000000..3e345b5 --- /dev/null +++ b/tests/unit/scripts/refresh-apm-locks.bats @@ -0,0 +1,99 @@ +#!/usr/bin/env bats +# @file tests/unit/scripts/refresh-apm-locks.bats +# @brief Behavior tests for scripts/refresh-apm-locks.sh lockfile refresh flow. +# Stubs mise, chezmoi, apm, and prettier on PATH and runs an isolated +# copy of the script so the committed lockfiles are never touched. + +load '../../test_helpers/load.bash' + +setup() { + export CALL_FILE="$BATS_TEST_TMPDIR/calls" + BIN="$BATS_TEST_TMPDIR/bin" + mkdir -p "$BIN" + + # mise resolves pinned tool paths to our stubs. + cat >"$BIN/mise" <>"$CALL_FILE" +if [ "\$1" = which ]; then echo "$BIN/\$2"; exit 0; fi +exit 0 +STUB + + # chezmoi apply only needs to create the target ~/.apm directory. + cat >"$BIN/chezmoi" <>"$CALL_FILE" +mkdir -p "\${@: -1}" +exit 0 +STUB + + # apm lock --update --global writes the user-scope lockfile under \$HOME/.apm. + cat >"$BIN/apm" <>"$CALL_FILE" +mkdir -p "\$HOME/.apm" +printf 'lockfile_version: 1\n# stub lock\n' >"\$HOME/.apm/apm.lock.yaml" +exit 0 +STUB + + # prettier records the files it was asked to format. + cat >"$BIN/prettier" <>"$CALL_FILE" +exit 0 +STUB + + chmod +x "$BIN"/* + export PATH="$BIN:$PATH" + + # Isolated repo copy so cp writes lockfiles into the temp tree, never the real repo. + REPO="$BATS_TEST_TMPDIR/repo" + mkdir -p "$REPO/scripts" "$REPO/home/.chezmoitemplates/apm" "$REPO/.github/actions/write-chezmoi-config" + cp "$DOTFILES_ROOT/scripts/refresh-apm-locks.sh" "$REPO/scripts/refresh-apm-locks.sh" + cp "$DOTFILES_ROOT/scripts/apm-lib.sh" "$REPO/scripts/apm-lib.sh" + cat >"$REPO/.github/actions/write-chezmoi-config/write-chezmoi-config.sh" <<'STUB' +#!/usr/bin/env bash +exit 0 +STUB + chmod +x "$REPO/.github/actions/write-chezmoi-config/write-chezmoi-config.sh" + + LOCK_DIR="$REPO/home/.chezmoitemplates/apm" +} + +@test "personal context writes the personal lockfile and formats it" { + run bash "$REPO/scripts/refresh-apm-locks.sh" personal + assert_success + assert_line --partial "wrote home/.chezmoitemplates/apm/apm.lock.personal.yaml" + + assert_file_exists "$LOCK_DIR/apm.lock.personal.yaml" + assert_file_contains "$LOCK_DIR/apm.lock.personal.yaml" 'lockfile_version: 1' + + run cat "$CALL_FILE" + assert_line --partial "prettier --write $LOCK_DIR/apm.lock.personal.yaml" +} + +@test "work context writes the work lockfile only" { + run bash "$REPO/scripts/refresh-apm-locks.sh" work + assert_success + + assert_file_exists "$LOCK_DIR/apm.lock.work.yaml" + assert_file_not_exists "$LOCK_DIR/apm.lock.personal.yaml" +} + +@test "no arguments refreshes both contexts" { + run bash "$REPO/scripts/refresh-apm-locks.sh" + assert_success + + assert_file_exists "$LOCK_DIR/apm.lock.personal.yaml" + assert_file_exists "$LOCK_DIR/apm.lock.work.yaml" + + run cat "$CALL_FILE" + assert_line --partial "prettier --write $LOCK_DIR/apm.lock.personal.yaml $LOCK_DIR/apm.lock.work.yaml" +} + +@test "rejects an unknown context before writing anything" { + run bash "$REPO/scripts/refresh-apm-locks.sh" bogus + assert_failure + assert_line --partial "Unsupported context: bogus" + assert_file_not_exists "$LOCK_DIR/apm.lock.bogus.yaml" +} diff --git a/tests/unit/statusline-git.bats b/tests/unit/statusline-git.bats new file mode 100644 index 0000000..75c5173 --- /dev/null +++ b/tests/unit/statusline-git.bats @@ -0,0 +1,46 @@ +#!/usr/bin/env bats +# @file statusline-git.bats +# @brief Exercises the git branch/staged/modified segment and the rate-limit bar +# in home/dot_claude/statusline-command.sh against a REAL repo. The shared +# statusline fixtures use a nonexistent current_dir, so `cd "$current_dir"` +# fails there and this code path is otherwise never reached. + +load '../test_helpers/load.bash' + +STATUSLINE="$DOTFILES_ROOT/home/dot_claude/statusline-command.sh" + +setup() { + # Real repo on the branch "feature-x" with one staged and one modified file. + REPO="$BATS_TEST_TMPDIR/myrepo" + mkdir -p "$REPO" + git -C "$REPO" init -q -b feature-x + git -C "$REPO" config user.email "test@example.com" + git -C "$REPO" config user.name "Test" + printf 'initial\n' >"$REPO/tracked.txt" + git -C "$REPO" add tracked.txt + git -C "$REPO" commit -q -m init + printf 'changed\n' >>"$REPO/tracked.txt" # tracked.txt modified, unstaged -> ~1 + printf 'new\n' >"$REPO/staged.txt" + git -C "$REPO" add staged.txt # staged.txt added to index -> +1 + + # Payload mirroring Claude Code's runtime contract, pointed at the real repo. + INPUT="$BATS_TEST_TMPDIR/input.json" + printf '{"model":{"display_name":"Sonnet 4.6"},"context_window":{"used_percentage":17},"workspace":{"current_dir":"%s"},"rate_limits":{"five_hour":{"used_percentage":50,"resets_at":1747339200}}}' "$REPO" >"$INPUT" +} + +@test "renders branch with staged and modified counts" { + run sh "$STATUSLINE" <"$INPUT" + assert_success + assert_line --partial 'myrepo' + assert_line --partial 'feature-x' + assert_line --partial '+1' + assert_line --partial '~1' +} + +@test "renders the rate-limit bar for the given percentage" { + run sh "$STATUSLINE" <"$INPUT" + assert_success + assert_line --partial '5h' + assert_line --partial '█████░░░░░' + assert_line --partial '50%' +} diff --git a/tests/unit/write-chezmoi-config.bats b/tests/unit/write-chezmoi-config.bats index ea2a21e..063efd3 100644 --- a/tests/unit/write-chezmoi-config.bats +++ b/tests/unit/write-chezmoi-config.bats @@ -10,12 +10,8 @@ load '../test_helpers/load.bash' SCRIPT="$DOTFILES_ROOT/.github/actions/write-chezmoi-config/write-chezmoi-config.sh" setup() { - TMPHOME="$(mktemp -d)" - export HOME="$TMPHOME" -} - -teardown() { - rm -rf "$TMPHOME" + export HOME="$BATS_TEST_TMPDIR/home" + mkdir -p "$HOME" } @test "personal context: exits success and writes chezmoi.yaml" {