aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-27 14:19:32 -0500
committerCraig Jennings <c@cjennings.net>2026-07-27 14:19:32 -0500
commit9da363f35b2b9ebc5ffefb41cf092b006c56a695 (patch)
tree1533b4ce93f3a8713886d0be2cad2471808f121c /tests
parenta2d3e77f82ccf5701d9490c1d1fc2d3c75916eae (diff)
downloaddotemacs-9da363f35b2b9ebc5ffefb41cf092b006c56a695.tar.gz
dotemacs-9da363f35b2b9ebc5ffefb41cf092b006c56a695.zip
refactor(agenda): replace the dedicated frame with a full-frame F8
- F8 now fills the whole frame instead of three quarters. - The agenda rebuilds itself on every five-minute wall-clock mark. - Quitting restores the window layout it took over. - The dedicated agenda frame and its tests are gone. - S-<f8> returns to the force-rescan. The frame bought live shared state. It paid for that with a read-only deny policy, engage-routing and a snapshot failure path. All of it existed only because the agenda shared a process with my working frames. A full-frame F8 needs none of it. The refresh rebuilds only a visible agenda. It keeps point on its line. The body is guarded, because a signal in a repeating timer resignals every tick.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-integration-org-agenda-frame-load-order.el89
-rw-r--r--tests/test-org-agenda-config--auto-refresh.el170
-rw-r--r--tests/test-org-agenda-config-display.el69
-rw-r--r--tests/test-org-agenda-frame.el1167
4 files changed, 215 insertions, 1280 deletions
diff --git a/tests/test-integration-org-agenda-frame-load-order.el b/tests/test-integration-org-agenda-frame-load-order.el
deleted file mode 100644
index a8eeaa47..00000000
--- a/tests/test-integration-org-agenda-frame-load-order.el
+++ /dev/null
@@ -1,89 +0,0 @@
-;;; test-integration-org-agenda-frame-load-order.el --- Frame allowlist survives load order -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Regression test for a load-order bug in the Full Agenda frame's read-only
-;; shadow.
-;;
-;; Components integrated:
-;; - org-agenda-frame (real, loaded in a subprocess)
-;; - org-agenda (real, loaded BEFORE the frame module to reproduce the bug)
-;;
-;; The bug: cj/--agenda-frame-shadow-mutations walks org-agenda-mode-map and
-;; keeps a key only when the frame map already binds it to a `commandp' value.
-;; The view/redo handlers (day-view, week-view, safe-redo) were defined LOWER in
-;; the file than the `with-eval-after-load' that ran the walk. When org-agenda
-;; was already loaded at frame-load time -- the normal startup order and every
-;; reload -- the walk fired before those defuns existed, read them as not-yet
-;; commands, and denied d/w/g/r, the very keys the allowlist grants. Moving the
-;; walk to the end of the file (after the defuns) fixed it.
-;;
-;; This test can't reproduce the ordering in-process (the module is already
-;; loaded), so it drives a fresh Emacs that requires org-agenda first, then the
-;; frame module, and inspects the resulting keymap.
-;;
-;; Validates:
-;; - d/w/g/r keep their allowlist commands in the org-first load order
-;; - the controlled status/priority mutations survive the shadow walk
-;; - a non-allowlisted mutation key (t) is still denied
-;;
-;;; Code:
-
-(require 'ert)
-
-(defconst test-oaf--repo-root
- (file-name-directory (directory-file-name
- (file-name-directory (or load-file-name buffer-file-name))))
- "Repo root, one level up from tests/.")
-
-(defun test-oaf--lookup-in-subprocess (keys)
- "Load org-agenda then org-agenda-frame in a fresh Emacs, return KEYS' bindings.
-Returns an alist of (KEY . BINDING-SYMBOL-NAME-OR-nil)."
- (let* ((root test-oaf--repo-root)
- (form
- (prin1-to-string
- `(progn
- (setq load-prefer-newer t)
- (package-initialize)
- (require 'org-agenda) ; the bad order: org first
- (require 'org-agenda-frame)
- (princ (prin1-to-string
- (mapcar
- (lambda (k)
- (cons k (let ((b (lookup-key cj/agenda-frame-mode-map (kbd k))))
- (and (symbolp b) (symbol-name b)))))
- ',keys))))))
- (out (with-output-to-string
- (with-current-buffer standard-output
- (call-process
- (expand-file-name invocation-name invocation-directory)
- nil t nil
- "--batch" "--no-site-file" "--no-site-lisp"
- "-L" root
- "-L" (expand-file-name "modules" root)
- "-L" (expand-file-name "themes" root)
- "--eval" form)))))
- (car (read-from-string out))))
-
-(ert-deftest test-integration-org-agenda-frame-allowlist-survives-org-first-load ()
- "Integration: with org-agenda loaded before the frame module, the allowlisted
-view/redo and controlled task-mutation keys survive while t stays denied."
- (skip-unless (file-exists-p (expand-file-name "modules/org-agenda-frame.el"
- test-oaf--repo-root)))
- (let ((got (test-oaf--lookup-in-subprocess
- '("d" "w" "g" "r" "C-c t" "C-c C-t"
- "M-<up>" "M-<right>" "s-<down>" "s-<left>" "t"))))
- (should (equal "cj/--agenda-frame-day-view" (cdr (assoc "d" got))))
- (should (equal "cj/--agenda-frame-week-view" (cdr (assoc "w" got))))
- (should (equal "cj/--agenda-frame-safe-redo" (cdr (assoc "g" got))))
- (should (equal "cj/--agenda-frame-safe-redo" (cdr (assoc "r" got))))
- (should (equal "org-agenda-todo" (cdr (assoc "C-c t" got))))
- (should (equal "org-agenda-todo" (cdr (assoc "C-c C-t" got))))
- (should (equal "org-agenda-priority-up" (cdr (assoc "M-<up>" got))))
- (should (equal "org-agenda-todo-nextset" (cdr (assoc "M-<right>" got))))
- (should (equal "org-agenda-priority-down" (cdr (assoc "s-<down>" got))))
- (should (equal "org-agenda-todo-previousset" (cdr (assoc "s-<left>" got))))
- ;; t is a real org mutation key; it must be denied, not allowlisted.
- (should (equal "cj/--agenda-frame-denied-readonly" (cdr (assoc "t" got))))))
-
-(provide 'test-integration-org-agenda-frame-load-order)
-;;; test-integration-org-agenda-frame-load-order.el ends here
diff --git a/tests/test-org-agenda-config--auto-refresh.el b/tests/test-org-agenda-config--auto-refresh.el
new file mode 100644
index 00000000..58ef729b
--- /dev/null
+++ b/tests/test-org-agenda-config--auto-refresh.el
@@ -0,0 +1,170 @@
+;;; test-org-agenda-config--auto-refresh.el --- Tests for agenda auto-refresh -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the wall-clock-aligned auto-refresh behind the F8 agenda:
+;; the next-mark arithmetic, the on-screen-agenda lookup, the timer body's
+;; error containment, and start/stop timer ownership.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'org-agenda-config)
+
+;; -- Fixtures ----------------------------------------------------------------
+
+(defun test-org-agenda-auto-refresh--time (hh mm ss)
+ "Return an Emacs time value for today at HH:MM:SS local time."
+ (let ((now (decode-time)))
+ (encode-time (list ss mm hh
+ (nth 3 now) (nth 4 now) (nth 5 now)
+ nil -1 (nth 8 now)))))
+
+(defmacro test-org-agenda-auto-refresh--with-agenda-window (&rest body)
+ "Run BODY with a window displaying a buffer in `org-agenda-mode'.
+Sets `major-mode' directly rather than calling the mode function: the
+lookup under test only asks what mode the window's buffer is in, and
+`org-agenda-mode' setup wants a real agenda build behind it."
+ (declare (indent 0))
+ `(let ((buffer (get-buffer-create "*Org Agenda*")))
+ (unwind-protect
+ (save-window-excursion
+ (with-current-buffer buffer
+ (setq major-mode 'org-agenda-mode))
+ (set-window-buffer (selected-window) buffer)
+ ,@body)
+ (kill-buffer buffer))))
+
+;; -- cj/--org-agenda-seconds-to-next-mark ------------------------------------
+
+(ert-deftest test-org-agenda-config-next-mark-mid-interval ()
+ "Normal: a time between marks returns the remainder to the next one."
+ (should (= 120 (cj/--org-agenda-seconds-to-next-mark
+ (test-org-agenda-auto-refresh--time 9 3 0) 300))))
+
+(ert-deftest test-org-agenda-config-next-mark-exactly-on-mark ()
+ "Boundary: a time exactly on a mark returns a full period, never zero.
+A zero delay would fire the timer immediately and again a period later."
+ (should (= 300 (cj/--org-agenda-seconds-to-next-mark
+ (test-org-agenda-auto-refresh--time 9 5 0) 300))))
+
+(ert-deftest test-org-agenda-config-next-mark-one-second-before ()
+ "Boundary: one second short of a mark returns one second."
+ (should (= 1 (cj/--org-agenda-seconds-to-next-mark
+ (test-org-agenda-auto-refresh--time 9 4 59) 300))))
+
+(ert-deftest test-org-agenda-config-next-mark-lands-on-a-multiple ()
+ "Normal: TIME plus the result is always a whole multiple of PERIOD.
+This is the property that makes the refresh land on :00/:05 rather than
+five minutes after whenever the agenda happened to open."
+ (dolist (seconds '(0 1 59 60 61 149 150 299))
+ (let* ((time (test-org-agenda-auto-refresh--time 9 0 0))
+ (start (+ (floor (float-time time)) seconds))
+ (delay (cj/--org-agenda-seconds-to-next-mark start 300)))
+ (should (zerop (mod (+ start delay) 300))))))
+
+(ert-deftest test-org-agenda-config-next-mark-rejects-nonpositive-period ()
+ "Error: a zero or negative period is a caller bug, not a silent no-op."
+ (should-error (cj/--org-agenda-seconds-to-next-mark (current-time) 0))
+ (should-error (cj/--org-agenda-seconds-to-next-mark (current-time) -300)))
+
+;; -- cj/--org-agenda-refresh-window ------------------------------------------
+
+(ert-deftest test-org-agenda-config-refresh-window-finds-displayed-agenda ()
+ "Normal: the lookup returns the window showing an agenda buffer."
+ (test-org-agenda-auto-refresh--with-agenda-window
+ (should (eq (cj/--org-agenda-refresh-window) (selected-window)))))
+
+(ert-deftest test-org-agenda-config-refresh-window-nil-when-not-displayed ()
+ "Boundary: an agenda buffer that exists but is off-screen is not a target.
+Rebuilding an invisible agenda costs time and shows nobody anything."
+ (let ((buffer (get-buffer-create "*Org Agenda*")))
+ (unwind-protect
+ (progn
+ (with-current-buffer buffer (setq major-mode 'org-agenda-mode))
+ (should-not (cj/--org-agenda-refresh-window)))
+ (kill-buffer buffer))))
+
+(ert-deftest test-org-agenda-config-refresh-window-nil-with-no-agenda ()
+ "Boundary: no agenda buffer anywhere returns nil rather than erroring."
+ (should-not (cj/--org-agenda-refresh-window)))
+
+;; -- cj/--org-agenda-auto-refresh (the timer body) ---------------------------
+
+(ert-deftest test-org-agenda-config-auto-refresh-noop-without-agenda ()
+ "Boundary: the tick does nothing, and signals nothing, with no agenda up."
+ (let ((called nil))
+ (cl-letf (((symbol-function 'org-agenda-redo)
+ (lambda (&rest _) (setq called t))))
+ (cj/--org-agenda-auto-refresh)
+ (should-not called))))
+
+(ert-deftest test-org-agenda-config-auto-refresh-redoes-visible-agenda ()
+ "Normal: the tick redoes the agenda shown on screen."
+ (let ((called nil))
+ (cl-letf (((symbol-function 'org-agenda-redo)
+ (lambda (&rest _) (setq called t))))
+ (test-org-agenda-auto-refresh--with-agenda-window
+ (cj/--org-agenda-auto-refresh))
+ (should called))))
+
+(ert-deftest test-org-agenda-config-auto-refresh-contains-errors ()
+ "Error: a failing redo must not escape the timer body.
+An unguarded signal in a repeating timer resignals on every tick, which is
+how a five-minute timer turns into an endless backtrace."
+ (cl-letf (((symbol-function 'org-agenda-redo)
+ (lambda (&rest _) (error "simulated redo failure")))
+ ((symbol-function 'cj/log-silently) (lambda (&rest _) nil)))
+ (test-org-agenda-auto-refresh--with-agenda-window
+ (should (progn (cj/--org-agenda-auto-refresh) t)))))
+
+(ert-deftest test-org-agenda-config-auto-refresh-restores-point-line ()
+ "Normal: the tick leaves point on the line it started on.
+A refresh that scrolls the reader back to the top every five minutes is
+worse than no refresh."
+ (cl-letf (((symbol-function 'org-agenda-redo) (lambda (&rest _) nil)))
+ (test-org-agenda-auto-refresh--with-agenda-window
+ (with-current-buffer "*Org Agenda*"
+ (erase-buffer)
+ (dotimes (i 10) (insert (format "agenda line %d\n" i)))
+ (goto-char (point-min))
+ (forward-line 4))
+ (cj/--org-agenda-auto-refresh)
+ (with-current-buffer "*Org Agenda*"
+ (should (= 5 (line-number-at-pos)))))))
+
+;; -- start / stop ------------------------------------------------------------
+
+(ert-deftest test-org-agenda-config-auto-refresh-start-owns-one-timer ()
+ "Normal: starting twice leaves exactly one timer, not two.
+Re-loading the module in a live daemon re-runs the arming form, so a
+non-idempotent start would stack a second ticker on every reload."
+ (let ((cj/--org-agenda-refresh-timer nil))
+ (unwind-protect
+ (progn
+ (cj/org-agenda-auto-refresh-start)
+ (let ((first cj/--org-agenda-refresh-timer))
+ (cj/org-agenda-auto-refresh-start)
+ (should (timerp cj/--org-agenda-refresh-timer))
+ (should-not (memq first timer-list))
+ (should (memq cj/--org-agenda-refresh-timer timer-list))))
+ (cj/org-agenda-auto-refresh-stop))))
+
+(ert-deftest test-org-agenda-config-auto-refresh-stop-clears-timer ()
+ "Normal: stopping cancels the timer and clears the handle."
+ (let ((cj/--org-agenda-refresh-timer nil))
+ (cj/org-agenda-auto-refresh-start)
+ (let ((timer cj/--org-agenda-refresh-timer))
+ (cj/org-agenda-auto-refresh-stop)
+ (should-not cj/--org-agenda-refresh-timer)
+ (should-not (memq timer timer-list)))))
+
+(ert-deftest test-org-agenda-config-auto-refresh-stop-is-safe-when-stopped ()
+ "Boundary: stopping an already-stopped refresh is a no-op, not an error."
+ (let ((cj/--org-agenda-refresh-timer nil))
+ (should (progn (cj/org-agenda-auto-refresh-stop) t))
+ (should-not cj/--org-agenda-refresh-timer)))
+
+(provide 'test-org-agenda-config--auto-refresh)
+;;; test-org-agenda-config--auto-refresh.el ends here
diff --git a/tests/test-org-agenda-config-display.el b/tests/test-org-agenda-config-display.el
index af4c7ea0..f039985d 100644
--- a/tests/test-org-agenda-config-display.el
+++ b/tests/test-org-agenda-config-display.el
@@ -2,6 +2,8 @@
;;; Commentary:
;; Tests for the display-buffer rule used by the F8 org agenda view.
+;; The agenda takes the whole frame; these pin that, and pin the two ways
+;; it previously failed to (a fraction of the frame, or shrunk to fit).
;;; Code:
@@ -10,39 +12,58 @@
(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
(require 'org-agenda-config)
-(ert-deftest test-org-agenda-config-display-rule-uses-configured-height ()
- "Normal: the agenda display rule uses the configured frame fraction."
- (let ((cj/org-agenda-window-height 0.75))
- (should (equal (cdr (assoc 'window-height
- (cddr (cj/--org-agenda-display-rule))))
- 0.75))))
+(defun test-org-agenda-config-display--actions ()
+ "Return the display-action function list from the agenda rule."
+ (car (cdr (cj/--org-agenda-display-rule))))
+
+(defun test-org-agenda-config-display--alist ()
+ "Return the action alist from the agenda rule."
+ (cddr (cj/--org-agenda-display-rule)))
+
+(ert-deftest test-org-agenda-config-display-rule-takes-full-frame ()
+ "Normal: the agenda display rule claims the whole frame."
+ (should (memq 'display-buffer-full-frame
+ (test-org-agenda-config-display--actions))))
+
+(ert-deftest test-org-agenda-config-display-rule-reuses-agenda-window ()
+ "Normal: an agenda already on screen is reused rather than re-displayed."
+ (should (memq 'display-buffer-reuse-mode-window
+ (test-org-agenda-config-display--actions))))
+
+(ert-deftest test-org-agenda-config-display-rule-sets-no-window-height ()
+ "Regression: no height fraction survives.
+The rule used to hand the agenda 0.75 of the frame; a leftover
+`window-height' entry would cap the full-frame window right back down."
+ (should-not (assoc 'window-height (test-org-agenda-config-display--alist))))
(ert-deftest test-org-agenda-config-display-rule-does-not-fit-to-buffer ()
"Regression: F8 agenda should not shrink to fit compact agenda contents."
- (let ((cj/org-agenda-window-height 0.75))
- (should-not (eq (cdr (assoc 'window-height
- (cddr (cj/--org-agenda-display-rule))))
- 'fit-window-to-buffer))))
-
-(ert-deftest test-org-agenda-config-display-rule-creates-large-window ()
- "Integration: the agenda rule creates a window near the configured height."
- (let ((cj/org-agenda-window-height 0.75)
- (display-buffer-alist (list (cj/--org-agenda-display-rule)))
+ (should-not (eq (cdr (assoc 'window-height
+ (test-org-agenda-config-display--alist)))
+ 'fit-window-to-buffer)))
+
+(ert-deftest test-org-agenda-config-display-rule-window-not-dedicated ()
+ "Regression: the agenda window must not be dedicated.
+With the agenda owning the only window, a dedicated one leaves RET on an
+item (`org-agenda-switch-to') nowhere to put the file, so it splits or
+opens a frame instead of replacing the agenda."
+ (should-not (cdr (assoc 'dedicated (test-org-agenda-config-display--alist)))))
+
+(ert-deftest test-org-agenda-config-display-rule-creates-sole-window ()
+ "Integration: displaying the agenda leaves it as the frame's only window."
+ (let ((display-buffer-alist (list (cj/--org-agenda-display-rule)))
(buffer (get-buffer-create "*Org Agenda*")))
(unwind-protect
(save-window-excursion
(delete-other-windows)
+ (split-window-below)
(with-current-buffer buffer
(erase-buffer)
- (dotimes (_ 3)
- (insert "agenda line\n")))
- (let* ((before-height (window-total-height))
- (window (display-buffer buffer))
- (actual-ratio (/ (float (window-total-height window))
- before-height)))
- (should (= 2 (length (window-list))))
- (should (> actual-ratio 0.65))
- (should (< actual-ratio 0.85))))
+ (dotimes (_ 3) (insert "agenda line\n")))
+ (let ((window (display-buffer buffer)))
+ (should (= 1 (length (window-list))))
+ (should (eq window (car (window-list))))
+ (should (eq (window-buffer window) buffer))))
(kill-buffer buffer))))
(provide 'test-org-agenda-config-display)
diff --git a/tests/test-org-agenda-frame.el b/tests/test-org-agenda-frame.el
deleted file mode 100644
index 167cf2ff..00000000
--- a/tests/test-org-agenda-frame.el
+++ /dev/null
@@ -1,1167 +0,0 @@
-;;; test-org-agenda-frame.el --- Tests for the fullscreen agenda frame -*- lexical-binding: t; -*-
-
-;;; Commentary:
-;; Phase 1 of the org-agenda fullscreen frame (spec:
-;; docs/specs/2026-07-17-org-agenda-fullscreen-frame-spec.org). Frame lookup
-;; is mocked (frame-list / frame-live-p / frame-parameter), the house pattern
-;; from test-dirvish-config-popup.el, since --batch can't create real frames.
-
-;;; Code:
-
-(require 'ert)
-(require 'cl-lib)
-
-(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
-(require 'org-agenda-frame)
-
-;; org-agenda isn't loaded in batch (no package-initialize), so declare the
-;; command list special and bound for the registration tests to let-bind.
-(defvar org-agenda-custom-commands nil)
-
-;;; cj/--agenda-frame — locate the marked frame
-
-(ert-deftest test-org-agenda-frame-find-returns-marked-live-frame ()
- "Normal: returns the live frame carrying the `cj/agenda-frame' marker."
- (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fb fc)))
- ((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'fb)))))
- (should (eq (cj/--agenda-frame) 'fb))))
-
-(ert-deftest test-org-agenda-frame-find-nil-when-none-marked ()
- "Boundary: no frame carries the marker -> nil."
- (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fc)))
- ((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'frame-parameter) (lambda (_f _p) nil)))
- (should (null (cj/--agenda-frame)))))
-
-(ert-deftest test-org-agenda-frame-find-ignores-dead-marked-frame ()
- "Error: a marked but dead frame is not returned."
- (cl-letf (((symbol-function 'frame-list) (lambda () '(fa fb)))
- ((symbol-function 'frame-live-p) (lambda (f) (not (eq f 'fb))))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'fb)))))
- (should (null (cj/--agenda-frame)))))
-
-;;; cj/--agenda-frame-p — is FRAME a live agenda frame
-
-(ert-deftest test-org-agenda-frame-p-true-for-live-marked ()
- "Normal: a live marked frame is an agenda frame."
- (cl-letf (((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'fa)))))
- (should (cj/--agenda-frame-p 'fa))))
-
-(ert-deftest test-org-agenda-frame-p-nil-for-unmarked ()
- "Boundary: a live unmarked frame is not an agenda frame."
- (cl-letf (((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'frame-parameter) (lambda (_f _p) nil)))
- (should (null (cj/--agenda-frame-p 'fa)))))
-
-(ert-deftest test-org-agenda-frame-p-nil-for-dead-marked ()
- "Error: a dead marked frame is not an agenda frame."
- (cl-letf (((symbol-function 'frame-live-p) (lambda (_f) nil))
- ((symbol-function 'frame-parameter)
- (lambda (_f p) (eq p 'cj/agenda-frame))))
- (should (null (cj/--agenda-frame-p 'fa)))))
-
-;;; cj/--agenda-frame-working-frame — route source files to a non-agenda frame
-
-(ert-deftest test-org-agenda-working-frame-returns-non-agenda-frame ()
- "Normal: with no recorded launch frame, returns the first live non-agenda frame."
- (let ((cj/--agenda-frame-launch-frame nil))
- (cl-letf (((symbol-function 'frame-list) (lambda () '(agenda work)))
- ((symbol-function 'frame-live-p) (lambda (f) (memq f '(agenda work))))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'agenda)))))
- (should (eq (cj/--agenda-frame-working-frame) 'work)))))
-
-(ert-deftest test-org-agenda-working-frame-prefers-live-launch-frame ()
- "Normal: the recorded launch frame wins when live and non-agenda."
- (let ((cj/--agenda-frame-launch-frame 'w2))
- (cl-letf (((symbol-function 'frame-list) (lambda () '(agenda w1 w2)))
- ((symbol-function 'frame-live-p) (lambda (f) (memq f '(agenda w1 w2))))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'agenda)))))
- (should (eq (cj/--agenda-frame-working-frame) 'w2)))))
-
-(ert-deftest test-org-agenda-working-frame-falls-back-when-launch-dead ()
- "Boundary: a dead recorded launch frame falls back to another non-agenda frame."
- (let ((cj/--agenda-frame-launch-frame 'gone))
- (cl-letf (((symbol-function 'frame-list) (lambda () '(agenda work)))
- ((symbol-function 'frame-live-p) (lambda (f) (memq f '(agenda work))))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'agenda)))))
- (should (eq (cj/--agenda-frame-working-frame) 'work)))))
-
-(ert-deftest test-org-agenda-working-frame-nil-when-only-agenda ()
- "Error: the agenda frame is the only live frame -> nil (caller creates one)."
- (let ((cj/--agenda-frame-launch-frame nil))
- (cl-letf (((symbol-function 'frame-list) (lambda () '(agenda)))
- ((symbol-function 'frame-live-p) (lambda (f) (eq f 'agenda)))
- ((symbol-function 'frame-parameter)
- (lambda (f p) (and (eq p 'cj/agenda-frame) (eq f 'agenda)))))
- (should (null (cj/--agenda-frame-working-frame))))))
-
-;;; cj/--agenda-frame-command — the dedicated F seven-day view
-
-(defun test-org-agenda-frame--block-settings ()
- "Return the per-block settings alist of the agenda-frame command."
- (let* ((cmd (cj/--agenda-frame-command))
- (blocks (nth 2 cmd))
- (agenda-block (car blocks)))
- (nth 2 agenda-block)))
-
-(defun test-org-agenda-frame--general-settings ()
- "Return the general (view-wide) settings alist of the agenda-frame command."
- (nth 3 (cj/--agenda-frame-command)))
-
-(ert-deftest test-org-agenda-frame-command-key-is-F ()
- "Normal: the command's key is F (collision-free with the existing d)."
- (should (equal (nth 0 (cj/--agenda-frame-command)) "F")))
-
-(ert-deftest test-org-agenda-frame-command-today-anchored-7-day ()
- "Normal: the default span is seven days anchored to today, not Monday.
-The span is the frame-span variable (default 7), evaluated the way org
-evaluates custom-command settings."
- (let ((s (test-org-agenda-frame--block-settings)))
- (should (equal (default-value 'cj/--agenda-frame-span) 7))
- (should (equal (eval (cadr (assq 'org-agenda-span s)) t)
- (default-value 'cj/--agenda-frame-span)))
- (should (equal (cadr (assq 'org-agenda-start-day s)) "0d"))
- ;; start-on-weekday nil is what un-anchors the span from Monday.
- (should (assq 'org-agenda-start-on-weekday s))
- (should (null (cadr (assq 'org-agenda-start-on-weekday s))))))
-
-(ert-deftest test-org-agenda-frame-command-span-follows-variable ()
- "Normal: the block span reads `cj/--agenda-frame-span', so d/w can change it
-and a redo -- which re-evaluates the lprops -- picks up the new span."
- (let ((cj/--agenda-frame-span 1))
- (should (equal (eval (cadr (assq 'org-agenda-span
- (test-org-agenda-frame--block-settings)))
- t)
- 1)))
- (let ((cj/--agenda-frame-span 7))
- (should (equal (eval (cadr (assq 'org-agenda-span
- (test-org-agenda-frame--block-settings)))
- t)
- 7))))
-
-(ert-deftest test-org-agenda-frame-day-view-sets-span-1-and-redoes ()
- "Normal: d sets the span to one day and refreshes via the safe redo."
- (let ((cj/--agenda-frame-span 7) redone)
- (cl-letf (((symbol-function 'cj/--agenda-frame-safe-redo)
- (lambda (&rest _) (setq redone t))))
- (cj/--agenda-frame-day-view)
- (should (equal cj/--agenda-frame-span 1))
- (should redone))))
-
-(ert-deftest test-org-agenda-frame-week-view-sets-span-7-and-redoes ()
- "Normal: w restores the seven-day span and refreshes."
- (let ((cj/--agenda-frame-span 1) redone)
- (cl-letf (((symbol-function 'cj/--agenda-frame-safe-redo)
- (lambda (&rest _) (setq redone t))))
- (cj/--agenda-frame-week-view)
- (should (equal cj/--agenda-frame-span 7))
- (should redone))))
-
-(ert-deftest test-org-agenda-frame-command-tight-prefix-format ()
- "Normal: the view sets its own prefix format with a narrow category column.
-Without it the global agenda format applies, whose 25-char category pad
-leaves a wide blank gutter between the source name and the item."
- (let ((s (test-org-agenda-frame--block-settings)))
- (should (assq 'org-agenda-prefix-format s))
- (should (string-match-p "%-10:c"
- (cadr (assq 'org-agenda-prefix-format s))))))
-
-(ert-deftest test-org-agenda-frame-command-skips-done-items ()
- "Normal: completed tasks never appear in the Full Agenda.
-The global agenda deliberately shows scheduled done items, so the dedicated
-view needs its own skip function to exclude every done-state keyword."
- (let* ((settings (test-org-agenda-frame--block-settings))
- (skip (assq 'org-agenda-skip-function settings)))
- (should skip)
- (should (equal (eval (cadr skip) t)
- '(org-agenda-skip-entry-if 'todo 'done)))))
-
-(ert-deftest test-org-agenda-frame-command-render-excludes-scheduled-done ()
- "Boundary: a scheduled high-priority DONE item is absent from rendered output.
-An equally scheduled active task remains, proving the whole date was not
-skipped."
- (require 'org-agenda)
- (let* ((file (make-temp-file "agenda-frame-done-" nil ".org"))
- (today (format-time-string "%Y-%m-%d %a"))
- (org-agenda-files (list file))
- (org-agenda-custom-commands (list (cj/--agenda-frame-command)))
- (org-agenda-sticky nil)
- (agenda-buffer "*Org Agenda*"))
- (unwind-protect
- (progn
- (with-temp-file file
- (insert "#+TODO: TODO | DONE\n"
- "* DONE [#A] completed-scheduled-marker\n"
- "SCHEDULED: <" today ">\n"
- "* TODO [#A] active-scheduled-marker\n"
- "SCHEDULED: <" today ">\n"))
- (org-agenda nil cj/--agenda-frame-command-key)
- (with-current-buffer agenda-buffer
- (should-not (string-match-p "completed-scheduled-marker"
- (buffer-string)))
- (should (string-match-p "active-scheduled-marker"
- (buffer-string)))))
- (when (get-buffer agenda-buffer)
- (kill-buffer agenda-buffer))
- (delete-file file))))
-
-(ert-deftest test-org-agenda-frame-do-redo-leaves-sticky-alone ()
- "Normal: the redo binds current-window but never touches sticky.
-org-agenda-redo handles the in-place rebuild itself (it binds sticky nil
-and redirects the buffer name); a sticky t reaching org-agenda-prepare
-mid-redo makes it throw \\='exit with no catch and the tick fails."
- (let ((org-agenda-sticky nil)
- seen-sticky seen-setup (params '()))
- (with-temp-buffer
- (insert "agenda line\n")
- (cl-letf (((symbol-function 'org-agenda-redo)
- (lambda (&rest _)
- (setq seen-sticky org-agenda-sticky
- seen-setup org-agenda-window-setup)))
- ((symbol-function 'frame-parameter) (lambda (_f p) (alist-get p params)))
- ((symbol-function 'set-frame-parameter)
- (lambda (_f p v) (setf (alist-get p params) v))))
- (cj/--agenda-frame-do-redo 'af (current-buffer) nil)
- (should (null seen-sticky))
- (should (eq seen-setup 'current-window))))))
-
-(ert-deftest test-org-agenda-frame-command-follow-mode-off ()
- "Boundary: follow-mode is forced off locally so a global default can't split."
- (let ((s (test-org-agenda-frame--block-settings)))
- (should (assq 'org-agenda-start-with-follow-mode s))
- (should (null (cadr (assq 'org-agenda-start-with-follow-mode s))))))
-
-(ert-deftest test-org-agenda-frame-command-sticky-and-current-window ()
- "Normal: current-window in the settings; sticky deliberately NOT there.
-The general settings are baked into the buffer's series-redo-cmd and
-re-applied on every redo; a sticky t there makes org-agenda-use-sticky-p
-true mid-redo (the buffer exists), and org-agenda-prepare throws \\='exit
-with no catch -- every refresh tick fails. Stickiness belongs only in
-the spawn wrapper, where it names the buffer."
- (let ((g (test-org-agenda-frame--general-settings)))
- (should-not (assq 'org-agenda-sticky g))
- ;; org evaluates custom-command setting values via org-let, so the stored
- ;; form is (quote current-window); eval it the way org would.
- (should (eq (eval (cadr (assq 'org-agenda-window-setup g)) t)
- 'current-window))))
-
-;;; cj/--agenda-frame-register-command — idempotent registration
-
-(ert-deftest test-org-agenda-frame-register-adds-entry ()
- "Normal: registration inserts the F entry into org-agenda-custom-commands."
- (let ((org-agenda-custom-commands '(("d" "Daily" nil))))
- (cj/--agenda-frame-register-command)
- (should (assoc "F" org-agenda-custom-commands))
- (should (assoc "d" org-agenda-custom-commands))))
-
-(ert-deftest test-org-agenda-frame-register-is-idempotent ()
- "Boundary: registering twice leaves exactly one F entry."
- (let ((org-agenda-custom-commands nil))
- (cj/--agenda-frame-register-command)
- (cj/--agenda-frame-register-command)
- (should (= 1 (seq-count (lambda (e) (equal (car e) "F"))
- org-agenda-custom-commands)))))
-
-;;; Default-deny policy — denial handlers
-
-(ert-deftest test-org-agenda-frame-denied-readonly-messages ()
- "Normal: the read-only denial shows the read-only message and acts on nothing."
- (let (captured)
- (cl-letf (((symbol-function 'message)
- (lambda (fmt &rest args) (setq captured (apply #'format fmt args)))))
- (cj/--agenda-frame-denied-readonly))
- (should (string-match-p "read-only" captured))
- (should (string-match-p "working frame" captured))))
-
-(ert-deftest test-org-agenda-frame-denied-fixed-view-messages ()
- "Normal: the view-change denial shows the fixed-view message."
- (let (captured)
- (cl-letf (((symbol-function 'message)
- (lambda (fmt &rest args) (setq captured (apply #'format fmt args)))))
- (cj/--agenda-frame-denied-fixed-view))
- (should (string-match-p "day (d) and week (w)" captured))))
-
-;;; Default-deny policy — the keymap
-
-(ert-deftest test-org-agenda-frame-map-catch-all-is-readonly-deny ()
- "Normal: the [t] default binding denies with the read-only handler."
- (should (eq (lookup-key cj/agenda-frame-mode-map [t])
- 'cj/--agenda-frame-denied-readonly)))
-
-(ert-deftest test-org-agenda-frame-map-navigation-allowed ()
- "Normal: navigation keys resolve to their org-agenda commands."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "n")) 'org-agenda-next-line))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "p")) 'org-agenda-previous-line))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-g")) 'keyboard-quit)))
-
-(ert-deftest test-org-agenda-frame-map-point-motion-and-isearch-allowed ()
- "Normal: read-only point motion and isearch work in the frame.
-C-a/C-e/C-f/C-b move point and C-s/C-r search; all are read-only and
-must not hit the deny catch-all."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-a")) 'move-beginning-of-line))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-e")) 'move-end-of-line))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-f")) 'forward-char))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-b")) 'backward-char))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-s")) 'isearch-forward))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-r")) 'isearch-backward)))
-
-(ert-deftest test-org-agenda-frame-map-engage-routed ()
- "Normal: RET and TAB route to the working-frame engage command."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "RET"))
- 'cj/--agenda-frame-engage-open))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "TAB"))
- 'cj/--agenda-frame-engage-open)))
-
-(ert-deftest test-org-agenda-frame-map-engage-gui-function-keys ()
- "Boundary: the GUI [return]/[tab] events engage too, not the [t] deny handler.
-Without these, the catch-all suppresses their translation to RET/TAB in a
-graphical frame and RET would be denied instead of opening the item."
- (should (eq (lookup-key cj/agenda-frame-mode-map [return])
- 'cj/--agenda-frame-engage-open))
- (should (eq (lookup-key cj/agenda-frame-mode-map [tab])
- 'cj/--agenda-frame-engage-open)))
-
-(ert-deftest test-org-agenda-frame-map-lifecycle-keys ()
- "Normal: q/Q/x close the frame; r and g take the safe-redo path.
-g is the muscle-memory agenda refresh; it must refresh, not hit the
-fixed-view deny handler."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "q")) 'cj/--agenda-frame-close))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "Q")) 'cj/--agenda-frame-close))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "x")) 'cj/--agenda-frame-close))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "r")) 'cj/--agenda-frame-safe-redo))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "g")) 'cj/--agenda-frame-safe-redo)))
-
-(ert-deftest test-org-agenda-frame-map-view-changers-fixed-view-deny ()
- "Boundary: view-changing keys are explicitly denied with the fixed-view message,
-not caught by the read-only catch-all."
- (dolist (key '("y" "f" "b" "j"))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd key))
- 'cj/--agenda-frame-denied-fixed-view))))
-
-(ert-deftest test-org-agenda-frame-map-day-week-view-keys ()
- "Normal: d and w toggle the span (day / week) rather than being denied.
-d shrinks the frame to the current day, w restores the seven-day span."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "d"))
- 'cj/--agenda-frame-day-view))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "w"))
- 'cj/--agenda-frame-week-view)))
-
-(ert-deftest test-org-agenda-frame-map-controlled-task-mutations ()
- "Normal: status and priority changes are the frame's controlled mutations.
-C-c t is Craig's requested status chord, C-c C-t keeps Org's standard agenda
-chord, and both Meta and the configured Super arrows support the same
-priority/status operations."
- (dolist (binding '(("C-c t" . org-agenda-todo)
- ("C-c C-t" . org-agenda-todo)
- ("M-<up>" . org-agenda-priority-up)
- ("M-<down>" . org-agenda-priority-down)
- ("M-<left>" . org-agenda-todo-previousset)
- ("M-<right>" . org-agenda-todo-nextset)
- ("s-<up>" . org-agenda-priority-up)
- ("s-<down>" . org-agenda-priority-down)
- ("s-<left>" . org-agenda-todo-previousset)
- ("s-<right>" . org-agenda-todo-nextset)))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd (car binding)))
- (cdr binding)))))
-
-(ert-deftest test-org-agenda-frame-shadow-denies-org-mutations-preserves-allowlist ()
- "Boundary: with the frame mode active over `org-agenda-mode-map', mutating
-org-agenda keys are denied and the allowlist still works.
-The `[t]' default cannot shadow org-agenda-mode-map's explicit bindings, so
-the shadow walk must add explicit denies for every non-allowlisted key --
-single keys (t/I/k/z/s/.), the C-c mutators (schedule/deadline/clock), and
-C-x (save-all) -- while leaving the allowlist (navigation, engage, refresh,
-d/w, C-c C-o) intact."
- (require 'org-agenda)
- (cj/--agenda-frame-shadow-mutations)
- (with-temp-buffer
- (use-local-map org-agenda-mode-map)
- (cj/agenda-frame-mode 1)
- ;; escaping mutators are now explicitly denied (not org's commands)
- (dolist (chord '("t" "I" "k" "z" "s" "." "C-c C-s" "C-c C-d"
- "C-c C-x C-i" "C-x C-s"))
- (should (eq (key-binding (kbd chord))
- 'cj/--agenda-frame-denied-readonly)))
- ;; the allowlist survives the walk
- (should (eq (key-binding (kbd "n")) 'org-agenda-next-line))
- (should (eq (key-binding (kbd "p")) 'org-agenda-previous-line))
- (should (eq (key-binding (kbd "RET")) 'cj/--agenda-frame-engage-open))
- (should (eq (key-binding (kbd "g")) 'cj/--agenda-frame-safe-redo))
- (should (eq (key-binding (kbd "d")) 'cj/--agenda-frame-day-view))
- (should (eq (key-binding (kbd "w")) 'cj/--agenda-frame-week-view))
- (should (eq (key-binding (kbd "C-c t")) 'org-agenda-todo))
- (should (eq (key-binding (kbd "C-c C-t")) 'org-agenda-todo))
- (should (eq (key-binding (kbd "M-<up>")) 'org-agenda-priority-up))
- (should (eq (key-binding (kbd "M-<right>")) 'org-agenda-todo-nextset))
- (should (eq (key-binding (kbd "s-<down>")) 'org-agenda-priority-down))
- (should (eq (key-binding (kbd "s-<left>")) 'org-agenda-todo-previousset))
- (should (eq (key-binding (kbd "C-c C-o")) 'cj/--agenda-frame-open-link))
- (should (eq (key-binding (kbd "q")) 'cj/--agenda-frame-close))))
-
-(ert-deftest test-org-agenda-frame-map-frame-controls-bound ()
- "Normal: the frame's own controls work from inside the frame.
-S-<f8> must close/toggle and C-M-<f8> must force-rescan; unbound, the
-catch-all denies them and the frame can't be closed by its own key."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "S-<f8>"))
- 'cj/agenda-frame-toggle))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-M-<f8>"))
- 'cj/org-agenda-refresh-files)))
-
-(ert-deftest test-org-agenda-frame-map-C-x-C-c-closes-frame ()
- "Normal: C-x C-c in the agenda frame closes the frame, not the daemon.
-The global save-buffers-kill-terminal would kill Emacs itself here (a
-make-frame frame has no client), so the intuitive close gesture must be
-remapped to the frame close."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "C-x C-c"))
- 'cj/--agenda-frame-close)))
-
-(ert-deftest test-org-agenda-frame-map-machinery-punched-through ()
- "Boundary: input machinery is punched through the [t] catch-all.
-switch-frame events, mouse-wheel scrolling, mouse-1 clicks, and the help
-prefix must fall through to their global bindings (an explicit nil shadows
-the default in this map); otherwise every frame-focus change and every
-scroll spams the deny message."
- (dolist (key (list [switch-frame]
- [wheel-up] [wheel-down] [wheel-left] [wheel-right]
- [double-wheel-up] [double-wheel-down]
- [triple-wheel-up] [triple-wheel-down]
- [mouse-1] [down-mouse-1] [drag-mouse-1]
- (kbd "C-h")))
- ;; accept-default t: a punched key returns nil (falls through to the
- ;; global map); an unpunched key returns the catch-all deny handler.
- (should (null (lookup-key cj/agenda-frame-mode-map key t)))))
-
-(ert-deftest test-org-agenda-frame-map-global-escape-chords-denied ()
- "Boundary: global chords bound elsewhere (M-SPC / M-S-SPC swap ai-term
-agents) are denied by an explicit binding, not left to the [t] catch-all.
-A keymap's default binding does not shadow an explicit binding in a
-lower-priority map, so without an explicit deny here M-SPC follows its
-global binding and escapes the read-only frame into ai-term."
- (dolist (key '("M-SPC" "M-S-SPC"))
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd key))
- 'cj/--agenda-frame-denied-readonly))))
-
-(ert-deftest test-org-agenda-frame-map-unpunched-still-denied ()
- "Normal: an ordinary unbound key still hits the catch-all after the punches."
- (should (eq (lookup-key cj/agenda-frame-mode-map (kbd "t") t)
- 'cj/--agenda-frame-denied-readonly)))
-
-(ert-deftest test-org-agenda-frame-maybe-enable-readds-kill-buffer-hook ()
- "Normal: the finalize re-enable also re-adds the buffer-local kill hook.
-org-agenda-redo's kill-all-local-variables strips the hook installed at
-spawn; without the re-add, killing the buffer after the first refresh tick
-orphans the frame."
- (with-temp-buffer
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'agenda))
- ((symbol-function 'get-buffer-window) (lambda (_b _f) 'win)))
- (cj/--agenda-frame-maybe-enable-mode)
- (should (memq 'cj/--agenda-frame-on-kill-buffer
- (buffer-local-value 'kill-buffer-hook (current-buffer)))))))
-
-(ert-deftest test-org-agenda-frame-overlay-removable-after-local-var-wipe ()
- "Error: the failure overlay is found by property, not a buffer-local var.
-kill-all-local-variables (every redo) wipes buffer-local vars while the
-overlay object survives erase-buffer, so a var-held overlay could never be
-removed after a later success -- the failure banner would stick forever."
- (with-temp-buffer
- (insert "x\n")
- (cj/--agenda-frame-show-failure-overlay (current-buffer))
- ;; Simulate the org-agenda-mode reset between failure and success.
- (kill-all-local-variables)
- (cj/--agenda-frame-remove-overlay (current-buffer))
- ;; The visible banner (a before-string overlay) must be gone.
- (should (= 0 (seq-count (lambda (o) (overlay-get o 'before-string))
- (overlays-in (point-min) (point-max)))))))
-
-(ert-deftest test-org-agenda-frame-map-mutation-keys-denied ()
- "Boundary: a mutation key (t = org-agenda-todo) is denied, never allowlisted.
-It is denied two ways depending on whether the shadow walk has run: the `[t]'
-catch-all handles it (lookup returns nil) before the walk, and the walk binds
-it explicitly to the deny handler once `org-agenda-mode-map' is present. Both
-are a read-only denial; the test asserts the outcome, not which path produced
-it, so it holds whether or not org-agenda is loaded in the test process."
- (let ((b (lookup-key cj/agenda-frame-mode-map (kbd "t"))))
- (should (or (null b) (eq b 'cj/--agenda-frame-denied-readonly)))))
-
-;;; Default-deny policy — the minor mode + finalize re-enable
-
-(ert-deftest test-org-agenda-frame-mode-toggles ()
- "Normal: the minor mode turns on and off in a buffer."
- (with-temp-buffer
- (cj/agenda-frame-mode 1)
- (should cj/agenda-frame-mode)
- (cj/agenda-frame-mode -1)
- (should-not cj/agenda-frame-mode)))
-
-(ert-deftest test-org-agenda-frame-maybe-enable-in-agenda-frame ()
- "Normal: after a build in the agenda frame, the policy is re-enabled."
- (with-temp-buffer
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'agenda))
- ((symbol-function 'get-buffer-window) (lambda (_b _f) 'win)))
- (cj/--agenda-frame-maybe-enable-mode)
- (should cj/agenda-frame-mode))))
-
-(ert-deftest test-org-agenda-frame-maybe-enable-skips-other-buffers ()
- "Boundary: a build not shown in the agenda frame leaves the policy off."
- (with-temp-buffer
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'agenda))
- ((symbol-function 'get-buffer-window) (lambda (_b _f) nil)))
- (cj/--agenda-frame-maybe-enable-mode)
- (should-not cj/agenda-frame-mode))))
-
-(ert-deftest test-org-agenda-frame-maybe-enable-no-frame ()
- "Boundary: no agenda frame at all -> policy stays off, no error."
- (with-temp-buffer
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () nil)))
- (cj/--agenda-frame-maybe-enable-mode)
- (should-not cj/agenda-frame-mode))))
-
-;;; Engage routing — frame target + open
-
-(ert-deftest test-org-agenda-frame-target-frame-uses-working-frame ()
- "Normal: the engage target is the working frame when one exists."
- (cl-letf (((symbol-function 'cj/--agenda-frame-working-frame) (lambda () 'work))
- ((symbol-function 'make-frame) (lambda (&rest _) (error "should not create"))))
- (should (eq (cj/--agenda-frame-target-frame) 'work))))
-
-(ert-deftest test-org-agenda-frame-target-frame-creates-when-none ()
- "Boundary: no working frame -> a normal frame is created."
- (cl-letf (((symbol-function 'cj/--agenda-frame-working-frame) (lambda () nil))
- ((symbol-function 'make-frame) (lambda (&rest _) 'new)))
- (should (eq (cj/--agenda-frame-target-frame) 'new))))
-
-(ert-deftest test-org-agenda-frame-engage-open-no-item-errors ()
- "Error: engaging on a line with no source item signals a user-error."
- (cl-letf (((symbol-function 'cj/--agenda-frame-item-marker) (lambda () nil)))
- (should-error (cj/--agenda-frame-engage-open) :type 'user-error)))
-
-(ert-deftest test-org-agenda-frame-engage-open-routes-to-source ()
- "Normal: engage opens the item's source buffer at the item's position."
- (let ((source (generate-new-buffer " *frame-engage-source*")))
- (unwind-protect
- (progn
- (with-current-buffer source (insert "line one\nline two\nline three\n"))
- (let ((marker (set-marker (make-marker) 10 source))
- focused opened)
- (cl-letf (((symbol-function 'cj/--agenda-frame-item-marker) (lambda () marker))
- ((symbol-function 'cj/--agenda-frame-target-frame) (lambda () 'work))
- ((symbol-function 'select-frame-set-input-focus)
- (lambda (f &rest _) (setq focused f)))
- ((symbol-function 'pop-to-buffer-same-window)
- (lambda (b &rest _) (setq opened b) (set-buffer b)))
- ((symbol-function 'org-fold-show-context) (lambda (&rest _) nil)))
- (cj/--agenda-frame-engage-open)
- (should (eq focused 'work))
- (should (eq opened source))
- (should (eq (current-buffer) source))
- (should (= (point) (line-beginning-position))))))
- (kill-buffer source))))
-
-;;; Frame lifecycle — sticky buffer, timer-cancel, teardown cleanup
-
-(ert-deftest test-org-agenda-frame-sticky-buffer-name ()
- "Normal: the sticky buffer is *Org Agenda(F)*; nil when absent."
- (should (null (cj/--agenda-frame-sticky-buffer)))
- (let ((buf (get-buffer-create "*Org Agenda(F)*")))
- (unwind-protect
- (should (eq (cj/--agenda-frame-sticky-buffer) buf))
- (kill-buffer buf))))
-
-(ert-deftest test-org-agenda-frame-cancel-timer-safe-when-none ()
- "Boundary: cancelling with no timer set does nothing and does not error."
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () nil)))
- (should-not (cj/--agenda-frame-cancel-timer))))
-
-;;; Frame lifecycle — toggle dispatch
-
-(ert-deftest test-org-agenda-frame-toggle-spawns-when-none ()
- "Normal: with no agenda frame, toggle spawns one."
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-spawn) (lambda () 'spawned)))
- (should (eq (cj/--agenda-frame-toggle) 'spawned))))
-
-(ert-deftest test-org-agenda-frame-toggle-deletes-when-selected ()
- "Normal: toggle from within the agenda frame deletes it."
- (let (deleted)
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'selected-frame) (lambda () 'af))
- ((symbol-function 'cj/--agenda-frame-delete)
- (lambda () (setq deleted t))))
- (cj/--agenda-frame-toggle)
- (should deleted))))
-
-(ert-deftest test-org-agenda-frame-toggle-raises-when-unfocused ()
- "Normal: toggle from a working frame raises the existing agenda frame."
- (let (raised)
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'selected-frame) (lambda () 'work))
- ((symbol-function 'cj/--agenda-frame-raise)
- (lambda (f) (setq raised f))))
- (cj/--agenda-frame-toggle)
- (should (eq raised 'af)))))
-
-;;; Frame lifecycle — delete
-
-(ert-deftest test-org-agenda-frame-delete-deletes-live-frame ()
- "Normal: delete removes the live agenda frame."
- (let (deleted)
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'frame-live-p) (lambda (f) (eq f 'af)))
- ((symbol-function 'delete-frame) (lambda (f &rest _) (setq deleted f))))
- (cj/--agenda-frame-delete)
- (should (eq deleted 'af)))))
-
-(ert-deftest test-org-agenda-frame-delete-noop-when-none ()
- "Boundary: delete with no agenda frame does nothing."
- (let (called)
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () nil))
- ((symbol-function 'delete-frame) (lambda (_f &rest _) (setq called t))))
- (cj/--agenda-frame-delete)
- (should-not called))))
-
-;;; Frame lifecycle — cleanup on frame death and buffer kill
-
-(ert-deftest test-org-agenda-frame-on-delete-cancels-and-kills-buffer ()
- "Normal: deleting the agenda frame cancels its timer and kills the sticky buffer."
- (let ((buf (get-buffer-create "*Org Agenda(F)*"))
- cancelled)
- (unwind-protect
- (cl-letf (((symbol-function 'cj/--agenda-frame-p) (lambda (_f) t))
- ((symbol-function 'cj/--agenda-frame-cancel-timer)
- (lambda (&optional _f) (setq cancelled t))))
- (cj/--agenda-frame-on-delete-frame 'af)
- (should cancelled)
- (should-not (buffer-live-p buf)))
- (when (buffer-live-p buf) (kill-buffer buf)))))
-
-(ert-deftest test-org-agenda-frame-on-delete-ignores-non-agenda-frame ()
- "Boundary: a non-agenda frame deletion triggers no cleanup."
- (let (cancelled)
- (cl-letf (((symbol-function 'cj/--agenda-frame-p) (lambda (_f) nil))
- ((symbol-function 'cj/--agenda-frame-cancel-timer)
- (lambda (&optional _f) (setq cancelled t))))
- (cj/--agenda-frame-on-delete-frame 'work)
- (should-not cancelled))))
-
-(ert-deftest test-org-agenda-frame-on-kill-buffer-deletes-frame ()
- "Normal: killing the dedicated buffer deletes the frame."
- (let (deleted)
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'frame-live-p) (lambda (f) (eq f 'af)))
- ((symbol-function 'delete-frame) (lambda (f &rest _) (setq deleted f))))
- (cj/--agenda-frame-on-kill-buffer)
- (should (eq deleted 'af)))))
-
-(ert-deftest test-org-agenda-frame-on-kill-buffer-guarded-during-teardown ()
- "Boundary: during a teardown the buffer-kill hook does not re-delete the frame."
- (let (deleted (cj/--agenda-frame-tearing-down t))
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'delete-frame) (lambda (f &rest _) (setq deleted f))))
- (cj/--agenda-frame-on-kill-buffer)
- (should-not deleted))))
-
-;;; Auto-dim suspension while the agenda frame lives
-
-(ert-deftest test-org-agenda-frame-spawn-suspends-auto-dim ()
- "Normal: spawning the frame turns auto-dim off and remembers it was on.
-The refresh tick's selection swing marks the working window non-selected;
-auto-dim's debounced dim then lands after the tick and the working frame
-visibly dims every five minutes."
- (defvar auto-dim-other-buffers-mode)
- (let ((auto-dim-other-buffers-mode t)
- (cj/--agenda-frame-dim-was-on nil)
- calls)
- (cl-letf (((symbol-function 'auto-dim-other-buffers-mode)
- (lambda (arg) (push arg calls)))
- ((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'af))
- ((symbol-function 'select-frame-set-input-focus) (lambda (_f &rest _) nil))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda) (lambda (&rest _) nil))
- ((symbol-function 'delete-other-windows) (lambda (&rest _) nil))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-start-timer) (lambda (_f) nil)))
- (cj/--agenda-frame-spawn)
- (should (equal calls '(-1)))
- (should cj/--agenda-frame-dim-was-on))))
-
-(ert-deftest test-org-agenda-frame-spawn-leaves-auto-dim-when-off ()
- "Boundary: auto-dim already off -> spawn doesn't touch it, no restore later."
- (defvar auto-dim-other-buffers-mode)
- (let ((auto-dim-other-buffers-mode nil)
- (cj/--agenda-frame-dim-was-on nil)
- calls)
- (cl-letf (((symbol-function 'auto-dim-other-buffers-mode)
- (lambda (arg) (push arg calls)))
- ((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'af))
- ((symbol-function 'select-frame-set-input-focus) (lambda (_f &rest _) nil))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda) (lambda (&rest _) nil))
- ((symbol-function 'delete-other-windows) (lambda (&rest _) nil))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-start-timer) (lambda (_f) nil)))
- (cj/--agenda-frame-spawn)
- (should (null calls))
- (should-not cj/--agenda-frame-dim-was-on))))
-
-(ert-deftest test-org-agenda-frame-on-delete-restores-auto-dim ()
- "Normal: closing the frame restores auto-dim when spawn had turned it off."
- (let ((cj/--agenda-frame-dim-was-on t)
- calls)
- (cl-letf (((symbol-function 'auto-dim-other-buffers-mode)
- (lambda (arg) (push arg calls)))
- ((symbol-function 'cj/--agenda-frame-p) (lambda (_f) t))
- ((symbol-function 'cj/--agenda-frame-cancel-timer)
- (lambda (&optional _f) nil)))
- (cj/--agenda-frame-on-delete-frame 'af)
- (should (equal calls '(1)))
- (should-not cj/--agenda-frame-dim-was-on))))
-
-(ert-deftest test-org-agenda-frame-on-delete-no-dim-restore-when-untouched ()
- "Boundary: closing without a suspended auto-dim doesn't enable it."
- (let ((cj/--agenda-frame-dim-was-on nil)
- calls)
- (cl-letf (((symbol-function 'auto-dim-other-buffers-mode)
- (lambda (arg) (push arg calls)))
- ((symbol-function 'cj/--agenda-frame-p) (lambda (_f) t))
- ((symbol-function 'cj/--agenda-frame-cancel-timer)
- (lambda (&optional _f) nil)))
- (cj/--agenda-frame-on-delete-frame 'af)
- (should (null calls)))))
-
-;;; Frame lifecycle — transactional spawn rollback
-
-(ert-deftest test-org-agenda-frame-spawn-rolls-back-on-failure ()
- "Error: a failure after make-frame deletes the partial frame, restores the
-working frame, and signals a user-error."
- (let (deleted focus)
- (cl-letf (((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'pf))
- ((symbol-function 'select-frame-set-input-focus)
- (lambda (f &rest _) (setq focus f)))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda) (lambda (&rest _) (error "boom")))
- ((symbol-function 'frame-live-p) (lambda (f) (memq f '(pf launch))))
- ((symbol-function 'delete-frame) (lambda (f &rest _) (setq deleted f))))
- (should-error (cj/--agenda-frame-spawn) :type 'user-error)
- (should (eq deleted 'pf))
- (should (eq focus 'launch)))))
-
-;;; Phase 2 — wall-clock alignment
-
-(ert-deftest test-org-agenda-frame-seconds-to-next-mark-aligned ()
- "Normal: a time exactly on a 5-minute boundary yields a full period."
- ;; 1000000200 is divisible by 300 (a :00/:05 wall-clock mark).
- (should (= (cj/--agenda-frame-seconds-to-next-mark 1000000200 300) 300)))
-
-(ert-deftest test-org-agenda-frame-seconds-to-next-mark-midway ()
- "Boundary: partway through a period returns the remainder to the next mark."
- ;; 1000000200 + 120 -> 180 seconds remain to the next 300 mark.
- (should (= (cj/--agenda-frame-seconds-to-next-mark 1000000320 300) 180)))
-
-;;; Phase 2 — deterministic point restoration
-
-(defun test-org-agenda-frame--make-agenda-buffer (lines source)
- "Insert LINES into the current buffer; each is (TEXT . SRC-POS).
-A non-nil SRC-POS puts an org-marker into SOURCE at that position on the line."
- (dolist (spec lines)
- (let ((start (point)))
- (insert (car spec) "\n")
- (when (cdr spec)
- (put-text-property start (1+ start) 'org-marker
- (set-marker (make-marker) (cdr spec) source))))))
-
-(ert-deftest test-org-agenda-frame-restore-point-duplicate-nearest ()
- "Normal: a source marker occurring twice restores the occurrence nearest the old line."
- (let ((src (generate-new-buffer " *rp-src*")))
- (unwind-protect
- (with-temp-buffer
- (with-current-buffer src (insert "aaaaaaaaaa\n"))
- (test-org-agenda-frame--make-agenda-buffer
- '(("header" . nil) ("item @2" . 3) ("filler" . nil)
- ("filler" . nil) ("item @5" . 3))
- src)
- (let ((old (set-marker (make-marker) 3 src)))
- (cj/--agenda-frame-restore-point old 4))
- ;; lines 2 and 5 both point at src pos 3; nearest to old-line 4 is line 5.
- (should (= (line-number-at-pos) 5)))
- (kill-buffer src))))
-
-(ert-deftest test-org-agenda-frame-restore-point-missing-clamps ()
- "Boundary: a gone marker clamps the old line into range and lands on an item."
- (let ((src (generate-new-buffer " *rp-src*")))
- (unwind-protect
- (with-temp-buffer
- (with-current-buffer src (insert "aaaaaaaaaa\n"))
- (test-org-agenda-frame--make-agenda-buffer
- '(("header" . nil) ("item" . 3) ("item" . 5))
- src)
- ;; old-line 99 is past the end; clamp to last line (an item).
- (let ((gone (set-marker (make-marker) 99 src)))
- (cj/--agenda-frame-restore-point gone 99))
- (should (get-text-property (line-beginning-position) 'org-marker)))
- (kill-buffer src))))
-
-(ert-deftest test-org-agenda-frame-restore-point-header-goes-to-first-item ()
- "Boundary: with no marker match, a header line moves to the first item."
- (let ((src (generate-new-buffer " *rp-src*")))
- (unwind-protect
- (with-temp-buffer
- (with-current-buffer src (insert "aaaaaaaaaa\n"))
- (test-org-agenda-frame--make-agenda-buffer
- '(("header" . nil) ("item one" . 3) ("item two" . 5))
- src)
- (cj/--agenda-frame-restore-point nil 1) ; line 1 is the header
- (should (= (line-number-at-pos) 2))
- (should (get-text-property (line-beginning-position) 'org-marker)))
- (kill-buffer src))))
-
-(ert-deftest test-org-agenda-frame-restore-point-empty-buffer-start ()
- "Error: an empty (item-less) view leaves point at buffer start."
- (with-temp-buffer
- (test-org-agenda-frame--make-agenda-buffer
- '(("only a header" . nil) ("no items here" . nil)) nil)
- (cj/--agenda-frame-restore-point nil 2)
- (should (= (point) (point-min)))))
-
-;;; Phase 2 — snapshot marker cloning
-
-(ert-deftest test-org-agenda-frame-clone-and-reinstall-markers ()
- "Normal: cloned markers survive nulling the originals and reinstall live."
- (let ((src (generate-new-buffer " *clone-src*")))
- (unwind-protect
- (let (clones)
- (with-temp-buffer
- (with-current-buffer src (insert "0123456789\n"))
- (test-org-agenda-frame--make-agenda-buffer '(("item" . 4)) src)
- (setq clones (cj/--agenda-frame-snapshot-markers (current-buffer)))
- ;; Simulate org-agenda-reset-markers nulling the buffer's originals.
- (let ((orig (get-text-property (point-min) 'org-marker)))
- (set-marker orig nil)))
- ;; Reinstall into a fresh buffer copy; the clone must still be live.
- (with-temp-buffer
- (test-org-agenda-frame--make-agenda-buffer '(("item" . nil)) nil)
- (cj/--agenda-frame-reinstall-markers (current-buffer) clones)
- (let ((m (get-text-property (point-min) 'org-marker)))
- (should (markerp m))
- (should (eq (marker-buffer m) src))
- (should (= (marker-position m) 4)))))
- (kill-buffer src))))
-
-;;; Phase 2 — failure latch (report once per consecutive-failure run)
-
-(ert-deftest test-org-agenda-frame-failure-latch-reports-once ()
- "Normal: the first failure of a run reports; subsequent ones stay silent."
- (let ((params '()))
- (cl-letf (((symbol-function 'frame-parameter)
- (lambda (_f p) (alist-get p params)))
- ((symbol-function 'set-frame-parameter)
- (lambda (_f p v) (setf (alist-get p params) v))))
- (should (cj/--agenda-frame-record-failure 'af)) ; 0 -> 1, report
- (should-not (cj/--agenda-frame-record-failure 'af)) ; 1 -> 2, silent
- (cj/--agenda-frame-clear-failure 'af)
- (should (cj/--agenda-frame-record-failure 'af))))) ; reset -> report again
-
-;;; Phase 2 — timer start + duplicate prevention
-
-(ert-deftest test-org-agenda-frame-start-timer-sets-timer ()
- "Normal: start-timer schedules and stores a timer when none exists."
- (let (stored)
- (cl-letf (((symbol-function 'frame-parameter) (lambda (_f _p) nil))
- ((symbol-function 'run-at-time) (lambda (&rest _) 'the-timer))
- ((symbol-function 'set-frame-parameter)
- (lambda (_f _p v) (setq stored v))))
- (should (eq (cj/--agenda-frame-start-timer 'af) 'the-timer))
- (should (eq stored 'the-timer)))))
-
-(ert-deftest test-org-agenda-frame-start-timer-no-duplicate ()
- "Boundary: a frame already carrying a live timer is not given a second one."
- (let (called)
- (cl-letf (((symbol-function 'frame-parameter) (lambda (_f _p) 'existing))
- ((symbol-function 'timerp) (lambda (x) (eq x 'existing)))
- ((symbol-function 'run-at-time) (lambda (&rest _) (setq called t) 'new)))
- (cj/--agenda-frame-start-timer 'af)
- (should-not called))))
-
-;;; Phase 2 — do-redo orchestration (success and failure branches)
-
-(ert-deftest test-org-agenda-frame-do-redo-success-clears-and-releases ()
- "Normal: a successful redo clears the failure latch and releases the snapshot."
- (let ((params (list (cons 'cj/agenda-frame-fail-count 3)))
- released)
- (with-temp-buffer
- (insert "agenda line\n")
- (cl-letf (((symbol-function 'org-agenda-redo) (lambda (&rest _) nil))
- ((symbol-function 'frame-parameter) (lambda (_f p) (alist-get p params)))
- ((symbol-function 'set-frame-parameter)
- (lambda (_f p v) (setf (alist-get p params) v)))
- ((symbol-function 'cj/--agenda-frame-release-snapshot)
- (lambda (_s) (setq released t))))
- (cj/--agenda-frame-do-redo 'af (current-buffer) nil)
- (should (equal (alist-get 'cj/agenda-frame-fail-count params) 0))
- (should released)))))
-
-(ert-deftest test-org-agenda-frame-do-redo-error-restores-reenables-reports ()
- "Error: a redo that fails mid-rebuild restores the last-good buffer verbatim,
-re-enables the policy, shows one overlay, and reports once."
- (let ((params '()) msgs)
- (with-temp-buffer
- (insert "good agenda content\n")
- (unwind-protect
- (cl-letf (((symbol-function 'org-agenda-redo)
- (lambda (&rest _) (erase-buffer) (insert "PARTIAL") (error "boom")))
- ((symbol-function 'frame-parameter) (lambda (_f p) (alist-get p params)))
- ((symbol-function 'set-frame-parameter)
- (lambda (_f p v) (setf (alist-get p params) v)))
- ((symbol-function 'message)
- (lambda (fmt &rest a) (push (apply #'format fmt a) msgs))))
- (cj/--agenda-frame-do-redo 'af (current-buffer) nil)
- (should cj/agenda-frame-mode) ; policy re-enabled
- (should (= 1 (length (cj/--agenda-frame-failure-overlays
- (current-buffer))))) ; overlay shown
- (should (string-match-p "good agenda content" (buffer-string))) ; restored
- (should-not (string-match-p "PARTIAL" (buffer-string)))
- (should (= 1 (seq-count (lambda (m) (string-match-p "refresh failed" m))
- msgs))))
- (cj/agenda-frame-mode -1)
- (cj/--agenda-frame-remove-overlay (current-buffer))))))
-
-(ert-deftest test-org-agenda-frame-do-redo-follows-item-across-shift ()
- "Normal: on a successful redo, point follows the same source item even when
-lines shift and the buffer's own markers are nulled (the reset-markers case).
-Guards against restoring by raw line number after the item moved."
- (let ((src (generate-new-buffer " *shift-src*"))
- (params '()))
- (unwind-protect
- (with-temp-buffer
- (with-current-buffer src (insert "0123456789\n"))
- ;; Before: item B (src pos 5) sits on line 3, and point is on it.
- (test-org-agenda-frame--make-agenda-buffer
- '(("header" . nil) ("item A" . 3) ("item B" . 5)) src)
- (goto-char (point-min)) (forward-line 2) ; line 3, item B
- (cl-letf (((symbol-function 'frame-parameter) (lambda (_f p) (alist-get p params)))
- ((symbol-function 'set-frame-parameter)
- (lambda (_f p v) (setf (alist-get p params) v)))
- ((symbol-function 'org-agenda-redo)
- (lambda (&rest _)
- ;; Null the buffer's originals (as org-agenda-reset-markers
- ;; does), then rebuild with item B shifted to line 4.
- (save-excursion
- (goto-char (point-min))
- (while (not (eobp))
- (let ((m (get-text-property (line-beginning-position) 'org-marker)))
- (when (markerp m) (set-marker m nil)))
- (forward-line 1)))
- (erase-buffer)
- (test-org-agenda-frame--make-agenda-buffer
- '(("header" . nil) ("new item" . 1) ("item A" . 3) ("item B" . 5))
- src))))
- (cj/--agenda-frame-do-redo 'af (current-buffer) nil))
- ;; Point should be on the rebuilt item B (src pos 5), now line 4 --
- ;; not clamped to old line 3 (which is now item A, src pos 3).
- (let ((m (get-text-property (line-beginning-position) 'org-marker)))
- (should (markerp m))
- (should (= (marker-position m) 5))))
- (kill-buffer src))))
-
-;;; Phase 2 — snapshot round-trip, release, safe-redo window contract, overlay
-
-(ert-deftest test-org-agenda-frame-restore-snapshot-round-trip ()
- "Normal: snapshot then restore reinstates text and a live cloned marker."
- (let ((src (generate-new-buffer " *ss-src*")))
- (unwind-protect
- (with-temp-buffer
- (with-current-buffer src (insert "0123456789\n"))
- (test-org-agenda-frame--make-agenda-buffer '(("item alpha" . 4)) src)
- (let ((snap (cj/--agenda-frame-snapshot (current-buffer) nil)))
- (erase-buffer)
- (insert "CORRUPT")
- (cj/--agenda-frame-restore-snapshot (current-buffer) snap nil)
- (should (string-match-p "item alpha" (buffer-string)))
- (let ((m (get-text-property (point-min) 'org-marker)))
- (should (markerp m))
- (should (eq (marker-buffer m) src))
- (should (= (marker-position m) 4)))
- (cj/--agenda-frame-release-snapshot snap)
- (should-not (marker-buffer (cdr (car (plist-get snap :markers)))))))
- (kill-buffer src))))
-
-(ert-deftest test-org-agenda-frame-safe-redo-selects-and-restores-window ()
- "Normal: the active tick runs the redo and restores the prior window (no focus theft)."
- (let (redone (prev (selected-window)))
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer)
- (lambda () (current-buffer)))
- ((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'get-buffer-window) (lambda (&rest _) prev))
- ((symbol-function 'cj/--agenda-frame-do-redo)
- (lambda (&rest _) (setq redone t))))
- (cj/--agenda-frame-safe-redo)
- (should redone)
- (should (eq (selected-window) prev)))))
-
-(ert-deftest test-org-agenda-frame-overlay-idempotent ()
- "Boundary: showing the failure overlay twice keeps a single overlay."
- (with-temp-buffer
- (insert "x\n")
- (unwind-protect
- (progn
- (cj/--agenda-frame-show-failure-overlay (current-buffer))
- (let ((first (car (cj/--agenda-frame-failure-overlays (current-buffer)))))
- (cj/--agenda-frame-show-failure-overlay (current-buffer))
- (should (equal (cj/--agenda-frame-failure-overlays (current-buffer))
- (list first)))
- (should (= 1 (seq-count (lambda (o) (overlay-get o 'before-string))
- (overlays-in (point-min) (point-max)))))))
- (cj/--agenda-frame-remove-overlay (current-buffer)))))
-
-;;; Phase 2 — public command + F8-family rebind
-
-(ert-deftest test-org-agenda-frame-public-toggle-wraps-private ()
- "Normal: the interactive command delegates to the private toggle."
- (let (called)
- (cl-letf (((symbol-function 'cj/--agenda-frame-toggle)
- (lambda () (setq called t))))
- (call-interactively 'cj/agenda-frame-toggle)
- (should called))))
-
-(ert-deftest test-org-agenda-frame-parameters-normal-tiled-frame ()
- "Boundary: the agenda frame is a normal frame (the tiling WM places it
-side by side with the working frame), carrying the marker and a distinct name."
- (let ((params (cj/--agenda-frame-make-parameters)))
- (should (assq cj/--agenda-frame-parameter params))
- (should (equal (cdr (assq 'name params)) "Full Agenda"))
- ;; No fullscreen request -- a fullboth frame would cover the whole output
- ;; instead of tiling beside the working frame.
- (should-not (assq 'fullscreen params))))
-
-(ert-deftest test-org-agenda-frame-spawn-binds-sticky-and-current-window ()
- "Normal: spawn dynamically binds sticky + current-window around the render.
-The custom command's own settings apply too late to name the buffer, so
-without these bindings the buffer is plain *Org Agenda* -- which matches
-the 0.75 below-selected display rule and splits the new frame with the
-working buffer left on top."
- (let (seen-sticky seen-setup)
- (cl-letf (((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'af))
- ((symbol-function 'select-frame-set-input-focus) (lambda (_f &rest _) nil))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda)
- (lambda (&rest _)
- (setq seen-sticky org-agenda-sticky
- seen-setup org-agenda-window-setup)))
- ((symbol-function 'delete-other-windows) (lambda (&rest _) nil))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-start-timer) (lambda (_f) nil)))
- (cj/--agenda-frame-spawn)
- (should (eq seen-sticky t))
- (should (eq seen-setup 'current-window)))))
-
-(ert-deftest test-org-agenda-frame-spawn-forces-single-window ()
- "Boundary: spawn collapses the frame to one window after rendering.
-A display rule that still splits the frame must not leave a second window
-showing the launch buffer."
- (let (collapsed)
- (cl-letf (((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'af))
- ((symbol-function 'select-frame-set-input-focus) (lambda (_f &rest _) nil))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda) (lambda (&rest _) nil))
- ((symbol-function 'delete-other-windows)
- (lambda (&rest _) (setq collapsed t)))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-start-timer) (lambda (_f) nil)))
- (cj/--agenda-frame-spawn)
- (should collapsed))))
-
-(ert-deftest test-org-agenda-frame-spawn-resets-span-to-7 ()
- "Normal: a fresh spawn opens at the documented seven-day default, even when a
-prior frame's session left `cj/--agenda-frame-span' at the day view."
- (let ((cj/--agenda-frame-span 1))
- (cl-letf (((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'af))
- ((symbol-function 'select-frame-set-input-focus) (lambda (_f &rest _) nil))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda) (lambda (&rest _) nil))
- ((symbol-function 'delete-other-windows) (lambda (&rest _) nil))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-start-timer) (lambda (_f) nil)))
- (cj/--agenda-frame-spawn)
- (should (equal cj/--agenda-frame-span 7)))))
-
-(ert-deftest test-org-agenda-frame-spawn-starts-timer ()
- "Normal: a successful spawn starts the refresh timer for the new frame."
- (let (timed)
- (cl-letf (((symbol-function 'selected-frame) (lambda () 'launch))
- ((symbol-function 'make-frame) (lambda (&rest _) 'af))
- ((symbol-function 'select-frame-set-input-focus) (lambda (_f &rest _) nil))
- ((symbol-function 'cj/build-org-agenda-list) (lambda (&rest _) nil))
- ((symbol-function 'org-agenda) (lambda (&rest _) nil))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'cj/--agenda-frame-start-timer)
- (lambda (f) (setq timed f))))
- (should (eq (cj/--agenda-frame-spawn) 'af))
- (should (eq timed 'af)))))
-
-(ert-deftest test-org-agenda-frame-safe-redo-inhibits-redisplay ()
- "Normal: the tick runs with redisplay inhibited.
-The rebuild takes visible time; without this, the agenda window is the
-selected window for the whole rebuild and the user watches their cursor
-go hollow every five minutes -- indistinguishable from focus theft."
- (let (seen (prev (selected-window)))
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer)
- (lambda () (current-buffer)))
- ((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'get-buffer-window) (lambda (&rest _) prev))
- ((symbol-function 'cj/--agenda-frame-do-redo)
- (lambda (&rest _) (setq seen inhibit-redisplay))))
- (cj/--agenda-frame-safe-redo)
- (should (eq seen t)))))
-
-(ert-deftest test-org-agenda-frame-safe-redo-skips-during-minibuffer ()
- "Boundary: a tick while a minibuffer is active is skipped entirely.
-Reselecting windows under an active minibuffer session can break it; the
-next tick catches up."
- (let (redone (prev (selected-window)))
- (cl-letf (((symbol-function 'active-minibuffer-window) (lambda () 'mini))
- ((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer)
- (lambda () (current-buffer)))
- ((symbol-function 'frame-live-p) (lambda (_f) t))
- ((symbol-function 'get-buffer-window) (lambda (&rest _) prev))
- ((symbol-function 'cj/--agenda-frame-do-redo)
- (lambda (&rest _) (setq redone t))))
- (cj/--agenda-frame-safe-redo)
- (should-not redone))))
-
-(ert-deftest test-org-agenda-frame-safe-redo-noop-when-not-shown ()
- "Boundary: a tick with the buffer not shown in the frame does not redo or error."
- (let (redone)
- (cl-letf (((symbol-function 'cj/--agenda-frame) (lambda () 'af))
- ((symbol-function 'cj/--agenda-frame-sticky-buffer) (lambda () nil))
- ((symbol-function 'get-buffer-window) (lambda (&rest _) nil))
- ((symbol-function 'cj/--agenda-frame-do-redo)
- (lambda (&rest _) (setq redone t))))
- (cj/--agenda-frame-safe-redo)
- (should-not redone))))
-
-(ert-deftest test-org-agenda-frame-install-keys-rebinds-f8-family ()
- "Normal: S-<f8> toggles the frame; the force-rescan moves to C-M-<f8>."
- (let ((map (make-sparse-keymap)))
- (cj/--agenda-frame-install-keys map)
- (should (eq (lookup-key map (kbd "S-<f8>")) 'cj/agenda-frame-toggle))
- (should (eq (lookup-key map (kbd "C-M-<f8>")) 'cj/org-agenda-refresh-files))))
-
-(provide 'test-org-agenda-frame)
-;;; test-org-agenda-frame.el ends here