test(contract): pin the OCS-capabilities consumer contract with astrolabe

Add a Pact consumer test for capabilities.allowed_doc_types ->
NextcloudClient.capabilities() -> GET /ocs/v2.php/cloud/capabilities, pinning
the astrolabe.semantic_search.enabled_doc_types block the search/scan/purge
gates read. Covers the two meaningful provider states: some sources approved
(parsed to the allow-set) and every source disabled (empty frozenset, distinct
from the fail-open None). Produces the nextcloud-mcp-server -> astrolabe pact.

On the provider side (astrolabe's consent-purge pact), register the
"an admin can purge indexed documents" provider state and opt the broker source
into pending pacts, so that authenticated contract reports as pending instead of
failing provider verification until the live-stack auth test-hook is stood up
(ADR-029 phase 4). Already-verified interactions (GET /api/v1/status) stay
blocking.

---

_This PR was generated with the help of AI, and reviewed by a Human_
This commit is contained in:
Chris Coutinho
2026-06-16 16:45:21 +02:00
parent 68b4f5f1f7
commit f7fefee9da
2 changed files with 139 additions and 2 deletions
@@ -70,7 +70,22 @@ pytestmark = [
# fixtures, etc.). Keep the keys identical to the astrolabe ``given(...)``
# strings. Unhandled states fall through to ``_dispatch_state`` which logs and
# no-ops, so state-less interactions still verify.
def _state_admin_can_purge() -> None:
"""Provider state for astrolabe's consent-purge pact
(``POST /api/v1/vector-sync/purge``).
Full verification of this authenticated endpoint (admin OAuth token +
Nextcloud admin-group check + Qdrant delete) needs the live-stack auth
test-hook that is the ADR-029 phase-4 follow-up. Until then the interaction
rides the broker's pending flow (see ``include_pending`` below); this handler
is registered so the dispatcher recognises the state by name rather than
logging an "unhandled state" warning.
"""
return None
_PROVIDER_STATES: dict[str, Callable[[], None]] = {
"an admin can purge indexed documents": _state_admin_can_purge,
# "a webhook is registered for user alice": _state_webhook_registered,
# "vector sync has indexed documents": _state_vector_sync_ran,
# "the search index returns a hit for 'budget'": _state_search_has_hit,
@@ -103,9 +118,23 @@ def test_verify_astrolabe_consumer_pacts() -> None:
verifier.state_handler(_dispatch_state, teardown=True)
if _BROKER_URL and _BROKER_USERNAME and _BROKER_PASSWORD:
verifier.broker_source(
_BROKER_URL, username=_BROKER_USERNAME, password=_BROKER_PASSWORD
# selector=True to opt into pending pacts: a new/authenticated contract
# (e.g. the consent-purge endpoint) reports as *pending* instead of
# failing this build until provider verification of the authenticated
# surface is stood up (ADR-029 phase 4). Already-verified interactions
# (GET /api/v1/status) stay blocking. Empty consumer selectors keep the
# default "latest pacts for this provider" fetch.
broker = verifier.broker_source(
_BROKER_URL,
username=_BROKER_USERNAME,
password=_BROKER_PASSWORD,
selector=True,
)
broker.include_pending()
provider_branch = os.environ.get("PACT_PROVIDER_BRANCH")
if provider_branch:
broker.provider_branch(provider_branch)
broker.build()
else:
assert _LOCAL_PACT_DIR is not None # guaranteed by module skipif
verifier.add_source(_LOCAL_PACT_DIR)