aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/org-agenda-config.el15
-rw-r--r--modules/org-refile-config.el15
-rw-r--r--modules/quick-video-capture.el13
-rw-r--r--modules/wrap-up.el3
-rw-r--r--tests/test-architecture-startup-contracts.el104
-rw-r--r--todo.org14
6 files changed, 142 insertions, 22 deletions
diff --git a/modules/org-agenda-config.el b/modules/org-agenda-config.el
index 9e64a04b..c37ec1c9 100644
--- a/modules/org-agenda-config.el
+++ b/modules/org-agenda-config.el
@@ -199,13 +199,14 @@ improves performance from several seconds to instant."
(- (float-time) (float-time start-time)))))))
(setq org-agenda-files files)))
-;; Build cache asynchronously after startup to avoid blocking Emacs
-(run-with-idle-timer
- 10 ; Wait 10 seconds after Emacs is idle
- nil ; Don't repeat
- (lambda ()
- (cj/log-silently "Building org-agenda files cache in background...")
- (cj/build-org-agenda-list)))
+;; Build cache asynchronously after startup to avoid blocking Emacs.
+(unless noninteractive
+ (run-with-idle-timer
+ 10 ; Wait 10 seconds after Emacs is idle
+ nil ; Don't repeat
+ (lambda ()
+ (cj/log-silently "Building org-agenda files cache in background...")
+ (cj/build-org-agenda-list))))
(defun cj/org-agenda-refresh-files ()
"Force rebuild of agenda files cache.
diff --git a/modules/org-refile-config.el b/modules/org-refile-config.el
index 63343e9f..16b37bf9 100644
--- a/modules/org-refile-config.el
+++ b/modules/org-refile-config.el
@@ -101,13 +101,14 @@ so caching improves performance from 15-20 seconds to instant."
(- (float-time) (float-time start-time)))))))
(setq org-refile-targets targets)))
-;; Build cache asynchronously after startup to avoid blocking Emacs
-(run-with-idle-timer
- 5 ; Wait 5 seconds after Emacs is idle
- nil ; Don't repeat
- (lambda ()
- (cj/log-silently "Building org-refile targets cache in background...")
- (cj/build-org-refile-targets)))
+;; Build cache asynchronously after startup to avoid blocking Emacs.
+(unless noninteractive
+ (run-with-idle-timer
+ 5 ; Wait 5 seconds after Emacs is idle
+ nil ; Don't repeat
+ (lambda ()
+ (cj/log-silently "Building org-refile targets cache in background...")
+ (cj/build-org-refile-targets))))
(defun cj/org-refile-refresh-targets ()
"Force rebuild of refile targets cache.
diff --git a/modules/quick-video-capture.el b/modules/quick-video-capture.el
index 4e62309e..0e9567dd 100644
--- a/modules/quick-video-capture.el
+++ b/modules/quick-video-capture.el
@@ -125,12 +125,13 @@ It's designed to be idempotent - safe to call multiple times."
;; Deferred initialization strategy:
;; 1. Try to load shortly after Emacs is idle following init
;; 2. Fallback timer ensures loading within 2 seconds regardless
-(add-hook 'after-init-hook
- (lambda ()
- (run-with-idle-timer 0.5 nil #'cj/setup-video-download)))
+(unless noninteractive
+ (add-hook 'after-init-hook
+ (lambda ()
+ (run-with-idle-timer 0.5 nil #'cj/setup-video-download)))
-;; Fallback: ensure initialization within 2 seconds of loading this file
-(run-with-timer 2 nil #'cj/setup-video-download)
+ ;; Fallback: ensure initialization within 2 seconds of loading this file
+ (run-with-timer 2 nil #'cj/setup-video-download))
;; If someone manually triggers capture before initialization
(with-eval-after-load 'org-capture
@@ -145,4 +146,4 @@ It's designed to be idempotent - safe to call multiple times."
;; the download through yt-dlp and task-spooler.
(provide 'quick-video-capture)
-;;; quick-video-capture.el ends here \ No newline at end of file
+;;; quick-video-capture.el ends here
diff --git a/modules/wrap-up.el b/modules/wrap-up.el
index 523d55b2..93b74776 100644
--- a/modules/wrap-up.el
+++ b/modules/wrap-up.el
@@ -25,7 +25,8 @@
(defun cj/bury-buffers-after-delay ()
"Run cj/bury-buffers after a delay."
(run-with-timer 1 nil 'cj/bury-buffers))
-(add-hook 'emacs-startup-hook 'cj/bury-buffers-after-delay)
+(unless noninteractive
+ (add-hook 'emacs-startup-hook 'cj/bury-buffers-after-delay))
(cj/log-silently "<-- end of init file.")
diff --git a/tests/test-architecture-startup-contracts.el b/tests/test-architecture-startup-contracts.el
new file mode 100644
index 00000000..a3a0e09b
--- /dev/null
+++ b/tests/test-architecture-startup-contracts.el
@@ -0,0 +1,104 @@
+;;; test-architecture-startup-contracts.el --- Startup architecture smoke tests -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Lightweight source-level checks for cross-module startup contracts. These
+;; deliberately avoid requiring package-heavy modules; the goal is to catch
+;; accidental load-order and batch-startup regressions early without building a
+;; full static analyzer.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'ert)
+
+(defconst test-architecture--repo-root
+ (file-name-directory
+ (directory-file-name
+ (file-name-directory (or load-file-name buffer-file-name))))
+ "Repository root for architecture contract tests.")
+
+(defun test-architecture--module-files ()
+ "Return all direct module source files."
+ (directory-files (expand-file-name "modules" test-architecture--repo-root)
+ t "\\.el\\'"))
+
+(defun test-architecture--file-string (file)
+ "Return FILE contents as a string."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (buffer-string)))
+
+(defun test-architecture--read-top-level-forms (file)
+ "Read top-level forms from FILE."
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (let (forms)
+ (condition-case nil
+ (while t
+ (push (read (current-buffer)) forms))
+ (end-of-file (nreverse forms))))))
+
+(defun test-architecture--contains-timer-form-p (form)
+ "Return non-nil when FORM contains a timer scheduling call."
+ (cond
+ ((atom form) nil)
+ ((memq (car form) '(run-with-timer run-at-time run-with-idle-timer)) t)
+ (t (or (test-architecture--contains-timer-form-p (car form))
+ (test-architecture--contains-timer-form-p (cdr form))))))
+
+(defun test-architecture--noninteractive-guard-p (form)
+ "Return non-nil when FORM is guarded against batch/noninteractive startup."
+ (and (consp form)
+ (or (and (eq (car form) 'unless)
+ (eq (cadr form) 'noninteractive))
+ (and (eq (car form) 'when)
+ (equal (cadr form) '(not noninteractive))))))
+
+(defun test-architecture--definition-form-p (form)
+ "Return non-nil when FORM defines code but does not execute its body now."
+ (and (consp form)
+ (memq (car form) '(defun defmacro defsubst cl-defun cl-defmacro))))
+
+(defun test-architecture--unguarded-top-level-timer-forms (file)
+ "Return top-level timer scheduling forms in FILE that are not batch-guarded."
+ (let (violations)
+ (dolist (form (test-architecture--read-top-level-forms file))
+ (when (and (test-architecture--contains-timer-form-p form)
+ (not (test-architecture--definition-form-p form))
+ (not (test-architecture--noninteractive-guard-p form)))
+ (push (prin1-to-string form) violations)))
+ (nreverse violations)))
+
+(ert-deftest test-architecture-custom-prefix-owned-by-keybindings ()
+ "Only keybindings.el may globally own the exact C-; prefix."
+ (let ((owner (expand-file-name "modules/keybindings.el"
+ test-architecture--repo-root))
+ offenders)
+ (dolist (file (test-architecture--module-files))
+ (let ((contents (test-architecture--file-string file)))
+ (when (and (not (string= file owner))
+ (or (string-match-p "(keymap-global-set[[:space:]\n]+\"C-;\"" contents)
+ (string-match-p "(global-set-key[[:space:]\n]+(kbd[[:space:]\n]+\"C-;\"" contents)))
+ (push (file-relative-name file test-architecture--repo-root) offenders))))
+ (should (string-match-p "(keymap-global-set[[:space:]\n]+\"C-;\""
+ (test-architecture--file-string owner)))
+ (should-not offenders)))
+
+(ert-deftest test-architecture-top-level-timers-are-batch-guarded ()
+ "Top-level timer scheduling must be guarded by noninteractive.
+
+Function definitions may contain timer calls; this test only rejects timer
+scheduling that can run while a module is being required in batch/test mode."
+ (let (offenders)
+ (dolist (file (test-architecture--module-files))
+ (let ((violations (test-architecture--unguarded-top-level-timer-forms file)))
+ (when violations
+ (push (format "%s: %s"
+ (file-relative-name file test-architecture--repo-root)
+ (string-join violations " "))
+ offenders))))
+ (should-not offenders)))
+
+(provide 'test-architecture-startup-contracts)
+;;; test-architecture-startup-contracts.el ends here
diff --git a/todo.org b/todo.org
index 917cef5d..4c7bfed0 100644
--- a/todo.org
+++ b/todo.org
@@ -853,7 +853,8 @@ Done 2026-05-15:
- Updated =tests/test-coverage-summary.el= to assert the policy and the
displayed project-module percentage.
-*** TODO [#B] Add a lightweight architecture smoke test for startup contracts :tests:
+*** DONE [#B] Add a lightweight architecture smoke test for startup contracts :tests:
+CLOSED: [2026-05-15 Fri]
After the above refactors start, add one or two smoke tests that protect the
architecture instead of individual functions.
@@ -867,6 +868,17 @@ Candidate checks:
Keep this small. The goal is to catch accidental return to hidden load-order
coupling, not to build a full static analyzer.
+Done 2026-05-15:
+- Added =tests/test-architecture-startup-contracts.el= with two source-level
+ smoke checks:
+ - only =keybindings.el= may globally own the exact =C-;= prefix;
+ - top-level timer scheduling forms must be guarded by =noninteractive= so
+ batch/test loads do not schedule startup timers.
+- Gated existing startup timers in =org-agenda-config.el=,
+ =org-refile-config.el=, =quick-video-capture.el=, and =wrap-up.el=.
+- Focused tests passed for the new architecture smoke file and the affected
+ agenda/refile helpers.
+
** PROJECT [#B] Module-by-module review and hardening :review:
Review every file in =modules/= and capture concrete bugs, tests, refactors,