aboutsummaryrefslogtreecommitdiff
path: root/tests/test-calendar-sync--sync-dispatch.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-08-03 09:39:08 -0500
committerCraig Jennings <c@cjennings.net>2026-08-03 09:39:08 -0500
commiteb0c2191b77e795cca7884e0690ea51c515527fd (patch)
treeb6686bca1c7627d24e5fb6fd0b2df525db56d435 /tests/test-calendar-sync--sync-dispatch.el
parent0eb65b6dcc2db54663b39dc7201c3dcf151add74 (diff)
downloaddotemacs-eb0c2191b77e795cca7884e0690ea51c515527fd.tar.gz
dotemacs-eb0c2191b77e795cca7884e0690ea51c515527fd.zip
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.
Diffstat (limited to 'tests/test-calendar-sync--sync-dispatch.el')
-rw-r--r--tests/test-calendar-sync--sync-dispatch.el39
1 files changed, 39 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--sync-dispatch.el b/tests/test-calendar-sync--sync-dispatch.el
index 22deeef0..9b12b167 100644
--- a/tests/test-calendar-sync--sync-dispatch.el
+++ b/tests/test-calendar-sync--sync-dispatch.el
@@ -77,5 +77,44 @@ than crashing."
(should (equal (list cal) ics-calls))
(should (null api-calls)))))
+(ert-deftest test-calendar-sync--sync-dispatch-error-leaf-signal-is-contained ()
+ "Error: a syncer that signals marks the calendar failed instead of propagating.
+
+Resolving a `:secret-host' feed reads authinfo.gpg, and a cold gpg-agent makes
+that signal a `file-error' before any process starts — so the failure arrives
+synchronously, where the async callbacks that normally record a failure never
+run."
+ (let ((failed '())
+ (calendar-sync--calendar-states (make-hash-table :test 'equal)))
+ (cl-letf (((symbol-function 'calendar-sync--sync-calendar-ics)
+ (lambda (_) (signal 'file-error '("Decryption failed"))))
+ ((symbol-function 'calendar-sync--mark-sync-failed)
+ (lambda (name reason) (push (cons name reason) failed))))
+ (calendar-sync--sync-calendar
+ '(:name "google" :url "https://x/y.ics" :file "/tmp/c.org"))
+ (should (equal "google" (car (car failed)))))))
+
+(ert-deftest test-calendar-sync--sync-all-continues-past-a-failing-calendar ()
+ "Error: one calendar's synchronous failure does not stop the ones after it.
+
+This is the whole cost of leaving the signal uncontained: on a machine whose
+feeds resolve through authinfo, the first calendar's decryption error aborted
+the entire run, so calendars that would have synced fine never got the chance."
+ (let ((synced '())
+ (calendar-sync--calendar-states (make-hash-table :test 'equal))
+ (calendar-sync-calendars
+ '((:name "bad" :url "https://x/a.ics" :file "/tmp/a.org")
+ (:name "good" :url "https://x/b.ics" :file "/tmp/b.org"))))
+ (cl-letf (((symbol-function 'calendar-sync--sync-calendar-ics)
+ (lambda (cal)
+ (if (equal (plist-get cal :name) "bad")
+ (signal 'file-error '("Decryption failed"))
+ (push (plist-get cal :name) synced))))
+ ((symbol-function 'calendar-sync--mark-sync-failed)
+ (lambda (&rest _) nil))
+ ((symbol-function 'message) (lambda (&rest _) nil)))
+ (calendar-sync--sync-all-calendars)
+ (should (equal '("good") synced)))))
+
(provide 'test-calendar-sync--sync-dispatch)
;;; test-calendar-sync--sync-dispatch.el ends here