diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-pearl-accounts.el | 481 |
1 files changed, 481 insertions, 0 deletions
diff --git a/tests/test-pearl-accounts.el b/tests/test-pearl-accounts.el new file mode 100644 index 0000000..c0514a8 --- /dev/null +++ b/tests/test-pearl-accounts.el @@ -0,0 +1,481 @@ +;;; test-pearl-accounts.el --- Tests for pearl multi-account support -*- 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: + +;; Unit tests for the multi-account layer: the `:api-key-source' credential +;; resolver, `pearl--resolve-account', and `pearl--current-account-context'. +;; Live auth-source and the environment are stubbed so the tests need no real +;; credentials and never touch the user's `~/.authinfo.gpg'. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) + +;;; :api-key-source resolver + +(ert-deftest test-pearl-resolve-api-key-source-literal () + "A `:literal' source returns the embedded key verbatim." + (should (string-equal "lin_lit_123" + (pearl--resolve-api-key-source '(:literal "lin_lit_123") "work")))) + +(ert-deftest test-pearl-resolve-api-key-source-env-present () + "An `:env' source reads the named environment variable." + (let ((process-environment (cons "PEARL_TEST_KEY=lin_env_abc" process-environment))) + (should (string-equal "lin_env_abc" + (pearl--resolve-api-key-source '(:env "PEARL_TEST_KEY") "work"))))) + +(ert-deftest test-pearl-resolve-api-key-source-env-missing-errors-names-account () + "An unset `:env' variable errors, and the message names the account." + (let ((process-environment (list "PEARL_UNRELATED=1"))) ; var deliberately absent + (let ((err (should-error + (pearl--resolve-api-key-source '(:env "PEARL_NOPE_VAR") "work") + :type 'error))) + (should (string-match-p "work" (error-message-string err)))))) + +(ert-deftest test-pearl-resolve-api-key-source-auth-source () + "An `:auth-source' source returns the secret auth-source yields." + (cl-letf (((symbol-function 'auth-source-search) + (lambda (&rest _) (list (list :secret "lin_authinfo_xyz"))))) + (should (string-equal "lin_authinfo_xyz" + (pearl--resolve-api-key-source + '(:auth-source :host "api.linear.app" :user "work") "work"))))) + +(ert-deftest test-pearl-resolve-api-key-source-auth-source-secret-function () + "auth-source may hand back the secret as a thunk; the resolver funcalls it." + (cl-letf (((symbol-function 'auth-source-search) + (lambda (&rest _) (list (list :secret (lambda () "lin_thunk_secret")))))) + (should (string-equal "lin_thunk_secret" + (pearl--resolve-api-key-source + '(:auth-source :host "api.linear.app" :user "work") "work"))))) + +(ert-deftest test-pearl-resolve-api-key-source-auth-source-missing-errors-names-account () + "auth-source finding nothing errors with the account name, not lookup internals." + (cl-letf (((symbol-function 'auth-source-search) (lambda (&rest _) nil))) + (let ((err (should-error + (pearl--resolve-api-key-source + '(:auth-source :host "api.linear.app" :user "personal") "personal") + :type 'error))) + (should (string-match-p "personal" (error-message-string err)))))) + +(ert-deftest test-pearl-resolve-api-key-source-unknown-form-errors () + "An unrecognized source tag errors rather than silently yielding nil." + (should-error (pearl--resolve-api-key-source '(:bogus "x") "work") :type 'error)) + +;;; pearl--resolve-account + +(ert-deftest test-pearl-resolve-account-returns-full-context () + "A named account resolves to a context plist with every field populated." + (let ((pearl-accounts + '(("work" :api-key-source (:literal "lin_work_key") + :org-file "/tmp/work-linear.org" + :default-team-id "TEAM_WORK" + :url "https://example.test/graphql")))) + (let ((ctx (pearl--resolve-account "work"))) + (should (string-equal "work" (plist-get ctx :name))) + (should (string-equal "lin_work_key" (plist-get ctx :api-key))) + (should (string-equal "/tmp/work-linear.org" (plist-get ctx :org-file))) + (should (string-equal "TEAM_WORK" (plist-get ctx :default-team-id))) + (should (string-equal "https://example.test/graphql" (plist-get ctx :url)))))) + +(ert-deftest test-pearl-resolve-account-expands-org-file () + "A `~'-relative `:org-file' is expanded to an absolute path in the context." + (let ((pearl-accounts + '(("home" :api-key-source (:literal "k") :org-file "~/pearl-home.org")))) + (should (string-equal (expand-file-name "~/pearl-home.org") + (plist-get (pearl--resolve-account "home") :org-file))))) + +(ert-deftest test-pearl-resolve-account-url-defaults-to-graphql-endpoint () + "An account without `:url' inherits the package's default Linear endpoint." + (let ((pearl-accounts '(("work" :api-key-source (:literal "k") :org-file "/tmp/w.org"))) + (pearl-graphql-url "https://api.linear.app/graphql")) + (should (string-equal "https://api.linear.app/graphql" + (plist-get (pearl--resolve-account "work") :url))))) + +(ert-deftest test-pearl-resolve-account-unknown-name-errors () + "Resolving a name absent from `pearl-accounts' errors." + (let ((pearl-accounts '(("work" :api-key-source (:literal "k") :org-file "/tmp/w.org")))) + (should-error (pearl--resolve-account "ghost") :type 'error))) + +(ert-deftest test-pearl-resolve-account-missing-key-errors-names-account () + "A bad credential source surfaces as an account-named error from resolve." + (let ((pearl-accounts + '(("work" :api-key-source (:auth-source :host "api.linear.app" :user "work") + :org-file "/tmp/w.org")))) + (cl-letf (((symbol-function 'auth-source-search) (lambda (&rest _) nil))) + (let ((err (should-error (pearl--resolve-account "work") :type 'error))) + (should (string-match-p "work" (error-message-string err))))))) + +(ert-deftest test-pearl-resolve-account-does-not-mutate-global-key () + "Resolving an account returns its key in the context, never `setq's the global." + (let ((pearl-accounts '(("work" :api-key-source (:literal "lin_work") :org-file "/tmp/w.org"))) + (pearl-api-key "lin_legacy_global")) + (pearl--resolve-account "work") + (should (string-equal "lin_legacy_global" pearl-api-key)))) + +;;; pearl--current-account-context + +(ert-deftest test-pearl-current-account-context-accounts-nil-returns-nil () + "With no accounts configured the context is nil — legacy single-account mode." + (let ((pearl-accounts nil) + (pearl-active-account nil) + (pearl-default-account nil)) + (should (null (pearl--current-account-context))))) + +(ert-deftest test-pearl-current-account-context-resolves-default-on-first-need () + "Accounts set, none active: the default is resolved and becomes active." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) + (pearl-default-account "home") + (pearl-active-account nil)) + (let ((ctx (pearl--current-account-context))) + (should (string-equal "home" (plist-get ctx :name))) + (should (string-equal "home" pearl-active-account))))) + +(ert-deftest test-pearl-current-account-context-no-default-errors () + "Accounts set with neither an active nor a default account errors." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-default-account nil) + (pearl-active-account nil)) + (should-error (pearl--current-account-context) :type 'error))) + +(ert-deftest test-pearl-current-account-context-uses-active-over-default () + "An explicit active account wins over the configured default." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) + (pearl-default-account "home") + (pearl-active-account "work")) + (should (string-equal "work" (plist-get (pearl--current-account-context) :name))))) + +;;; pearl--with-account-context macro + +(ert-deftest test-pearl-with-account-context-binds-globals () + "Inside the macro the legacy globals reflect the context's values." + (let ((pearl-api-key "legacy") (pearl-graphql-url "legacy-url") + (pearl-org-file-path "/legacy.org") (pearl-default-team-id "LEGACY")) + (pearl--with-account-context + '(:name "work" :api-key "kw" :url "https://w.test/gql" + :org-file "/tmp/w.org" :default-team-id "TEAM_W") + (should (string-equal "kw" pearl-api-key)) + (should (string-equal "https://w.test/gql" pearl-graphql-url)) + (should (string-equal "/tmp/w.org" pearl-org-file-path)) + (should (string-equal "TEAM_W" pearl-default-team-id))))) + +(ert-deftest test-pearl-with-account-context-nil-is-passthrough () + "A nil context leaves the globals untouched (legacy single-account mode)." + (let ((pearl-api-key "legacy") (pearl-org-file-path "/legacy.org")) + (pearl--with-account-context nil + (should (string-equal "legacy" pearl-api-key)) + (should (string-equal "/legacy.org" pearl-org-file-path))))) + +(ert-deftest test-pearl-with-account-context-restores-after () + "The globals are restored to their prior values after the macro body." + (let ((pearl-api-key "legacy") (pearl-org-file-path "/legacy.org")) + (pearl--with-account-context + '(:name "work" :api-key "kw" :url "u" :org-file "/tmp/w.org") + (ignore)) + (should (string-equal "legacy" pearl-api-key)) + (should (string-equal "/legacy.org" pearl-org-file-path)))) + +;;; Dispatch-time snapshot (HP1) + +(ert-deftest test-pearl-request-async-callback-runs-under-dispatch-context () + "request-async re-establishes the dispatch-time context around its callback. +A switch to another account after dispatch must not bleed into the callback — +the callback sees the key and org-file of the account that was active when the +request fired." + (let ((pearl-accounts + (list (list "work" :api-key-source '(:literal "kw") :org-file "/tmp/w.org") + (list "home" :api-key-source '(:literal "kh") :org-file "/tmp/h.org"))) + (pearl-default-account "work") + (pearl-active-account "work") + (pearl--account-context nil) + (captured nil) (seen-key nil) (seen-file nil)) + (cl-letf (((symbol-function 'request) + (lambda (_url &rest args) (setq captured (plist-get args :success))))) + (pearl--graphql-request-async + "q" nil + (lambda (_data) (setq seen-key pearl-api-key + seen-file pearl-org-file-path)))) + ;; The user switches accounts before the response lands. + (setq pearl-active-account "home") + (funcall captured :data '((data))) + (should (string-equal "kw" seen-key)) + (should (string-equal (expand-file-name "/tmp/w.org") seen-file)))) + +(ert-deftest test-pearl-request-async-uses-active-account-key-at-dispatch () + "With accounts configured, request-async builds headers from the active account." + (let ((pearl-accounts + (list (list "work" :api-key-source '(:literal "lin_work_dispatch") + :org-file "/tmp/w.org"))) + (pearl-default-account "work") + (pearl-active-account "work") + (pearl--account-context nil) + (seen-auth nil)) + (cl-letf (((symbol-function 'request) + (lambda (_url &rest args) + (setq seen-auth (cdr (assoc "Authorization" (plist-get args :headers))))))) + (pearl--graphql-request-async "q" nil #'ignore)) + (should (string-equal "lin_work_dispatch" seen-auth)))) + +(ert-deftest test-pearl-request-async-legacy-mode-uses-global-key () + "With no accounts configured, request-async uses the legacy global key unchanged." + (let ((pearl-accounts nil) + (pearl-active-account nil) + (pearl-default-account nil) + (pearl-api-key "lin_legacy_global") + (pearl--account-context nil) + (seen-auth nil)) + (cl-letf (((symbol-function 'request) + (lambda (_url &rest args) + (setq seen-auth (cdr (assoc "Authorization" (plist-get args :headers))))))) + (pearl--graphql-request-async "q" nil #'ignore)) + (should (string-equal "lin_legacy_global" seen-auth)))) + +;;; #+LINEAR-ACCOUNT header + buffer ownership + +(ert-deftest test-pearl-read-buffer-account-present () + "The buffer-account reader returns the #+LINEAR-ACCOUNT name." + (with-temp-buffer + (insert "#+title: x\n#+LINEAR-ACCOUNT: work\n#+LINEAR-SOURCE: (:type filter)\n") + (should (string-equal "work" (pearl--read-buffer-account))))) + +(ert-deftest test-pearl-read-buffer-account-absent () + "A buffer with no #+LINEAR-ACCOUNT marker reads as nil." + (with-temp-buffer + (insert "#+title: x\n#+LINEAR-SOURCE: (:type filter)\n") + (should (null (pearl--read-buffer-account))))) + +(ert-deftest test-pearl-build-org-content-emits-account-header () + "When an account name is supplied, the content carries #+LINEAR-ACCOUNT." + (let ((content (pearl--build-org-content nil nil nil nil "work"))) + (should (string-match-p "^#\\+LINEAR-ACCOUNT: work$" content)))) + +(ert-deftest test-pearl-build-org-content-omits-account-header-in-legacy () + "With no account name the content omits #+LINEAR-ACCOUNT (legacy file)." + (let ((content (pearl--build-org-content nil nil nil nil nil))) + (should-not (string-match-p "LINEAR-ACCOUNT" content)))) + +;;; pearl--require-account-context buffer guard + +(ert-deftest test-pearl-require-account-context-legacy-returns-nil () + "In legacy mode the guard is a no-op, even for a mutation." + (let ((pearl-accounts nil) (pearl-active-account nil) (pearl-default-account nil)) + (with-temp-buffer + (should (null (pearl--require-account-context t)))))) + +(ert-deftest test-pearl-require-account-context-matching-account-proceeds () + "A buffer owned by the active account passes the guard and yields its context." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work") (pearl-default-account "work")) + (with-temp-buffer + (insert "#+LINEAR-ACCOUNT: work\n") + (let ((ctx (pearl--require-account-context t))) + (should (string-equal "work" (plist-get ctx :name))))))) + +(ert-deftest test-pearl-require-account-context-wrong-account-refuses-mutation () + "A mutation from another account's file refuses, naming both accounts." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) + (pearl-active-account "home") (pearl-default-account "home")) + (with-temp-buffer + (insert "#+LINEAR-ACCOUNT: work\n") + (let ((err (should-error (pearl--require-account-context t) :type 'error))) + (should (string-match-p "work" (error-message-string err))) + (should (string-match-p "home" (error-message-string err))))))) + +(ert-deftest test-pearl-require-account-context-wrong-account-refuses-nonmutation () + "Even a non-mutating buffer command refuses against another account's file — +fetching the active account's issues into it would cross workspaces." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) + (pearl-active-account "home") (pearl-default-account "home")) + (with-temp-buffer + (insert "#+LINEAR-ACCOUNT: work\n") + (should-error (pearl--require-account-context nil) :type 'error)))) + +(ert-deftest test-pearl-require-account-context-unmarked-mutation-refuses () + "An unmarked buffer refuses a mutation until a refresh stamps ownership." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work") (pearl-default-account "work")) + (with-temp-buffer + (insert "#+LINEAR-SOURCE: (:type filter)\n") + (should-error (pearl--require-account-context t) :type 'error)))) + +(ert-deftest test-pearl-require-account-context-unmarked-nonmutation-proceeds () + "An unmarked buffer lets a render/list command through (it stamps ownership)." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work") (pearl-default-account "work")) + (with-temp-buffer + (insert "#+LINEAR-SOURCE: (:type filter)\n") + (let ((ctx (pearl--require-account-context nil))) + (should (string-equal "work" (plist-get ctx :name))))))) + +;;; Guard wiring — commands refuse before any API call + +(ert-deftest test-pearl-save-issue-refuses-wrong-account-buffer () + "`pearl-save-issue' from another account's buffer errors before touching the API." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) + (pearl-active-account "home") (pearl-default-account "home") + (api-called nil)) + (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) + (with-temp-buffer + (insert "#+LINEAR-ACCOUNT: work\n#+LINEAR-SOURCE: (:type filter)\n" + "* View\n** TODO thing\n:PROPERTIES:\n:LINEAR-ID: abc\n:END:\n") + (org-mode) + (goto-char (point-max)) + (should-error (pearl-save-issue) :type 'error) + (should-not api-called))))) + +(ert-deftest test-pearl-edit-description-refuses-unmarked-buffer () + "`pearl-edit-description' refuses to mutate an unmarked buffer under accounts." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work") (pearl-default-account "work") + (api-called nil)) + (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) + (with-temp-buffer + (insert "#+LINEAR-SOURCE: (:type filter)\n* View\n** TODO thing\n" + ":PROPERTIES:\n:LINEAR-ID: abc\n:END:\n") + (org-mode) + (goto-char (point-max)) + (should-error (pearl-edit-description) :type 'error) + (should-not api-called))))) + +;;; Cache isolation + switch + indicator + +(ert-deftest test-pearl-clear-account-scoped-state-nils-every-cache () + "The helper clears every account-scoped cache it names." + (let ((pearl--cache-teams '(x)) (pearl--cache-states '(x)) + (pearl--cache-team-collections '(x)) (pearl--cache-views '(x)) + (pearl--cache-viewer '(:id "v"))) + (pearl--clear-account-scoped-state) + (should (null pearl--cache-teams)) + (should (null pearl--cache-states)) + (should (null pearl--cache-team-collections)) + (should (null pearl--cache-views)) + (should (null pearl--cache-viewer)))) + +(ert-deftest test-pearl-switch-account-sets-active-and-clears-caches () + "Switching accounts sets `pearl-active-account' and clears the lookup caches." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w-nope.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h-nope.org"))) + (pearl-active-account "work") + (pearl--cache-teams '(stale)) (pearl--cache-viewer '(:id "stale"))) + (pearl-switch-account "home") + (should (string-equal "home" pearl-active-account)) + (should (null pearl--cache-teams)) + (should (null pearl--cache-viewer)))) + +(ert-deftest test-pearl-switch-account-unknown-name-errors () + "Switching to an unconfigured account errors (via the resolver)." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work")) + (should-error (pearl-switch-account "ghost") :type 'error))) + +(ert-deftest test-pearl-mode-line-lighter-shows-active-account () + "The lighter names the active account when accounts are configured." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work")) + (should (string-equal " Pearl[work]" (pearl--mode-line-lighter))))) + +(ert-deftest test-pearl-mode-line-lighter-plain-in-legacy () + "Without accounts the lighter is the plain \" Pearl\"." + (let ((pearl-accounts nil) (pearl-active-account nil)) + (should (string-equal " Pearl" (pearl--mode-line-lighter))))) + +;;; Saved-query :account guard + +(ert-deftest test-pearl-run-saved-query-refuses-other-account-before-network () + "A saved query tagged for another account refuses before any compilation or fetch." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org") + ("home" :api-key-source (:literal "kh") :org-file "/tmp/h.org"))) + (pearl-active-account "home") (pearl-default-account "home") + (pearl-saved-queries '(("Work bugs" :account "work" :filter (:assignee :me)))) + (api-called nil)) + (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) + (let ((err (should-error (pearl-run-saved-query "Work bugs") :type 'error))) + (should (string-match-p "work" (error-message-string err)))) + (should-not api-called)))) + +(ert-deftest test-pearl-run-saved-query-account-match-proceeds-to-fetch () + "A saved query tagged for the active account compiles and dispatches." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org"))) + (pearl-active-account "work") (pearl-default-account "work") + (pearl-saved-queries '(("Work bugs" :account "work" :filter (:assignee :me)))) + (api-called nil)) + (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) + (pearl-run-saved-query "Work bugs") + (should api-called)))) + +(ert-deftest test-pearl-run-saved-query-account-tag-ignored-in-legacy () + "With no accounts configured, an `:account' tag is inert (shared query)." + (let ((pearl-accounts nil) (pearl-active-account nil) (pearl-default-account nil) + (pearl-api-key "lin_legacy") + (pearl-saved-queries '(("Work bugs" :account "work" :filter (:assignee :me)))) + (api-called nil)) + (cl-letf (((symbol-function 'request) (lambda (&rest _) (setq api-called t)))) + (pearl-run-saved-query "Work bugs") + (should api-called)))) + +;;; pearl-load-api-key-from-env legacy-only + +(ert-deftest test-pearl-load-api-key-from-env-refuses-under-accounts () + "The env loader refuses once accounts are configured, to not bypass the resolver." + (let ((pearl-accounts '(("work" :api-key-source (:literal "kw") :org-file "/tmp/w.org")))) + (should-error (pearl-load-api-key-from-env) :type 'error))) + +;;; Refresh stamps ownership on a legacy file (closes the guard's "refresh first" loop) + +(ert-deftest test-pearl-update-source-header-stamps-missing-account () + "Refreshing an unmarked file under accounts mode stamps #+LINEAR-ACCOUNT, +so a later mutation isn't permanently refused with \"refresh it under an +account first\" — the refresh the guard tells the user to run must resolve." + (let ((pearl--account-context '(:name "work" :api-key "kw" :org-file "/tmp/w.org"))) + (with-temp-buffer + (insert "#+title: x\n#+LINEAR-SOURCE: (:type filter)\n" + "#+LINEAR-RUN-AT: 2020-01-01 00:00\n#+LINEAR-COUNT: 0\n" + "#+LINEAR-TRUNCATED: no\n* View\n") + (pearl--update-source-header 1 nil) + (should (string-equal "work" (pearl--read-buffer-account)))))) + +(ert-deftest test-pearl-update-source-header-leaves-existing-account () + "A file already stamped keeps its marker untouched on refresh." + (let ((pearl--account-context '(:name "work" :api-key "kw" :org-file "/tmp/w.org"))) + (with-temp-buffer + (insert "#+title: x\n#+LINEAR-ACCOUNT: work\n#+LINEAR-SOURCE: (:type filter)\n" + "#+LINEAR-RUN-AT: 2020-01-01 00:00\n#+LINEAR-COUNT: 0\n" + "#+LINEAR-TRUNCATED: no\n* View\n") + (pearl--update-source-header 1 nil) + (should (string-equal "work" (pearl--read-buffer-account))) + ;; exactly one marker, not duplicated + (should (= 1 (count-matches "^#\\+LINEAR-ACCOUNT:" (point-min) (point-max))))))) + +(ert-deftest test-pearl-update-source-header-legacy-mode-no-stamp () + "In legacy mode (no bound context) refresh adds no account marker." + (let ((pearl--account-context nil)) + (with-temp-buffer + (insert "#+title: x\n#+LINEAR-SOURCE: (:type filter)\n" + "#+LINEAR-RUN-AT: 2020-01-01 00:00\n#+LINEAR-COUNT: 0\n" + "#+LINEAR-TRUNCATED: no\n* View\n") + (pearl--update-source-header 1 nil) + (should (null (pearl--read-buffer-account)))))) + +(provide 'test-pearl-accounts) +;;; test-pearl-accounts.el ends here |
