;;; beadwork-cli-test.el --- Tests for beadwork-cli.el -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Philip Munksgaard ;; SPDX-License-Identifier: GPL-3.0-or-later ;;; Commentary: ;; ERT tests for the beadwork CLI interface layer. ;;; Code: (require 'ert) (require 'cl-lib) (require 'beadwork-cli) ;;;; Test helpers (defvar beadwork-test--sample-issue '((id . "bw-test--abc") (title . "Test issue") (status . "open") (priority . 2) (type . "task") (assignee . "alice") (description . "A test issue description") (labels . ["backend" "urgent"]) (blocked_by . ["bw-test--xyz"]) (blocks . ["bw-test--def"]) (comments . [((text . "First comment") (timestamp . "2026-05-13T14:00:00Z")) ((text . "Second comment") (timestamp . "2026-05-13T14:01:00Z"))]) (created . "2026-05-13T14:00:00Z") (updated_at . "2026-05-13T14:01:00Z")) "A sample issue alist for testing.") (defvar beadwork-test--sample-issue-minimal '((id . "bw-test--min") (title . "Minimal issue") (status . "open") (priority . 3) (type . "bug") (assignee . "") (description . "") (labels . []) (blocked_by . []) (blocks . []) (comments . []) (created . "2026-05-13T14:00:00Z") (updated_at . "2026-05-13T14:00:00Z")) "A minimal issue alist for testing.") (defmacro beadwork-test--with-mock-cli (exit-code stdout stderr &rest body) "Execute BODY with `call-process' mocked. EXIT-CODE is the fake exit code. STDOUT is a string to write to the output buffer. STDERR is a string to write to the stderr file." (declare (indent 3)) `(cl-letf (((symbol-function 'call-process) (lambda (_program _infile destination _display &rest _args) (when (and (listp destination) (car destination)) (with-current-buffer (car destination) (insert ,stdout))) (when (and (listp destination) (cadr destination) (stringp (cadr destination))) (with-temp-file (cadr destination) (insert ,stderr))) ,exit-code)) ((symbol-function 'beadwork-cli-project-root) (lambda () "/tmp/fake-project/"))) ,@body)) (defmacro beadwork-test--with-mock-run (return-value &rest body) "Execute BODY with `beadwork-cli-run' returning RETURN-VALUE." (declare (indent 1)) `(cl-letf (((symbol-function 'beadwork-cli-run) (lambda (&rest _args) ,return-value)) ((symbol-function 'beadwork-cli-project-root) (lambda () "/tmp/fake-project/"))) ,@body)) (defmacro beadwork-test--with-mock-run-capture (capture-var return-value &rest body) "Execute BODY with `beadwork-cli-run' returning RETURN-VALUE. CAPTURE-VAR is set to the list of args passed to `beadwork-cli-run'." (declare (indent 2)) `(let ((,capture-var nil)) (cl-letf (((symbol-function 'beadwork-cli-run) (lambda (&rest args) (setq ,capture-var args) ,return-value)) ((symbol-function 'beadwork-cli-project-root) (lambda () "/tmp/fake-project/"))) ,@body))) ;;;; Issue accessor tests (ert-deftest beadwork-test-issue-id () "Test `beadwork-cli-issue-id'." (should (equal "bw-test--abc" (beadwork-cli-issue-id beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-title () "Test `beadwork-cli-issue-title'." (should (equal "Test issue" (beadwork-cli-issue-title beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-status () "Test `beadwork-cli-issue-status'." (should (equal "open" (beadwork-cli-issue-status beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-priority () "Test `beadwork-cli-issue-priority'." (should (equal 2 (beadwork-cli-issue-priority beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-type () "Test `beadwork-cli-issue-type'." (should (equal "task" (beadwork-cli-issue-type beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-assignee () "Test `beadwork-cli-issue-assignee'." (should (equal "alice" (beadwork-cli-issue-assignee beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-description () "Test `beadwork-cli-issue-description'." (should (equal "A test issue description" (beadwork-cli-issue-description beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-labels () "Test `beadwork-cli-issue-labels' converts vector to list." (should (equal '("backend" "urgent") (beadwork-cli-issue-labels beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-labels-empty () "Test `beadwork-cli-issue-labels' with empty vector." (should (equal nil (beadwork-cli-issue-labels beadwork-test--sample-issue-minimal)))) (ert-deftest beadwork-test-issue-blocked-by () "Test `beadwork-cli-issue-blocked-by' converts vector to list." (should (equal '("bw-test--xyz") (beadwork-cli-issue-blocked-by beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-blocks () "Test `beadwork-cli-issue-blocks' converts vector to list." (should (equal '("bw-test--def") (beadwork-cli-issue-blocks beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-comments () "Test `beadwork-cli-issue-comments' converts vector to list." (let ((comments (beadwork-cli-issue-comments beadwork-test--sample-issue))) (should (= 2 (length comments))) (should (equal "First comment" (alist-get 'text (car comments)))))) (ert-deftest beadwork-test-issue-created () "Test `beadwork-cli-issue-created'." (should (equal "2026-05-13T14:00:00Z" (beadwork-cli-issue-created beadwork-test--sample-issue)))) (ert-deftest beadwork-test-issue-updated () "Test `beadwork-cli-issue-updated'." (should (equal "2026-05-13T14:01:00Z" (beadwork-cli-issue-updated beadwork-test--sample-issue)))) ;;;; CLI run tests (ert-deftest beadwork-test-cli-run-parses-json () "Test that `beadwork-cli-run' parses JSON output." (beadwork-test--with-mock-cli 0 "{\"id\": \"bw-test--abc\", \"title\": \"Hello\"}" "" (let ((result (beadwork-cli-run "show" "bw-test--abc"))) (should (equal "bw-test--abc" (alist-get 'id result))) (should (equal "Hello" (alist-get 'title result)))))) (ert-deftest beadwork-test-cli-run-empty-output () "Test that `beadwork-cli-run' returns nil for empty output." (beadwork-test--with-mock-cli 0 "" "" (should (null (beadwork-cli-run "delete" "bw-test--abc"))))) (ert-deftest beadwork-test-cli-run-error-on-nonzero () "Test that `beadwork-cli-run' signals error on non-zero exit." (beadwork-test--with-mock-cli 1 "" "something went wrong" (should-error (beadwork-cli-run "show" "bad-id") :type 'beadwork-cli-error))) (ert-deftest beadwork-test-cli-run-error-on-bad-json () "Test that `beadwork-cli-run' signals error on invalid JSON." (beadwork-test--with-mock-cli 0 "not json at all" "" (should-error (beadwork-cli-run "show" "bw-test--abc") :type 'beadwork-cli-error))) ;;;; CLI command wrapper tests (ert-deftest beadwork-test-cli-list-no-filters () "Test `beadwork-cli-list' with no filters." (beadwork-test--with-mock-run-capture captured-args (vector beadwork-test--sample-issue) (let ((result (beadwork-cli-list))) (should (equal '("list") captured-args)) (should (= 1 (length result))) (should (equal "bw-test--abc" (beadwork-cli-issue-id (car result))))))) (ert-deftest beadwork-test-cli-list-with-filters () "Test `beadwork-cli-list' passes filter arguments." (beadwork-test--with-mock-run-capture captured-args (vector) (beadwork-cli-list '(:status "open" :priority 1 :type "bug")) (should (member "--status" captured-args)) (should (member "open" captured-args)) (should (member "--priority" captured-args)) (should (member "1" captured-args)) (should (member "--type" captured-args)) (should (member "bug" captured-args)))) (ert-deftest beadwork-test-cli-list-with-boolean-flags () "Test `beadwork-cli-list' passes boolean flags." (beadwork-test--with-mock-run-capture captured-args (vector) (beadwork-cli-list '(:all t :deferred t :overdue t)) (should (member "--all" captured-args)) (should (member "--deferred" captured-args)) (should (member "--overdue" captured-args)))) (ert-deftest beadwork-test-cli-show () "Test `beadwork-cli-show' passes the correct ID." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (let ((result (beadwork-cli-show "bw-test--abc"))) (should (equal '("show" "bw-test--abc") captured-args)) (should (equal "bw-test--abc" (beadwork-cli-issue-id result)))))) (ert-deftest beadwork-test-cli-create-minimal () "Test `beadwork-cli-create' with only a title." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-create "New issue") (should (equal '("create" "New issue") captured-args)))) (ert-deftest beadwork-test-cli-create-with-options () "Test `beadwork-cli-create' with all options." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-create "New issue" '(:priority 1 :type "bug" :description "desc")) (should (member "--priority" captured-args)) (should (member "1" captured-args)) (should (member "--type" captured-args)) (should (member "bug" captured-args)) (should (member "--description" captured-args)) (should (member "desc" captured-args)))) (ert-deftest beadwork-test-cli-update () "Test `beadwork-cli-update' passes correct arguments." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-update "bw-test--abc" '(:title "Updated" :priority 0)) (should (equal "update" (car captured-args))) (should (equal "bw-test--abc" (cadr captured-args))) (should (member "--title" captured-args)) (should (member "Updated" captured-args)) (should (member "--priority" captured-args)) (should (member "0" captured-args)))) (ert-deftest beadwork-test-cli-close-no-reason () "Test `beadwork-cli-close' without reason." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-close "bw-test--abc") (should (equal '("close" "bw-test--abc") captured-args)))) (ert-deftest beadwork-test-cli-close-with-reason () "Test `beadwork-cli-close' with reason." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-close "bw-test--abc" "duplicate") (should (equal '("close" "bw-test--abc" "--reason" "duplicate") captured-args)))) (ert-deftest beadwork-test-cli-start () "Test `beadwork-cli-start'." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-start "bw-test--abc") (should (equal '("start" "bw-test--abc") captured-args)))) (ert-deftest beadwork-test-cli-start-with-assignee () "Test `beadwork-cli-start' with assignee." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-start "bw-test--abc" "bob") (should (equal '("start" "bw-test--abc" "--assignee" "bob") captured-args)))) (ert-deftest beadwork-test-cli-reopen () "Test `beadwork-cli-reopen'." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-reopen "bw-test--abc") (should (equal '("reopen" "bw-test--abc") captured-args)))) (ert-deftest beadwork-test-cli-comment () "Test `beadwork-cli-comment'." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-comment "bw-test--abc" "my comment") (should (equal '("comment" "bw-test--abc" "my comment") captured-args)))) (ert-deftest beadwork-test-cli-comment-with-author () "Test `beadwork-cli-comment' with author." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-comment "bw-test--abc" "my comment" "bob") (should (equal '("comment" "bw-test--abc" "my comment" "--author" "bob") captured-args)))) (ert-deftest beadwork-test-cli-label-add () "Test `beadwork-cli-label-add'." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-label-add "bw-test--abc" "frontend" "urgent") (should (equal '("label" "bw-test--abc" "+frontend" "+urgent") captured-args)))) (ert-deftest beadwork-test-cli-label-remove () "Test `beadwork-cli-label-remove'." (beadwork-test--with-mock-run-capture captured-args beadwork-test--sample-issue (beadwork-cli-label-remove "bw-test--abc" "backend") (should (equal '("label" "bw-test--abc" "-backend") captured-args)))) (ert-deftest beadwork-test-cli-dep-add () "Test `beadwork-cli-dep-add'." (beadwork-test--with-mock-run-capture captured-args nil (beadwork-cli-dep-add "bw-test--abc" "bw-test--def") (should (equal '("dep" "add" "bw-test--abc" "blocks" "bw-test--def") captured-args)))) (ert-deftest beadwork-test-cli-dep-remove () "Test `beadwork-cli-dep-remove'." (beadwork-test--with-mock-run-capture captured-args nil (beadwork-cli-dep-remove "bw-test--abc" "bw-test--def") (should (equal '("dep" "remove" "bw-test--abc" "blocks" "bw-test--def") captured-args)))) (ert-deftest beadwork-test-cli-history () "Test `beadwork-cli-history'." (let ((sample-history (vector '((hash . "abc123") (timestamp . "2026-05-13 14:00") (author . "beadwork") (intent . "create bw-test--abc"))))) (beadwork-test--with-mock-run-capture captured-args sample-history (let ((result (beadwork-cli-history "bw-test--abc"))) (should (equal '("history" "bw-test--abc") captured-args)) (should (= 1 (length result))))))) (ert-deftest beadwork-test-cli-history-with-limit () "Test `beadwork-cli-history' with limit." (beadwork-test--with-mock-run-capture captured-args (vector) (beadwork-cli-history "bw-test--abc" 5) (should (equal '("history" "bw-test--abc" "--limit" "5") captured-args)))) (ert-deftest beadwork-test-cli-ready () "Test `beadwork-cli-ready'." (beadwork-test--with-mock-run (vector beadwork-test--sample-issue) (let ((result (beadwork-cli-ready))) (should (= 1 (length result)))))) (ert-deftest beadwork-test-cli-blocked () "Test `beadwork-cli-blocked'." (beadwork-test--with-mock-run (vector beadwork-test--sample-issue) (let ((result (beadwork-cli-blocked))) (should (= 1 (length result)))))) ;;;; Project root detection tests (ert-deftest beadwork-test-project-root-signals-when-not-found () "Test that `beadwork-cli-project-root' signals error when no project." (cl-letf (((symbol-function 'beadwork-cli--detect-project-root) (lambda (&optional _dir) nil))) (should-error (beadwork-cli-project-root) :type 'beadwork-project-error))) (ert-deftest beadwork-test-project-root-cache-clearing () "Test that `beadwork-cli-clear-project-cache' clears the cache." (let ((beadwork-cli--project-root-cache '(("/tmp" . "/tmp")))) (beadwork-cli-clear-project-cache) (should (null beadwork-cli--project-root-cache)))) (ert-deftest beadwork-test-has-beadwork-branch () "Test `beadwork-cli--has-beadwork-branch-p' with mocked git." (cl-letf (((symbol-function 'call-process) (lambda (_program _infile _dest _display &rest args) (if (and (equal (car args) "rev-parse") (member "refs/heads/beadwork" args)) 0 1)))) (should (beadwork-cli--has-beadwork-branch-p "/tmp/")))) (ert-deftest beadwork-test-no-beadwork-branch () "Test `beadwork-cli--has-beadwork-branch-p' when branch missing." (cl-letf (((symbol-function 'call-process) (lambda (_program _infile _dest _display &rest _args) 128))) (should-not (beadwork-cli--has-beadwork-branch-p "/tmp/")))) ;;;; Error type hierarchy tests (ert-deftest beadwork-test-error-hierarchy () "Test that beadwork error types form a proper hierarchy." (should (condition-case nil (signal 'beadwork-cli-error '("test")) (beadwork-error t))) (should (condition-case nil (signal 'beadwork-project-error '("test")) (beadwork-error t)))) (provide 'beadwork-cli-test) ;;; beadwork-cli-test.el ends here