Skip to content

fix(diff): run the unified exit specs on Neovim 0.8.0 and correct the exit answers docs - #572

Merged
ThomasK33 merged 18 commits into
mainfrom
fix/diff-exit-followups
Nov 13, 2026
Merged

ThomasK33 merged 18 commits into
mainfrom
fix/diff-exit-followups

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

Keep the exit tests and the "Exit answers" docs true after #558 and #564.

  1. fix(diff): say the unified layout runs on Neovim 0.8.0, and run its specs there #564 made the unified layout work on Neovim 0.8.0 and removed the "needs Neovim 0.9" skips from eight specs. diff_exit_reject_spec.lua came in with fix(diff): answer every openDiff call that is open when Neovim exits #558 and still skipped the unified layout below 0.9 in three places. So 14 unified exit scenarios stayed pending in the v0.8.0 job, and the file kept a comment that is no longer true.
  2. The advisor and the verifier of fix(diff): answer every openDiff call that is open when Neovim exits #558 found README sentences that say more than the code does. They concern the exits that run no VimLeavePre.

What changed

  • tests/integration/diff_exit_reject_spec.lua:
    • The three skips and the comment "The unified layout needs Neovim 0.9" are gone.
    • Three new cases (each in the vertical and unified layouts) start the exit inside an autocmd, with two waiting calls. A direct :qa! in an autocmd that is not nested, and SIGTERM while such an autocmd waits in vim.wait: no call is answered. A direct :qa! in a nested autocmd: both calls get DIFF_REJECTED. Each case first checks that both calls were waiting. The two no-answer cases also check that the child ended by itself.
    • The spec has 34 passes on Neovim 0.8.0 and 34 on 0.12.3. On main it has 14 passes and 14 pending on 0.8.0, and 28 passes on 0.12.3.
  • README.md, Global Invariants, "Exit answers":
    • The "close_window" sentence now says "waiting calls". A call that a nested :qa! cuts off during its openDiff setup has no response sender yet. The server stop answers it with -32603 after the ClaudeCodeDiffClosed events. Every call still gets one answer before close code 1001.
    • The sub-bullet for exits without VimLeavePre keeps SIGKILL, a crash and a :qa! from inside an autocmd that is not nested, as on main, and adds :noautocmd qa! and a Lua os.exit(), which also run no VimLeavePre. It adds one rule: a :qa!, SIGTERM or SIGHUP that Neovim handles while an autocmd that is not nested is still running can end Neovim without VimLeavePre too. A synchronous LSP format on save, which waits in vim.wait, is the common example. It says "can" because Neovim looks only at the autocmd that runs when it handles the exit, so a nested = true handler that such an autocmd started (with :doautocmd or nvim_exec_autocmds) still gets VimLeavePre. It promises nothing for a signal during a plain Lua loop, because Neovim handles a signal only at its next event check.
  • ARCHITECTURE.md, lua/claudecode/README.md and the comment of cleanup_at_exit in lua/claudecode/diff.lua say the same.

Scope

Tests and docs. The only change under lua/ is that comment in diff.lua. verify-diff-lifecycle passes (93,477 distinct states, and 5,557 for the live case), and its covered.lock records the new file hash. The plugin does not answer calls in the exits that the README lists as running no VimLeavePre (SIGKILL, a crash, :noautocmd qa!, os.exit(), and :qa! in an autocmd that is not nested), because none of its exit code runs in them. The docs say so.

Blast Radius

None at runtime. The v0.8.0 CI job runs 20 more tests of this spec, and the stable job runs 6 more.

Verification

  • Spec on Neovim 0.8.0 and 0.12.3: 34 passes, 0 failures, in 5 of 5 runs on each version, and in 3 of 3 runs again after the last commits, which change comments, docs and the lock only. Mutants:
    • The autocmd's :qa! replaced by a no-op: the first no-answer case is red on both versions (the file then runs into the runner's 40 s limit, because each affected case waits 20 s).
    • cleanup_at_exit returns at once: 8 cases are red on both versions, among them the nested :qa! case in the vertical layout. The no-answer cases stay green, because they pin what Neovim does. A Neovim that changes it turns them red, and the README has to change.
  • Probes on 0.8.0 and 0.12.3 (vertical and unified, two waiting calls, 6 runs per row, a VimLeavePre marker and the answers in one log). In an autocmd that is not nested, each of these runs no VimLeavePre and answers no call, in all 54 runs per version: a direct :qa!, a timer :qa! during vim.wait, SIGTERM and SIGHUP during vim.wait, SIGTERM and SIGHUP in the Lua part of a handler that waits later, SIGTERM in the first of two handlers when the second waits, SIGTERM in a Vim script loop, and SIGTERM in a :%s loop. In a nested = true autocmd the same cases answer both calls. With no autocmd, a timer :qa!, SIGTERM and SIGHUP answer both calls (18 of 18). :noautocmd qa! and a Lua os.exit() from a timer run no VimLeavePre and answer no call (9 of 9 on each version, three layouts), and a plain :qa! and :cq run it and answer both calls (9 of 9). A non-nested autocmd that starts a nested = true handler which waits in vim.wait, with SIGTERM during the wait, runs VimLeavePre and answers both calls (6 of 6 on each version).
  • Waiting calls with close_window, :qa! and SIGTERM, three layouts: both answers come before the first ClaudeCodeDiffClosed (6 of 6 on each version). A nested :qa! that cuts a call off in its setup: the -32603 comes after every ClaudeCodeDiffClosed event (12 of 12 on each version), and no id is answered twice.
  • Independent reads with a real WebSocket client on Neovim 0.8.0, 0.9.5, 0.10.4, 0.11.5 and 0.12.3: a verifier (330 runs), an advisor (225 runs) and two claim checks (375 runs in the first, and 140 runs for the nested-handler case in the second). They found that the first wording dropped the direct :qa! case, that the second promised VimLeavePre for a signal during a Lua loop, and that "ends" is false when the running autocmd started a nested handler. The sub-bullet above says only what survives all three, with "can". A last narrow check found only that "Without VimLeavePre" was too general, because eventignore can skip VimLeavePre and a :qa! still rejects diffs while buffers unload. The README now says "In these exits".
  • At the head after the merge of main (fix(diff): split before diffthis so the proposal window saves no diff mode #573): mise run format-check and mise run all pass (1892 successes, 0 failures, 0 errors, 35 pending), and the integration runner passes on 0.12.3 (844) and on 0.8.0 (840). The commits of this PR change comments, docs, the spec header and the lock only. check.sh --hash-only passes for the four verify skills, and the full verify-diff-lifecycle check passes (93,477 and 5,557 states).

Generated with xum • Model: anthropic:claude-sonnet-5-5 • Thinking: xhigh

#564 made the unified layout work on Neovim 0.8.0 (it uses vim.diff there) and
removed the "needs Neovim 0.9" skips from eight specs. diff_exit_reject_spec.lua
came in with #558 and still skipped the unified layout below 0.9 in three places,
so 14 unified exit scenarios stayed pending in the v0.8.0 job. The skips and the
comment that said the layout needs Neovim 0.9 are gone.

The spec has 28 passes on Neovim 0.8.0 and 28 on 0.12.3. Before: 14 passes and 14
pending on 0.8.0. No other claim in the repo says that the unified layout needs
Neovim 0.9. The remaining nvim-0.9 checks are about 'statuscolumn', `:w ++p` and
`nvim -l`.
…iting calls

The sentence said that with on_new_file_reject = "close_window" both diffs have
their answers before the first ClaudeCodeDiffClosed fires. That holds for calls
that are waiting. A call that a nested `:qa!` cuts off during its openDiff setup
has no response sender yet, and the server stop answers it with -32603 after the
ClaudeCodeDiffClosed events.

Probed on Neovim 0.8.0 and 0.12.3 in the vertical, horizontal and unified layouts
with both values of on_new_file_reject. Waiting calls with close_window: the
answers come before the first ClaudeCodeDiffClosed in 6 of 6 runs (:qa! and
SIGTERM, three layouts) on each version. A nested `:qa!` in the handler of the
second call, and in the handler of the only call: the -32603 comes after every
ClaudeCodeDiffClosed event in 12 of 12 runs on each version, and no id is
answered twice. The order of the ClaudeCodeDiffClosed events among themselves
differs between the two versions, and the sentence does not depend on it.
…ts without VimLeavePre

The "Exit answers" bullet said that SIGTERM and SIGHUP run VimLeavePre and give each
waiting call a DIFF_REJECTED. When the signal arrives while an autocmd that is not
`nested` runs and pumps the event loop (for example a handler that waits in
`vim.wait`, such as a synchronous LSP format on save), Neovim runs no VimLeavePre.
No waiting call gets an answer, the client sees 1006, and the lock file stays. The
exits-without-VimLeavePre sub-bullet now says so.

Probed on Neovim 0.8.0 and 0.12.3 (SIGTERM and SIGHUP, vertical and unified, two
waiting calls): no answer line, and the handler never ends. The verifier of the
previous commit found it with 24 of 24 runs on each version and a control: the same
signal during `vim.wait` in a scheduled callback (no autocmd) answers both calls.

The same commit makes the version note of the new sentence agree with the bullet: the
new facts hold on Neovim 0.8.0, 0.9.5, 0.10.4, 0.11.5 and 0.12.3.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-12T23:40:09.812077Z 206bef3 Manual request
🔒 Security Review ✅ Completed 2026-11-12T23:40:55.157664Z 206bef3 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0a9ea0a943

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a9ea0a943

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md Resolved
…ops the exit

The sub-bullet for exits without VimLeavePre said that a SIGTERM, SIGHUP or `:qa!`
that comes while an autocmd that is not `nested` runs gets no answer. That is true
only for an autocmd that pumps the event loop, for example with `vim.wait`. An
autocmd that does not pump it (a busy wait or an ordinary callback) lets the exit
run after it returns, and the waiting calls get their DIFF_REJECTED.

Probed on Neovim 0.8.0 and 0.12.3 (SIGTERM and SIGHUP, vertical and unified, two
waiting calls): a non-nested autocmd that busy-waits 600 ms after the signal ends,
and then both calls are answered, in 8 of 8 runs. With `vim.wait` in the same
autocmd, no call is answered (8 of 8 runs before).

Codex review round 1, finding P2 on README.md:1164.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 2df66832ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 2df66832ca

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

…cmd condition

Commit 2df6683 put "runs and pumps the event loop" on the whole list, so a `:qa!` run
directly in an autocmd that is not `nested` dropped out of the exits without VimLeavePre,
and the new sentence said such an autocmd does not stop the exit. It does: a direct `:qa!`
in a non-nested autocmd runs no VimLeavePre. The advisor of the PR found it.

The sub-bullet now says:
- SIGKILL, a crash and a `:qa!` from inside an autocmd that is not `nested` run no
  VimLeavePre (as on main).
- A `:qa!`, SIGTERM or SIGHUP that comes while such an autocmd pumps the event loop (for
  example with `vim.wait`) runs none either.
- A signal that comes while such an autocmd runs without pumping waits until it returns,
  and the exit then runs VimLeavePre.

The note "The signal case was probed on Neovim 0.8.0 and 0.12.3" is gone. ARCHITECTURE.md,
the module README and the comment of cleanup_at_exit say the same, and the comment no
longer says that SIGTERM and SIGHUP always run VimLeavePre.

Probed on Neovim 0.8.0 and 0.12.3, vertical and unified layouts, two waiting calls, 6 runs
per cell: in a non-nested autocmd, a direct `:qa!`, a timer `:qa!` during vim.wait, SIGTERM
and SIGHUP during vim.wait run no VimLeavePre and answer no call (24 of 24 on each
version). SIGTERM and SIGHUP during a busy wait (no pump) end the autocmd, run VimLeavePre
and answer both calls (12 of 12). In a `nested = true` autocmd every one of the six cases
runs VimLeavePre and answers both calls (36 of 36).
… cleanup_at_exit comment

Only the file hash of lua/claudecode/diff.lua changed, by a comment. The model passes (93,477
distinct states, and 5,557 for the live case).
The README says which exits run no VimLeavePre when the exit starts inside an autocmd. The
spec now runs four of them with two waiting calls, in the vertical and unified layouts:

- a direct `:qa!` in an autocmd that is not nested: no call is answered
- SIGTERM while such an autocmd waits in vim.wait: no call is answered
- SIGTERM while such an autocmd busy-waits (no pump): both calls get DIFF_REJECTED
- a direct `:qa!` in a nested autocmd: both calls get DIFF_REJECTED

Each case first checks that both calls were waiting. The child has no server, so the cases
without VimLeavePre show only that no answer comes. The first two pin what Neovim does: a
Neovim that changes it turns them red, and the README sentence has to change.

The spec has 36 passes on Neovim 0.8.0 and 0.12.3, 20 of 20 runs on each. A mutant
that makes cleanup_at_exit return at once turns the SIGTERM busy-wait case red in both
layouts and the nested case red in the vertical layout.
# Conflicts:
#	.agents/skills/verify-diff-lifecycle/covered.lock
The last two commits tried to say when an exit that starts in an autocmd runs VimLeavePre,
and each was wrong in a new case. The first dropped a direct `:qa!`. The second promised
VimLeavePre for a signal that comes while an autocmd "runs without pumping the event loop".
Neovim handles a signal at its next event check (vim.wait, a Vim script loop, `:%s`, idle),
not during a Lua loop. When that check comes while any autocmd that is not `nested` still
runs (the same handler waits later, the next handler for the event waits, a Vim script loop
or `:%s`), no VimLeavePre runs.

The sub-bullet now keeps only the rule that holds in every case: SIGKILL, a crash and a
`:qa!` from inside an autocmd that is not `nested` run no VimLeavePre, and neither does a
`:qa!`, SIGTERM or SIGHUP that Neovim handles while such an autocmd is still running. A
synchronous LSP format on save, which waits in vim.wait, is the common example. Claude
gets no answer, the socket closes with 1006 and the lock file stays. The text promises
nothing for a signal during a plain Lua loop. ARCHITECTURE.md, the module README and the
comment of cleanup_at_exit say the same.

Probed on Neovim 0.8.0 and 0.12.3, vertical and unified layouts, two waiting calls, 6 runs
per row. In an autocmd that is not nested, each of these runs no VimLeavePre and answers no
call (all 54 runs per version): a direct `:qa!`, a timer `:qa!` during vim.wait, SIGTERM
and SIGHUP during vim.wait, SIGTERM and SIGHUP in the Lua part of a handler that waits
later, SIGTERM in the first of two handlers when the second waits, SIGTERM in a Vim script
loop, and SIGTERM in a `:%s` loop. In a nested autocmd the same cases answer both calls and
run VimLeavePre (the `:%s` case in the vertical layout answers both calls but a user
VimLeavePre autocmd never logs, which is the hang that the verifier found on main). With no
autocmd, a timer `:qa!`, SIGTERM and SIGHUP answer both calls and run VimLeavePre (18 of 18).
… comment of cleanup_at_exit

Only the file hash of lua/claudecode/diff.lua changed, by a comment. The model passes.
…nd drop the busy-wait case

The two "gives no answer" cases also passed when the child never exited: `run_child` stops
a child that runs 20 s and reads the log at once, so a no-op instead of `:qa!`, or a SIGTERM
that never came, left an empty answer list. Both cases now assert that the child ended by
itself (`code ~= -1`). With the autocmd's `:qa!` replaced by a no-op, the first case is red on
Neovim 0.8.0 and 0.12.3 (the file then runs into the 40 s limit of the runner, because each
affected case waits 20 s).

The busy-wait case (SIGTERM while a non-nested autocmd runs a Lua loop, both calls answered)
is gone. The README no longer promises anything for it, and Neovim does not keep that outcome
when an event check comes later in the same autocmd. The existing SIGTERM case without an
autocmd already pins the VimLeavePre reject path.

The spec has 34 passes on Neovim 0.8.0 and 34 on 0.12.3 (28, and 6 new cases: the two
no-answer cases and the nested `:qa!` case, in two layouts).
# Conflicts:
#	.agents/skills/verify-diff-lifecycle/covered.lock
… two more exits without VimLeavePre

The last claim check found that "a `:qa!`, SIGTERM or SIGHUP that Neovim handles while an
autocmd that is not `nested` is still running ends Neovim without VimLeavePre" is false
when that autocmd has started a `nested = true` handler (with `:doautocmd` or
`nvim_exec_autocmds`, as the plugin does for its ClaudeCodeDiff* events) and the handler
runs at that moment. Neovim looks only at the `nested` flag of the autocmd that runs when it
handles the exit. Then VimLeavePre runs, the calls are answered, and the lock file goes.

The rule now says such an exit "can end Neovim without VimLeavePre" in the README, the
ARCHITECTURE.md paragraph, the module README and the comment of cleanup_at_exit. The list
keeps "a `:qa!` from inside an autocmd that is not nested", the direct case that was probed.

The list of exits that run no VimLeavePre gains `:noautocmd qa!` and a Lua `os.exit()`.
Probed on Neovim 0.8.0 and 0.12.3 (vertical, horizontal and unified layouts, two waiting
calls, 9 runs per row): both run no VimLeavePre and answer no call. A plain `:qa!` and `:cq`
run VimLeavePre and answer both calls (9 of 9 each). A non-nested autocmd that starts a
`nested = true` handler which waits in vim.wait, with SIGTERM during the wait, runs
VimLeavePre and answers both calls (6 of 6 on each version).
… comment of cleanup_at_exit

Only the file hash of lua/claudecode/diff.lua changed, by a comment. The model passes.
With eventignore skipping VimLeavePre, a :qa! still rejects diffs while buffers unload. So "Without VimLeavePre" was too general.
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 0f30c87e0b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 0f30c87e0b

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 206bef384c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 206bef384c

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 692b460 Nov 13, 2026
7 checks passed
@ThomasK33
ThomasK33 deleted the fix/diff-exit-followups branch November 13, 2026 00:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant