"""Docs-drift guards: docs/PLUGIN-HOOKS.md must track the live contract. PLUGIN-HOOKS.md is the canonical plugin-author reference. These tests fail when the contract surface moves without the doc: a version bump that skips the doc's version example, or a new/renamed BasePlugin hook with no doc mention. Keeping this structural (not manual review) is what lets sister sites trust the doc. """ import inspect from pathlib import Path from shopdb import __contract_version__ from shopdb.plugins.base import BasePlugin HOOKS_DOC = Path(__file__).resolve().parent.parent / 'docs' / 'PLUGIN-HOOKS.md' def test_hooks_doc_exists(): assert HOOKS_DOC.exists(), 'docs/PLUGIN-HOOKS.md is missing' def test_hooks_doc_points_at_the_generated_version(): """The doc must name the symbol and send the reader to the generated map. This used to require the literal value in the page, which is what made it stale everywhere else: nine documents copied a contract version and every one of them was wrong, including a pin an external author would have failed to load with. A doc that points at docs/PROJECT-MAP.md cannot go stale, because the map is generated from shopdb/__init__.py. See tests/test_docs_versions.py, which enforces the same rule the other way round: no document may declare a version literal at all. """ text = HOOKS_DOC.read_text() assert '__contract_version__' in text, ( 'docs/PLUGIN-HOOKS.md should still name the symbol a plugin pins against.') assert 'PROJECT-MAP.md' in text, ( 'docs/PLUGIN-HOOKS.md should send the reader to the generated map for ' 'the current value rather than restating it.') def test_every_public_hook_is_documented(): """Every public BasePlugin method must be mentioned in the doc.""" text = HOOKS_DOC.read_text() hooks = [ name for name, member in inspect.getmembers( BasePlugin, predicate=inspect.isfunction) if not name.startswith('_') ] assert hooks, 'No public hooks found on BasePlugin (introspection broke?)' missing = [hook for hook in hooks if hook not in text] assert not missing, ( 'BasePlugin hooks missing from docs/PLUGIN-HOOKS.md: ' + ', '.join(missing) + '. Add a section (or mention) for each before shipping the hook.' ) def test_doc_does_not_reference_removed_hooks(): """Hooks removed from the contract must not be documented as current. They may appear in "Removed" notes; this only guards section headings. """ text = HOOKS_DOC.read_text() for removed in ('get_searchable_fields', 'get_event_handlers'): assert not hasattr(BasePlugin, removed) assert f'### `{removed}' not in text, ( f'{removed} was removed from the contract but still has a ' f'section heading in docs/PLUGIN-HOOKS.md' )