generated from coulomb/repo-seed
Complete WP-0010: HTTP remote federation with cache
Some checks failed
ci / validate-registry (push) Has been cancelled
Some checks failed
ci / validate-registry (push) Has been cancelled
Extend federation manifest schema for url sources with auth and TTL metadata. Fetch remote capability indexes over HTTP(S), cache under registry/federation/cache/, and fall back to stale cache on fetch failure. Add --refresh flag, seven federation tests, and updated federation docs.
This commit is contained in:
208
tests/test_federation.py
Normal file
208
tests/test_federation.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import urllib.error
|
||||
import yaml
|
||||
|
||||
from reuse_surface.federation import (
|
||||
compose_federated_index,
|
||||
fetch_remote_index_text,
|
||||
load_federation_manifest,
|
||||
resolve_source_index_path,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
REMOTE_INDEX = """
|
||||
version: 1
|
||||
domain: helix_forge
|
||||
updated: "2026-06-15"
|
||||
capabilities:
|
||||
- id: capability.remote.sample
|
||||
name: Remote Sample
|
||||
domain: helix_forge
|
||||
vector: D2/A0/C0/R0
|
||||
owner: example
|
||||
path: registry/capabilities/capability.remote.sample.md
|
||||
summary: Sample capability from a remote index
|
||||
tags: [sample]
|
||||
consumption_modes: [planning]
|
||||
"""
|
||||
|
||||
|
||||
def _remote_manifest() -> dict:
|
||||
return {
|
||||
"version": 1,
|
||||
"domain": "helix_forge",
|
||||
"collision_policy": "warn",
|
||||
"sources": [
|
||||
{
|
||||
"repo": "local",
|
||||
"index": "registry/indexes/capabilities.yaml",
|
||||
"enabled": True,
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"repo": "remote-repo",
|
||||
"url": "https://example.com/capabilities.yaml",
|
||||
"enabled": True,
|
||||
"required": False,
|
||||
"cache_ttl_seconds": 3600,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def test_manifest_with_url_source_validates():
|
||||
manifest = _remote_manifest()
|
||||
schema_path = ROOT / "schemas" / "federation.schema.yaml"
|
||||
from jsonschema import Draft202012Validator
|
||||
|
||||
schema = yaml.safe_load(schema_path.read_text(encoding="utf-8"))
|
||||
errors = list(Draft202012Validator(schema).iter_errors(manifest))
|
||||
assert errors == []
|
||||
|
||||
|
||||
def test_fetch_remote_index_text():
|
||||
source = {"repo": "remote-repo", "url": "https://example.com/capabilities.yaml"}
|
||||
payload = REMOTE_INDEX.encode("utf-8")
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return payload
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=FakeResponse()):
|
||||
text = fetch_remote_index_text(source["url"], source)
|
||||
assert "capability.remote.sample" in text
|
||||
|
||||
|
||||
def test_remote_fetch_writes_cache(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
|
||||
source = {
|
||||
"repo": "remote-repo",
|
||||
"url": "https://example.com/capabilities.yaml",
|
||||
"cache_ttl_seconds": 3600,
|
||||
}
|
||||
payload = REMOTE_INDEX.encode("utf-8")
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return payload
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=FakeResponse()):
|
||||
path, warnings = resolve_source_index_path(source)
|
||||
assert path is not None
|
||||
assert warnings == []
|
||||
assert path.exists()
|
||||
assert "capability.remote.sample" in path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_remote_fetch_uses_fresh_cache_without_refetch(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
|
||||
source = {
|
||||
"repo": "remote-repo",
|
||||
"url": "https://example.com/capabilities.yaml",
|
||||
"cache_ttl_seconds": 3600,
|
||||
}
|
||||
payload = REMOTE_INDEX.encode("utf-8")
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return payload
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=FakeResponse()) as urlopen:
|
||||
resolve_source_index_path(source)
|
||||
path, warnings = resolve_source_index_path(source)
|
||||
assert warnings == []
|
||||
assert path is not None
|
||||
urlopen.assert_called_once()
|
||||
|
||||
|
||||
def test_remote_fetch_falls_back_to_stale_cache(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
|
||||
source = {
|
||||
"repo": "remote-repo",
|
||||
"url": "https://example.com/capabilities.yaml",
|
||||
"cache_ttl_seconds": 0,
|
||||
}
|
||||
payload = REMOTE_INDEX.encode("utf-8")
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return payload
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=FakeResponse()):
|
||||
resolve_source_index_path(source)
|
||||
|
||||
with patch(
|
||||
"urllib.request.urlopen",
|
||||
side_effect=urllib.error.URLError("network down"),
|
||||
):
|
||||
path, warnings = resolve_source_index_path(source)
|
||||
assert path is not None
|
||||
assert any("stale cache" in warning for warning in warnings)
|
||||
|
||||
|
||||
def test_compose_merges_remote_capabilities(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
|
||||
source = {
|
||||
"repo": "remote-repo",
|
||||
"url": "https://example.com/capabilities.yaml",
|
||||
"enabled": True,
|
||||
"cache_ttl_seconds": 3600,
|
||||
}
|
||||
payload = REMOTE_INDEX.encode("utf-8")
|
||||
|
||||
class FakeResponse:
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return payload
|
||||
|
||||
manifest = _remote_manifest()
|
||||
with patch("urllib.request.urlopen", return_value=FakeResponse()):
|
||||
federated, warnings = compose_federated_index(manifest)
|
||||
ids = {item["id"] for item in federated["capabilities"]}
|
||||
assert "capability.remote.sample" in ids
|
||||
assert "capability.registry.register" in ids
|
||||
remote = next(
|
||||
item for item in federated["capabilities"] if item["id"] == "capability.remote.sample"
|
||||
)
|
||||
assert remote["source_repo"] == "remote-repo"
|
||||
assert remote["source_url"] == "https://example.com/capabilities.yaml"
|
||||
|
||||
|
||||
def test_load_production_manifest_still_validates():
|
||||
manifest = load_federation_manifest()
|
||||
assert manifest["domain"] == "helix_forge"
|
||||
assert any(source["repo"] == "reuse-surface" for source in manifest["sources"])
|
||||
Reference in New Issue
Block a user