PLUGIN-QUICKSTART Step 7 is now a concrete 7-item checklist (view conventions, route auto-discovery + ADR-009 gating meta, api client shape, nav/report hooks, settings auto-nesting, verification). New tests/test_docs_contract.py introspects BasePlugin and fails CI when a public hook is missing from PLUGIN-HOOKS.md or the documented contract version drifts - it immediately caught two undocumented hooks (get_provisioning_note, get_config_schema), now documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
62 lines
2.3 KiB
Python
62 lines
2.3 KiB
Python
"""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_declares_current_contract_version():
|
|
"""The doc's version example must match the live __contract_version__."""
|
|
text = HOOKS_DOC.read_text()
|
|
expected = f"__contract_version__ = '{__contract_version__}'"
|
|
assert expected in text, (
|
|
f'docs/PLUGIN-HOOKS.md version example is stale: expected {expected}. '
|
|
f'Update the "Contract version" section when bumping the contract.'
|
|
)
|
|
|
|
|
|
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'
|
|
)
|