From 5a4493dbd5ba1882effc2cf6a328a3528118f490 Mon Sep 17 00:00:00 2001 From: boskodev790 Date: Thu, 23 Jul 2026 13:55:49 -0400 Subject: [PATCH] fix(vault-sync): surface deleted-citation error as VaultSyncError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sync_vault's forward pass caught ArtifactNotFoundError to turn a deleted-citation conflict into a clean VaultSyncError, but propose_page validates cited ids and converts the miss to ProposalError before it surfaces. the two are disjoint hierarchies (ProposalError(RuntimeError) vs ArtifactNotFoundError(KeyError)), so the handler was dead and the ProposalError escaped as an uncaught traceback — bypassing the CLI's own `except VaultSyncError` renderer, which turns domain errors into a one-line message. catch ProposalError alongside ArtifactNotFoundError so a vault edit to a page whose cited claim/entity/source was deleted lands as the documented VaultSyncError. driving `vouch sync` on that scenario now prints "Error: vault edit references unknown artifact: unknown claim id: ..." instead of a python traceback. this does not keep watch_vault running — that loop catches only KeyboardInterrupt and still ends on either exception; the fix is about the surfaced error type, not loop survival. --- CHANGELOG.md | 7 +++++++ src/vouch/vault_sync.py | 10 ++++++++-- tests/test_vault_sync.py | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) 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 ----------------------------------------------------------