;;; test-pearl-normalize.el --- Tests for API model normalization -*- 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 normalizers that flatten raw json-read Linear responses into ;; internal plists. Driven by the shared fixtures so the renderer-facing ;; contract is locked: vectors become lists, absent/`:json-false' fields become ;; nil, and a null comment author falls back to the bot/external actor. ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'testutil-fixtures (expand-file-name "testutil-fixtures.el")) ;;; node-list (ert-deftest test-pearl-normalize-node-list-vector-to-list () "A connection's nodes vector is returned as a list." (should (equal '(1 2 3) (pearl--node-list '((nodes . [1 2 3])))))) (ert-deftest test-pearl-normalize-node-list-empty-and-missing () "An empty or missing nodes connection yields an empty list." (should (null (pearl--node-list '((nodes . []))))) (should (null (pearl--node-list '((pageInfo . nil)))))) ;;; normalize-issue -- fully populated (ert-deftest test-pearl-normalize-issue-full () "A full issue normalizes every field, flattening nested objects." (let ((i (pearl--normalize-issue (testutil-linear-fixture-issue-full)))) (should (string= "ENG-42" (plist-get i :identifier))) (should (string= "Fix the thing" (plist-get i :title))) (should (= 2 (plist-get i :priority))) (should (string= "In Progress" (plist-get (plist-get i :state) :name))) (should (string= "started" (plist-get (plist-get i :state) :type))) (should (string= "Craig" (plist-get (plist-get i :assignee) :name))) (should (string= "ENG" (plist-get (plist-get i :team) :key))) (should (string= "Platform" (plist-get (plist-get i :project) :name))) (should (string= "Cycle 12" (plist-get (plist-get i :cycle) :name))) ;; labels: vector of nodes -> list of (:id :name) plists (should (equal '("bug" "backend") (mapcar (lambda (l) (plist-get l :name)) (plist-get i :labels)))))) ;;; normalize-issue -- null / missing optional fields (ert-deftest test-pearl-normalize-issue-null-fields () "Absent or null optional fields normalize to nil, not an error." (let ((i (pearl--normalize-issue (testutil-linear-fixture-issue-null-fields)))) (should (string= "ENG-7" (plist-get i :identifier))) (should (null (plist-get i :description))) (should (null (plist-get i :assignee))) (should (null (plist-get i :project))) (should (null (plist-get i :cycle))) (should (null (plist-get i :labels))) ;; state is still present (should (string= "Todo" (plist-get (plist-get i :state) :name))))) (ert-deftest test-pearl-normalize-issue-nil-input () "Normalizing nil yields nil." (should (null (pearl--normalize-issue nil)))) (ert-deftest test-pearl-normalize-issue-omits-comments-when-absent () "An issue fetched without comments has a nil :comments, not an empty list." (let ((i (pearl--normalize-issue (testutil-linear-fixture-issue-full)))) (should (null (plist-get i :comments))))) ;;; normalize-comment -- author fallback (ert-deftest test-pearl-normalize-comment-user-author () "A comment with a user takes the user's name as author." (let* ((raw (car (pearl--node-list (cdr (assoc 'comments (testutil-linear-fixture-issue-with-comments)))))) (c (pearl--normalize-comment raw))) (should (string= "Alice" (plist-get c :author))) (should (string= "First comment" (plist-get c :body))))) (ert-deftest test-pearl-normalize-comment-null-user-falls-back-to-bot () "A comment with a null user falls back to the bot actor's name. `Comment.user' is null for integration/bot comments, so the renderer must not assume a user is present." (let ((c (pearl--normalize-comment '((id . "cm-bot") (body . "Deployed") (createdAt . "2026-05-20T00:00:00Z") (user) (botActor . ((name . "GitHub"))))))) (should (string= "GitHub" (plist-get c :author))))) (ert-deftest test-pearl-normalize-comment-null-user-no-actor-nil () "A null user with no bot or external actor leaves :author nil. The renderer is responsible for showing a placeholder; the normalizer reports the absence honestly rather than inventing a name." (let ((c (pearl--normalize-comment '((id . "cm-x") (body . "x") (user))))) (should (null (plist-get c :author))))) (ert-deftest test-pearl-normalize-comment-bot-without-name-default () "A bot actor with no name falls back to the literal \"automation\"." (let ((c (pearl--normalize-comment '((id . "cm-b") (body . "x") (user) (botActor . ((id . "b1"))))))) (should (string= "automation" (plist-get c :author))))) ;;; normalize-custom-view (ert-deftest test-pearl-normalize-custom-view-personal () "A personal (shared=false) workspace-wide view: :shared nil, :team nil." (let* ((views (cdr (assoc 'nodes (assoc 'customViews (assoc 'data (testutil-linear-fixture-custom-views)))))) (cv (pearl--normalize-custom-view (elt views 0)))) (should (string= "My open work" (plist-get cv :name))) (should (null (plist-get cv :shared))) (should (null (plist-get cv :team))) (should (string= "Craig" (plist-get (plist-get cv :owner) :name))))) (ert-deftest test-pearl-normalize-custom-view-shared-with-team () "A shared team view: :shared t and a normalized :team plist." (let* ((views (cdr (assoc 'nodes (assoc 'customViews (assoc 'data (testutil-linear-fixture-custom-views)))))) (cv (pearl--normalize-custom-view (elt views 1)))) (should (eq t (plist-get cv :shared))) (should (string= "ENG" (plist-get (plist-get cv :team) :key))))) (provide 'test-pearl-normalize) ;;; test-pearl-normalize.el ends here