Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Test

on:
push:
branches: [ main, master, develop ]
pull_request:
branches: [ main, master, develop ]
workflow_dispatch: # Allow manual triggering from GitHub UI

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Emacs
uses: purcell/setup-emacs@master
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow uses purcell/setup-emacs@master which references an unstable moving target. For better reproducibility and stability, it's recommended to pin to a specific version tag or commit SHA instead of using @master. This prevents unexpected breaks when the upstream action changes.

Suggested change
uses: purcell/setup-emacs@master
uses: purcell/setup-emacs@v5

Copilot uses AI. Check for mistakes.
with:
version: 29.1
- name: Lint and compile
uses: leotaku/elisp-check@v1.4.0
with:
check: melpa
file: cursor-agent.el
- name: Run tests
uses: leotaku/elisp-check@v1.4.0
with:
check: ert
file: cursor-agent-test.el
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ tramp
.DS_Store
Thumbs.db

# IDE
.vscode/
.idea/

# Temporary files
*.tmp
*.bak
*.swp

# Test artifacts
test-results.xml
test-output.log
*.log

# GitHub Actions local runner
/bin/act
41 changes: 36 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ If you want to track the package via Git and get updates:

2. **Run `doom sync`** to install

3. **Add configuration to `~/.config/doom/config.el`:**
3. **Add configuration to `~/.config/doom/config.el`** — use `after! cursor-agent` only for `setq` and keybindings. **Do not** `unload-feature` or `load` the package in `after!`; that causes recursive load errors (see [Known issues](#known-issues)):
```elisp
(after! cursor-agent
(setq cursor-agent-default-model "gpt-5")
Expand All @@ -252,13 +252,16 @@ If you're developing or want to keep it in a separate directory:
1. **Add to `~/.config/doom/packages.el`:**
```elisp
(package! cursor-agent
:recipe (:local-repo "~/workspaces/personal/cursor-agent.el"
:files ("cursor-agent.el")))
:recipe (:local-repo "/path/to/cursor-agent.el" ; or "~/workspaces/personal/cursor-agent.el"
:files ("cursor-agent.el")
:build (:not compile))) ; optional: faster iteration when not byte-compiling
```

2. **Run `doom sync`**

3. **Configure in `~/.config/doom/config.el`** as shown in Method 2
3. **Configure in `~/.config/doom/config.el`** as in Method 2. **Do not** `unload-feature` or `load` cursor-agent in `after!` — it causes recursive load errors.

With `:local-repo`, Doom/Straight may only autoload a few commands at startup. Run `M-x cursor-agent-install` (or any autoloaded command) once to load the full file; after that, all commands are available. See [Known issues](#known-issues).

### Doom Emacs Keybindings

Expand Down Expand Up @@ -286,10 +289,20 @@ The recommended keybinding layout for Doom Emacs:
### Doom Emacs Tips

- **No `doom sync` needed** for Method 1 (local file) - just restart Emacs
- **Use `after!` macro** for package configuration when using `package!`
- **Use `after!` macro** for package configuration when using `package!` — only for `setq` and keybindings, **not** for `unload-feature` or `load` of cursor-agent (causes recursive load)
- **Use `map!` macro** for keybindings (Doom's DSL)
- **vterm module**: If you have Doom's `:term vterm` module enabled, vterm will be available automatically

## Known issues

### Doom Emacs / Straight: commands unavailable until first use

With Doom and Straight (`package!` with `:local-repo` or `:host github`), only a subset of commands may appear in `M-x` at startup. **Run `M-x cursor-agent-install`** (or any other autoloaded command) **once** to load the full package; after that, all commands are available. This comes from how Doom/Straight generate and apply autoloads; the package itself has `;;;###autoload` on all commands.

### Doom Emacs: do not unload/reload in config

**Do not** use `unload-feature` and `load` (or `load-file`) of `cursor-agent.el` inside an `after! cursor-agent` block. That causes a **recursive load** between the Straight build path and your source path (e.g. workspace or `:local-repo`). Use `after! cursor-agent` only for configuration (`setq`, keybindings). The package is already loaded by Doom/Straight.

## Compatibility

This package is **designed and tested primarily for vanilla Emacs**. It uses only standard Emacs features and has no distribution-specific dependencies.
Expand Down Expand Up @@ -341,6 +354,24 @@ The package will automatically fall back to `shell-mode`. For better experience,
M-x package-install RET vterm RET
```

## Testing

Tests are included in `cursor-agent-test.el` using ERT (Emacs Lisp Regression Testing). Run tests locally:

```elisp
M-x load-file RET cursor-agent-test.el RET
M-x ert RET cursor-agent-test-.* RET
```

Or from the command line:

```bash
emacs --batch -l ert -l cursor-agent.el -l cursor-agent-test.el \
--eval "(ert-run-tests-batch-and-exit 'cursor-agent-test-.* t)"
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command-line test execution syntax is incorrect. The ert-run-tests-batch-and-exit function expects a selector (like a string or t), not a quoted symbol with a pattern. The correct syntax should be (ert-run-tests-batch-and-exit \"cursor-agent-test-\") (with a string) or (ert-run-tests-batch-and-exit t) to run all tests. The current syntax 'cursor-agent-test-.* is not valid ERT selector syntax.

Suggested change
--eval "(ert-run-tests-batch-and-exit 'cursor-agent-test-.* t)"
--eval "(ert-run-tests-batch-and-exit \"cursor-agent-test-\")"

Copilot uses AI. Check for mistakes.
```

Tests run automatically on GitHub Actions for all pushes and pull requests.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Expand Down
261 changes: 261 additions & 0 deletions cursor-agent-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
;;; cursor-agent-test.el --- Unit tests for cursor-agent.el

Comment on lines +1 to +2
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file is missing required package metadata headers. According to Emacs Lisp conventions and MELPA requirements, test files should include standard package headers such as Author, Maintainer, Version, Package-Requires, Keywords, and URL, similar to the main cursor-agent.el file. This is especially important since the CI/CD workflow uses elisp-check with the 'melpa' check, which validates package headers.

Suggested change
;;; cursor-agent-test.el --- Unit tests for cursor-agent.el
;;; cursor-agent-test.el --- Unit tests for cursor-agent.el -*- lexical-binding: t; -*-
;; Author: cursor-agent maintainers
;; Maintainer: cursor-agent maintainers
;; Version: 0.1
;; Package-Requires: ((emacs "24.3"))
;; Keywords: tools, lisp
;; URL: https://example.com/cursor-agent

Copilot uses AI. Check for mistakes.
;; This file demonstrates how to write unit tests for Emacs Lisp using ERT
;; ERT (Emacs Lisp Regression Testing) is built into Emacs

(require 'ert)
(require 'cl-lib)

;; Ensure cursor-agent.el is loaded before tests are defined
;; This is necessary in CI environments where require might not work as expected
;; elisp-check may load cursor-agent.el automatically, but we ensure it's loaded here too
(let ((test-dir (file-name-directory
(or load-file-name
(and (boundp 'buffer-file-name) buffer-file-name)
default-directory))))
(add-to-list 'load-path test-dir)
;; Always load the file to ensure all functions are available
;; Use require first (standard way), then fallback to load-file if needed
(unless (featurep 'cursor-agent)
(condition-case nil
(require 'cursor-agent nil t)
(error nil)))
;; If still not loaded or function not found, force load with load-file
(unless (fboundp 'cursor-agent-verify-setup)
(let ((main-file (expand-file-name "cursor-agent.el" test-dir)))
(when (file-exists-p main-file)
(load-file main-file)))))

;; ============================================================================
;; Test Setup and Utilities
;; ============================================================================

(defun cursor-agent-test--mock-executable-find (command)
"Mock executable-find to return t for 'agent' command."
(string= command "agent"))

(defun cursor-agent-test--mock-shell-command-to-string (command)
"Mock shell-command-to-string to return authenticated status."
"authenticated")

;; ============================================================================
;; Tests for cursor-agent-installed-p
;; ============================================================================

(ert-deftest cursor-agent-test-installed-p-returns-t-when-found ()
"Test that cursor-agent-installed-p returns t when agent is found."
(cl-letf (((symbol-function 'executable-find) #'cursor-agent-test--mock-executable-find))
(should (cursor-agent-installed-p))))

(ert-deftest cursor-agent-test-installed-p-returns-nil-when-not-found ()
"Test that cursor-agent-installed-p returns nil when agent is not found."
(cl-letf (((symbol-function 'executable-find) (lambda (cmd) nil)))
(should-not (cursor-agent-installed-p))))

;; ============================================================================
;; Tests for cursor-agent-check-auth
;; ============================================================================

(ert-deftest cursor-agent-test-check-auth-returns-t-when-authenticated ()
"Test that cursor-agent-check-auth returns t when authenticated."
(cl-letf (((symbol-function 'executable-find) #'cursor-agent-test--mock-executable-find)
((symbol-function 'shell-command-to-string) #'cursor-agent-test--mock-shell-command-to-string))
(should (cursor-agent-check-auth))))

;; ============================================================================
;; Tests for Configuration Variables
;; ============================================================================

(ert-deftest cursor-agent-test-default-command-is-agent ()
"Test that cursor-agent-command defaults to 'agent'."
(should (string= cursor-agent-command "agent")))

(ert-deftest cursor-agent-test-default-output-format-is-text ()
"Test that cursor-agent-default-output-format defaults to 'text'."
(should (string= cursor-agent-default-output-format "text")))

(ert-deftest cursor-agent-test-use-force-defaults-to-nil ()
"Test that cursor-agent-use-force defaults to nil."
(should-not cursor-agent-use-force))

;; ============================================================================
;; Tests for Helper Functions
;; ============================================================================

(ert-deftest cursor-agent-test-local-bin-in-path-p-with-path ()
"Test cursor-agent--local-bin-in-path-p when ~/.local/bin is in PATH."
(let ((original-path (getenv "PATH")))
(unwind-protect
(progn
(setenv "PATH" (concat (expand-file-name "~/.local/bin") ":" original-path))
(should (cursor-agent--local-bin-in-path-p)))
(setenv "PATH" original-path))))

(ert-deftest cursor-agent-test-get-path-instruction-for-zsh ()
"Test cursor-agent--get-path-instruction returns zsh instructions."
(let ((instruction (cursor-agent--get-path-instruction "/bin/zsh")))
(should (string-match-p "zshrc" instruction))
(should (string-match-p "\\.local/bin" instruction))))

(ert-deftest cursor-agent-test-get-path-instruction-for-bash ()
"Test cursor-agent--get-path-instruction returns bash instructions."
(let ((instruction (cursor-agent--get-path-instruction "/bin/bash")))
(should (string-match-p "bashrc" instruction))
(should (string-match-p "\\.local/bin" instruction))))

;; ============================================================================
;; Tests for Buffer Creation
;; ============================================================================

(ert-deftest cursor-agent-test-buffer-creation ()
"Test that functions create buffers with correct names."
(let ((buffer-name "*cursor-agent-test*"))
(unwind-protect
(progn
(with-current-buffer (get-buffer-create buffer-name)
(erase-buffer)
(insert "Test content")
(compilation-mode)
(should (buffer-live-p (get-buffer buffer-name)))
(should (eq major-mode 'compilation-mode)))
(should (buffer-live-p (get-buffer buffer-name))))
(when (get-buffer buffer-name)
(kill-buffer buffer-name)))))

;; ============================================================================
;; Integration Tests (require actual agent command)
;; ============================================================================

;; Helper to force load a specific function from a file
(defun cursor-agent-test--force-load-function (file func-name)
"Force load FUNC-NAME from FILE.
FUNC-NAME should be a symbol like 'cursor-agent-verify-setup."
(let ((buf (find-file-noselect file)))
(with-current-buffer buf
(goto-char (point-min))
(let ((search-pattern (format "(defun %s" func-name)))
(when (search-forward search-pattern nil t)
(beginning-of-defun)
(let ((form-start (point)))
(end-of-defun)
(let ((form-text (buffer-substring form-start (point))))
(condition-case err
(progn
(eval (read form-text))
;; Verify it worked - if still not bound, try reading from buffer directly
(unless (fboundp func-name)
(goto-char form-start)
(let ((form (read (current-buffer))))
(eval form))))
(error (message "Error force-loading function %s: %s" func-name (error-message-string err)))))))))))

;; Ensure cursor-agent is loaded before running integration tests
(defun cursor-agent-test--ensure-loaded ()
"Ensure cursor-agent.el is loaded before tests run.
Loads all expected functions, force-loading any that aren't bound after load-file."
(let ((expected-functions
'(cursor-agent-install
cursor-agent-verify-setup
cursor-agent-prompt
cursor-agent-interactive
cursor-agent-region
cursor-agent-resume
cursor-agent-list-sessions
cursor-agent-login
cursor-agent-status
cursor-agent-list-models
cursor-agent-mcp-list
cursor-agent-shell-mode
cursor-agent-update
cursor-agent-readme))
(all-loaded t))
;; Check if all functions are loaded
(dolist (func expected-functions)
(unless (fboundp func)
(setq all-loaded nil)))
;; If not all loaded, try to load
(unless all-loaded
;; First try to require the feature
(unless (featurep 'cursor-agent)
(condition-case nil
(require 'cursor-agent nil t)
(error nil)))
;; Check again after require
(setq all-loaded t)
(dolist (func expected-functions)
(unless (fboundp func)
(setq all-loaded nil)))
;; If still not all loaded, try to find and load the file
(unless all-loaded
;; Try multiple strategies to find the main file
(let ((main-file nil)
(test-file (cond
((boundp 'load-file-name) (and load-file-name (symbol-value 'load-file-name)))
((boundp 'buffer-file-name) (and buffer-file-name (symbol-value 'buffer-file-name)))
(t nil))))
;; Strategy 1: Use test file directory
(when test-file
(let ((test-dir (file-name-directory test-file)))
(when test-dir
(add-to-list 'load-path test-dir)
(setq main-file (expand-file-name "cursor-agent.el" test-dir))
(unless (file-exists-p main-file)
(setq main-file nil)))))
;; Strategy 2: Try locate-library
(unless (and main-file (file-exists-p main-file))
(let ((found (locate-library "cursor-agent" t)))
(when found
(setq main-file found))))
;; Strategy 3: Try current directory
(unless (and main-file (file-exists-p main-file))
(let ((candidate (expand-file-name "cursor-agent.el" default-directory)))
(when (file-exists-p candidate)
(setq main-file candidate))))
;; Now load and force-evaluate any missing functions
(when (and main-file (file-exists-p main-file))
(load-file main-file)
;; Force evaluation of any functions that still aren't bound
;; This ensures all functions are available even if load-file doesn't evaluate them
(dolist (func expected-functions)
(unless (fboundp func)
(cursor-agent-test--force-load-function main-file func)))))))))

(ert-deftest cursor-agent-test-verify-setup-structure ()
"Test that cursor-agent-verify-setup has correct structure."
(cursor-agent-test--ensure-loaded)
(should (fboundp 'cursor-agent-verify-setup))
(should (commandp 'cursor-agent-verify-setup)))

(ert-deftest cursor-agent-test-all-commands-are-defined ()
"Test that all expected commands are defined."
(cursor-agent-test--ensure-loaded)
(let ((expected-commands
'(cursor-agent-install
cursor-agent-verify-setup
cursor-agent-prompt
cursor-agent-interactive
cursor-agent-region
cursor-agent-resume
cursor-agent-list-sessions
cursor-agent-login
cursor-agent-status
cursor-agent-list-models
cursor-agent-mcp-list
cursor-agent-shell-mode
cursor-agent-update
cursor-agent-readme)))
(dolist (cmd expected-commands)
(should (fboundp cmd))
(should (commandp cmd)))))

;; ============================================================================
;; Test Runner
;; ============================================================================

(defun cursor-agent-test-run-all ()
"Run all cursor-agent tests."
(interactive)
(ert-run-tests "cursor-agent-test-" t))
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test selector pattern "cursor-agent-test-" matches test names that start with this prefix, but the selector should be a regexp pattern. While this will work for substring matching, using a proper regexp like "^cursor-agent-test-" would be more explicit and matches the pattern used in the README documentation where "cursor-agent-test-.*" is shown.

Suggested change
(ert-run-tests "cursor-agent-test-" t))
(ert-run-tests "^cursor-agent-test-" t))

Copilot uses AI. Check for mistakes.

;; Provide the test module
(provide 'cursor-agent-test)
Loading