|
| 1 | +-- Fixture for issue #285: |
| 2 | +-- "[BUG] ClaudeCodeAdd fails when adding a file in a directory with a `$`" |
| 3 | +-- https://github.com/coder/claudecode.nvim/issues/285 |
| 4 | +-- |
| 5 | +-- The sample tree under fixtures/issue-285/sample/ contains a REAL file whose |
| 6 | +-- parent directory is literally named "$post": |
| 7 | +-- fixtures/issue-285/sample/src/routes/$post/index.tsx |
| 8 | +-- |
| 9 | +-- :ClaudeCodeAdd and the openFile MCP tool both pass the path through |
| 10 | +-- vim.fn.expand(), which substitutes "$post" with the (undefined) env var -> |
| 11 | +-- the path becomes ".../src/routes//index.tsx", which does not exist, so the |
| 12 | +-- command reports "File or directory does not exist". |
| 13 | +-- |
| 14 | +-- Usage (from repo root): |
| 15 | +-- source fixtures/nvim-aliases.sh && vv issue-285 |
| 16 | +-- The $-path file opens automatically. Then either: |
| 17 | +-- * press <leader>x -> runs :Repro285 (self-contained verdict), or |
| 18 | +-- * run :Repro285 -> drives the REAL :ClaudeCodeAdd + openFile on the |
| 19 | +-- $-path file and echoes a one-line PASS/FAIL verdict. |
| 20 | +-- For a fully hand-driven check: :ClaudeCodeStart, then |
| 21 | +-- :ClaudeCodeAdd <paste the absolute path printed in the banner> (FAILS), vs |
| 22 | +-- :ClaudeCodeAdd % (works), |
| 23 | +-- then read :messages. |
| 24 | + |
| 25 | +local config_dir = vim.fn.stdpath("config") |
| 26 | +local repo_root = vim.fn.fnamemodify(config_dir, ":h:h") |
| 27 | +vim.opt.rtp:prepend(repo_root) |
| 28 | + |
| 29 | +vim.g.mapleader = " " |
| 30 | +vim.g.maplocalleader = "\\" |
| 31 | +vim.o.laststatus = 2 |
| 32 | + |
| 33 | +local ok, claudecode = pcall(require, "claudecode") |
| 34 | +assert(ok, "Failed to load claudecode.nvim from repo root: " .. tostring(claudecode)) |
| 35 | + |
| 36 | +claudecode.setup({ |
| 37 | + auto_start = false, |
| 38 | + log_level = "info", |
| 39 | + terminal = { provider = "native", auto_close = false }, |
| 40 | +}) |
| 41 | + |
| 42 | +-- Resolve the real $-path sample file shipped with this fixture. |
| 43 | +local sample = repo_root .. "/fixtures/issue-285/sample/src/routes/$post/index.tsx" |
| 44 | + |
| 45 | +-- Open it so the buffer name itself carries the `$` (this is the file a user |
| 46 | +-- would be "adding to the buffer"). fnameescape keeps `$` literal for :edit. |
| 47 | +vim.cmd("edit " .. vim.fn.fnameescape(sample)) |
| 48 | + |
| 49 | +local banner_buf = vim.api.nvim_create_buf(false, true) |
| 50 | +vim.api.nvim_buf_set_lines(banner_buf, 0, -1, false, { |
| 51 | + "claudecode.nvim -- issue #285 reproduction fixture", |
| 52 | + "", |
| 53 | + "Sample file (exists on disk, parent dir is literally '$post'):", |
| 54 | + " " .. sample, |
| 55 | + "", |
| 56 | + "Press <leader>x (or run :Repro285) for a one-line PASS/FAIL verdict.", |
| 57 | + "", |
| 58 | + "Manual check (with the fix applied, both succeed):", |
| 59 | + " :ClaudeCodeStart", |
| 60 | + " :ClaudeCodeAdd " .. sample, |
| 61 | + " :ClaudeCodeAdd % (current buffer; % is still expanded, not taken literally)", |
| 62 | + " :messages", |
| 63 | + "", |
| 64 | + "Before this fix, the first form errored with:", |
| 65 | + " [ClaudeCode] [command] [ERROR] ClaudeCodeAdd: File or directory does", |
| 66 | + " not exist: .../src/routes//index.tsx (note the '//' -- $post vanished)", |
| 67 | +}) |
| 68 | +vim.bo[banner_buf].modifiable = false |
| 69 | +vim.bo[banner_buf].buftype = "nofile" |
| 70 | + |
| 71 | +-- Capture the plugin logger so the verdict can show the exact error it logs. |
| 72 | +local logger = require("claudecode.logger") |
| 73 | +local function with_captured_logger(fn) |
| 74 | + local saved = { error = logger.error, debug = logger.debug, warn = logger.warn, info = logger.info } |
| 75 | + local lines = {} |
| 76 | + local function cap(level) |
| 77 | + return function(component, ...) |
| 78 | + local parts = {} |
| 79 | + for _, v in ipairs({ ... }) do |
| 80 | + parts[#parts + 1] = tostring(v) |
| 81 | + end |
| 82 | + lines[#lines + 1] = { level = level, msg = table.concat(parts, " ") } |
| 83 | + end |
| 84 | + end |
| 85 | + logger.error, logger.debug, logger.warn, logger.info = cap("error"), cap("debug"), cap("warn"), cap("info") |
| 86 | + local ok_run, err = pcall(fn) |
| 87 | + logger.error, logger.debug, logger.warn, logger.info = saved.error, saved.debug, saved.warn, saved.info |
| 88 | + return lines, ok_run, err |
| 89 | +end |
| 90 | + |
| 91 | +local function last_error(lines) |
| 92 | + for i = #lines, 1, -1 do |
| 93 | + if lines[i].level == "error" then |
| 94 | + return lines[i].msg |
| 95 | + end |
| 96 | + end |
| 97 | + return nil |
| 98 | +end |
| 99 | + |
| 100 | +---Drive the REAL :ClaudeCodeAdd and openFile on the $-path sample, then echo a |
| 101 | +---one-line verdict. Self-contained: if the integration isn't started, the |
| 102 | +---run-state guard is stubbed so the expand()/filereadable() gate is still |
| 103 | +---reached (that gate is where #285 lives -- no Claude connection is involved). |
| 104 | +local function repro_285() |
| 105 | + local cc = require("claudecode") |
| 106 | + cc.state = cc.state or {} |
| 107 | + local restore_server = false |
| 108 | + if not cc.state.server then |
| 109 | + cc.state.server = { _stub = true } |
| 110 | + restore_server = true |
| 111 | + end |
| 112 | + local saved_send = cc.send_at_mention |
| 113 | + local reached_send = false |
| 114 | + cc.send_at_mention = function() |
| 115 | + reached_send = true |
| 116 | + return true, nil |
| 117 | + end |
| 118 | + |
| 119 | + local lines = with_captured_logger(function() |
| 120 | + vim.cmd({ cmd = "ClaudeCodeAdd", args = { sample } }) |
| 121 | + end) |
| 122 | + local add_err = last_error(lines) |
| 123 | + |
| 124 | + local open_ok, open_err = pcall(require("claudecode.tools.open_file").handler, { |
| 125 | + filePath = sample, |
| 126 | + makeFrontmost = false, |
| 127 | + }) |
| 128 | + local open_msg = type(open_err) == "table" and tostring(open_err.data or open_err.message) or tostring(open_err) |
| 129 | + |
| 130 | + cc.send_at_mention = saved_send |
| 131 | + if restore_server then |
| 132 | + cc.state.server = nil |
| 133 | + end |
| 134 | + |
| 135 | + local add_bug = (add_err ~= nil) and not reached_send |
| 136 | + local open_bug = (not open_ok) and open_msg:find("not found", 1, true) ~= nil |
| 137 | + local reproduced = add_bug or open_bug |
| 138 | + |
| 139 | + local report = {} |
| 140 | + report[#report + 1] = { |
| 141 | + reproduced and "issue #285 REPRODUCED" or "issue #285 FIXED", |
| 142 | + reproduced and "ErrorMsg" or "MoreMsg", |
| 143 | + } |
| 144 | + vim.api.nvim_echo(report, true, {}) |
| 145 | + -- Detail lines land in :messages. |
| 146 | + vim.api.nvim_echo({ |
| 147 | + { (" ClaudeCodeAdd : %s"):format(add_err or (reached_send and "accepted (reached send)" or "no error")) }, |
| 148 | + }, true, {}) |
| 149 | + vim.api.nvim_echo({ |
| 150 | + { (" openFile tool : %s"):format(open_ok and "opened" or open_msg) }, |
| 151 | + }, true, {}) |
| 152 | +end |
| 153 | + |
| 154 | +vim.api.nvim_create_user_command("Repro285", repro_285, { desc = "Repro #285 ($ in path)" }) |
| 155 | +vim.keymap.set("n", "<leader>x", repro_285, { desc = "Repro #285 ($ in path)" }) |
0 commit comments