aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-19 06:54:01 -0500
committerCraig Jennings <c@cjennings.net>2026-04-19 06:54:01 -0500
commit151d49d01dfe441b59d6dab30033b02d5b13523c (patch)
tree36449853cecc49d084b85a8ead6748b2ac11dac6
parentbdf3058542bbce298b435b301c7863b9d81d5bf4 (diff)
downloaddotemacs-151d49d01dfe441b59d6dab30033b02d5b13523c.tar.gz
dotemacs-151d49d01dfe441b59d6dab30033b02d5b13523c.zip
fix(reconcile): restore repo iteration under projects-dir and code-dir
The outer dolist in cj/check-for-open-work guarded its body with (boundp 'base-dir), which always returns nil under lexical-binding because base-dir is a lexical loop variable. Every repo under projects-dir and code-dir was silently skipped; only org-dir and user-emacs-directory (both top-level defvars) still got reconciled. Remove the bogus boundp check. Add regression tests covering the entry point itself — the existing suite only exercised the helpers.
-rw-r--r--modules/reconcile-open-repos.el2
-rw-r--r--tests/test-reconcile--check-for-open-work.el181
2 files changed, 182 insertions, 1 deletions
diff --git a/modules/reconcile-open-repos.el b/modules/reconcile-open-repos.el
index a7236754..87c16a31 100644
--- a/modules/reconcile-open-repos.el
+++ b/modules/reconcile-open-repos.el
@@ -102,7 +102,7 @@ Returns a list of directory paths that contain a .git subdirectory."
;; these are constants defined in init.el
;; recursively find and check all git repos under these directories
(dolist (base-dir (list projects-dir code-dir))
- (when (and (boundp 'base-dir) base-dir (file-directory-p base-dir))
+ (when (and base-dir (file-directory-p base-dir))
(dolist (repo (cj/find-git-repos base-dir))
(cj/reconcile-git-directory repo))))
diff --git a/tests/test-reconcile--check-for-open-work.el b/tests/test-reconcile--check-for-open-work.el
new file mode 100644
index 00000000..e4615dab
--- /dev/null
+++ b/tests/test-reconcile--check-for-open-work.el
@@ -0,0 +1,181 @@
+;;; test-reconcile--check-for-open-work.el --- Tests for cj/check-for-open-work -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the top-level entry point that iterates `projects-dir',
+;; `code-dir', `org-dir', and `user-emacs-directory'.
+;;
+;; Regression guard: a prior version used `(boundp 'base-dir)' under
+;; lexical-binding, which always returned nil, causing every repo under
+;; `projects-dir' and `code-dir' to be silently skipped.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'testutil-reconcile-open-repos)
+(require 'reconcile-open-repos)
+
+;; Declare as special so `let' creates dynamic bindings the SUT can observe.
+(defvar projects-dir)
+(defvar code-dir)
+(defvar org-dir)
+
+(defvar test-reconcile-calls nil
+ "Directories passed to `cj/reconcile-git-directory' during a test.")
+
+(defmacro test-reconcile-with-mocked-reconcile (&rest body)
+ "Run BODY with `cj/reconcile-git-directory' recording its argument.
+Uses `setq' so recorded calls remain readable after BODY returns — a
+`let' binding would be gone by the time the outer `should' runs."
+ (declare (indent 0))
+ `(progn
+ (setq test-reconcile-calls nil)
+ (cl-letf (((symbol-function 'cj/reconcile-git-directory)
+ (lambda (dir) (push dir test-reconcile-calls)))
+ ((symbol-function 'message) (lambda (_fmt &rest _args))))
+ ,@body)))
+
+(defun test-reconcile-called-with-p (path)
+ "Return non-nil if `cj/reconcile-git-directory' was called with PATH."
+ (cl-some (lambda (d)
+ (string= (file-name-as-directory d)
+ (file-name-as-directory path)))
+ test-reconcile-calls))
+
+;;; Normal Cases
+
+(ert-deftest test-reconcile-check-for-open-work-normal-reconciles-projects-dir-repo ()
+ "A repo under `projects-dir' is passed to `cj/reconcile-git-directory'.
+Regression: lexical-binding + `(boundp 'base-dir)' used to silently skip this."
+ (reconcile-test-with-temp-dirs
+ ("projects/repo-a/.git/")
+ (let ((projects-dir (expand-file-name "projects" test-root))
+ (code-dir nil)
+ (org-dir nil))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "projects/repo-a" test-root))))))
+
+(ert-deftest test-reconcile-check-for-open-work-normal-reconciles-code-dir-repo ()
+ "A repo under `code-dir' is passed to `cj/reconcile-git-directory'.
+Regression: lexical-binding + `(boundp 'base-dir)' used to silently skip this."
+ (reconcile-test-with-temp-dirs
+ ("code/archsetup/.git/")
+ (let ((projects-dir nil)
+ (code-dir (expand-file-name "code" test-root))
+ (org-dir nil))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "code/archsetup" test-root))))))
+
+(ert-deftest test-reconcile-check-for-open-work-normal-reconciles-both-dirs ()
+ "Repos under both `projects-dir' and `code-dir' are reconciled in one run."
+ (reconcile-test-with-temp-dirs
+ ("projects/proj-a/.git/" "code/code-a/.git/")
+ (let ((projects-dir (expand-file-name "projects" test-root))
+ (code-dir (expand-file-name "code" test-root))
+ (org-dir nil))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "projects/proj-a" test-root)))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "code/code-a" test-root))))))
+
+(ert-deftest test-reconcile-check-for-open-work-normal-reconciles-every-repo ()
+ "Every repo under `projects-dir' is reconciled, not just the first."
+ (reconcile-test-with-temp-dirs
+ ("projects/a/.git/" "projects/b/.git/" "projects/c/.git/")
+ (let ((projects-dir (expand-file-name "projects" test-root))
+ (code-dir nil)
+ (org-dir nil))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (dolist (repo '("projects/a" "projects/b" "projects/c"))
+ (should (test-reconcile-called-with-p
+ (expand-file-name repo test-root)))))))
+
+(ert-deftest test-reconcile-check-for-open-work-normal-reconciles-org-dir ()
+ "`org-dir' is reconciled individually (the dir itself, not its children)."
+ (reconcile-test-with-temp-dirs
+ ("orgdir/.git/")
+ (let ((projects-dir nil)
+ (code-dir nil)
+ (org-dir (expand-file-name "orgdir" test-root)))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "orgdir" test-root))))))
+
+(ert-deftest test-reconcile-check-for-open-work-normal-reconciles-user-emacs-directory ()
+ "`user-emacs-directory' is always reconciled individually."
+ (reconcile-test-with-temp-dirs
+ ("emacsdir/.git/")
+ (let ((projects-dir nil)
+ (code-dir nil)
+ (org-dir nil)
+ (user-emacs-directory (expand-file-name "emacsdir" test-root)))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "emacsdir" test-root))))))
+
+;;; Boundary Cases
+
+(ert-deftest test-reconcile-check-for-open-work-boundary-nil-projects-dir ()
+ "Nil `projects-dir' doesn't crash; other dirs still process."
+ (reconcile-test-with-temp-dirs
+ ("code/repo/.git/")
+ (let ((projects-dir nil)
+ (code-dir (expand-file-name "code" test-root))
+ (org-dir nil))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "code/repo" test-root))))))
+
+(ert-deftest test-reconcile-check-for-open-work-boundary-nonexistent-projects-dir ()
+ "Non-existent `projects-dir' is skipped without error; `code-dir' processes."
+ (reconcile-test-with-temp-dirs
+ ("code/repo/.git/")
+ (let ((projects-dir (expand-file-name "does-not-exist" test-root))
+ (code-dir (expand-file-name "code" test-root))
+ (org-dir nil))
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (test-reconcile-called-with-p
+ (expand-file-name "code/repo" test-root))))))
+
+(ert-deftest test-reconcile-check-for-open-work-boundary-empty-dirs-produce-no-calls ()
+ "Empty `projects-dir' and `code-dir' produce no repo-level reconcile calls."
+ (reconcile-test-with-temp-dirs
+ ("projects/" "code/")
+ (let ((projects-dir (expand-file-name "projects" test-root))
+ (code-dir (expand-file-name "code" test-root))
+ (org-dir nil)
+ (user-emacs-directory (expand-file-name "emacs-d" test-root))) ;; non-existent
+ (test-reconcile-with-mocked-reconcile
+ (cj/check-for-open-work))
+ (should (null test-reconcile-calls)))))
+
+;;; Error / Edge Cases
+
+(ert-deftest test-reconcile-check-for-open-work-error-emits-complete-message ()
+ "Emits the terminal `Complete.' message after iteration."
+ (reconcile-test-with-temp-dirs
+ ("projects/" "code/")
+ (let ((projects-dir (expand-file-name "projects" test-root))
+ (code-dir (expand-file-name "code" test-root))
+ (org-dir nil)
+ (messages nil))
+ (cl-letf (((symbol-function 'cj/reconcile-git-directory) (lambda (_dir)))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (push (apply #'format fmt args) messages))))
+ (cj/check-for-open-work))
+ (should (cl-some (lambda (m) (string-match-p "Complete\\." m)) messages)))))
+
+(provide 'test-reconcile--check-for-open-work)
+;;; test-reconcile--check-for-open-work.el ends here