diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-16 00:31:58 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-16 00:31:58 -0500 |
| commit | d384b3bb7abf7be1d6d66d06809ffdc98583ec83 (patch) | |
| tree | 35aaa1926878b3c9f9f2b6de6600a4fb728d0297 /tests | |
| parent | 3236b5da26e1d92f17694601b31e9efae058a6ff (diff) | |
| download | dotemacs-d384b3bb7abf7be1d6d66d06809ffdc98583ec83.tar.gz dotemacs-d384b3bb7abf7be1d6d66d06809ffdc98583ec83.zip | |
fix(ai-config): force tab-width=8 in gptel org-mode prompt buffers
gptel's `gptel--with-buffer-copy-internal` copies the source buffer's `major-mode` symbol but doesn't run mode hooks. An inherited-org-mode prompt buffer keeps `tab-width` at this config's global default of 4 instead of the 8 that `org-mode-hook` would set. When gptel later parses the prompt buffer with `org-element`, Org's `tab-width=8` guard raises "Tab width in Org files must be 8, not 4."
I was hitting this on every second `gptel-magit-generate-message` from COMMIT_EDITMSG. `vc-config.el` sets `git-commit-major-mode 'org-mode'`, and the diffs contained list-shaped content that `org-element--list-struct` parsed.
The advice forces `tab-width=8` in the prompt buffer when its inherited mode is org-mode. It's a local workaround for an upstream gap. An upstream patch to run `(delay-mode-hooks (funcall major-mode))` in the buffer-copy is the real fix. I'll send it next.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-ai-config-gptel-prompt-tab-width.el | 106 | ||||
| -rw-r--r-- | tests/testutil-ai-config.el | 15 |
2 files changed, 121 insertions, 0 deletions
diff --git a/tests/test-ai-config-gptel-prompt-tab-width.el b/tests/test-ai-config-gptel-prompt-tab-width.el new file mode 100644 index 00000000..6f857cdc --- /dev/null +++ b/tests/test-ai-config-gptel-prompt-tab-width.el @@ -0,0 +1,106 @@ +;;; test-ai-config-gptel-prompt-tab-width.el --- Tests for gptel prompt-buffer tab-width fix -*- lexical-binding: t; -*- + +;;; Commentary: +;; Regression tests for the `org-element--list-struct: Tab width in Org +;; files must be 8, not 4' error that fires when `gptel-magit' is +;; invoked from a `COMMIT_EDITMSG' buffer where `git-commit-major-mode' +;; is `org-mode'. +;; +;; Root cause: gptel's `gptel--with-buffer-copy-internal' +;; (gptel-request.el:945) sets the prompt buffer's `major-mode' to the +;; source buffer's mode as a *symbol*, without running mode hooks. So +;; an inherited `org-mode' prompt buffer keeps the global default +;; `tab-width' (4 in this config) — `org-mode-hook' never runs to set +;; 8. When gptel later parses the prompt buffer with `org-element', +;; Org's `tab-width=8' guard raises. +;; +;; Fix: an `:around' advice on `gptel--with-buffer-copy-internal' in +;; ai-config.el sets `tab-width=8' inside the prompt buffer when its +;; inherited `major-mode' is `org-mode'. +;; +;; These tests verify the advice fires for org-mode source buffers and +;; stays out of the way for other modes. Each test pins +;; `default-value' of `tab-width' to 4 — matching Craig's real config +;; — so the advice's effect is observable in batch tests (where the +;; default would otherwise be Emacs's built-in 8). + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory)) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) + +;; testutil-ai-config provides the gptel--with-buffer-copy-internal stub +;; and the gptel stubs ai-config needs to load. +(require 'testutil-ai-config) +(require 'ai-config) + +(defmacro test-ai-config--with-default-tab-width (sentinel &rest body) + "Bind `default-value' of `tab-width' to SENTINEL during BODY. +Restores the prior default on unwind. New buffers created inside BODY +inherit SENTINEL as their `tab-width'." + (declare (indent 1)) + `(let ((saved (default-value 'tab-width))) + (unwind-protect + (progn (setq-default tab-width ,sentinel) + ,@body) + (setq-default tab-width saved)))) + +;;; Normal Cases + +(ert-deftest test-ai-config-gptel-prompt-tab-width-org-mode-source-sets-8 () + "Normal: source buffer in `org-mode' → prompt buffer has `tab-width=8'. +Regression net for the `org-element--list-struct: Tab width must be 8' +error when gptel-magit runs from COMMIT_EDITMSG with +`git-commit-major-mode=org-mode'." + (test-ai-config--with-default-tab-width 4 + (let ((recorded nil)) + (with-temp-buffer + (setq major-mode 'org-mode) + (gptel--with-buffer-copy-internal + (current-buffer) nil nil + (lambda () (setq recorded tab-width)))) + (should (= recorded 8))))) + +;;; Boundary Cases + +(ert-deftest test-ai-config-gptel-prompt-tab-width-text-mode-source-not-overridden () + "Boundary: source buffer in `text-mode' → advice does NOT set `tab-width=8'. +Only `org-mode' source triggers the override; other modes keep the +inherited default." + (test-ai-config--with-default-tab-width 4 + (let ((recorded nil)) + (with-temp-buffer + (setq major-mode 'text-mode) + (gptel--with-buffer-copy-internal + (current-buffer) nil nil + (lambda () (setq recorded tab-width)))) + (should (= recorded 4))))) + +(ert-deftest test-ai-config-gptel-prompt-tab-width-fundamental-mode-source-not-overridden () + "Boundary: source buffer in `fundamental-mode' → no override. +Confirms the advice's `(eq major-mode 'org-mode)' gate is strict." + (test-ai-config--with-default-tab-width 4 + (let ((recorded nil)) + (with-temp-buffer + (setq major-mode 'fundamental-mode) + (gptel--with-buffer-copy-internal + (current-buffer) nil nil + (lambda () (setq recorded tab-width)))) + (should (= recorded 4))))) + +(ert-deftest test-ai-config-gptel-prompt-tab-width-advice-does-not-leak () + "Boundary: advice's `setq-local' stays buffer-local to the prompt buffer. +After body-thunk completes and the prompt buffer is killed, the source +buffer's `tab-width' is unchanged." + (with-temp-buffer + (setq major-mode 'org-mode) + (setq-local tab-width 4) + (gptel--with-buffer-copy-internal + (current-buffer) nil nil + (lambda () nil)) + (should (= tab-width 4)))) + +(provide 'test-ai-config-gptel-prompt-tab-width) +;;; test-ai-config-gptel-prompt-tab-width.el ends here diff --git a/tests/testutil-ai-config.el b/tests/testutil-ai-config.el index c7486222..f56a38e1 100644 --- a/tests/testutil-ai-config.el +++ b/tests/testutil-ai-config.el @@ -74,6 +74,21 @@ ;; so the magit integration only activates when magit is provided. ;; See test-ai-config-gptel-magit-lazy-loading.el for magit stub tests. +;; Stub `gptel--with-buffer-copy-internal' so the advice attached to it +;; in ai-config.el can be exercised without loading real gptel-request. +;; Mirrors the salient bits of the real function (gptel-request.el:945): +;; create a temp buffer, copy major-mode as a symbol (no hooks), call +;; body-thunk inside. See test-ai-config-gptel-prompt-tab-width.el. +(unless (fboundp 'gptel--with-buffer-copy-internal) + (defun gptel--with-buffer-copy-internal (buf _start _end body-thunk) + "Stub: create temp buffer, copy major-mode from BUF, run BODY-THUNK." + (let ((temp (generate-new-buffer " *gptel-prompt-stub*" t))) + (unwind-protect + (with-current-buffer temp + (setq major-mode (buffer-local-value 'major-mode buf)) + (funcall body-thunk)) + (kill-buffer temp))))) + ;; Stub ai-conversations (provide 'ai-conversations) |
