From eb0c2191b77e795cca7884e0690ea51c515527fd Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 3 Aug 2026 09:39:08 -0500 Subject: fix(calendar-sync): run the sync from a timer instead of the agenda hook The hourly timer armed itself from org-agenda-mode-hook, so a session where I never opened the agenda never synced at all. After a reboot that is every session until the first agenda call. I found it with all three calendar files still frozen at the pre-reboot write, five hours stale. The deferral had a real reason. Feed URLs behind :secret-host live in authinfo.gpg, and starting at load would prompt for a passphrase on a cold gpg-agent. The cost was worse than what it bought. A user timer owns the schedule now, every ten minutes. calendar-sync-auto-start defaults to nil so the editor no longer arms its own. scripts/calendar-sync-run is the batch entry point, and it blocks until every calendar leaves the syncing state. The pipeline is asynchronous end to end, so a batch Emacs that returns early exits zero having written nothing. Installing it on a machine that resolves its feeds through :secret-host surfaced two bugs the inline-url machine could never show: - cj/auth-source-secret-value called auth-source-search behind nothing but a declare-function, which quiets the compiler and loads nothing. An interactive Emacs always has auth-source in by the time anyone calls, so the omission stayed invisible until a batch -Q sync died on a void function. The require is guarded on fboundp. An unconditional one reloads auth-source over whatever is already there, which replaced a caller's stubbed search mid-call and sent an existing test out to the real authinfo. - A synchronous failure in one calendar aborted the whole loop. The async callbacks record their own failures, but they never run when the error lands before a process starts. Resolving a :secret-host feed signals outright on a cold agent. Failures are contained per calendar now, so one bad feed no longer costs the other two. A failed row prints the reason it recorded. Batch Emacs discards the *Messages* buffer the interactive path logs to, so without it the journal shows only "error", with no way to tell a cold agent from a revoked token. --- tests/test-calendar-sync--batch-results.el | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/test-calendar-sync--batch-results.el (limited to 'tests/test-calendar-sync--batch-results.el') diff --git a/tests/test-calendar-sync--batch-results.el b/tests/test-calendar-sync--batch-results.el new file mode 100644 index 00000000..03ee2aee --- /dev/null +++ b/tests/test-calendar-sync--batch-results.el @@ -0,0 +1,59 @@ +;;; test-calendar-sync--batch-results.el --- Batch result collection tests -*- lexical-binding: t; -*- + +;;; Commentary: +;; `calendar-sync--batch-results' reads the per-calendar state table and +;; returns one (NAME . STATUS) pair per requested calendar. The batch runner +;; turns that into an exit code, so a calendar that never reached the table at +;; all has to read as `never' rather than nil -- a nil status would compare +;; equal to nothing and quietly drop out of the failure count. + +;;; Code: + +(require 'ert) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'calendar-sync) + +(defun test-calendar-sync-batch-results--with-states (states body) + "Run BODY with STATES (an alist of NAME . PLIST) in the state table." + (let ((calendar-sync--calendar-states (make-hash-table :test 'equal))) + (dolist (entry states) + (puthash (car entry) (cdr entry) calendar-sync--calendar-states)) + (funcall body))) + +(ert-deftest test-calendar-sync-batch-results-reports-each-status () + "Normal: every requested calendar comes back with its recorded status." + (test-calendar-sync-batch-results--with-states + '(("google" . (:status ok)) + ("proton" . (:status error :last-error "boom"))) + (lambda () + (should (equal (calendar-sync--batch-results '("google" "proton")) + '(("google" . ok) ("proton" . error))))))) + +(ert-deftest test-calendar-sync-batch-results-empty-names-is-empty () + "Boundary: no calendars requested yields no rows, not an error." + (test-calendar-sync-batch-results--with-states + '(("google" . (:status ok))) + (lambda () + (should (equal (calendar-sync--batch-results '()) '()))))) + +(ert-deftest test-calendar-sync-batch-results-missing-calendar-reads-never () + "Error: a calendar absent from the state table reads `never', never nil. +A nil status would drop out of the failure count and report success for a +calendar that never ran." + (test-calendar-sync-batch-results--with-states + '(("google" . (:status ok))) + (lambda () + (should (equal (calendar-sync--batch-results '("google" "absent")) + '(("google" . ok) ("absent" . never))))))) + +(ert-deftest test-calendar-sync-batch-results-preserves-request-order () + "Boundary: rows come back in the order asked for, not hash order." + (test-calendar-sync-batch-results--with-states + '(("a" . (:status ok)) ("b" . (:status ok)) ("c" . (:status ok))) + (lambda () + (should (equal (mapcar #'car (calendar-sync--batch-results '("c" "a" "b"))) + '("c" "a" "b")))))) + +(provide 'test-calendar-sync--batch-results) +;;; test-calendar-sync--batch-results.el ends here -- cgit v1.2.3