aboutsummaryrefslogtreecommitdiff
path: root/tests
Commit message (Collapse)AuthorAgeFilesLines
* test(external-open): cover xdg-open, open-this-file-with, find-file-autoCraig Jennings2026-05-141-0/+120
| | | | | | | | | | Sibling tests in `test-external-open-lib-*.el` covered the pure helpers (`cj/external-open-command`, `cj/external-open-launcher-p`). This batch covers the user-facing wrappers: - `cj/xdg-open`: posix path triggers `call-process` with the open program; errors when no file is associated; errors when no command resolves on the host. - `cj/open-this-file-with`: errors outside a file-visiting buffer; posix path spawns a detached process via `call-process-shell-command` with `nohup ... >/dev/null 2>&1 &`. - `cj/find-file-auto`: routes a `.mp4` (in `default-open-extensions`) to `cj/xdg-open`, passes `.txt` through to the original `find-file`, and falls through cleanly for a nil filename argument. Host predicates, call-process, and the underlying `cj/xdg-open` are stubbed throughout.
* test(prog-go): cover go-setup, staticcheck, debug, keybindingsCraig Jennings2026-05-141-0/+144
| | | | | | | | | | | Same shape as the prog-python and prog-webdev setup tests: company-mode + electric-pair-mode + LSP gating + buffer-local preferences. New tests cover: - `cj/go-setup`: tab-width 4, standard-indent 4, indent-tabs-mode t (Go convention); company-mode + electric-pair-mode both fire; LSP starts when gopls is on PATH, skips otherwise. - `cj/go-staticcheck`: runs `compile` with `./...` when staticcheck is on disk; messages and skips compile otherwise. - `cj/go-debug`: starts `gud-gdb` with `dlv debug` when delve is on PATH; messages otherwise. - `cj/go-mode-keybindings`: wires `C-; f` -> gofmt, `S-<f5>` -> staticcheck, `S-<f6>` -> debug. External modes and process primitives are stubbed.
* test(org-webclipper): cover lazy init, protocol handler, EWW captureCraig Jennings2026-05-141-0/+167
| | | | | | | | | | | Sibling `test-org-webclipper-process.el` covers the content-processing helper. This batch fills in the rest: - `cj/webclipper-ensure-initialized`: registers the webclip protocol entry + the W and w capture templates on first call, is idempotent on subsequent calls. - `cj/org-protocol-webclip`: stashes the url+title, triggers capture; missing title falls back to "Untitled". - `cj/org-protocol-webclip-handler`: errors when no url stashed, returns processed org content on success (with the first heading stripped + subs demoted), wraps fetch failures in a "Failed to clip" error. - `cj/org-webclipper-EWW`: routes through `org-eww-copy-for-org-mode` for eww-mode and `org-w3m-copy-for-org-mode` for w3m-mode; errors on any other source mode. Top-level defvars for `org-protocol-protocol-alist`, `org-capture-templates`, and `webclipped-file` make let-bindings dynamic under lexical scope. org-web-tools and the eww/w3m copy commands are stubbed.
* test(org-refile-config): cover refresh-targets, org-refile, refile-in-fileCraig Jennings2026-05-141-0/+82
| | | | | | | | | | Sibling tests covered the ensure-org-mode helper and the build-targets pipeline. The three wrapper commands were uncovered: - `cj/org-refile-refresh-targets`: passes the `force-rebuild` flag to `cj/build-org-refile-targets`. - `cj/org-refile`: builds the cache, then calls `org-refile` with the four positional args. - `cj/org-refile-in-file`: scopes `org-refile-targets` to the current file at maxlevel 6, then saves after the refile completes. Top-level `defvar` for `org-refile-targets` so the let-binding inside the function is dynamic under lexical scope.
* test(org-contacts-config): cover template helpers, props-matching, email ↵Craig Jennings2026-05-141-0/+129
| | | | | | | | | | | | | | aggregator Sibling tests covered the capture-finalize hook and the email-string parser. This batch fills in the rest: - `cj/org-contacts-template-name` / `-email`: fall back to `read-string` outside mu4e; pull from `mu4e-message-field` when inside. - `cj/org-contacts-new`: delegates to `org-capture` with key "C". - `cj/org-contacts--props-matching`: filters an entry's props by regexp. - `cj/get-all-contact-emails`: formats each entry as "Name <email>", expands multi-email strings. - `cj/insert-contact-email`: inserts the completing-read selection at point. mu4e and org-contacts primitives are stubbed.
* test(org-reveal-config): cover command wrappers + preview helpersCraig Jennings2026-05-141-0/+197
| | | | | | | | | | | | | | | Sibling tests covered the header template, title-to-filename slug, and the headers-remove pass. This file fills in the rest: - `cj/--reveal-preview-export-on-save`: exports in org-mode, skips otherwise. - `cj/--reveal-ensure-header`: inserts when absent, no-op (no prompt) when already present. - `cj/reveal-export`: errors outside org-mode, opens HTML in browser otherwise. - `cj/reveal-preview-start`: installs the buffer-local after-save-hook + exports once; errors outside org-mode. - `cj/reveal-preview-stop`: removes the hook + messages. - `cj/reveal-insert-header`: errors outside org-mode, errors when headers already present, inserts and reports otherwise. - `cj/reveal-remove-headers`: errors outside org-mode, messages the removed line count. - `cj/reveal-new`: errors when target file already exists. ox-reveal's `org-reveal-export-to-html` and `browse-url-of-file` are stubbed.
* test(org-noter-config): cover document path, find/create, session, start, insertCraig Jennings2026-05-141-0/+243
| | | | | | | | | | | | | | | Sibling tests covered the preferred-split, title-to-slug, notes-template, and in-document / in-notes-file predicates. This batch fills in the rest: - `cj/org-noter--get-document-path`: pdf-view-mode uses buffer-file-name, nov-mode uses `nov-file-name`, unrelated mode returns nil. - `cj/org-noter--extract-document-title`: strips the extension. - `cj/org-noter--find-notes-file`: returns the file containing the doc-path, nil when no doc or no match. - `cj/org-noter--create-notes-file`: writes the template when absent. - `cj/org-noter--session-active-p`: nil when unbound, non-nil when set. - `cj/org-noter--toggle-notes-window`: deletes when visible, requests start when hidden. - `cj/org-noter-start`: routes through the cond -- toggle in doc+session, switch-window in notes+session, message elsewhere. - `cj/org-noter-insert-note-dwim`: with active session inserts directly; without one, starts then inserts. org-noter / pdf-view / nov / org-id primitives are stubbed.
* test(slack-config): cover credentials, start/stop, reactions, notify, kill-allCraig Jennings2026-05-141-0/+251
| | | | | | | | | | | | | | | | Sibling `test-slack-config-reactions.el` covered the post-command-hook safety advice and the empty-buffer guard. This batch covers the rest: - `cj/slack--get-credential`: string secret, function secret, missing entry. - `cj/slack-start`: errors when token / cookie missing, registers + starts when both present. - `cj/slack-stop`: calls `slack-ws-close` + messages. - `cj/slack--reaction-candidates`: includes the (Other...) escape hatch. - `cj/slack-select-reaction`: emoji-name return for a curated pick, delegation to `slack-message-reaction-input` for Other. - `cj/slack-notify`: fires for IMs (not self), skips self-messages and non-IM non-mentions. - `cj/slack-test-notify`: fires the notify pipeline directly. - `cj/slack-mark-read-and-bury`: marks via the latest-ts + buries; outside slack, just buries. - `cj/slack-close-all-buffers`: closes buffers with `slack-current-buffer` local, leaves non-slack buffers alone. The close-all assertions check identity (the 2 slack buffers killed, the non-slack one preserved) instead of an exact total -- ERT-internal buffers leaking into `(buffer-list)` from earlier tests would otherwise inflate the count, and `cl-letf` over the `buffer-list` subr is unreliable under native-comp.
* test(org-roam-config): cover tag predicate, find-node, finalize hook, ↵Craig Jennings2026-05-141-0/+167
| | | | | | | | | | | | | | | insert-immediate Sibling tests covered the slug helper, link-description, demote-subtree, format-roam-node, and the copy-todo-to-today hook. This batch covers the tag/find/insert family: - `cj/org-roam-filter-by-tag`: returns a predicate; keeps matching nodes, rejects non-matching. - `cj/org-roam-list-notes-by-tag`: returns file paths for matches, empty list when no matches. - `cj/org-roam-add-node-to-agenda-files-finalize-hook`: removes itself, adds the captured buffer's file to `org-agenda-files` when not aborted, skips on abort. - `cj/org-roam-find-node`: calls `org-roam-node-find` with a filter predicate + templates. - `cj/org-roam-find-node-topic` / `-recipe`: delegate with the right tag + key (+ subdir). - `cj/org-roam-node-insert-immediate`: appends `:immediate-finish t` to the first capture template before calling `org-roam-node-insert`. Top-level `defvar`s for `org-note-abort`, `org-agenda-files`, `org-roam-capture-templates`, and `org-capture-after-finalize-hook` make the let-bindings reach the dynamic vars under lexical scope.
* test(hugo-config): cover new-post, export, navigation, preview, publishCraig Jennings2026-05-141-0/+235
| | | | | | | | | | | | | | | Sibling tests covered the pure helpers (`cj/hugo--post-file-path`, `cj/hugo--post-template`, `cj/hugo--post-metadata`, `cj/hugo--collect-drafts`, `cj/hugo-open-blog-dir-external`, `cj/hugo-toggle-draft`). This batch covers the remaining commands and preview helpers: - `cj/hugo-new-post`: writes the template to a slugged file, errors when one already exists. - `cj/hugo-export-post`: errors outside org-mode, delegates to `org-hugo-export-to-md` otherwise. - `cj/hugo-open-blog-dir`: ensures the dir exists and opens dired on it. - `cj/hugo-open-draft`: messages when no drafts, otherwise picks via `completing-read` and opens. - `cj/hugo--preview-filter`: opens the browser when Hugo prints "Web Server is available", ignores arbitrary output. - `cj/hugo--preview-sentinel`: clears the process var on clean exit, messages on a non-zero exit. - `cj/hugo-preview`: stops a running server, starts one when stopped (verifying `start-process` got the "server" arg). - `cj/hugo-publish`: hands off to `magit-status-setup-buffer` on `website-dir`. ox-hugo's `org-hugo-slug` is stubbed via `defun` (the package isn't installed in batch); `require`, `org-hugo-export-to-md`, `dired`, `find-file`, `browse-url`, `start-process`, `kill-process`, and `magit-status-setup-buffer` are stubbed per-test.
* test(ai-config): cover gptel switch/add/toggle commands + small helpersCraig Jennings2026-05-141-0/+152
| | | | | | | | | | | | | Second pass on ai-config. First batch covered the auth-source, API-key caching, file-context, clear-buffer, context-clear, and model-heading helpers. This batch covers the gptel command wrappers and a couple of small helpers: - `cj/gptel--refresh-org-prefix`: refreshes the org-mode entry in `gptel-prompt-prefix-alist`. - `cj/gptel-backend-and-model`: formats a vector-backend's name + the current model + a timestamp; falls back to "AI:" when no backend is bound. - `cj/gptel-switch-backend`: picks a backend and one of its models, updates both gptel vars; errors on an invalid backend choice. - `cj/gptel-add-buffer-file`: hands the visited file off to `cj/gptel--add-file-to-context`, messages otherwise. - `cj/gptel-add-this-buffer`: calls `gptel-add` with the (4) prefix-arg. - `cj/toggle-gptel`: deletes the window when *AI-Assistant* is already shown. gptel and projectile primitives are stubbed throughout.
* test(music-config): cover playlist commands + random-aware navigationCraig Jennings2026-05-141-0/+261
| | | | | | | | | | | | | Second pass on music-config. The first batch covered assertion guards, the M3U picker, EMMS lazy setup, and the smaller commands. This batch covers the remaining interactive playlist commands and the random-history navigation pair: - `cj/music-playlist-load`: loads the selected file via `emms-play-playlist`, errors when the chosen file isn't on disk. - `cj/music-playlist-reload`: replays the buffer-local playlist file, errors when none is associated. - `cj/music-playlist-edit`: opens the M3U file in another window when the buffer is clean. - `cj/music-next`: delegates to `emms-next` or `emms-random` based on `emms-random-playlist`. - `cj/music-previous`: emms-previous when not random; with random + history pops the top, finds it in the playlist, selects + starts; with random + no history messages; with random and a missing track, messages. - `cj/music--consume-track`: no-op when consume-mode is off, kills the selected track when on. EMMS primitives (`emms-playlist-clear`, `emms-play-playlist`, `emms-stop`, `emms-random`, `emms-next`, `emms-previous`, `emms-start`, `emms-playlist-select`, `emms-playlist-mode-kill-track`) are stubbed throughout.
* test(test-runner): cover per-project state + small interactive wrappersCraig Jennings2026-05-141-0/+273
| | | | | | | | | | | | | | | | | | | `test-test-runner.el` covered the `cj/test--do-*` helpers and the focus-add / focus-remove / extract-test-names families. The state machinery and small wrappers were the gap. 22 new tests cover: - `cj/test--project-root`: nil when projectile is absent, expanded trailing-slash when present. - `cj/test--state-key`: falls back to `default-directory` without a project root. - `cj/test--project-state`: creates the entry lazily, reuses on second call. - `cj/test--state-get` / `cj/test--state-put`: roundtrip + default fallback. - `cj/test--current-focused-files` / `cj/test--current-mode` getter/setter pairs. - `cj/test--sync-legacy-state`: mirrors per-project state into the public legacy vars. - `cj/test--remember-loaded-project-root`: pushes once via `cl-pushnew`, no-op without a root. - `cj/test--file-in-directory-p`: true for a subpath, false for a sibling. - `cj/test--get-test-directory`: prefers `tests/` subdir, falls back to the global default. - `cj/test--get-test-files`: lists `test-*.el` files only, basenames. - `cj/test--ensure-test-dir-in-load-path`: adds the dir. - `cj/test-focus-clear` / `cj/test-toggle-mode` / `cj/test-view-focused` / `cj/test-run-all`: behavior + messaging.
* test(custom-buffer-file): cover print spooler, diff, eww, email helpersCraig Jennings2026-05-141-0/+189
| | | | | | | | | | | | | Sibling tests cover the move/rename/copy/clear family. The remaining helpers and dispatchers were uncovered: - `cj/print--resolve-spooler`: explicit-string on PATH, explicit-string missing (error), `auto` prefers lpr, `auto` falls back to lp, `auto` with nothing on PATH (error), `auto` returns cached value, invalid spooler symbol (error). - `cj/copy-buffer-name`: places the current buffer's name on the kill ring with a confirmation message. - `cj/view-buffer-in-eww`: hands the file off to `eww-open-file` when visiting a file, errors otherwise. - `cj/--email-handle-is-type-p`: prefix-matches content-types, handles nil handle without signaling. - `cj/--diff-with-regular-diff`: writes a unified diff into the target buffer with header + diff output. - `cj/diff-buffer-with-file`: errors when not visiting a file, messages "No differences" when diff -q returns 0. 16 new tests total. `call-process`, `executable-find`, `eww-open-file`, `mm-handle-type`, `diff-mode` are stubbed.
* test(prog-python+webdev): cover the per-buffer setup mode hooksCraig Jennings2026-05-142-0/+191
| | | | | | | | | | | | `cj/python-setup` and `cj/webdev-setup` are the per-buffer preference functions wired into `python-ts-mode` / `typescript-ts-mode` / `tsx-ts-mode` / `js-ts-mode`. Sibling tests covered the command builders and prettier/mypy wrappers but not these. 9 new tests across two files cover, for each setup: - the setq-local pairs (fill-column, tab-width, standard-indent, indent-tabs-mode) land at the language's defaults (Python: 80/4/4/nil; webdev: 100/2/2/nil), - the mode helpers (company, flyspell-prog, superword, electric-pair-local) all get called, - LSP gating: starts when the language server binary is on PATH, skips otherwise. Plus a check that `cj/webdev-keybindings` binds `C-; f` to the prettier wrapper. External modes and `executable-find` / `lsp-deferred` are stubbed so the tests run without a Python or Node toolchain.
* test(transcription-config): cover status helpers + interactive commandsCraig Jennings2026-05-141-0/+194
| | | | | | | | | | | | | Sibling tests cover the audio-file predicate, path/duration helpers, log cleanup, sentinel helpers, and the process-environment builder. The gap was the running-list helpers and the interactive commands. 15 new tests cover: - `cj/--running-transcriptions`: filters by `running` status, handles empty list. - `cj/--cleanup-completed-transcriptions`: prunes the list down to running entries only. - `cj/--count-active-transcriptions`: counts running, zero when empty. - `cj/--transcription-modeline-string`: nil when idle, includes the active count when busy. - `cj/transcriptions-buffer`: renders the empty-state message or per-entry rows. - `cj/transcription-kill`: errors with no entries, calls `kill-process` on a live one. - `cj/transcription-switch-backend`: completing-read picks the new backend, var + message updated. - `cj/transcribe-audio-at-point`: errors outside dired / when no file at point, delegates otherwise.
* test(dirvish-config): cover five interactive wrappersCraig Jennings2026-05-141-0/+209
| | | | | | | | | | | | Sibling tests covered the pure helpers; the interactive wrappers were the gap. 13 new tests cover: - `cj/dirvish-open-html-in-eww`: HTML file -> `eww-open-file`, non-HTML -> message. - `cj/dirvish-duplicate-file`: copies file with `-copy` suffix, errors on no-file / directory / declined-overwrite. - `cj/dired-mark-all-visible-files`: marks file lines, skips directory lines (via `cj/--dired-line-is-directory-p`). - `cj/dired-copy-path-as-kill`: errors outside dired-mode, kills resolved path, formats as org-link when asked. - `cj/dirvish-open-file-manager-here`: uses `cj/external-open-command` when available, falls back to `xdg-open` shell command otherwise, messages when current dir can't be resolved. The decline-overwrite test uses real temp files instead of `cl-letf`-overriding `file-directory-p` + `file-exists-p` -- native-comp's trampoline cache breaks when several built-ins get redefined in the same test.
* test(custom-comments): cover the eight interactive comment wrappersCraig Jennings2026-05-141-0/+192
| | | | | | The internal `cj/--comment-*` helpers were covered in the sibling test files; their interactive wrappers were the gap. 11 new tests drive each wrapper through its full prompt cycle (read-string / read-from-minibuffer / completing-read all stubbed) and assert the wrapper produces output containing the supplied text. The length-option branches for `cj/comment-simple-divider` are exercised across all three values (fill-column / half-column / match-text), and `cj/comment-hyphen`'s thin delegate to `cj/comment-inline-border` with "-" is covered too. The wrappers split between `read-string` and `read-from-minibuffer` for their text input (box uses minibuffer; the rest use read-string); the tests stub each function explicitly to drive the right path.
* test(music-config): cover assertion guards, M3U picker, EMMS lazy setupCraig Jennings2026-05-141-0/+224
| | | | | | | | | | | | | | music-config has a large sibling-test fleet for the pure helpers; the gap was a handful of small dispatchers and assertion guards. 13 new tests, EMMS primitives stubbed throughout: - `cj/music--assert-m3u-files-exist`: passes a non-empty list through, errors on empty. - `cj/music--sync-playlist-file`: sets the buffer-local file ref + resets point. - `cj/music--select-m3u-file`: returns the chosen path, returns nil on Cancel, errors when no files exist. - `cj/emms--setup`: no-ops when emms already loaded, requires when absent. - `cj/music-playlist-clear`: stops playback, clears the playlist, nils the file ref. - `cj/music-add-directory-recursive`: calls `emms-add-directory-tree` for an existing dir, user-errors for non-dirs. - `cj/music--find-track-in-playlist`: returns the buffer position when present, nil when absent.
* fix(ai-vterm): force buffer swap after F9 toggle-off in lone windowCraig Jennings2026-05-141-30/+46
| | | | | | The original fix (9600611) set a flag at toggle-off and let the next toggle-on detect it. The flag mechanism is right, but the toggle-off itself wasn't observable when bury-buffer couldn't switch the lone window onto a different buffer -- `bury-buffer' falls back to `switch-to-prev-buffer', which no-ops when the window's prev-buffer history contains only the agent itself (common right after a `C-x 1' that cleared the surrounding windows' histories). Without an observable swap, the second F9 found the agent still displayed and routed back through toggle-off, looping the user with no visible effect. Dispatcher now explicitly forces the window onto another buffer (`(other-buffer agent-buf t)`) when the lone window is still showing the agent after `bury-buffer'. The round-trip test now exercises the real `bury-buffer' path instead of simulating it; a new test asserts the lone window's buffer is non-agent after toggle-off.
* feat(org-agenda): add VERIFICATION and IN-PROGRESS blocks around SCHEDULECraig Jennings2026-05-131-0/+86
| | | | | | | | | | The main "d" agenda view grows two new blocks. A VERIFICATION block lists tasks in the VERIFY TODO state, placed just above the day's SCHEDULE. An IN-PROGRESS block lists tasks in the DOING TODO state, placed just under SCHEDULE. The full block order is now: OVERDUE -> HIGH PRIORITY -> VERIFICATION -> SCHEDULE -> IN-PROGRESS -> PRIORITY B. Scope matches the other blocks (every entry in `org-agenda-files`). Scheduled and deadlined entries are included -- a VERIFY task with a date appears in both VERIFICATION and SCHEDULE, mirroring how HIGH PRIORITY behaves. Habits are skipped via `cj/org-skip-subtree-if-habit`; PROJECT-keyword parents wouldn't match `(todo "VERIFY")` exact-state filters anyway, so no extra skip there. Two new header defvars (`cj/main-agenda-verify-title`, `cj/main-agenda-doing-title`) for symmetry with the existing four. Both blocks reference the shared `cj/--main-agenda-prefix-format` so a format tweak still lands in one place. Five new tests in `test-org-agenda-config-skip-functions.el` lock the block order, each new block's header / prefix-format / skip-function, and the include-scheduled-entries contract.
* test(custom-text-enclose): cover the seven interactive wrappersCraig Jennings2026-05-131-0/+225
| | | | The internal `cj/--*` helpers were already tested; the public dispatchers (`cj/surround-word-or-region`, `cj/wrap-word-or-region`, `cj/unwrap-word-or-region`, and the four `*-in-region-or-buffer` line helpers) were the gap. 18 new tests cover the region-active branch, the thing-at-point-word branch, and the no-word-no-region message branch for the first three; the region-vs-whole-buffer branches for the line-oriented four; plus a tab-vs-space pair for indent and a partial-dedent assertion. `read-string` is stubbed for the prompt-driven wrappers; `transient-mark-mode` is let-bound on so `use-region-p` returns t in batch.
* test(ai-config): cover auth-source, api-key caching, context, clear-bufferCraig Jennings2026-05-131-0/+183
| | | | | | | | | | | | | The big-module coverage push starts with ai-config (was 53/191, 27.7%). New test file covers seven helpers that don't depend on a live gptel install: - `cj/auth-source-secret`: returns string secrets, funcall's function secrets, errors on missing. - `cj/anthropic-api-key` / `cj/openai-api-key`: cache the result so subsequent calls don't re-hit auth-source. - `cj/gptel--add-file-to-context`: adds existing files, skips nil and missing paths. - `cj/gptel-clear-buffer`: erases in a gptel-org buffer and reinserts the fresh prefix; no-ops + messages elsewhere. - `cj/gptel-context-clear`: three cond branches (`remove-all`, `clear`, alist-fallback) with appropriate `fboundp` stubs. - `cj/gptel-insert-model-heading`: inserts at the response-begin position with the backend/model heading. External primitives (`auth-source-search`, `gptel-add-file`, `gptel-context-remove-all`, etc.) are stubbed. The fallback test declares `gptel-context--alist` as a top-level `defvar` so `setq` inside the function reaches the dynamic binding under lexical scoping.
* test(jumper): cover the three interactive wrappers + setup-keysCraig Jennings2026-05-131-0/+175
| | | | `test-jumper.el` covered the internal helpers (`jumper--do-store-location`, `jumper--do-jump-to-location`, `jumper--do-remove-location`) thoroughly. The interactive entry points (`jumper-store-location`, `jumper-jump-to-location`, `jumper-remove-location`) and `jumper-setup-keys` were the gap. 10 new tests capture `message` and stub `completing-read` to cover: fresh-store messaging, duplicate detection, no-space full-registers path, no-locations / already-there / multi-prompt jump branches, no-locations / prompt-and-delete / cancel remove branches, and the prefix-key wiring.
* test(custom-whitespace): cover the six interactive dispatchersCraig Jennings2026-05-131-0/+183
| | | | The internal `cj/--*` helpers were exhaustively tested; the public dispatchers (`cj/remove-leading-trailing-whitespace`, `cj/collapse-whitespace-line-or-region`, `cj/ensure-single-blank-line`, `cj/delete-blank-lines-region-or-buffer`, `cj/delete-all-whitespace`, `cj/hyphenate-whitespace-in-region`) were not. 15 new tests cover the region / line / whole-buffer branches plus the two `yes-or-no-p` prompt paths (accept and decline). `transient-mark-mode` is let-bound on so `use-region-p` returns t in batch.
* test(ui-config): cover transparency apply/toggle and cursor-type setterCraig Jennings2026-05-131-0/+126
| | | | The previous test file covered only `cj/--buffer-cursor-state` and the cursor-color hook. `cj/apply-transparency`, `cj/toggle-transparency`, and `cj/set-cursor-type` were the gap. New tests stub the frame-mutating primitives so they run in batch and cover: alpha cons construction in both enabled and disabled states, the terminal-frame skip, the error-message path on `set-frame-parameter` failure, the toggle's flip + re-apply, double-toggle idempotence, and the cursor-type pass-through (including nil for invisible).
* test(system-defaults): cover the helper functionsCraig Jennings2026-05-131-0/+155
| | | | | | system-defaults.el is mostly `setq` configuration, but the testable helpers (`cj/disabled`, the two minibuffer-gc hooks, `unpropertize-kill-ring`, `cj/log-comp-warning`) were uncovered. New test file mirrors the sandbox pattern from test-system-defaults-vc-follow-symlinks.el and asserts each function's observable behavior: gc-threshold flip on minibuffer entry/exit, kill-ring property strip with empty-ring boundary, comp warning written with timestamp + non-comp type ignored so the default *Warnings* path still works. 8 new tests across Normal / Boundary cases.
* test(browser-config): cover the public wrappersCraig Jennings2026-05-131-0/+68
| | | | The internal `cj/--do-*` helpers were already tested. The `cj/apply-browser-choice` and `cj/initialize-browser` wrappers were the gap -- they call `cj/--do-*` then `message` and own the wiring logic. New tests stub `message` and `cj/--do-apply-browser-choice` to cover each branch (success, invalid plist, loaded, first-available, no-browsers).
* refactor(org-agenda): extract main-agenda prefix format into a defvarCraig Jennings2026-05-131-0/+17
| | | | | | `org-agenda-custom-commands` inlined ` %i %-15:c%?-15t% s` four times across the "d" command's overdue / high-priority / schedule / priority-B blocks. New `cj/--main-agenda-prefix-format` defvar holds the literal once; every block now references the symbol so a format tweak lands in one place. Regression test walks the "d" command's blocks and asserts each `org-agenda-prefix-format` cell resolves to the shared symbol -- a block that silently diverges fails the check.
* feat(setup): scripts/setup-telega.sh prepares the docker environment for telegaCraig Jennings2026-05-131-0/+87
| | | | | | Verifies docker is installed and the daemon is reachable, then either pulls a public image (when `TELEGA_DOCKER_IMAGE` is set) or announces the in-Emacs build path (`M-x telega-server-build`) for the user to run once. Telegram auth (phone + verification code) is interactive on first `M-x telega` and not scripted here. Same shape as setup-email.sh: helpers are sourceable for bats, `main` runs only under direct execution. 7 bats tests stub `docker` and `command` so the suite never talks to the real daemon.
* feat(telega): add telega.el module with docker-backed TDLibCraig Jennings2026-05-131-0/+25
| | | | | | | | New `modules/telega-config.el` configures telega.el as an in-Emacs Telegram client. `telega-use-docker' is on so TDLib runs in a container instead of needing a system-level build -- pairs with a follow-up `scripts/setup-telega.sh' for fresh-clone installs. First-run auth (phone + verification code) is interactive inside `M-x telega' and isn't scripted here. Launcher binding: `C-; G` (mnemonic: teleGram). `C-; t` and `C-; m t` were both taken (test-runner, music's "repeat track"), so the launcher landed on a free top-level letter. Two tests cover the wiring: module loads, launcher is bound.
* feat(vterm): show tmux scrollback history in placeCraig Jennings2026-05-131-8/+40
| | | | | | `cj/vterm-tmux-history' previously used `pop-to-buffer', which routed the history view through display-buffer-alist -- in an agent window that often meant a split or a hand-off to another window, costing the agent its frame slot. `switch-to-buffer' instead drops the history into the selected window directly; the existing quit handler already restores the origin in that same window via `set-window-buffer'. New test asserts the in-place behavior: starting single-window with a vterm origin, invoking the command leaves `(one-window-p)` t with the history buffer in the original slot. The existing render test no longer needs its `pop-to-buffer' stub since `switch-to-buffer' works in batch.
* fix(ai-vterm): preserve single-window layout across F9 toggleCraig Jennings2026-05-131-0/+170
| | | | | | | | When the agent buffer is the only window in the frame, F9 buries it (correct) but the next F9 redisplayed it as a side split instead of restoring the full-frame layout -- the display-saved path always called `display-buffer-in-direction`, which insists on a split. New `cj/--ai-vterm-last-was-bury` flag tracks which toggle-off path ran. `cj/ai-vterm` sets it to t in the bury branch (one-window-p) and nil in the delete-window branch. `cj/--ai-vterm-display-saved` checks the flag at toggle-on: if t and the frame is still single-window, it replaces the selected window's buffer in place rather than splitting. Either branch consumes the flag so it never stays stale. 5 tests in test-ai-vterm--single-window-toggle.el cover the flag's set/clear paths, the still-one-window guard, and the end-to-end roundtrip.
* feat(org-agenda): use project name as todo.org categoryCraig Jennings2026-05-131-0/+137
| | | | | | The %c column on agenda blocks rendered every project's todo.org as "todo:" -- org defaults the buffer category to the filename without extension, so every entry looked alike. An org-mode-hook now overrides org-category with the parent directory's basename (stripping a single leading dot, so ~/.emacs.d/todo.org reads as "emacs.d") whenever a todo.org file opens and its category is still the filename default. Explicit #+CATEGORY: keywords still win. 14 tests in test-org-agenda-config-category.el cover the helper's normal/boundary/error paths and the hook's override + explicit-category-preserved cases.
* fix(ui-navigation): clear dedicated before toggling window splitCraig Jennings2026-05-131-0/+110
| | | | | | | | toggle-window-split swapped buffers between two windows via two set-window-buffer calls. If either window was strongly dedicated (e.g. *Org Agenda* via the display-buffer-alist rule), the dedicated window rejected the second swap. Both panes ended up showing the dedicated buffer. The non-dedicated buffer never made it across. The fix clears dedicated on both windows before the swap. The user explicitly invoked a layout change, so preserving per-window dedicated through the operation would just re-trigger the same wedge on the next toggle. Tests in tests/test-ui-navigation--toggle-window-split.el cover the no-dedicated baseline, the bug-trigger (dedicated selected window), the post-toggle cleared state, and the one-window / three-window no-op boundaries.
* fix(org-agenda): skip CANCELLED entries from main agenda SCHEDULECraig Jennings2026-05-131-0/+35
| | | | | | | | The "d" command's (agenda ...) block had no org-agenda-skip-function. The global org-agenda-skip-scheduled-if-done is nil. CANCELLED tasks with a SCHEDULED date rendered in the forward-looking schedule unfiltered. The fix adds an org-agenda-skip-function to the SCHEDULE block: (org-agenda-skip-entry-if 'todo '("CANCELLED")). The scope is deliberate. Only CANCELLED is filtered, not DONE or FAILED. A scheduled DONE task is a record of when something happened and stays visible. Tests cover the configuration: the form must appear on the agenda block and must not appear on the overdue, hi-pri, or priority-B blocks.
* fix(calibredb-epub): point cj/nov--file-path at nov-file-nameCraig Jennings2026-05-131-0/+9
| | | | | | The fallback chain was checking `nov-epub-filename` and `nov-epub-file`, but neither symbol exists in nov.el — the real var is `nov-file-name`, set by `nov-mode` from the visited file. Both `boundp` arms always returned nil, so the fallbacks were dead code. The bug was dormant rather than active: `buffer-file-name` always holds the EPUB path for normal nov buffers and covered the first arm of the `or`. I replaced both wrong-named arms with a single live arm on `nov-file-name`, and added a Boundary test that exercises it.
* test(calibredb-epub): cover seven untested ebook helpersCraig Jennings2026-05-131-0/+216
| | | | | | I added tests for `cj/calibredb-clear-filters`, `cj/force-nov-mode-for-epub`, `cj/--nov-image-padding-cols` (the helper extracted in the previous commit), `cj/nov--natural-window-width`, `cj/nov--metadata-get`, `cj/nov--file-path`, and `cj/nov-jump-to-calibredb`. Each gets Normal, Boundary, and Error cases where they apply: 22 tests in all. The calibredb stubs sit in a new `test-calibredb-epub--with-calibredb-stubs` macro so the three jump-to-calibredb cases stay focused on their branch. Coverage on `calibredb-epub-config.el` moves from 7/104 (6.7%) to 95/133 (71.4%).
* test(prog-webdev): cover cj/webdev-format-bufferCraig Jennings2026-05-121-0/+93
| | | | I added `tests/test-prog-webdev-format.el` — 7 ERT tests over `cj/--webdev-format-command` (plain path, spaces-quoted, `.tsx`) and `cj/webdev-format-buffer` (prettier present → the region command targets `buffer-file-name`; the `file.ts` fallback when the buffer has no file; point clamps to `point-max` when the format shrinks the buffer; prettier missing → `user-error`). `executable-find` and `shell-command-on-region` are stubbed. `cj/webdev-keybindings` was already covered by `test-prog-webdev--format-wiring.el`.
* test(keyboard-compat): cover the terminal and GUI setup functionsCraig Jennings2026-05-121-0/+101
| | | | | | `cj/keyboard-compat-terminal-setup` (8 `define-key`s into `input-decode-map` for arrow escape sequences) and `cj/keyboard-compat-gui-setup` (18 `M-<UPPER>` → `M-S-<lower>` translations into `key-translation-map`) had no tests — that's the bulk of the module's executable lines. I added `tests/test-keyboard-compat-setup.el` — 7 ERT tests that `let`-bind those keymaps to fresh copies, stub `env-terminal-p` / `env-gui-p`, and check the decode/translate entries land, with completeness loops over all 8 arrow sequences and all 18 Meta-Shift letters, plus the gate-off boundary for each. `cj/--icon-blank-in-terminal` was already covered. `lookup-key` on an ESC-prefixed string can return a meta-prefix event count instead of nil, so the "no-op when not on a terminal" case asserts the keymap is still empty rather than checking individual lookups.
* test(selection-framework): cover both branches of the C-s dispatchCraig Jennings2026-05-121-0/+68
| | | | `cj/consult-line-or-repeat` had only a keybinding test. I added `tests/test-selection-framework--consult-line-or-repeat.el` — 4 ERT tests covering the fresh-search branch, the repeat-on-second-call branch, the nil `last-command` boundary, and `commandp`, with `consult-line` and `vertico-repeat` stubbed. It reuses the `use-package`-shadow trick from the keybindings test, so the module loads without dragging in the vertico/consult/embark/company/prescient stack.
* refactor(org-drill): hoist the commands out of :config and clear the ↵Craig Jennings2026-05-121-0/+6
| | | | | | | | byte-compile warnings The `cj/drill-*` defuns and `cj/drill-map` lived inside the `use-package org-drill` `:config` block, so the byte-compiler never registered them — every cross-reference between them warned ("function `cj/drill-this-file' is not known", and so on). I moved all of that to module top level, where the compiler sees it. The ten `(setq org-drill-...)` lines became a `:custom` block (no more "assignment to free variable"). Added `(require 'user-constants)` and `(require 'keybindings)` for `drill-dir` and `cj/custom-keymap`, plus `declare-function` for `org-drill`, `org-drill-resume`, `org-capture`, and `org-refile`. The module byte-compiles clean now, and `C-; D` still mounts the drill submenu with the same leaf keys. I also gave `tests/test-org-drill-first-function.el` a `cj/custom-keymap` stub: its "loads without error" test does a bare `load` of the module, which now runs the keymap mount at load time instead of deferring it inside `:config`.
* fix(ai-vterm): make F9 toggle the agent from inside an agent bufferCraig Jennings2026-05-121-0/+42
| | | | | | vterm binds `<f1>`..`<f12>` to `vterm--self-insert`, so a plain `<f9>` typed while point is in an agent buffer goes to the terminal program instead of the global toggle. That's invisible most of the time — you press F9 from another window — but it bites when the agent buffer is the only window in the frame, because there's nowhere else to press it from. I re-bound the F9 family in `vterm-mode-map` (via `with-eval-after-load 'vterm`) so that `<f9>`, `C-<f9>`, and `M-<f9>` reach `cj/ai-vterm`, `cj/ai-vterm-pick-project`, and `cj/ai-vterm-pick-buffer` from there too. The C-/M- variants aren't actually in vterm's intercept set, but binding them keeps things uniform. New `tests/test-ai-vterm--f9-in-vterm.el`: 4 ERT tests over the `vterm-mode-map` and global bindings. F12's `cj/vterm-toggle` has the same shape of bug and isn't touched here.
* feat(dirvish): start org-drill on the .org file at point with SCraig Jennings2026-05-121-0/+86
| | | | | | `S` ("study") in `dirvish-mode-map` opens the `.org` file at point and runs `cj/drill-this-file` on it, so I can drill any deck straight from the file list. It `user-error`s on no file, on a directory, or on a non-`.org` file. `D` and `O` were already taken (duplicate-file, open-with-command), so I went with `S`. It shadows dired's `dired-do-symlink`, which I never use from dirvish and which stays on `M-x` anyway. New `tests/test-dirvish-config-drill.el`: 6 ERT tests with `dired-get-filename`, `find-file`, and `cj/drill-this-file` mocked. I also fixed the stale `P` line in the module commentary — `P` is the print command now, not copy-path.
* feat(org-drill): drill any Org file from anywhereCraig Jennings2026-05-121-0/+152
| | | | | | | | `org-drill` has no fixed home — with `org-drill-scope` left at its default it just drills the current buffer. So the only thing in my config tied to a location was `cj/drill-start`, which forced a pick from `drill-dir`. `C-; D f` (`cj/drill-this-file`) drills whatever Org buffer is current, so a drill file living anywhere works. It `user-error`s when the buffer isn't an Org buffer. `C-u C-; D s` (and `C-u C-; D e`) now prompts for the directory to pick from instead of always using `drill-dir`. Bare `C-; D s` is unchanged. I pulled the picking logic into `cj/--drill-files-in`, `cj/--drill-pick-dir`, and `cj/--drill-pick-file` so it's unit-testable. New `tests/test-org-drill-config.el`: 12 ERT tests over those helpers, `cj/drill-this-file`, `cj/drill-start`, and the keymap.
* test(prog-python): cover the command builders and the S-f5/S-f6 wiringCraig Jennings2026-05-121-0/+144
| | | | I added tests/test-prog-python-commands.el — 13 ERT tests over `cj/--python-mypy-command`, `cj/--python-debug-command`, `cj/python-mypy`, `cj/python-debug`, and `cj/python-mode-keybindings`. Normal, boundary, and error case per function. `executable-find`, `compile`, and `pdb` are stubbed at the boundary. None of the module's functions had coverage before this.
* fix(nov): center the EPUB text by setting window margins directlyCraig Jennings2026-05-121-1/+60
| | | | | | | | | | | | The 80% width from `4d9a206' wasn't actually narrowing the page: `cj/nov-apply-preferences' set `nov-text-width' to t (nov renders the text unfilled, one long line per paragraph) and counted on `visual-fill-column-mode' to set the window's display margins, but those margins never got applied in nov-mode buffers (even after manually re-running the layout), so the text wrapped at the full window width. The cause is still unknown. This drops `visual-fill-column' from nov entirely: - `nov-text-width' is a column count (~80% of the window's natural width), so nov's `shr' fills the text itself. - `cj/nov-update-layout' sets the window's left/right margins directly to `(natural - text-width) / 2' each, centering the block, and pushes the fringes out to the window edge so they don't show as thin lines beside the text. When the width changes it re-renders, restoring the reading position approximately. - `cj/nov-apply-preferences' adds a `kill-buffer-hook' that drops the margins and fringes when the EPUB buffer goes away, so a later buffer in that window isn't left indented. - `+'/`=' and `-'/`_' adjust `cj/nov-margin-percent' and re-flow + re-center. The text-width math moved into a `cj/nov--natural-window-width' helper alongside the existing `cj/nov--text-width'. Known nit: the centering is a touch left-of-center because shr wraps at word boundaries, so the rendered text is a bit narrower than `nov-text-width' and the right margin ends up slightly larger. Logged as a follow-up.
* feat(nov): default the EPUB text column to 80% of the windowCraig Jennings2026-05-121-3/+3
| | | | `1c5c8bd' had set `cj/nov-margin-percent' to 12 (~76% text); 10 gives a round 80%. Adjust per-buffer with the `+'/`-' keys; clamp is unchanged (0..25, i.e. 50%..100%).
* feat(org): add reveal.js present command and header removalCraig Jennings2026-05-121-0/+64
| | | | | | `C-; p SPC' (`cj/reveal-present') is the new fast path: it inserts reveal.js headers if the buffer doesn't have them (prompting for a title), saves the buffer, exports to self-contained HTML, and opens it in the browser. It's the export-and-open you'd otherwise reach by doing `C-; p h' then `C-; p e'. `C-; p H' (`cj/reveal-remove-headers') strips the header block this module inserts, and `C-; p h' (`cj/reveal-insert-header') now errors instead of duplicating headers when a reveal block is already present. The header logic moved into small helpers (`cj/--reveal-has-header-p', `cj/--reveal-remove-headers', `cj/--reveal-ensure-header', `cj/--reveal-keyword-regexp', and the `cj/--reveal-header-keywords' list), and `test-org-reveal-config-headers.el' drives them directly: detection on a reveal vs. a plain Org buffer, removal of the generated block (and only the leading block), body preservation, and the no-duplicate-headers error.
* fix(org): give the F8 agenda window 75% of the frameCraig Jennings2026-05-121-0/+49
| | | | | | The agenda buffer's `display-buffer-alist' rule used `(window-height . fit-window-to-buffer)', so a sparse agenda opened as a sliver a few lines tall. The rule now takes `(window-height . cj/org-agenda-window-height)', a defcustom defaulting to 0.75 (the fraction of the frame the agenda window gets), and the rule itself moved into `cj/--org-agenda-display-rule' so it's testable. New `test-org-agenda-config-display.el' checks that the configured fraction flows through, that it's no longer `fit-window-to-buffer', and (integration) that `display-buffer' produces a window near that size. `(use-package alert)' gained an `:if (or (not noninteractive) (require 'alert nil t))' guard: the batch test runner loads this module without `package-initialize', so the optional notification package may be installed but not yet on the load path, and the unconditional `:config' setq's would error.