Harden federated compose against malformed member indexes (REUSE-WP-0020)
Repointing the production hub's 50 Gitea-hosted federation sources to Forgejo ahead of the 2026-08-31 CoulombCore retirement took /v1/federated to HTTP 500. One member index (evidence-binder) has capability rows with no `id`, and compose_federated_index dereferenced item["id"] unguarded. Its Gitea copy was a stale snapshot returning a non-mapping, so those rows had never been parsed. A single malformed member index must not take down the whole endpoint. Extract _read_index_entries(): unparseable YAML, a non-mapping body, an empty file, and a non-list `capabilities` each degrade to a warning and an empty row list, and rows without an `id` are skipped individually. A failed source stays listed with count 0 so it remains visible to operators rather than silently disappearing. Also fix wall-clock rot in tests/test_plan_check.py, which was already failing at clean HEAD: three tests pinned the compose date to a literal that has now aged past STALE_DAYS. Add workplan REUSE-WP-0020 covering the full cutover. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
fa1235474f
commit
0c6b1e2538
5 changed files with 403 additions and 16 deletions
|
|
@ -205,4 +205,81 @@ def test_compose_merges_remote_capabilities(tmp_path, monkeypatch):
|
|||
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"])
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from reuse_surface.plan_check import STALE_DAYS
|
||||
|
||||
|
||||
def _recent_date() -> str:
|
||||
"""A compose date that is always inside the staleness window.
|
||||
|
||||
A hardcoded literal here made the test rot: it passed when written and
|
||||
began failing once wall-clock time drifted past STALE_DAYS.
|
||||
"""
|
||||
fresh = datetime.now(timezone.utc) - timedelta(days=max(STALE_DAYS - 1, 0))
|
||||
return fresh.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
from reuse_surface.plan_check import (
|
||||
Match,
|
||||
|
|
@ -93,7 +107,7 @@ Build a single interface for issue tracking across Gitea, GitHub, and GitLab.
|
|||
def test_run_plan_check_reuse_verdict(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"reuse_surface.plan_check.load_federated_capabilities",
|
||||
lambda: (SAMPLE_CAPABILITIES, "2026-07-06"),
|
||||
lambda: (SAMPLE_CAPABILITIES, _recent_date()),
|
||||
)
|
||||
query = MatchQuery(
|
||||
source="intent",
|
||||
|
|
@ -236,7 +250,7 @@ def test_cmd_plan_check_file_request_flag(monkeypatch):
|
|||
|
||||
monkeypatch.setattr(
|
||||
"reuse_surface.plan_check.load_federated_capabilities",
|
||||
lambda: (SAMPLE_CAPABILITIES, "2026-07-06"),
|
||||
lambda: (SAMPLE_CAPABILITIES, _recent_date()),
|
||||
)
|
||||
filed_calls = []
|
||||
monkeypatch.setattr(
|
||||
|
|
@ -255,7 +269,7 @@ def test_cmd_plan_check_intent_json(monkeypatch):
|
|||
|
||||
monkeypatch.setattr(
|
||||
"reuse_surface.plan_check.load_federated_capabilities",
|
||||
lambda: (SAMPLE_CAPABILITIES, "2026-07-06"),
|
||||
lambda: (SAMPLE_CAPABILITIES, _recent_date()),
|
||||
)
|
||||
exit_code = main(
|
||||
["plan-check", "--intent", "issue tracking gitea github gitlab", "--format", "json"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue