Neovim plugin + Python CLI for editing Taskwarrior tasks as markdown.
bin/taskmd— Python CLI tool (parser, adapter, render, diff, apply)lua/taskwarrior/init.lua— thin orchestrator (~273 lines)lua/taskwarrior/{buffer,apply,capture,delegate,review,saved_views,projects, completion,commands,help,validate,taskmd,config,views,diff_preview,feedback, health,statusline,cmp}.lua— focused modules (split landed in v1.2.0)lua/telescope/_extensions/task.lua— Telescope extension (deliberately namedtaskeven after the v1.3.0 rename, because the extension name is the user-facing:Telescope <name>slug, not the module path)plugin/taskwarrior.lua— runtime entrypoint (registers:Tasklazily)doc/taskwarrior.txt— vim help referencetests/test_taskmd*.py— Python tests (pytest, 358 tests)tests/lua/spec/*_spec.lua— Lua tests (plenary busted, 121 assertions)
- Python tests:
uv run --with pytest python -m pytest tests/ -q --ignore=tests/e2e - Lua unit tests:
./tests/lua/bootstrap.sh - Lua e2e tests:
./tests/e2e/run.sh— spawns a temp TASKDATA, seeds fixtures, drives each feature against a realtaskCLI, and validates downstream output (mmdc for Mermaid, task export for mutations, window state for floats). This is where "verified" lives; unit tests catch syntax errors, e2e catches behaviour. - Integration tests use a temp TASKDATA dir — they don't touch real tasks
- The CLI must have zero external dependencies (stdlib only)
lua/taskwarrior/init.luashould remain a thin orchestrator (≤300 lines); business logic belongs in submodules
- Render demos:
demo/render-all.sh(validates tapes + renders + size-checks) - Validate tapes only:
demo/validate-tapes.sh - Never run
vhsdirectly — the render wrapper enforces env isolation to prevent leaking real task data - Pre-commit hook (
.githooks/pre-commit) blocks commits with unsafe tapes or oversized assets
- Python: type hints, argparse, no external deps
- Lua: follow NvChad/lazy.nvim patterns
- All Taskwarrior commands must include
rc.bulk=0 rc.confirmation=offto avoid interactive prompts
Before reporting a feature done, it must be verified against a real
Taskwarrior DB (use the tw_env fixture pattern from
tests/test_taskmd.py: temp TASKDATA + isolated .taskrc). Unit
tests that only assert "module loads", "command registers", or
"helper returns a list" do NOT verify the feature. They catch syntax
errors, nothing else.
Hard rule: every user-facing flow gets a smoke test.
A "user-facing flow" is any code path triggered by: a :Task* command
callback, a vim.ui.select / vim.ui.input callback, a buffer-local
keymap (e.g. g?, <CR>), a global keymap (e.g. <leader>tF), or any
popup/picker/buffer the user can hit. For each one, add an entry to
tests/lua/spec/smoke_user_flows_spec.lua that:
- Stubs
vim.ui.select/vim.ui.inputso the flow doesn't block. - Invokes the entry point headlessly (e.g.
tutor.start(),feedback.last_error(), the keymap callback). - Drains pending
vim.schedulecallbacks viavim.wait(50, …)so errors thrown inside scheduled closures fire before the assertion. - Asserts no real Lua / vim API error surfaced — match on
Error executing,stack traceback:,attempt to, vim error codes (E\d+:), or specific error fragments like'replacement string'. Do not treat intentional ERROR-levelvim.notify()calls as failures — those are user messages, not bugs.
The smoke test bar is "feature does not crash on the happy path of every selection / argument value". It is intentionally weaker than the feature-correctness bar (which requires asserting outputs). It exists to catch the class of bug that ships when unit tests verify primitives in isolation but no test ever drives the actual user journey.
Concrete example: the v1.4.1 verify-buffer bug
('replacement string' item contains newlines at init.lua:497)
shipped because every existing tutor test exercised _begin_session,
_cleanup, the argv prefix, and orphan recovery — but none invoked
:TaskTutor and selected "Show me the exact task commands first".
The smoke spec added in that commit reproduces the original failure
when the fix is reverted (verified) and is what you must add for any
new user-facing flow before claiming "shipped".
Bar per feature category:
-
Commands that mutate a task (
:TaskAppend,:TaskModifyField, …): seed a task, invoke the command headlessly,task exportthe UUID, assert the field changed. Stubbingvim.ui.input/selectis fine for driving the flow. -
Commands that read and render (
:TaskGraph,:TaskReport,:TaskInbox, dashboard, query blocks): actually run a downstream validator on the output. For Mermaid, pipe throughmmdc(it is installed). For markdown export, round-trip throughtaskmd apply. For reports, assert the buffer contents match the expected filter. -
Commands with side effects on Neovim state (
:TaskFloat,gffloat, embedded query blocks): assert the resulting buffer or window exists with the expected properties (nvim_list_wins,nvim_buf_get_lines). -
Rendering that places virt_text / signs on buffer lines (relative date chips, overdue badges, urgency bars): checking that the extmark exists with the right
virt_text_posis NOT enough.right_aligndraws at the window's right edge unconditionally and overwrites buffer text on wrapped long lines. Verify with a geometric layout check: computestrdisplaywidthof the visible line content, subtractstrdisplaywidthof the virt_text, assert the first wrap segment doesn't exceedcolumns − virt_text_width. Seegeometric_overlapsintests/e2e/spec/e2e_spec.lua. -
Background behaviour (granulation auto-stop): drive the real timer by advancing
vim.loop.now()-equivalent or by calling the internal check directly; verifytask +ACTIVE exportis empty. -
Degraded environment — missing or broken hard dependencies: the plugin shells out to
task(always),python3/bin/taskmd(live diff preview, optional CLI features),mmdc(:TaskGraph), andclaude(:TaskDelegate). Tests must cover the case where each binary is absent. CI installs Taskwarrior at the top of every job, and the e2e harness requirestaskfor its own seed step, so the only way to exercise the missing-binary path is to monkey- patchvim.fn.executablein a Lua spec. The plugin must: A. emit a clear, actionablevim.notify(..., WARN)at startup iftaskis missing — not a Lua trace fromvim.fn.system; B. short-circuitrun()intaskmd.lua(and any equivalent wrapper) so subsequent calls return"", 127without touchingvim.fn.system; C. throttle the missing-binary notify to at most one per session (no per-call spam); D. keep:checkhealth taskwarrioras the canonical post-install verification path for users who skipped reading the README. Seetests/lua/spec/degraded_env_spec.lua. Issue #2 was a manifestation of this gap —tasknot on PATH triggeredE475: Invalid value for argument cmd: 'task' is not executablewith no friendly fallback. Any new external-binary dep added in the future (mmdc, claude, future Rust helpers) must ship with a parallel degraded-env spec; otherwise the same class of bug reappears the moment a user installs the plugin without that dep. -
Concurrent state — external Taskwarrior changes: the plugin is not the only writer. CLI
task add, mobile sync, another editor, or a background hook can mutate Taskwarrior between the time we render a buffer and the time the user saves it. Any change to the save path (compute_diff,M.apply,apply.on_write) must hold the line against these scenarios: A. externaltask addbetween render and save → not marked done/deleted; B. externaltask modifyadding a field → field survives the save; C. externally completed task whose UUID is still in the buffer → no pending duplicate resurrected; D. both sides modified same task → conflict surfaced, buffer does not silently overwrite external; E.--force/:w!preserves the destructive escape hatch; F. no external change → ordinary edits still apply (control). Seetests/e2e/spec/external_changes_spec.lua(Lua round-trip),tests/lua/spec/diff_external_changes_spec.lua(pure compute_diff), andTestIntegrationConflictsintests/test_taskmd_extended.py(Python mirror). Taskwarrior timestamps have 1-second precision; e2e tests mustsleep 1.2sbetween render and external mutation to makemodified > rendered_athold reliably.
"Runs the test suite and all tests pass" is necessary but not sufficient
— the test suite must exercise the feature's real effect, not just its
existence. When the user says "verify all features," expand the test
suite to cover each feature's observable behaviour, don't just run what
already exists. Do not claim a feature is verified if the only check is
that pcall(require, "taskwarrior.foo") returned true.