diff --git a/pyproject.toml b/pyproject.toml index 01b51b1..ff5752f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,11 @@ pythonpath = ["src"] branch = true source = ["shard_wiki"] +[tool.coverage.report] +show_missing = true +# Quality floor for `pytest --cov` / `coverage report` (not forced on a bare `pytest` run). +fail_under = 90 + [tool.ruff] src = ["src", "tests"] target-version = "py311" diff --git a/tests/test_error_paths.py b/tests/test_error_paths.py new file mode 100644 index 0000000..afacfba --- /dev/null +++ b/tests/test_error_paths.py @@ -0,0 +1,76 @@ +"""Error-path / contract tests across modules (keeps the suite honest about failure behaviour).""" + +from collections.abc import Iterable + +import pytest + +from shard_wiki import InformationSpace +from shard_wiki.adapters import FolderAdapter, ShardAdapter, run_conformance +from shard_wiki.engine import EngineKernel +from shard_wiki.model import CapabilityProfile, Identity, Page, Placement +from shard_wiki.provenance import ProvenanceEnvelope +from shard_wiki.union import ResolutionKind, UnionGraph + + +def _folder(tmp_path, name, files, writable=False): + root = tmp_path / name + root.mkdir(parents=True, exist_ok=True) + for rel, text in files.items(): + (root / rel).write_text(text, encoding="utf-8") + return FolderAdapter(name, root, writable=writable) + + +def test_resolution_single_on_red_link_raises(): + u = UnionGraph("s") + res = u.resolve("ghost") + assert res.kind is ResolutionKind.RED_LINK + with pytest.raises(KeyError): + res.single() + + +def test_apply_unknown_overlay_raises(tmp_path): + space = InformationSpace("t") + space.attach(_folder(tmp_path, "w", {"Home.md": "x"}, writable=True)) + with pytest.raises(KeyError): + space.apply_overlay("does-not-exist") + + +def test_apply_overlay_for_unattached_shard_raises(tmp_path): + space = InformationSpace("t") + space.attach(_folder(tmp_path, "w", {"Home.md": "x"}, writable=True)) + # draft an overlay whose target shard is not attached -> apply can't find an adapter + ov = space.overlays.draft(Identity("ghost", "X"), "body", base_rev=None) + with pytest.raises(KeyError): + space.apply_overlay(ov.overlay_id) + + +def test_kernel_delete_missing_raises(): + with pytest.raises(KeyError): + EngineKernel("eng").delete("nope") + + +def test_placement_str(): + assert str(Placement("shardA", "sub/Page")) == "shardA/sub/Page" + + +class _BrokenProfileAdapter(ShardAdapter): + """profile() raises — the conformance battery must report failure, not crash.""" + + @property + def shard_id(self) -> str: + return "broken" + + def profile(self) -> CapabilityProfile: + raise RuntimeError("profile blew up") + + def keys(self) -> Iterable[str]: + return [] + + def read(self, key: str) -> Page: + return Page(Identity("broken", key), "x", ProvenanceEnvelope(source_shard="broken")) + + +def test_conformance_survives_a_broken_profile(): + report = run_conformance(_BrokenProfileAdapter()) + assert not report.ok + assert any(c.name == "profile-validates" and not c.ok for c in report.checks)