aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-03 19:07:48 -0500
committerCraig Jennings <c@cjennings.net>2026-06-03 19:07:48 -0500
commitc23592e3c175b4c31b775b57c3985bfcbef90d9c (patch)
tree35b1c7a90d1f313a8f97657eabc9454e4d82e1f7 /tests
parent8e0f0eedc91558e5d45edeb5a731103ba83e4d8b (diff)
downloadpearl-c23592e3c175b4c31b775b57c3985bfcbef90d9c.tar.gz
pearl-c23592e3c175b4c31b775b57c3985bfcbef90d9c.zip
feat(compose): @-mention Linear users from a picker
Mentioning a teammate meant recalling their exact Linear handle, dotted last names and all. Now a picker inserts it for you. In a comment or description compose buffer, typing @ at the start of a word pops a completing-read over the issue team's members, showing "Full Name (@handle)" so you can find someone by name, and inserts @displayName, the bare handle Linear resolves to a mention (no id or link wrapper). C-; L @ (pearl-mention-user) runs the same picker explicitly and also works inline in an issue buffer, resolving the team from the heading at point. An @ mid-word (an email) stays literal, and cancelling the picker leaves a literal @, so neither case is hijacked. The compose buffer now carries the issue's team id so the picker knows whom to offer. The @ trigger is bound only when a team is in context. @displayName survives the md/org round-trip unchanged, so it reaches Linear intact. Not yet confirmed: that writing a body with @displayName through the API fires Linear's mention notification (the web app does, and stored bodies carry the bare handle, so it almost certainly round-trips). Worth one self-mention write-test before relying on it.
Diffstat (limited to 'tests')
-rw-r--r--tests/test-pearl-compose.el4
-rw-r--r--tests/test-pearl-mention.el116
2 files changed, 118 insertions, 2 deletions
diff --git a/tests/test-pearl-compose.el b/tests/test-pearl-compose.el
index 4f4d611..db56ebc 100644
--- a/tests/test-pearl-compose.el
+++ b/tests/test-pearl-compose.el
@@ -107,7 +107,7 @@ Binds `captured' to the value the on-finish receives (or `:none'). Stubs
(test-pearl--in-issue
(let (sent)
(cl-letf (((symbol-function 'pearl--compose-in-buffer)
- (lambda (_label _instr initial on-finish)
+ (lambda (_label _instr initial on-finish &optional _team-id)
;; the composer would start empty for a new comment
(should (string= "" initial))
(funcall on-finish "see *bold* and `code`")))
@@ -136,7 +136,7 @@ Binds `captured' to the value the on-finish receives (or `:none'). Stubs
(test-pearl--in-issue
(let (seeded synced)
(cl-letf (((symbol-function 'pearl--compose-in-buffer)
- (lambda (_label _instr initial on-finish)
+ (lambda (_label _instr initial on-finish &optional _team-id)
(setq seeded initial)
(funcall on-finish "Edited description")))
((symbol-function 'pearl-sync-current-issue)
diff --git a/tests/test-pearl-mention.el b/tests/test-pearl-mention.el
new file mode 100644
index 0000000..dd0ef0d
--- /dev/null
+++ b/tests/test-pearl-mention.el
@@ -0,0 +1,116 @@
+;;; test-pearl-mention.el --- Tests for @-mentioning Linear users -*- 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 user @-mention picker: the candidate builder, the insert
+;; command (`pearl-mention-user'), the compose-buffer self-inserting `@'
+;; trigger, and the keymap binding.
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(defun test-pearl-mention--members ()
+ "Member node alists as `pearl--team-collection' returns them."
+ '(((id . "u1") (name . "Eric Bell") (displayName . "eric") (email . "e@x"))
+ ((id . "u2") (name . "vrezh.mikayelyan@deepsat.com")
+ (displayName . "vrezh.mikayelyan") (email . "v@x"))
+ ((id . "u3") (name . "Bot") (displayName . nil) (email . "b@x"))))
+
+;;; --mention-candidates
+
+(ert-deftest test-pearl-mention-candidates-maps-to-handle ()
+ "Each member maps a display string to its `@displayName' token; the display
+carries the full name so you can find a teammate without recalling the handle."
+ (let ((cands (pearl--mention-candidates (test-pearl-mention--members))))
+ (should (equal "@eric" (cdr (assoc "Eric Bell (@eric)" cands))))
+ (should (equal "@vrezh.mikayelyan"
+ (cdr (assoc "vrezh.mikayelyan@deepsat.com (@vrezh.mikayelyan)" cands))))))
+
+(ert-deftest test-pearl-mention-candidates-drops-handleless ()
+ "A member with no displayName has no mention handle and is dropped."
+ (let ((cands (pearl--mention-candidates (test-pearl-mention--members))))
+ (should-not (cl-find "@" (mapcar #'cdr cands)
+ :test (lambda (_ v) (string-match-p "Bot" (or v "")))))
+ (should (= 2 (length cands)))))
+
+;;; pearl-mention-user (insert at point)
+
+(ert-deftest test-pearl-mention-user-inserts-handle-at-point ()
+ "With a team in context, the command inserts the chosen `@displayName'."
+ (cl-letf (((symbol-function 'pearl--team-collection)
+ (lambda (_kind _team &optional _force) (test-pearl-mention--members)))
+ ((symbol-function 'completing-read)
+ (lambda (&rest _) "Eric Bell (@eric)")))
+ (with-temp-buffer
+ (insert "*** TODO Title\n:PROPERTIES:\n:LINEAR-ID: a\n:LINEAR-TEAM-ID: t1\n:END:\nPing ")
+ (org-mode)
+ (goto-char (point-max))
+ (pearl-mention-user)
+ (should (string-suffix-p "Ping @eric" (buffer-substring (line-beginning-position) (point)))))))
+
+(ert-deftest test-pearl-mention-user-no-team-errors ()
+ "Outside any team context (no compose team, no issue heading) it errors."
+ (with-temp-buffer
+ (insert "plain text, no Linear heading")
+ (org-mode)
+ (goto-char (point-max))
+ (should-error (pearl-mention-user) :type 'user-error)))
+
+;;; keymap
+
+(ert-deftest test-pearl-mention-bound-under-prefix ()
+ "C-; L @ resolves to the mention command."
+ (should (eq 'pearl-mention-user (lookup-key pearl-prefix-map "@"))))
+
+;;; compose-buffer self-inserting @
+
+(ert-deftest test-pearl-compose-at-self-insert-picks-at-boundary ()
+ "At a token boundary the `@' trigger pops the picker and inserts the handle."
+ (cl-letf (((symbol-function 'pearl--read-mention) (lambda (_team) "@eric")))
+ (with-temp-buffer
+ (setq-local pearl--compose-team-id "t1")
+ (insert "Ping ") ; point preceded by a space = boundary
+ (pearl--compose-at-self-insert)
+ (should (string= "Ping @eric" (buffer-string))))))
+
+(ert-deftest test-pearl-compose-at-self-insert-literal-mid-token ()
+ "Mid-token (e.g. inside an email) `@' inserts a literal at-sign, no picker."
+ (cl-letf (((symbol-function 'pearl--read-mention)
+ (lambda (_team) (error "picker should not run mid-token"))))
+ (with-temp-buffer
+ (setq-local pearl--compose-team-id "t1")
+ (insert "foo") ; point after a word char = not a boundary
+ (pearl--compose-at-self-insert)
+ (should (string= "foo@" (buffer-string))))))
+
+(ert-deftest test-pearl-compose-at-self-insert-cancel-leaves-literal ()
+ "Cancelling the picker (quit) leaves a literal `@' rather than aborting input."
+ (cl-letf (((symbol-function 'pearl--read-mention)
+ (lambda (_team) (signal 'quit nil))))
+ (with-temp-buffer
+ (setq-local pearl--compose-team-id "t1")
+ (insert "Ping ")
+ (pearl--compose-at-self-insert)
+ (should (string= "Ping @" (buffer-string))))))
+
+(provide 'test-pearl-mention)
+;;; test-pearl-mention.el ends here