Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ All notable changes to vouch are documented here. Format follows
per-prompt block, the session banner, `vouch status` and the opt-in
question all say so rather than calling it "this repo's" knowledge.

### Fixed
- `sync_vault` catches the `ProposalError` that `propose_page` raises for a
deleted citation (missing claim/entity/source id, with
`ArtifactNotFoundError` as cause), not just the raw
`ArtifactNotFoundError`. other proposal failures (empty title, page-kind
validation) surface as a neutral `vault edit rejected` `VaultSyncError`
rather than being mislabelled as an unknown artifact. the CLI's
`except VaultSyncError` renderer then prints a one-line `Error:` instead
of an uncaught traceback (#547).

## [1.5.0] — 2026-07-20

### Added
Expand Down
16 changes: 13 additions & 3 deletions src/vouch/vault_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
ClaimStatus,
PageStatus,
)
from .proposals import propose_page
from .proposals import ProposalError, propose_page
from .storage import (
ArtifactNotFoundError,
KBStore,
Expand Down Expand Up @@ -465,10 +465,20 @@ def sync_vault(
try:
r = vault_to_kb(store, vault_dir, actor=actor)
except ArtifactNotFoundError 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
# 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.
raise VaultSyncError(f"vault edit references unknown artifact: {e}") from e
except ProposalError as e:
# propose_page converts missing claim/entity/source ids to
# ProposalError (ArtifactNotFoundError as __cause__), so the
# handler above never fired and the error escaped as a traceback
# past the CLI's VaultSyncError renderer. other ProposalErrors
# (empty title, page-kind validation) stay distinct — don't
# mislabel them as unknown artifacts.
if isinstance(e.__cause__, ArtifactNotFoundError):
raise VaultSyncError(f"vault edit references unknown artifact: {e}") from e
raise VaultSyncError(f"vault edit rejected: {e}") from e
combined.pages_proposed.extend(r.pages_proposed)
combined.pages_skipped_unchanged.extend(r.pages_skipped_unchanged)
combined.pages_skipped_unknown_id.extend(r.pages_skipped_unknown_id)
Expand Down
79 changes: 79 additions & 0 deletions tests/test_vault_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,55 @@ 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")


def test_sync_vault_does_not_mislabel_non_artifact_proposal_errors(
store: KBStore, vault: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
"""other ProposalErrors from propose_page (e.g. empty title) must not
be reported as 'unknown artifact' — that wording is reserved for
missing claim/entity/source ids (#547 / CodeRabbit on #548). empty
titles are caught earlier by deserialise-and-skip, so drive the
non-artifact path by stubbing propose_page."""
from vouch.proposals import ProposalError

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",
)

def _reject(*_args: object, **_kwargs: object) -> None:
raise ProposalError("page title is empty")

monkeypatch.setattr("vouch.vault_sync.propose_page", _reject)

with pytest.raises(VaultSyncError, match="vault edit rejected") as ei:
sync_vault(store, vault, direction="forward")
assert "unknown artifact" not in str(ei.value)
assert "page title is empty" in str(ei.value)


# --- CLI surface ----------------------------------------------------------


Expand Down Expand Up @@ -291,6 +340,36 @@ def test_cli_sync_missing_vault_is_clean_error(
assert "Error:" in result.output


def test_cli_sync_deleted_citation_is_clean_error(
store: KBStore, vault: Path, monkeypatch: pytest.MonkeyPatch,
) -> None:
"""deleted-citation must hit the CLI's VaultSyncError renderer as a
one-line Error:, not an uncaught ProposalError traceback (#547)."""
monkeypatch.chdir(store.root)
runner = CliRunner()
runner.invoke(cli, ["sync", "--vault", str(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",
)
(store.kb_dir / "claims" / "alpha-claim.yaml").unlink()

result = runner.invoke(
cli, ["sync", "--vault", str(vault), "--direction", "forward"],
)
assert result.exit_code != 0
assert "Traceback" not in result.output
# documented CLI contract (#547): exactly one Error: line, not a
# multiline non-traceback rendering that still contains "Error:".
lines = [line for line in result.output.splitlines() if line.strip()]
assert lines == [
"Error: vault edit references unknown artifact: unknown claim id: alpha-claim"
]
assert "unknown artifact" in lines[0]
assert "unknown claim id" in lines[0]


Comment thread
coderabbitai[bot] marked this conversation as resolved.
def test_cli_sync_requires_vault_flag(
store: KBStore, monkeypatch: pytest.MonkeyPatch,
) -> None:
Expand Down
Loading