;;; test-pearl-filter.el --- Tests for the issue filter DSL -*- 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 Layer 1 issue-filter DSL: `pearl--build-issue-filter' ;; (and its predicate helpers) and `pearl--validate-issue-filter'. All ;; pure -- no network. Each authoring key is checked in isolation, then in ;; combination (sibling clauses AND-ed), with `:state'/`:open' precedence and a ;; json-encode round-trip; validation covers the error cases. ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'json) ;;; predicate helpers (ert-deftest test-pearl-filter-eq-helper () "`--eq' wraps a value in an eq comparator." (should (equal (pearl--eq "x") '(("eq" . "x")))) (should (equal (pearl--eq t) '(("eq" . t))))) (ert-deftest test-pearl-filter-in-nin-helpers-make-vectors () "`--in' / `--nin' encode their values as JSON arrays (vectors)." (should (equal (pearl--in '("a" "b")) '(("in" . ["a" "b"])))) (should (equal (pearl--nin '("a")) '(("nin" . ["a"]))))) ;;; compile-priority (ert-deftest test-pearl-filter-compile-priority-symbol-and-int () "Priority symbols map to numbers; integers pass through." (should (= 1 (pearl--compile-priority 'urgent))) (should (= 0 (pearl--compile-priority 'none))) (should (= 4 (pearl--compile-priority 'low))) (should (= 3 (pearl--compile-priority 3)))) ;;; build-issue-filter -- single dimensions (Normal) (ert-deftest test-pearl-filter-assignee-me () ":assignee :me compiles to assignee.isMe.eq true." (should (equal (pearl--build-issue-filter '(:assignee :me)) '(("assignee" ("isMe" ("eq" . t))))))) (ert-deftest test-pearl-filter-assignee-email () ":assignee with an email compiles to assignee.email.eq." (should (equal (pearl--build-issue-filter '(:assignee "x@y.com")) '(("assignee" ("email" ("eq" . "x@y.com"))))))) (ert-deftest test-pearl-filter-open () ":open t compiles to state.type nin the closed types." (should (equal (pearl--build-issue-filter '(:open t)) '(("state" ("type" ("nin" . ["completed" "canceled" "duplicate"]))))))) (ert-deftest test-pearl-filter-state-name () ":state compiles to state.name.eq." (should (equal (pearl--build-issue-filter '(:state "In Progress")) '(("state" ("name" ("eq" . "In Progress"))))))) (ert-deftest test-pearl-filter-state-type-list () ":state-type with a list compiles to state.type.in." (should (equal (pearl--build-issue-filter '(:state-type ("started" "unstarted"))) '(("state" ("type" ("in" . ["started" "unstarted"]))))))) (ert-deftest test-pearl-filter-state-type-single () ":state-type with a bare string is wrapped into a one-element array." (should (equal (pearl--build-issue-filter '(:state-type "started")) '(("state" ("type" ("in" . ["started"]))))))) (ert-deftest test-pearl-filter-project-team-cycle () ":project / :cycle compile to id.eq; :team to key.eq." (should (equal (pearl--build-issue-filter '(:project "p-1")) '(("project" ("id" ("eq" . "p-1")))))) (should (equal (pearl--build-issue-filter '(:team "ENG")) '(("team" ("key" ("eq" . "ENG")))))) (should (equal (pearl--build-issue-filter '(:cycle "c-1")) '(("cycle" ("id" ("eq" . "c-1"))))))) (ert-deftest test-pearl-filter-labels-any-of () ":labels compiles to labels.some.name.in (carries any of the listed labels)." (should (equal (pearl--build-issue-filter '(:labels ("bug" "p1"))) '(("labels" ("some" ("name" ("in" . ["bug" "p1"])))))))) (ert-deftest test-pearl-filter-priority-symbol () ":priority symbol compiles to priority.eq with the numeric value." (should (equal (pearl--build-issue-filter '(:priority high)) '(("priority" ("eq" . 2)))))) ;;; precedence (:state / :state-type win over :open) (ert-deftest test-pearl-filter-explicit-state-beats-open () "An explicit :state overrides :open." (should (equal (pearl--build-issue-filter '(:open t :state "Done")) '(("state" ("name" ("eq" . "Done"))))))) (ert-deftest test-pearl-filter-state-type-beats-open () ":state-type overrides :open." (should (equal (pearl--build-issue-filter '(:open t :state-type ("started"))) '(("state" ("type" ("in" . ["started"]))))))) ;;; composition (sibling clauses AND-ed) + ordering keys ignored (ert-deftest test-pearl-filter-composition-keeps-all-clauses () "Multiple keys produce sibling clauses; :sort/:order don't affect the filter." (let ((f (pearl--build-issue-filter '(:assignee :me :open t :project "p-1" :labels ("bug") :priority urgent :sort updated :order desc)))) (should (assoc "assignee" f)) (should (assoc "state" f)) (should (assoc "project" f)) (should (assoc "labels" f)) (should (assoc "priority" f)) ;; ordering keys are not part of the IssueFilter (should-not (assoc "sort" f)) (should-not (assoc "order" f)))) ;;; boundary (ert-deftest test-pearl-filter-empty-plist-empty-filter () "An empty plist compiles to an empty filter." (should (null (pearl--build-issue-filter '())))) (ert-deftest test-pearl-filter-priority-zero-kept () ":priority 0 (none) is kept, not treated as absent." (should (equal (pearl--build-issue-filter '(:priority 0)) '(("priority" ("eq" . 0)))))) ;;; json-encode round-trip (proves the alist shape renders the right JSON) (ert-deftest test-pearl-filter-json-encodes-as-expected () "The compiled filter json-encodes to the expected IssueFilter JSON." (should (string= (json-encode (pearl--build-issue-filter '(:assignee :me :open t))) (concat "{\"assignee\":{\"isMe\":{\"eq\":true}}," "\"state\":{\"type\":{\"nin\":" "[\"completed\",\"canceled\",\"duplicate\"]}}}")))) ;;; validation (Error cases) (ert-deftest test-pearl-filter-validate-accepts-good-filter () "A well-formed filter validates to t." (should (eq t (pearl--validate-issue-filter '(:assignee :me :open t :priority high :labels ("bug") :order desc))))) (ert-deftest test-pearl-filter-validate-rejects-unknown-key () "An unknown key is a user-error." (should-error (pearl--validate-issue-filter '(:bogus 1)) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-odd-plist () "A plist with an odd number of elements is a user-error." (should-error (pearl--validate-issue-filter '(:open)) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-bad-priority-symbol () "An unrecognized priority symbol is a user-error." (should-error (pearl--validate-issue-filter '(:priority huge)) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-priority-out-of-range () "A priority integer outside 0-4 is a user-error." (should-error (pearl--validate-issue-filter '(:priority 9)) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-bad-assignee () "An :assignee that is neither :me nor a string is a user-error." (should-error (pearl--validate-issue-filter '(:assignee 42)) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-empty-string () "An empty string for a value key is a user-error." (should-error (pearl--validate-issue-filter '(:project "")) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-bad-order () "An :order other than asc/desc is a user-error." (should-error (pearl--validate-issue-filter '(:order sideways)) :type 'user-error)) (ert-deftest test-pearl-filter-validate-rejects-non-string-label () "A non-string entry in :labels is a user-error." (should-error (pearl--validate-issue-filter '(:labels ("bug" 7))) :type 'user-error)) (provide 'test-pearl-filter) ;;; test-pearl-filter.el ends here