;;; test-pearl-resolve.el --- Tests for name->id resolution helpers -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; 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 . ;;; 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