Skip to content

fix: crystallize exits 0 on total approval failure (#56)#62

Merged
plind-junior merged 5 commits into
vouchdev:mainfrom
alpurkan17:fix/crystallize-failure-signal-56
May 25, 2026
Merged

fix: crystallize exits 0 on total approval failure (#56)#62
plind-junior merged 5 commits into
vouchdev:mainfrom
alpurkan17:fix/crystallize-failure-signal-56

Conversation

@alpurkan17

@alpurkan17 alpurkan17 commented May 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #56

Summary

crystallize() silently swallows all approval failures and exits 0, giving no indication that the operation completely failed.

Changes

  • CLI: After emitting result JSON, check approved vs failures counts. All failed → exit 1. Partial failures → warning on stderr.
  • Tests: Added test_crystallize_collects_approval_failures

Summary by CodeRabbit

  • Improvements
    • The crystallize CLI now reports approved/failed/total counts, exits with an error when all proposals fail, and emits a warning when some proposals fail, pointing to failure details in the output.
  • Tests
    • Added tests ensuring approval failures are collected and verifying CLI behavior for all-fail and partial-fail scenarios.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 23, 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: c537efa1-a62a-414e-9cfc-1753175af79c

📥 Commits

Reviewing files that changed from the base of the PR and between d39b8cb and 2837151.

📒 Files selected for processing (1)
  • tests/test_cli.py

📝 Walkthrough

Walkthrough

The CLI crystallize command now inspects the JSON result to compute approved/failed counts, prints an error and exits 1 if all proposals failed, or prints a warning if some failed. Tests verify failure collection and both CLI exit/warning behaviors under mocked approval failures.

Changes

Failure Visibility and Exit Code Handling

Layer / File(s) Summary
CLI Error Handling for Complete and Partial Failures
src/vouch/cli.py
After emitting crystallize JSON output, the CLI computes approved and failed proposal counts. When all proposals fail, it prints an error to stderr and raises SystemExit(1); when only some fail, it prints a warning to stderr indicating the failure count and points to failures in the JSON output.
Session-level failure collection test
tests/test_sessions.py
Imports patch from unittest.mock and adds test_crystallize_collects_approval_failures, which patches vouch.sessions.approve to raise ValueError("storage full"), then verifies crystallize returns zero approved items and two failure records with expected error and error_type.
CLI regression tests with mock embedder
tests/test_cli.py
Adds tests that patch vouch.sessions.approve to raise ValueError; one asserts the CLI exits with code 1 and stderr error when all proposals fail, the other asserts exit 0 and a stderr warning when only some proposals fail.

Sequence Diagram

sequenceDiagram
  participant CLI
  participant sessions_crystallize as sessions.crystallize
  participant approve as vouch.sessions.approve
  participant Storage
  CLI->>sessions_crystallize: call crystallize(session_id)
  sessions_crystallize->>approve: attempt approve(proposal)
  approve->>Storage: persist proposal
  Storage->>approve: error/exception (when failing)
  approve-->>sessions_crystallize: raise Exception or return success
  sessions_crystallize-->>CLI: {"approved": [...], "failures": [...]}
  CLI->>CLI: compute approved/failed counts
  CLI->>CLI: print JSON to stdout
  CLI->>CLI: if all failed -> print stderr error and SystemExit(1)
  CLI->>CLI: if partial failures -> print stderr warning
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • vouchdev/vouch#31: Also modifies src/vouch/cli.py error handling around crystallize/CLI flows and related exception reporting.

Poem

"I hop through code with whiskers bright,
I patch the CLI so errors light,
When proposals tumble, all or some,
I trumpet exits so you won't be glum,
A carrot for each failing crumb. 🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 clearly summarizes the main change: fixing the issue where crystallize exits with 0 on total approval failure. It directly addresses the primary objective and is specific and concise.
Linked Issues check ✅ Passed The PR implementation fully addresses all coding requirements from issue #56: exit code 1 on total approval failure, warning on partial failures, and test coverage for approval failure collection and CLI behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #56 requirements: CLI exit code handling, approval failure warnings, and comprehensive test coverage for the new behavior.

✏️ 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.

🧹 Nitpick comments (1)
tests/test_sessions.py (1)

49-63: ⚡ Quick win

Add CLI-level coverage for the new failure signaling contract.

This test verifies sess_mod.crystallize failure collection, but the new behavior in src/vouch/cli.py (stderr warning/error + exit code semantics) is still untested. Please add a CliRunner test for: (1) all approvals fail → exit code 1 + error: on stderr, and (2) partial failures → exit code 0 + warning: on stderr.

🤖 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 `@tests/test_sessions.py` around lines 49 - 63, Add a CLI-level test using
Click's CliRunner to exercise the crystallize command (invoke the CLI entrypoint
in vouch.cli, e.g. the cli/main function) and assert stderr/exit semantics:
create a session and two proposed claims as in
test_crystallize_collects_approval_failures, then (1) patch
vouch.sessions.approve to always raise ValueError and call the CLI crystallize
command, assert exit code == 1 and stderr contains "error:", and (2) patch
approve to raise for one approval but succeed for the other, call the CLI,
assert exit code == 0 and stderr contains "warning:". Ensure the test references
sess_mod.crystallize/session_start/session_end setup and uses CliRunner.invoke
against the same session id and approver arguments so behavior matches the
existing unit test.
🤖 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.

Nitpick comments:
In `@tests/test_sessions.py`:
- Around line 49-63: Add a CLI-level test using Click's CliRunner to exercise
the crystallize command (invoke the CLI entrypoint in vouch.cli, e.g. the
cli/main function) and assert stderr/exit semantics: create a session and two
proposed claims as in test_crystallize_collects_approval_failures, then (1)
patch vouch.sessions.approve to always raise ValueError and call the CLI
crystallize command, assert exit code == 1 and stderr contains "error:", and (2)
patch approve to raise for one approval but succeed for the other, call the CLI,
assert exit code == 0 and stderr contains "warning:". Ensure the test references
sess_mod.crystallize/session_start/session_end setup and uses CliRunner.invoke
against the same session id and approver arguments so behavior matches the
existing unit test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b1b5cdd-3e5f-4770-bdb4-fc2afd760b87

📥 Commits

Reviewing files that changed from the base of the PR and between 96028f5 and 77d573f.

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

@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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_cli.py (2)

136-141: ⚡ Quick win

Consider using context manager for patch lifecycle.

The manual start()/stop() pattern for embedder_patch is verbose and risks leaving the patch active if an exception occurs between start and stop (though the finally block mitigates this here). Using the patch as a context manager would be more idiomatic and safer:

with patch("vouch.embeddings.get_embedder", return_value=_MockEmbedder()):
    src = store.put_source(b"e")
🤖 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 `@tests/test_cli.py` around lines 136 - 141, Replace the manual
patch.start()/patch.stop() pattern with a context manager to ensure the patch
lifecycle is safe and concise: use patch("vouch.embeddings.get_embedder",
return_value=_MockEmbedder()) as a context manager around the call to
store.put_source so the mock embedder (_MockEmbedder) is applied only for the
scope where src = store.put_source(b"e") is executed, avoiding explicit
embedder_patch.start()/embedder_patch.stop().

158-163: ⚡ Quick win

Consider using context manager for patch lifecycle.

Same as the previous test: the manual start()/stop() pattern for embedder_patch would be clearer as a context manager:

with patch("vouch.embeddings.get_embedder", return_value=_MockEmbedder()):
    src = store.put_source(b"e")
🤖 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 `@tests/test_cli.py` around lines 158 - 163, Replace the manual
patch.start()/patch.stop() pattern with a context manager: use
patch("vouch.embeddings.get_embedder", return_value=_MockEmbedder()) as a
context manager around the call to store.put_source so the embedder mock is
automatically applied and torn down; update the block that currently creates
embedder_patch and calls embedder_patch.start()/embedder_patch.stop() and
instead wrap the store.put_source(b"e") invocation inside the with-patch context
to ensure proper lifecycle management.
🤖 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.

Inline comments:
In `@tests/test_cli.py`:
- Line 31: The return expression in tests/test_cli.py (the line returning
np.zeros((len(texts), self.dim), dtype=np.float32) if texts else np.zeros((0,
self.dim), dtype=np.float32)) exceeds the 100-char limit; split it across
multiple lines by using parentheses and line breaks (e.g., assign the two
np.zeros calls to clearly wrapped lines or use a conditional expression on
separate lines) so the same logic in the function/method containing this line
remains intact but each line is under 100 characters.

---

Nitpick comments:
In `@tests/test_cli.py`:
- Around line 136-141: Replace the manual patch.start()/patch.stop() pattern
with a context manager to ensure the patch lifecycle is safe and concise: use
patch("vouch.embeddings.get_embedder", return_value=_MockEmbedder()) as a
context manager around the call to store.put_source so the mock embedder
(_MockEmbedder) is applied only for the scope where src = store.put_source(b"e")
is executed, avoiding explicit embedder_patch.start()/embedder_patch.stop().
- Around line 158-163: Replace the manual patch.start()/patch.stop() pattern
with a context manager: use patch("vouch.embeddings.get_embedder",
return_value=_MockEmbedder()) as a context manager around the call to
store.put_source so the embedder mock is automatically applied and torn down;
update the block that currently creates embedder_patch and calls
embedder_patch.start()/embedder_patch.stop() and instead wrap the
store.put_source(b"e") invocation inside the with-patch context to ensure proper
lifecycle management.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 516917d1-8e16-4086-a416-183c89896df5

📥 Commits

Reviewing files that changed from the base of the PR and between 77d573f and e35679f.

📒 Files selected for processing (1)
  • tests/test_cli.py

Comment thread tests/test_cli.py Outdated

@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)
tests/test_cli.py (1)

26-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make NumPy optional for test collection (tests/test_cli.py + embeddings tests)

  • tests/test_cli.py still has hard import numpy as np statements (including around lines 28 and 31), so pytest collection will fail if NumPy isn’t installed.
  • Other test modules (tests/embeddings/test_core.py, tests/embeddings/test_dedup.py, tests/embeddings/test_integration.py, tests/embeddings/_fakes.py) also import NumPy at module import time, while numpy is only declared via optional embeddings / embeddings-fast extras—guard these imports or use pytest.importorskip/conditional skipping so the suite can collect without NumPy.
🤖 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 `@tests/test_cli.py` around lines 26 - 33, The tests import NumPy at module
scope causing collection to fail when NumPy is not installed; modify the test
modules (e.g., tests/test_cli.py and tests/embeddings/* and _fakes.py) to guard
NumPy usage by either calling pytest.importorskip("numpy") at the top of tests
that require it or by deferring the import inside helper classes/functions
(e.g., inside _MockEmbedder.encode and _MockEmbedder.encode_batch) using a local
try/except ImportError that raises pytest.skip or falls back to a
pure-Python/empty-array alternative; update references to np.zeros accordingly
and keep the public API of _MockEmbedder (methods encode and encode_batch)
unchanged so test logic continues to work when NumPy is available.
🤖 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 `@tests/test_cli.py`:
- Around line 26-33: The tests import NumPy at module scope causing collection
to fail when NumPy is not installed; modify the test modules (e.g.,
tests/test_cli.py and tests/embeddings/* and _fakes.py) to guard NumPy usage by
either calling pytest.importorskip("numpy") at the top of tests that require it
or by deferring the import inside helper classes/functions (e.g., inside
_MockEmbedder.encode and _MockEmbedder.encode_batch) using a local try/except
ImportError that raises pytest.skip or falls back to a pure-Python/empty-array
alternative; update references to np.zeros accordingly and keep the public API
of _MockEmbedder (methods encode and encode_batch) unchanged so test logic
continues to work when NumPy is available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d45952e-5277-4b29-bc03-4e624bdf4fd6

📥 Commits

Reviewing files that changed from the base of the PR and between e35679f and d39b8cb.

📒 Files selected for processing (1)
  • tests/test_cli.py

@plind-junior plind-junior merged commit 24d78fe into vouchdev:main May 25, 2026
5 checks passed
galuis116 added a commit to galuis116/vouch that referenced this pull request May 25, 2026
Leftover from the merge of main (which brought PR vouchdev#62's
test_sessions.py changes); ruff F401 was failing CI on PR vouchdev#77.
galuis116 added a commit to galuis116/vouch that referenced this pull request May 25, 2026
Resolves conflicts in CHANGELOG.md, tests/test_sessions.py, and
auto-merges src/vouch/sessions.py with the recently-merged:

  - vouchdev#61 (FTS5 indexing of crystallize summary page) — adds
    index_db.index_page(...) right after store.put_page(...) inside
    crystallize. Composes cleanly with this PR's strict-derivation
    _build_summary_body and the summary_page_id audit fix.
  - vouchdev#62 (test_crystallize_collects_approval_failures) — preserved as
    a sibling test.
  - vouchdev#75 (bundle import sha256 verification) — CHANGELOG entry only.
  - vouchdev#73 (bundle POSIX separators) — CHANGELOG entry only.

Both regression tests added by this PR
(test_crystallize_summary_page_does_not_leak_agent_controlled_fields
and test_crystallize_audit_event_records_summary_page_id) still pass
against the merged sessions.crystallize.
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.

bug: crystallize silently swallows approval failures and exits 0 on complete failure

2 participants