aboutsummaryrefslogtreecommitdiff
path: root/tests/test-pearl-resolve.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-24 13:44:34 -0500
committerCraig Jennings <c@cjennings.net>2026-05-24 13:44:34 -0500
commitb081d62276378b3168c92c06153fd59db0589535 (patch)
tree9be7f7d22e0c9b4a73432fe744c09bb456c671a9 /tests/test-pearl-resolve.el
downloadpearl-b081d62276378b3168c92c06153fd59db0589535.tar.gz
pearl-b081d62276378b3168c92c06153fd59db0589535.zip
feat: pearl — manage Linear issues from org-mode
Pearl fetches Linear issues into an org file and syncs edits back. It covers list / custom views / saved queries, per-issue and bulk rendering with comments inline, conflict-aware sync of descriptions, titles, and comments, field commands for priority / state / assignee / labels, and a transient dispatch menu. The render folds to a scannable outline and nests issues under a sortable parent. Based on and inspired by Gael Blanchemain's linear-emacs.
Diffstat (limited to 'tests/test-pearl-resolve.el')
-rw-r--r--tests/test-pearl-resolve.el141
1 files changed, 141 insertions, 0 deletions
diff --git a/tests/test-pearl-resolve.el b/tests/test-pearl-resolve.el
new file mode 100644
index 0000000..d8dda7d
--- /dev/null
+++ b/tests/test-pearl-resolve.el
@@ -0,0 +1,141 @@
+;;; test-pearl-resolve.el --- Tests for name->id resolution helpers -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;; Author: Craig Jennings <c@cjennings.net>
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for the per-team cached collection fetch (`--team-collection') and
+;; the name->id resolver (`--resolve-team-id'), which back the field commands,
+;; the ad-hoc filter, and saved queries. The HTTP boundary is stubbed; the
+;; cache is reset around each test.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(defmacro test-pearl--with-clean-caches (&rest body)
+ "Run BODY with the Linear caches reset and an API key set."
+ (declare (indent 0))
+ `(let ((pearl-api-key "test-key")
+ (pearl--cache-team-collections nil)
+ (pearl--cache-states nil)
+ (pearl--cache-teams nil)
+ (pearl--cache-issues nil))
+ ,@body))
+
+(defmacro test-pearl--counting-request (data counter &rest body)
+ "Run BODY with `request' stubbed to succeed with DATA, counting calls in COUNTER."
+ (declare (indent 2))
+ `(cl-letf (((symbol-function 'request)
+ (lambda (_url &rest args)
+ (cl-incf ,counter)
+ (let ((cb (plist-get args :success)))
+ (when cb (funcall cb :data ,data))))))
+ ,@body))
+
+;;; --team-collection
+
+(ert-deftest test-pearl-team-collection-fetches-and-caches ()
+ "A collection is fetched once and served from cache on the second call."
+ (test-pearl--with-clean-caches
+ (let ((calls 0))
+ (test-pearl--counting-request
+ '((data (team (projects (nodes . [((id . "p1") (name . "Foo"))
+ ((id . "p2") (name . "Bar"))]))))) calls
+ (let ((first (pearl--team-collection 'projects "team-1")))
+ (should (= 2 (length first)))
+ (should (string= "p1" (cdr (assoc 'id (car first)))))
+ (pearl--team-collection 'projects "team-1")
+ (should (= 1 calls)))))))
+
+(ert-deftest test-pearl-team-collection-force-refetches ()
+ "A force refresh bypasses the cache and fetches again."
+ (test-pearl--with-clean-caches
+ (let ((calls 0))
+ (test-pearl--counting-request
+ '((data (team (labels (nodes . [((id . "l1") (name . "bug"))]))))) calls
+ (pearl--team-collection 'labels "team-1")
+ (pearl--team-collection 'labels "team-1" t)
+ (should (= 2 calls))))))
+
+(ert-deftest test-pearl-team-collection-keyed-by-team ()
+ "Different teams cache separately."
+ (test-pearl--with-clean-caches
+ (let ((calls 0))
+ (test-pearl--counting-request
+ '((data (team (members (nodes . [((id . "u1") (name . "Craig"))]))))) calls
+ (pearl--team-collection 'members "team-1")
+ (pearl--team-collection 'members "team-2")
+ (should (= 2 calls))))))
+
+;;; --resolve-team-id
+
+(ert-deftest test-pearl-resolve-unique-name ()
+ "A name with one match resolves to that id."
+ (test-pearl--with-clean-caches
+ (cl-letf (((symbol-function 'pearl--team-collection)
+ (lambda (&rest _) '(((id . "p1") (name . "Foo"))
+ ((id . "p2") (name . "Bar"))))))
+ (should (string= "p1" (pearl--resolve-team-id 'projects "Foo" "team-1")))
+ ;; case-insensitive
+ (should (string= "p2" (pearl--resolve-team-id 'projects "bar" "team-1"))))))
+
+(ert-deftest test-pearl-resolve-no-match-is-nil ()
+ "A name with no match resolves to nil."
+ (test-pearl--with-clean-caches
+ (cl-letf (((symbol-function 'pearl--team-collection)
+ (lambda (&rest _) '(((id . "p1") (name . "Foo"))))))
+ (should-not (pearl--resolve-team-id 'projects "Missing" "team-1")))))
+
+(ert-deftest test-pearl-resolve-member-by-display-name-or-email ()
+ "Members match on name, displayName, or email."
+ (test-pearl--with-clean-caches
+ (cl-letf (((symbol-function 'pearl--team-collection)
+ (lambda (&rest _)
+ '(((id . "u1") (name . "Craig Jennings") (displayName . "cj")
+ (email . "c@x.com"))))))
+ (should (string= "u1" (pearl--resolve-team-id 'members "cj" "team-1")))
+ (should (string= "u1" (pearl--resolve-team-id 'members "c@x.com" "team-1"))))))
+
+(ert-deftest test-pearl-resolve-ambiguous-prompts ()
+ "When several nodes share a name, the resolver prompts and returns the choice."
+ (test-pearl--with-clean-caches
+ (cl-letf (((symbol-function 'pearl--team-collection)
+ (lambda (&rest _) '(((id . "u1") (name . "Alex") (displayName . "alex-a"))
+ ((id . "u2") (name . "Alex") (displayName . "alex-b")))))
+ ((symbol-function 'completing-read)
+ (lambda (_prompt collection &rest _)
+ ;; pick the entry whose id is u2
+ (car (rassoc "u2" collection)))))
+ (should (string= "u2" (pearl--resolve-team-id 'members "Alex" "team-1"))))))
+
+;;; clear-cache
+
+(ert-deftest test-pearl-clear-cache-resets ()
+ "Clearing the cache empties the collection and per-team caches."
+ (let ((pearl--cache-team-collections '(((projects . "t") . x)))
+ (pearl--cache-states '(("t" . y)))
+ (pearl--cache-teams '(z)))
+ (pearl-clear-cache)
+ (should-not pearl--cache-team-collections)
+ (should-not pearl--cache-states)
+ (should-not pearl--cache-teams)))
+
+(provide 'test-pearl-resolve)
+;;; test-pearl-resolve.el ends here