From 3d18c7d1d3ea3821aaf7d3c2e4f21c1499ab2969 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 12 Nov 2026 16:12:04 +0000 Subject: [PATCH 1/2] test(diff): fold the trailing-blank-line spec into the final-line-break spec diff_accept_trailing_newlines_spec.lua (#535) copied the setup of diff_accept_missing_final_newline_spec.lua, and 12 of its 28 cases repeated cases of that spec. Only two rows were new: the proposal "a\n\nb\n\n" and the old file "a\n". Both rows move into the tables of the final-line-break spec, which runs them in all three layouts, and the duplicate file goes. Every assertion stays: one answer, FILE_SAVED, and the accepted text equal to the proposal. With the fix of #535 reverted, the old trailing spec failed 8 of 28 cases and the final-line-break spec failed 9 of 94. The merged spec passes 136 of 136 at head, and with the fix reverted it fails the 16 blank-line cases of the unified layout, among them every case that either old spec failed. --- ...diff_accept_missing_final_newline_spec.lua | 6 +- .../diff_accept_trailing_newlines_spec.lua | 88 ------------------- 2 files changed, 5 insertions(+), 89 deletions(-) delete mode 100644 tests/integration/diff_accept_trailing_newlines_spec.lua diff --git a/tests/integration/diff_accept_missing_final_newline_spec.lua b/tests/integration/diff_accept_missing_final_newline_spec.lua index e6d936f46..d87349c10 100644 --- a/tests/integration/diff_accept_missing_final_newline_spec.lua +++ b/tests/integration/diff_accept_missing_final_newline_spec.lua @@ -7,7 +7,8 @@ end -- The accepted text goes back to Claude as the file's new contents, so it must match the proposal byte for byte. The -- vertical and horizontal layouts take that text from the proposal buffer. A new buffer has 'eol' on, so a proposal --- without a final line break was accepted with one: "a" came back as "a\n". +-- without a final line break was accepted with one: "a" came back as "a\n". The unified layout lost one line break of a +-- proposal that ends in blank lines: "a\n\n" came back as "a\n". describe("the text a proposed diff accepts, by how the proposal ends", function() local diff = require("claudecode.diff") local tools = require("claudecode.tools.init") @@ -75,11 +76,14 @@ describe("the text a proposed diff accepts, by how the proposal ends", function( { name = "one blank line after the last line", text = "a\n\n" }, { name = "two blank lines after the last line", text = "a\n\n\n" }, { name = "two blank lines only", text = "\n\n" }, + { name = "a blank line inside and one after the last line", text = "a\n\nb\n\n" }, } local old_files = { { name = "a file with other text", text = "old\n" }, { name = "a file without a final line break", text = "old" }, { name = "a file that does not exist yet", text = nil }, + -- Holds the first line of most proposals, so a proposal that adds blank lines to it is a pure insertion. + { name = "a file that is a line shorter", text = "a\n" }, } for _, layout in ipairs(all_layouts) do diff --git a/tests/integration/diff_accept_trailing_newlines_spec.lua b/tests/integration/diff_accept_trailing_newlines_spec.lua deleted file mode 100644 index 6ef413bfd..000000000 --- a/tests/integration/diff_accept_trailing_newlines_spec.lua +++ /dev/null @@ -1,88 +0,0 @@ -if vim == nil or vim.fn.has("nvim") ~= 1 then - describe("the text a proposed diff accepts", function() - pending("needs a real Neovim") - end) - return -end - --- The accepted text goes back to Claude as the file's new contents, so it must match the proposal byte for byte. --- A proposal that ends in blank lines lost one of them in the unified layout. -describe("the text a proposed diff accepts", function() - local diff = require("claudecode.diff") - local tools = require("claudecode.tools.init") - local tab = "✻ [Claude Code] trailing.txt ⧉" - local dir, answers - - local function open_diff(layout, old_text, new_text) - local file = dir .. "/trailing.txt" - local handle = assert(io.open(file, "wb")) - handle:write(old_text) - handle:close() - vim.cmd("edit " .. vim.fn.fnameescape(file)) - diff.setup({ terminal = { provider = "none" }, diff_opts = { layout = layout } }) - local call = tools.handle_invoke({ id = "trailing_spec" }, { - name = "openDiff", - arguments = { old_file_path = file, new_file_path = file, new_file_contents = new_text, tab_name = tab }, - }) - assert.is_true(call._deferred, "openDiff waits for the user") - _G.claude_deferred_responses[tostring(call.coroutine)] = function(response) - answers[#answers + 1] = response - end - return diff._get_active_diffs()[tab] - end - - before_each(function() - vim.o.columns, vim.o.lines = 160, 40 - dir = vim.fn.tempname() - vim.fn.mkdir(dir, "p") - tools.register_all() - _G.claude_deferred_responses = {} - answers = {} - end) - - after_each(function() - diff._cleanup_all_active_diffs("test cleanup") - vim.cmd("silent! tabonly!") - vim.cmd("silent! only!") - vim.cmd("silent! %bwipeout!") - vim.fn.delete(dir, "rf") - _G.claude_deferred_responses = nil - end) - - local layouts = { "vertical", "unified" } - - -- Each proposal ends in the shape under test. Each one runs against a file with other text, so the diff has hunks, - -- and against a file that is one line shorter, so the diff is a pure insertion. - local proposals = { - { name = "one blank line after the last line", text = "a\n\n" }, - { name = "two blank lines after the last line", text = "a\n\n\n" }, - { name = "a final line without a line break", text = "a" }, - { name = "a single final line break", text = "a\n" }, - { name = "a blank line only", text = "\n" }, - { name = "two blank lines only", text = "\n\n" }, - { name = "a blank line inside and one after the last line", text = "a\n\nb\n\n" }, - } - local old_files = { - { name = "a file with other text", text = "old\n" }, - { name = "a file that is a line shorter", text = "a\n" }, - } - - for _, layout in ipairs(layouts) do - describe("in the " .. layout .. " layout", function() - for _, old_file in ipairs(old_files) do - for _, proposal in ipairs(proposals) do - it(("keeps %s of a proposal against %s"):format(proposal.name, old_file.name), function() - local state = open_diff(layout, old_file.text, proposal.text) - - vim.api.nvim_set_current_win(state.new_window) - vim.cmd("write") - - assert.equal(1, #answers, "one answer") - assert.equal("FILE_SAVED", answers[1].content[1].text) - assert.equal(vim.inspect(proposal.text), vim.inspect(answers[1].content[2].text)) - end) - end - end - end) - end -end) From b209a0a995b4b7d7f0eed128293fa8ed7413ce3f Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 12 Nov 2026 17:42:37 +0000 Subject: [PATCH 2/2] test(diff): say which proposals the unified layout lost a line break for Only a proposal that ends in "\n\n" lost one. A proposal that ends in a single "\n" did not, so "ends in blank lines" was too wide. --- tests/integration/diff_accept_missing_final_newline_spec.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/diff_accept_missing_final_newline_spec.lua b/tests/integration/diff_accept_missing_final_newline_spec.lua index d87349c10..9bded6793 100644 --- a/tests/integration/diff_accept_missing_final_newline_spec.lua +++ b/tests/integration/diff_accept_missing_final_newline_spec.lua @@ -8,7 +8,8 @@ end -- The accepted text goes back to Claude as the file's new contents, so it must match the proposal byte for byte. The -- vertical and horizontal layouts take that text from the proposal buffer. A new buffer has 'eol' on, so a proposal -- without a final line break was accepted with one: "a" came back as "a\n". The unified layout lost one line break of a --- proposal that ends in blank lines: "a\n\n" came back as "a\n". +-- proposal that ends in "\n\n": "a\n\n" came back as "a\n". A proposal that ends in a single "\n", such as "a\n" or +-- "\n", kept it. describe("the text a proposed diff accepts, by how the proposal ends", function() local diff = require("claudecode.diff") local tools = require("claudecode.tools.init")