Skip to content

fix(cli): translate domain errors into clean ClickException output#31

Merged
plind-junior merged 2 commits into
vouchdev:mainfrom
dripsmvcp:fix/cli-clean-domain-errors
May 20, 2026
Merged

fix(cli): translate domain errors into clean ClickException output#31
plind-junior merged 2 commits into
vouchdev:mainfrom
dripsmvcp:fix/cli-clean-domain-errors

Conversation

@dripsmvcp

@dripsmvcp dripsmvcp commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Every vouch CLI command that calls into proposals / lifecycle / sessions / storage now produces a one-line Error: … for domain errors instead of a Python traceback. Closes the bug originally reported against approve / reject and the wider class it belongs to.

Root Cause

src/vouch/cli.py:194 (approve) and src/vouch/cli.py:207 (reject) caught only (ArtifactNotFoundError, ValueError), but proposals.approve() / proposals.reject() raise ProposalError, which subclasses RuntimeError — so it slipped past the except and surfaced as a traceback. The four propose-* shortcuts (cli.py:223, cli.py:241, cli.py:259, cli.py:274) had no error handling at all, so any validation failure ("claim text is empty", "unknown source/evidence id", etc.) also printed a traceback. The lifecycle and session commands had the same gap. The MCP and JSONL servers (server.py:359-363 / jsonl_server.py:445-457) already get this right — the CLI was the outlier.

Fix

Added a small _cli_errors context manager that translates (ArtifactNotFoundError, ValueError, ProposalError, LifecycleError) into click.ClickException, and applied it to every command that calls into the domain layer: show, approve, reject, propose-claim, propose-page, propose-entity, propose-relation, source add, supersede, contradict, archive, confirm, cite, session end, crystallize. import-apply keeps its existing except RuntimeError since bundle.import_apply raises bare RuntimeError for conflicts and that broader catch was intentional.

No behavior change on the happy path; only the failure rendering differs (Error: … instead of a traceback, exit code 1).

Test Plan

  • New regression file tests/test_cli.py using click.testing.CliRunner:
    • test_approve_already_decided_proposal_shows_clean_error (the originally-reported not pending case)
    • test_reject_empty_reason_shows_clean_error
    • test_propose_claim_empty_text_shows_clean_error
    • test_propose_claim_unknown_source_shows_clean_error
    • test_propose_entity_empty_name_shows_clean_error
    • test_show_missing_proposal_shows_clean_error (regression for the existing handling — must keep working after refactor)
  • Verified the new tests fail against unfixed code: 5 of 6 reproduce a traceback (the show one already passed since it caught ArtifactNotFoundError).
  • pytest tests/test_cli.py tests/test_storage.py tests/test_jsonl_server.py tests/test_index.py -q → 45 passed.
  • ruff check src/vouch/cli.py tests/test_cli.py → All checks passed.
    Fixes fix(cli): approve and reject commands don't catch ProposalError — raw traceback #29

Summary by CodeRabbit

  • Bug Fixes

    • CLI commands now show clean, user-facing error messages (no Python tracebacks).
    • Session endings are now persisted correctly so recent changes are reliably saved.
  • Tests

    • Added tests to verify CLI error handling across multiple command scenarios.

Review Change Stack

The CLI handlers for approve and reject caught only
(ArtifactNotFoundError, ValueError) but proposals.approve() /
proposals.reject() raise ProposalError -- a RuntimeError subclass. The
four propose-* shortcuts caught nothing at all. Result: a double-
approve, an empty rejection reason, an empty claim text, or an unknown
source id surfaced a raw Python traceback instead of the one-line
`Error: ...` the rest of the CLI emits.

Add a small `_cli_errors` context manager that translates
ArtifactNotFoundError / ValueError / ProposalError / LifecycleError
into click.ClickException, and apply it to every command that calls
into proposals, lifecycle, sessions, or storage. The MCP and JSONL
servers already do the equivalent in their own envelopes; this brings
the human-facing surface in line.

Adds tests/test_cli.py with regressions for approve, reject, propose-
claim, propose-entity, and show.
@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: da05d3f4-2e7c-49b1-a6e1-6b2fec180ee3

📥 Commits

Reviewing files that changed from the base of the PR and between b48b404 and 129480f.

📒 Files selected for processing (4)
  • src/vouch/cli.py
  • src/vouch/context.py
  • src/vouch/sessions.py
  • src/vouch/storage.py

📝 Walkthrough

Walkthrough

Adds a reusable _cli_errors() context manager and wraps CLI commands to convert domain exceptions into click.ClickException; adds regression tests for clean CLI error output and small typing and storage exception-chaining fixes.

Changes

CLI Error Handling Centralization

Layer / File(s) Summary
Error handler foundation
src/vouch/cli.py
Adds Iterator and contextmanager imports, updates CLI imports, and implements _cli_errors() that catches ArtifactNotFoundError, ValueError, ProposalError, and LifecycleError and re-raises click.ClickException with a single-line message.
CLI command wrappers
src/vouch/cli.py
Wraps CLI commands (show, approve, reject, propose-claim/page/entity/relation, source add, supersede, contradict, archive, confirm, cite, session end, crystallize) with with _cli_errors(): to centralize error translation.
CLI regression tests
tests/test_cli.py
New test module with store fixture, _assert_clean_error helper enforcing non-zero exit and Error: output without Traceback, and tests for approve/reject/propose/show error cases.
Context item typing
src/vouch/context.py
Adds ContextItemKind = Literal["claim","page","entity","relation","source"] and casts kind to ContextItemKind when building ContextItem.
Session persistence call
src/vouch/sessions.py
session_end now calls store.update_session(sess) after backfilling sess.proposal_ids instead of store.put_session(sess).
Storage exception chaining
src/vouch/storage.py
KBStore write methods (put_claim, put_page, put_entity, put_relation, put_evidence, put_session, put_proposal) now catch FileExistsError as e and re-raise the existing ValueError using from e to preserve the original cause.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • vouchdev/vouch#26: Related edits to KBStore put_* methods and file-existence handling; this PR adjusts re-raise chaining while #26 added exclusive write guards.

Poem

🐰 I hopped through traces, tangled and wild,
wrapped errors neat for each curious child.
Now "Error:" shines clean, no traceback to fear —
a tidy CLI meadow, calm and clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: translating domain errors to clean ClickException output in CLI commands.
Linked Issues check ✅ Passed The PR fully addresses issue #29 by introducing _cli_errors() context manager that catches ProposalError, ValueError, ArtifactNotFoundError, and LifecycleError, then applies it to all affected CLI commands including approve, reject, and others as required.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing CLI error handling: _cli_errors() helper, test coverage for error cases, type annotation improvements, and exception chaining fixes directly support the main objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vouch/cli.py (1)

14-48: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix import ordering to unblock CI.

Ruff I001 is failing on this import block, so the PR cannot merge until imports are sorted/formatted.

Suggested fix
-from collections.abc import Iterator
-from contextlib import contextmanager
+from collections.abc import Iterator
+from contextlib import contextmanager
...
-from .lifecycle import LifecycleError
-from .models import ProposalStatus
-from .proposals import (
-    ProposalError,
-    approve as do_approve,
-)
+from .lifecycle import LifecycleError
+from .models import ProposalStatus
+from .proposals import ProposalError, approve as do_approve

Then run:

ruff check src/vouch/cli.py --fix
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 14 - 48, The import block is not sorted per
Ruff's I001 rule; reorder the imports into standard-library, third-party, then
local-package groups and alphabetize within each group (e.g., keep
collections.abc, contextlib, pathlib first, then click/yaml, then all vouch
package imports), combine related vouch imports into grouped blocks and remove
duplicate grouping (e.g., consolidate the several from .proposals and .
imports), ensure symbols like build_context_pack, LifecycleError,
ProposalStatus, propose_claim/propose_entity/propose_page/propose_relation,
do_approve/do_reject, KBStore/discover_root are still imported, and then run
ruff check src/vouch/cli.py --fix to apply formatting automatically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/vouch/cli.py`:
- Around line 14-48: The import block is not sorted per Ruff's I001 rule;
reorder the imports into standard-library, third-party, then local-package
groups and alphabetize within each group (e.g., keep collections.abc,
contextlib, pathlib first, then click/yaml, then all vouch package imports),
combine related vouch imports into grouped blocks and remove duplicate grouping
(e.g., consolidate the several from .proposals and . imports), ensure symbols
like build_context_pack, LifecycleError, ProposalStatus,
propose_claim/propose_entity/propose_page/propose_relation,
do_approve/do_reject, KBStore/discover_root are still imported, and then run
ruff check src/vouch/cli.py --fix to apply formatting automatically.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 24761a5b-57bd-44d4-b94f-5d95d137d341

📥 Commits

Reviewing files that changed from the base of the PR and between abf9c8f and b48b404.

📒 Files selected for processing (2)
  • src/vouch/cli.py
  • tests/test_cli.py

The CI workflow runs `ruff check`, `mypy`, then `pytest`, and the
fix/cli-clean-domain-errors branch was failing all three on
pre-existing main-branch issues unrelated to the CLI change itself.

* ruff: add `from e` to the seven `raise ValueError(...) from
  FileExistsError` re-raises added by the recent exclusive-create
  guards in storage.put_* (B904), and let ruff re-sort the cli.py
  import block (I001).
* mypy: narrow the `kind` value flowing into `ContextItem(type=...)`
  with a `Literal` cast so the strict-typed field accepts what the
  search backends actually return.
* pytest: `session_end()` mutates a session that `session_start()` has
  already written, but `put_session()` now uses exclusive create and
  rejects the second write. Add `KBStore.update_session()` mirroring
  `update_claim` and have `session_end` call it; existing test_sessions
  coverage now passes again.

No behavior change for the CLI surface — these are infrastructure
fixes so the existing test_sessions / lint / type assertions pass on
this branch.
@plind-junior

Copy link
Copy Markdown
Collaborator

Thanks for going beyond the report and tackling the whole bug class — the _cli_errors extraction is cleaner than what I asked for and the test coverage looks good.
LGTM!

@plind-junior plind-junior merged commit c1feb23 into vouchdev:main May 20, 2026
5 checks passed
plind-junior added a commit that referenced this pull request May 22, 2026
fix(cli): translate domain errors into clean ClickException output
@coderabbitai coderabbitai Bot mentioned this pull request Jun 3, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): approve and reject commands don't catch ProposalError — raw traceback

2 participants