An index row with no id now says whether it looks like a SCOPE.md capability block, instead of leaving the author to work out why their repo contributes nothing to the federated index. That confusion is invisible without a diagnostic: the row is valid YAML, the file parses, and the member just silently disappears. Swept all 61 federation sources with --refresh: evidence-binder is the only affected member. The other ten zero-count repos are genuine empty scaffolds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
315 lines
9.5 KiB
Python
315 lines
9.5 KiB
Python
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"])
|
|
|
|
def _compose_with_remote_body(body: str, tmp_path, monkeypatch):
|
|
"""Compose with the remote member index serving `body` verbatim."""
|
|
monkeypatch.setattr("reuse_surface.federation.CACHE_DIR", tmp_path / "cache")
|
|
payload = body.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()):
|
|
return compose_federated_index(_remote_manifest())
|
|
|
|
|
|
def _local_ids_survived(federated) -> bool:
|
|
return any(item["source_repo"] == "local" for item in federated["capabilities"])
|
|
|
|
|
|
def test_compose_skips_capability_without_id(tmp_path, monkeypatch):
|
|
body = """
|
|
version: 1
|
|
domain: helix_forge
|
|
capabilities:
|
|
- type: library
|
|
title: No id at all
|
|
- id: capability.remote.sample
|
|
name: Remote Sample
|
|
"""
|
|
federated, warnings = _compose_with_remote_body(body, tmp_path, monkeypatch)
|
|
ids = {item["id"] for item in federated["capabilities"]}
|
|
assert "capability.remote.sample" in ids
|
|
assert _local_ids_survived(federated)
|
|
assert any("remote-repo" in w and "no id" in w for w in warnings)
|
|
|
|
|
|
def test_compose_skips_non_mapping_index(tmp_path, monkeypatch):
|
|
federated, warnings = _compose_with_remote_body(
|
|
"just a string, not a mapping\n", tmp_path, monkeypatch
|
|
)
|
|
assert _local_ids_survived(federated)
|
|
assert any("not a mapping" in w for w in warnings)
|
|
|
|
|
|
def test_compose_skips_empty_index(tmp_path, monkeypatch):
|
|
federated, warnings = _compose_with_remote_body("", tmp_path, monkeypatch)
|
|
assert _local_ids_survived(federated)
|
|
assert any("remote-repo" in w for w in warnings)
|
|
|
|
|
|
def test_compose_skips_unparseable_index(tmp_path, monkeypatch):
|
|
federated, warnings = _compose_with_remote_body(
|
|
"capabilities: [unclosed\n", tmp_path, monkeypatch
|
|
)
|
|
assert _local_ids_survived(federated)
|
|
assert any("unparseable" in w for w in warnings)
|
|
|
|
|
|
def test_compose_skips_capabilities_not_a_list(tmp_path, monkeypatch):
|
|
federated, warnings = _compose_with_remote_body(
|
|
"version: 1\ncapabilities: not-a-list\n", tmp_path, monkeypatch
|
|
)
|
|
assert _local_ids_survived(federated)
|
|
assert any("not a list" in w for w in warnings)
|
|
|
|
|
|
def test_malformed_source_still_listed_with_zero_count(tmp_path, monkeypatch):
|
|
federated, _ = _compose_with_remote_body(
|
|
"just a string, not a mapping\n", tmp_path, monkeypatch
|
|
)
|
|
remote = next(s for s in federated["sources"] if s["repo"] == "remote-repo")
|
|
assert remote["count"] == 0
|
|
|
|
|
|
def test_scope_shaped_row_names_the_mistake(tmp_path, monkeypatch):
|
|
"""A SCOPE.md capability block copied into an index should say so."""
|
|
body = """
|
|
version: 1
|
|
domain: helix_forge
|
|
capabilities:
|
|
- type: library
|
|
title: Evidence-to-target binding
|
|
description: Links evidence items to structured targets.
|
|
keywords: [evidence, binding]
|
|
"""
|
|
_, warnings = _compose_with_remote_body(body, tmp_path, monkeypatch)
|
|
hint = next(w for w in warnings if "remote-repo" in w)
|
|
assert "SCOPE.md capability block" in hint
|
|
assert "id/name/summary" in hint
|
|
|
|
|
|
def test_plain_missing_id_gets_no_scope_hint(tmp_path, monkeypatch):
|
|
body = """
|
|
version: 1
|
|
capabilities:
|
|
- name: Has a name but no id
|
|
summary: Something
|
|
"""
|
|
_, warnings = _compose_with_remote_body(body, tmp_path, monkeypatch)
|
|
hint = next(w for w in warnings if "remote-repo" in w)
|
|
assert "has no id" in hint
|
|
assert "SCOPE.md" not in hint
|