diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-calendar-sync--apply-single-exception.el | 37 | ||||
| -rw-r--r-- | tests/test-google-keep-config.el | 142 | ||||
| -rw-r--r-- | tests/test-prog-lsp.el | 66 |
3 files changed, 245 insertions, 0 deletions
diff --git a/tests/test-calendar-sync--apply-single-exception.el b/tests/test-calendar-sync--apply-single-exception.el index 3d2342708..f23104d98 100644 --- a/tests/test-calendar-sync--apply-single-exception.el +++ b/tests/test-calendar-sync--apply-single-exception.el @@ -105,5 +105,42 @@ (plist-get (calendar-sync--apply-single-exception occ exc) :url))))) +;;; Status re-derivation from overridden attendees (chime handoff 2026-06-24) + +(ert-deftest test-calendar-sync--apply-single-exception-declined-occurrence-rederives-status () + "Normal: a declined single occurrence re-derives :status from the override attendees." + (let ((calendar-sync-user-emails '("craig@example.com")) + (occ (list :start '(2026 6 24 16 0) :status "accepted" :uid "abc")) + (exc (list :start '(2026 6 24 16 0) + :attendees (list (list :email "craig@example.com" :partstat "DECLINED"))))) + (should (equal "declined" + (plist-get (calendar-sync--apply-single-exception occ exc) :status))))) + +(ert-deftest test-calendar-sync--apply-single-exception-no-attendee-override-keeps-status () + "Boundary: an exception with no attendee block leaves the inherited :status intact." + (let ((calendar-sync-user-emails '("craig@example.com")) + (occ (list :start '(2026 6 24 16 0) :status "accepted" :uid "abc")) + (exc (list :start '(2026 6 24 16 0) :summary "Moved"))) + (should (equal "accepted" + (plist-get (calendar-sync--apply-single-exception occ exc) :status))))) + +(ert-deftest test-calendar-sync--apply-single-exception-accepted-override-stays-accepted () + "Normal: an accepted attendee override keeps :status accepted." + (let ((calendar-sync-user-emails '("craig@example.com")) + (occ (list :start '(2026 6 24 16 0) :status "accepted" :uid "abc")) + (exc (list :start '(2026 6 24 16 0) + :attendees (list (list :email "craig@example.com" :partstat "ACCEPTED"))))) + (should (equal "accepted" + (plist-get (calendar-sync--apply-single-exception occ exc) :status))))) + +(ert-deftest test-calendar-sync--apply-single-exception-override-without-user-keeps-status () + "Boundary: override attendees that don't include the user leave :status intact." + (let ((calendar-sync-user-emails '("craig@example.com")) + (occ (list :start '(2026 6 24 16 0) :status "accepted" :uid "abc")) + (exc (list :start '(2026 6 24 16 0) + :attendees (list (list :email "someone@else.com" :partstat "DECLINED"))))) + (should (equal "accepted" + (plist-get (calendar-sync--apply-single-exception occ exc) :status))))) + (provide 'test-calendar-sync--apply-single-exception) ;;; test-calendar-sync--apply-single-exception.el ends here diff --git a/tests/test-google-keep-config.el b/tests/test-google-keep-config.el new file mode 100644 index 000000000..690355506 --- /dev/null +++ b/tests/test-google-keep-config.el @@ -0,0 +1,142 @@ +;;; test-google-keep-config.el --- Tests for google-keep-config -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the pure JSON-to-org core of google-keep-config.el (the part that +;; later extracts to a package) plus the parse-render-write chain. The bridge +;; subprocess + auth are the IO boundary, exercised live once the token is set. + +;;; Code: + +(require 'ert) +(require 'google-keep-config) + +(defun test-google-keep--note (&rest overrides) + "Build a note alist (parse-shaped) with OVERRIDES merged in." + (let ((base (list (cons 'id "abc") + (cons 'title "Groceries") + (cons 'text "milk\neggs") + (cons 'labels '("shopping" "home")) + (cons 'pinned nil) + (cons 'archived nil) + (cons 'color "WHITE") + (cons 'updated "2026-06-25T04:00:00Z")))) + (dolist (pair overrides base) + (setf (alist-get (car pair) base) (cdr pair))))) + +;;; cj/keep--parse-json + +(ert-deftest test-google-keep-parse-json-array () + "Normal: a JSON array parses to a list of note alists." + (let ((notes (cj/keep--parse-json + "[{\"id\":\"a\",\"title\":\"T\",\"labels\":[\"x\"],\"pinned\":true}]"))) + (should (= 1 (length notes))) + (should (equal "a" (alist-get 'id (car notes)))) + (should (equal '("x") (alist-get 'labels (car notes)))) + (should (eq t (alist-get 'pinned (car notes)))))) + +(ert-deftest test-google-keep-parse-json-empty () + "Boundary: an empty Keep ([]) parses to an empty list." + (should (null (cj/keep--parse-json "[]")))) + +;;; cj/keep--label-to-tag + +(ert-deftest test-google-keep-label-to-tag-plain () + "Normal: an alphanumeric label is unchanged." + (should (equal "shopping" (cj/keep--label-to-tag "shopping")))) + +(ert-deftest test-google-keep-label-to-tag-sanitizes () + "Boundary: spaces and punctuation become underscores (valid org tag chars)." + (should (equal "to_do_list_" (cj/keep--label-to-tag "to do/list!")))) + +;;; cj/keep--note-tags + +(ert-deftest test-google-keep-note-tags-labels () + "Normal: labels render as a trailing org-tag string." + (should (equal " :shopping:home:" (cj/keep--note-tags (test-google-keep--note))))) + +(ert-deftest test-google-keep-note-tags-archived () + "Normal: an archived note gains the archived tag." + (should (equal " :shopping:home:archived:" + (cj/keep--note-tags (test-google-keep--note (cons 'archived t)))))) + +(ert-deftest test-google-keep-note-tags-none () + "Boundary: no labels and not archived yields an empty tag string." + (should (equal "" (cj/keep--note-tags + (test-google-keep--note (cons 'labels nil)))))) + +;;; cj/keep--note-heading + +(ert-deftest test-google-keep-note-heading-full () + "Normal: a full note renders heading, properties, link, and body." + (let ((s (cj/keep--note-heading (test-google-keep--note)))) + (should (string-match-p "\\`\\* Groceries :shopping:home:\n" s)) + (should (string-match-p ":KEEP_ID: abc\n" s)) + (should (string-match-p ":UPDATED: 2026-06-25T04:00:00Z\n" s)) + (should (string-match-p "\\[\\[https://keep.google.com/#NOTE/abc\\]\\[open in Keep\\]\\]" s)) + (should (string-match-p "milk\neggs\n" s)))) + +(ert-deftest test-google-keep-note-heading-untitled () + "Boundary: an empty title falls back to (untitled)." + (let ((s (cj/keep--note-heading (test-google-keep--note (cons 'title ""))))) + (should (string-match-p "\\`\\* (untitled)" s)))) + +(ert-deftest test-google-keep-note-heading-empty-text () + "Boundary: an empty body emits no trailing text block." + (let ((s (cj/keep--note-heading + (test-google-keep--note (cons 'text "") (cons 'labels nil))))) + (should-not (string-match-p "open in Keep\\]\\]\n.+[^\n]" s)))) + +;;; cj/keep--sort-pinned-first + +(ert-deftest test-google-keep-sort-pinned-first () + "Normal: pinned notes come first, order otherwise preserved." + (let* ((a (test-google-keep--note (cons 'id "a") (cons 'pinned nil))) + (b (test-google-keep--note (cons 'id "b") (cons 'pinned t))) + (c (test-google-keep--note (cons 'id "c") (cons 'pinned nil))) + (sorted (cj/keep--sort-pinned-first (list a b c)))) + (should (equal '("b" "a" "c") (mapcar (lambda (n) (alist-get 'id n)) sorted))))) + +;;; cj/keep--render + +(ert-deftest test-google-keep-render-header-and-notes () + "Normal: the page carries the read-only header and a heading per note." + (let ((s (cj/keep--render (list (test-google-keep--note)) "2026-06-25 04:00"))) + (should (string-match-p "read-only view" s)) + (should (string-match-p "Last refresh: 2026-06-25 04:00" s)) + (should (string-match-p "^\\* Groceries" s)))) + +(ert-deftest test-google-keep-render-empty () + "Boundary: no notes still produces a valid header-only page." + (let ((s (cj/keep--render nil))) + (should (string-match-p "#\\+TITLE: Google Keep" s)) + (should-not (string-match-p "^\\* " s)))) + +;;; cj/keep--write-atomically + the parse-render-write chain + +(ert-deftest test-google-keep-write-atomically () + "Normal: content lands in the target file via temp + rename." + (let* ((dir (make-temp-file "keep-test-" t)) + (file (expand-file-name "keep.org" dir))) + (unwind-protect + (progn + (cj/keep--write-atomically "hello\n" file) + (should (equal "hello\n" + (with-temp-buffer (insert-file-contents file) + (buffer-string))))) + (delete-directory dir t)))) + +(ert-deftest test-google-keep-write-notes-chain () + "Normal: JSON in, a rendered org file out, with the note count returned." + (let* ((dir (make-temp-file "keep-test-" t)) + (keep-file (expand-file-name "keep.org" dir))) + (unwind-protect + (let ((n (cj/keep--write-notes + "[{\"id\":\"a\",\"title\":\"One\",\"labels\":[],\"pinned\":false,\"archived\":false,\"color\":\"WHITE\",\"updated\":\"2026-06-25T04:00:00Z\"}]"))) + (should (= 1 n)) + (should (string-match-p "^\\* One" + (with-temp-buffer (insert-file-contents keep-file) + (buffer-string))))) + (delete-directory dir t)))) + +(provide 'test-google-keep-config) +;;; test-google-keep-config.el ends here diff --git a/tests/test-prog-lsp.el b/tests/test-prog-lsp.el new file mode 100644 index 000000000..7e38111d0 --- /dev/null +++ b/tests/test-prog-lsp.el @@ -0,0 +1,66 @@ +;;; test-prog-lsp.el --- Startup smoke test for LSP config resolution -*- lexical-binding: t; -*- + +;;; Commentary: +;; A narrow smoke test of prog-lsp.el, the central LSP module. It pins the +;; invariants that should hold the moment the config loads, before any server +;; starts: lsp-enable-remote stays nil (so TRAMP files don't auto-start a slow +;; LSP), the file-watch-ignore defaults live in one idempotent place, the eldoc +;; provider is stripped from the global hook, and a mode never accrues a +;; duplicate lsp-deferred entry. The generic :config defaults are deferred to +;; lsp-mode's own load (see the make-test no-package-initialize note in +;; CLAUDE.md), so this tests the top-level :init and helper surface, which runs. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'use-package) +(require 'prog-lsp) + +;; lsp-mode's defcustom isn't loaded under make test, and prog-lsp's bare +;; `(defvar lsp-file-watch-ignored-directories)' only marks it special within +;; that file's unit. Declare it special here too so the `let' bindings below +;; bind dynamically (the helper reads it through the symbol via add-to-list). +(defvar lsp-file-watch-ignored-directories nil) + +(ert-deftest test-prog-lsp-enable-remote-nil () + "Normal: lsp-enable-remote is nil so LSP never auto-starts on TRAMP files." + (should (boundp 'lsp-enable-remote)) + (should (null lsp-enable-remote))) + +(ert-deftest test-prog-lsp-file-watch-adds-extras () + "Normal: the build/cache ignore patterns get appended to lsp's watch-ignore list." + (let ((lsp-file-watch-ignored-directories '("[/\\\\]\\.git\\'"))) + (cj/lsp--add-file-watch-ignored-extras) + (dolist (pattern cj/lsp-file-watch-ignored-extras) + (should (member pattern lsp-file-watch-ignored-directories))) + (should (member "[/\\\\]\\.git\\'" lsp-file-watch-ignored-directories)))) + +(ert-deftest test-prog-lsp-file-watch-idempotent () + "Boundary: adding the extras twice leaves each pattern present exactly once." + (let ((lsp-file-watch-ignored-directories '())) + (cj/lsp--add-file-watch-ignored-extras) + (cj/lsp--add-file-watch-ignored-extras) + (dolist (pattern cj/lsp-file-watch-ignored-extras) + (should (= 1 (cl-count pattern lsp-file-watch-ignored-directories + :test #'equal)))))) + +(ert-deftest test-prog-lsp-eldoc-provider-removed-globally () + "Normal: the global eldoc provider is stripped so lsp can't reattach it." + (let ((eldoc-documentation-functions + (list #'lsp-eldoc-function #'ignore))) + (cj/lsp--remove-eldoc-provider-global) + (should-not (memq 'lsp-eldoc-function eldoc-documentation-functions)) + (should (memq 'ignore eldoc-documentation-functions)))) + +(ert-deftest test-prog-lsp-no-duplicate-mode-hook () + "Boundary: a mode prog-lsp wires never holds more than one lsp-deferred entry. +prog-lsp and the per-language modules both add lsp-deferred for some modes; +add-hook dedups identical symbols, and this pins that invariant so a future +non-symbol (lambda) addition that breaks it gets caught." + (dolist (hook '(c-mode-hook python-mode-hook go-ts-mode-hook)) + (when (boundp hook) + (should (>= 1 (cl-count 'lsp-deferred (symbol-value hook))))))) + +(provide 'test-prog-lsp) +;;; test-prog-lsp.el ends here |
