diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a01124b..13e53176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -273,6 +273,13 @@ All notable changes to vouch are documented here. Format follows in config.yaml (#476). ### Fixed +- `sync_vault` catches the `ProposalError` that `propose_page` raises for a + deleted citation, not just `ArtifactNotFoundError`. a vault edit to a page + whose cited claim/entity/source was since removed hit propose_page's id + validation, which converts the miss to `ProposalError` — so the + `except ArtifactNotFoundError` handler never fired and the error escaped as + an uncaught traceback, bypassing the CLI's `except VaultSyncError` renderer. + it now surfaces as the intended one-line `VaultSyncError` (#547). - approve/reject/expire record the audit event *before* moving the proposal to decided/. a crash between the two used to leave a durable decision with no authoritative history; it now leaves a pending diff --git a/src/vouch/vault_sync.py b/src/vouch/vault_sync.py index d7a7f027..7ea9ffae 100644 --- a/src/vouch/vault_sync.py +++ b/src/vouch/vault_sync.py @@ -51,7 +51,7 @@ ClaimStatus, PageStatus, ) -from .proposals import propose_page +from .proposals import ProposalError, propose_page from .storage import ( ArtifactNotFoundError, KBStore, @@ -464,10 +464,16 @@ def sync_vault( if direction in {"forward", "both"}: try: r = vault_to_kb(store, vault_dir, actor=actor) - except ArtifactNotFoundError as e: + except (ArtifactNotFoundError, ProposalError) as e: # A vault edit referenced a claim/entity/source that no longer # exists in the KB. That's a *real* conflict the user has to # resolve in Obsidian, not a vouch bug; surface it cleanly. + # propose_page validates those ids and converts the miss to a + # ProposalError ("unknown claim/entity/source id"), not the raw + # ArtifactNotFoundError this used to catch — so the handler was + # dead and the ProposalError escaped as an uncaught traceback, + # bypassing the CLI's `except VaultSyncError` renderer. Catch + # both so it surfaces as the documented VaultSyncError. raise VaultSyncError(f"vault edit references unknown artifact: {e}") from e combined.pages_proposed.extend(r.pages_proposed) combined.pages_skipped_unchanged.extend(r.pages_skipped_unchanged) diff --git a/tests/test_vault_sync.py b/tests/test_vault_sync.py index b765b08c..fa15bb49 100644 --- a/tests/test_vault_sync.py +++ b/tests/test_vault_sync.py @@ -248,6 +248,27 @@ def test_sync_vault_rejects_missing_vault(store: KBStore, tmp_path: Path) -> Non sync_vault(store, tmp_path / "does-not-exist") +def test_sync_vault_surfaces_deleted_citation_as_vault_sync_error( + store: KBStore, vault: Path, +) -> None: + """A vault edit to a page whose cited claim was since deleted must + surface as a clean VaultSyncError, not the raw ProposalError that + propose_page raises for an unknown id — otherwise it escapes the dead + `except ArtifactNotFoundError` handler as an uncaught traceback, + bypassing the CLI's VaultSyncError renderer. Fixture cites `alpha-claim`.""" + kb_to_vault(store, vault) + mirror = vault / VAULT_DIR / "pages" / "alpha-page.md" + mirror.write_text( + mirror.read_text(encoding="utf-8").replace("Original body.", "Edited."), + encoding="utf-8", + ) + # Delete the cited claim so propose_page's id validation fails. + (store.kb_dir / "claims" / "alpha-claim.yaml").unlink() + + with pytest.raises(VaultSyncError, match="unknown artifact"): + sync_vault(store, vault, direction="forward") + + # --- CLI surface ----------------------------------------------------------