diff options
| author | Craig Jennings <c@cjennings.net> | 2026-08-03 09:39:08 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-08-03 09:39:08 -0500 |
| commit | eb0c2191b77e795cca7884e0690ea51c515527fd (patch) | |
| tree | b6686bca1c7627d24e5fb6fd0b2df525db56d435 /tests/test-calendar-sync--batch-report.el | |
| parent | 0eb65b6dcc2db54663b39dc7201c3dcf151add74 (diff) | |
| download | dotemacs-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--batch-report.el')
| -rw-r--r-- | tests/test-calendar-sync--batch-report.el | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--batch-report.el b/tests/test-calendar-sync--batch-report.el new file mode 100644 index 00000000..12811200 --- /dev/null +++ b/tests/test-calendar-sync--batch-report.el @@ -0,0 +1,89 @@ +;;; test-calendar-sync--batch-report.el --- Batch report output tests -*- lexical-binding: t; -*- + +;;; Commentary: +;; `calendar-sync-batch-run-and-report' is what the systemd timer runs, so its +;; printed rows are the only record that survives the process. Batch Emacs +;; discards *Messages* at exit, which is where the interactive failure path +;; logs its reason -- so a failed row has to carry its recorded `:last-error' +;; in the printed output or the journal shows "error" with no way to tell a +;; cold gpg-agent from a revoked feed token or a dead network. + +;;; Code: + +(require 'ert) +(require 'cl-lib) ;; cl-letf; calendar-sync pulls it in transitively, don't rely on that + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'calendar-sync) + +(defun test-calendar-sync-batch-report--capture (states results) + "Return the report's printed output for STATES and RESULTS. +STATES is an alist of NAME . PLIST seeded into the state table; RESULTS is +what `calendar-sync-batch-run' is stubbed to return, so the report is +exercised without driving a real sync." + (let ((calendar-sync--calendar-states (make-hash-table :test 'equal))) + (dolist (entry states) + (puthash (car entry) (cdr entry) calendar-sync--calendar-states)) + (cl-letf (((symbol-function 'calendar-sync-batch-run) + (lambda (&rest _) results))) + (with-output-to-string + (calendar-sync-batch-run-and-report))))) + +;;; Normal + +(ert-deftest test-calendar-sync-batch-report-failed-row-carries-its-reason () + "Normal: a failed calendar prints the recorded `:last-error' reason. +Without it the journal records only \"error\", and the operator cannot tell a +cold gpg-agent from a revoked token without re-running the sync by hand." + (let ((out (test-calendar-sync-batch-report--capture + '(("google" . (:status error :last-error "Decryption failed")) + ("proton" . (:status ok))) + '(("google" . error) ("proton" . ok))))) + (should (string-match-p "google: error" out)) + (should (string-match-p "Decryption failed" out)))) + +(ert-deftest test-calendar-sync-batch-report-ok-row-stays-bare () + "Normal: a calendar that synced prints its status and nothing more. +A stale `:last-error' from an earlier failure must not be appended to a row +that succeeded this run." + (let ((out (test-calendar-sync-batch-report--capture + '(("google" . (:status ok :last-error "Decryption failed"))) + '(("google" . ok))))) + (should (string-match-p "google: ok" out)) + (should-not (string-match-p "Decryption failed" out)))) + +;;; Boundary + +(ert-deftest test-calendar-sync-batch-report-failure-without-reason-still-prints () + "Boundary: a failed row with no recorded reason prints its status alone. +`never' and `syncing' never record a `:last-error', so the reason lookup has +to tolerate nil rather than printing \"nil\" or signalling." + (let ((out (test-calendar-sync-batch-report--capture + '(("google" . (:status syncing))) + '(("google" . syncing) ("absent" . never))))) + (should (string-match-p "google: syncing" out)) + (should (string-match-p "absent: never" out)) + (should-not (string-match-p "nil" out)))) + +;;; Error + +(defun test-calendar-sync-batch-report--exit-code (results) + "Return the report's exit code for RESULTS, discarding its printed output." + (let ((calendar-sync--calendar-states (make-hash-table :test 'equal))) + (cl-letf (((symbol-function 'calendar-sync-batch-run) + (lambda (&rest _) results))) + (with-temp-buffer + (let ((standard-output (current-buffer))) + (calendar-sync-batch-run-and-report)))))) + +(ert-deftest test-calendar-sync-batch-report-exit-code-tracks-failures () + "Error: the return value becomes the process exit code, so it stays 1 on any +non-ok row and 0 only when every calendar synced. Appending the reason to the +printed line must not disturb it." + (should (equal 1 (test-calendar-sync-batch-report--exit-code '(("google" . error))))) + (should (equal 1 (test-calendar-sync-batch-report--exit-code + '(("google" . ok) ("proton" . never))))) + (should (equal 0 (test-calendar-sync-batch-report--exit-code '(("google" . ok)))))) + +(provide 'test-calendar-sync--batch-report) +;;; test-calendar-sync--batch-report.el ends here |
