aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-integration-acceptance.el200
1 files changed, 200 insertions, 0 deletions
diff --git a/tests/test-integration-acceptance.el b/tests/test-integration-acceptance.el
new file mode 100644
index 0000000..f83c891
--- /dev/null
+++ b/tests/test-integration-acceptance.el
@@ -0,0 +1,200 @@
+;;; test-integration-acceptance.el --- End-to-end acceptance flow -*- 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:
+
+;; End-to-end acceptance flow exercising the integration contract between the
+;; query implementation and the org-representation implementation. The only
+;; stub is the single GraphQL chokepoint (`pearl--graphql-request-async'): a
+;; dispatcher routes by operation and returns canned, json-read-shaped data, so
+;; every layer above the wire — filter compilation, result classification,
+;; normalization, sorting, rendering, source-header round-trip, in-place merge,
+;; the dirty-buffer guard, the conflict-gated description sync, comment append,
+;; and a field-setter command — runs for real against a temp active file.
+;;
+;; Components integrated (all real unless noted):
+;; - pearl-run-saved-query -> --build-issue-filter -> --query-issues-async
+;; - --query-issues-async -> --graphql-request-async (MOCKED at the wire)
+;; - --render-query-result -> --normalize-issue -> --sort-issues
+;; - --update-org-from-issues (real disk write + buffer surface, surface MOCKED)
+;; - --read-active-source / --build-org-content header round-trip
+;; - pearl-refresh-current-view -> --merge-query-result (in-place merge)
+;; - pearl-sync-current-issue -> --sync-decision -> --update-issue-description-async
+;; - pearl-add-comment -> --create-comment-async -> --append-comment-to-issue
+;; - pearl-set-priority -> --push-issue-field -> --update-issue-async
+
+;;; Code:
+
+(require 'test-bootstrap (expand-file-name "test-bootstrap.el"))
+(require 'cl-lib)
+
+(defvar test-integration--ops nil
+ "Accumulator of GraphQL operations the dispatcher saw, newest first.")
+
+(defun test-integration--nodes ()
+ "Two raw issue nodes, the shape `--normalize-issue' reads."
+ (list
+ '((id . "u-eng-1") (identifier . "ENG-1") (title . "First issue")
+ (description . "Original body.") (priority . 2)
+ (state (name . "Todo") (type . "unstarted"))
+ (url . "https://linear.app/x/ENG-1"))
+ '((id . "u-eng-2") (identifier . "ENG-2") (title . "Second issue")
+ (description . "More text.") (priority . 1)
+ (state (name . "In Progress") (type . "started"))
+ (url . "https://linear.app/x/ENG-2"))))
+
+(defun test-integration--op (query)
+ "Classify QUERY into the operation it represents.
+`commentCreate' and `issueUpdate' are matched before the broader patterns
+because the description-update mutation contains both `IssueDescription' and
+`issueUpdate' in its text."
+ (cond ((string-match-p "commentCreate" query) 'comment-create)
+ ((string-match-p "issueUpdate" query) 'issue-update)
+ ((string-match-p "issues(filter:" query) 'list)
+ ((string-match-p "issue(id:" query) 'fetch-desc)
+ (t 'unknown)))
+
+(defun test-integration--response (query)
+ "Canned, json-read-shaped data for QUERY's operation."
+ (pcase (test-integration--op query)
+ ('list `((data (issues (nodes . ,(vconcat (test-integration--nodes)))
+ (pageInfo (hasNextPage . :json-false)
+ (endCursor . "c"))))))
+ ('fetch-desc '((data (issue (description . "Original body.")
+ (updatedAt . "2026-05-24T00:00:00.000Z")))))
+ ('comment-create '((data (commentCreate
+ (success . t)
+ (comment (id . "cnew") (body . "looks good")
+ (createdAt . "2026-05-24T15:00:00.000Z")
+ (user (name . "Craig")))))))
+ ('issue-update '((data (issueUpdate
+ (success . t)
+ (issue (id . "u-eng-1")
+ (updatedAt . "2026-05-24T16:00:00.000Z"))))))
+ (_ nil)))
+
+(defun test-integration--dispatch (query _variables success-fn _error-fn)
+ "Stand-in for `--graphql-request-async': record QUERY's op and reply canned."
+ (push (test-integration--op query) test-integration--ops)
+ (funcall success-fn (test-integration--response query)))
+
+(defmacro test-integration--with-env (&rest body)
+ "Run BODY against a temp active file with the GraphQL wire stubbed."
+ (declare (indent 0))
+ `(let* ((tmp (make-temp-file "pearl-itest" nil ".org"))
+ (pearl-org-file-path tmp)
+ (pearl-api-key "test-key")
+ (pearl-fold-after-update nil)
+ (pearl-saved-queries
+ '(("Open" :filter (:open t) :sort priority :order asc)
+ ("Bugs" :filter (:labels ("bug") :open t))))
+ (test-integration--ops nil))
+ (when (find-buffer-visiting tmp) (kill-buffer (find-buffer-visiting tmp)))
+ (cl-letf (((symbol-function 'pearl--graphql-request-async)
+ #'test-integration--dispatch)
+ ;; no windows in batch; keep the surface step a no-op
+ ((symbol-function 'pearl--surface-buffer) (lambda (b) b)))
+ (unwind-protect
+ (progn ,@body)
+ (let ((buf (find-buffer-visiting tmp)))
+ (when buf
+ (with-current-buffer buf (set-buffer-modified-p nil))
+ (kill-buffer buf)))
+ (ignore-errors (delete-file tmp))))))
+
+(ert-deftest test-integration-run-saved-query-renders-active-file ()
+ "A saved query fetches, renders to the active file with a source header, and surfaces a buffer."
+ (test-integration--with-env
+ (pearl-run-saved-query "Open")
+ (let ((buf (find-buffer-visiting pearl-org-file-path)))
+ (should (buffer-live-p buf))
+ (with-current-buffer buf
+ (goto-char (point-min))
+ (should (re-search-forward "^#\\+LINEAR-SOURCE: " nil t))
+ (goto-char (point-min))
+ (should (re-search-forward "^#\\+LINEAR-COUNT: 2$" nil t))
+ (goto-char (point-min))
+ (should (re-search-forward "First issue" nil t))
+ (goto-char (point-min))
+ (should (re-search-forward "Second issue" nil t))
+ ;; the serialized source reads back as the filter source
+ (should (eq 'filter (plist-get (pearl--read-active-source) :type)))))
+ ;; the fetch went through the real query chokepoint
+ (should (memq 'list test-integration--ops))))
+
+(ert-deftest test-integration-refresh-reruns-recorded-source ()
+ "Refresh reads the recorded source from the buffer and merges a re-fetch in place."
+ (test-integration--with-env
+ (pearl-run-saved-query "Open")
+ (with-current-buffer (find-buffer-visiting pearl-org-file-path)
+ (setq test-integration--ops nil)
+ (pearl-refresh-current-view)
+ ;; refresh issued a fresh list query for the same source
+ (should (memq 'list test-integration--ops))
+ ;; both issues survive the in-place merge
+ (goto-char (point-min))
+ (should (re-search-forward "First issue" nil t))
+ (goto-char (point-min))
+ (should (re-search-forward "Second issue" nil t)))))
+
+(ert-deftest test-integration-switch-source-protects-dirty-buffer ()
+ "Switching to a different saved query with unsaved edits does not overwrite them."
+ (test-integration--with-env
+ (pearl-run-saved-query "Open")
+ (with-current-buffer (find-buffer-visiting pearl-org-file-path)
+ (goto-char (point-max))
+ (insert "\nUNSAVED LOCAL EDIT\n")
+ (should (buffer-modified-p))
+ ;; the switch must defer rather than clobber the dirty buffer
+ (pearl-run-saved-query "Bugs")
+ (goto-char (point-min))
+ (should (re-search-forward "UNSAVED LOCAL EDIT" nil t)))))
+
+(ert-deftest test-integration-issue-commands-from-subtree ()
+ "From inside a rendered issue subtree: syncing an edited body pushes, adding a comment inserts it, and setting priority drives a field update — all through the real request path."
+ (test-integration--with-env
+ (pearl-run-saved-query "Open")
+ (with-current-buffer (find-buffer-visiting pearl-org-file-path)
+ ;; --- sync a description edit (remote unchanged -> clean push) ---
+ (goto-char (point-min))
+ (re-search-forward "First issue")
+ (re-search-forward "Original body\\.")
+ (insert " EDITED")
+ (setq test-integration--ops nil)
+ (pearl-sync-current-issue)
+ (should (memq 'fetch-desc test-integration--ops))
+ (should (memq 'issue-update test-integration--ops))
+ ;; --- add a comment under the same issue ---
+ (goto-char (point-min))
+ (re-search-forward "First issue")
+ (setq test-integration--ops nil)
+ (pearl-add-comment "looks good")
+ (should (memq 'comment-create test-integration--ops))
+ (goto-char (point-min))
+ (should (re-search-forward "^\\*\\*\\* Comments$" nil t))
+ (should (re-search-forward "looks good" nil t))
+ ;; --- set priority from inside the subtree ---
+ (goto-char (point-min))
+ (re-search-forward "First issue")
+ (setq test-integration--ops nil)
+ (pearl-set-priority "Urgent")
+ (should (memq 'issue-update test-integration--ops)))))
+
+(provide 'test-integration-acceptance)
+;;; test-integration-acceptance.el ends here