2026-06-16 01:21:01 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-06-16 02:09:57 +02:00
|
|
|
from reuse_surface.stats import (
|
|
|
|
|
collect_roster_stats,
|
|
|
|
|
collect_stats,
|
|
|
|
|
format_roster_stats_markdown,
|
|
|
|
|
format_stats_markdown,
|
|
|
|
|
)
|
2026-06-16 01:21:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_collect_stats_on_repo_root():
|
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
|
|
|
stats = collect_stats(root)
|
2026-06-16 12:35:49 +02:00
|
|
|
assert stats["capability_count"] == 2
|
2026-06-16 01:21:01 +02:00
|
|
|
assert stats["index_present"] is True
|
|
|
|
|
assert "discovery" in stats["histograms"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_format_stats_markdown_contains_count():
|
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
|
|
|
text = format_stats_markdown(collect_stats(root))
|
|
|
|
|
assert "Capabilities:" in text
|
2026-06-16 12:35:49 +02:00
|
|
|
assert "2" in text
|
2026-06-16 02:09:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_collect_roster_stats_federation_ready():
|
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
|
|
|
roster = root / "registry/federation/local-repo-roster.yaml"
|
|
|
|
|
stats = collect_roster_stats(roster, federation_ready=True)
|
2026-07-07 01:56:18 +02:00
|
|
|
assert stats["counts"]["total"] == 62
|
|
|
|
|
assert stats["counts"]["established"] == 62
|
2026-06-16 02:09:57 +02:00
|
|
|
assert "federation_readiness" in stats
|
|
|
|
|
text = format_roster_stats_markdown(stats)
|
REUSE-WP-0019-T06: hub freshness monitoring, docs, close workplan
reuse_surface/stats.py: _hub_summary() now reports composed_at, stale,
age_days, freshness_threshold_days (REUSE_SURFACE_FRESHNESS_DAYS env,
default 7), and a computed stale_warning. New hub_client.hub_federated()
backs it. format_stats_markdown surfaces a STALE marker when triggered.
.forgejo/workflows/ci.yml: new informational (non-failing) hub freshness
check against the live production hub on every push -- prints a
::warning:: annotation when stale, never fails the build.
docs/RegistryFederation.md: new section tying together the webhook (T02),
scheduled fallback (T03), and freshness visibility (T06) into one
explanation. docs/deploy/reuse-kubernetes.md: updated for the T03 Forgejo
migration and the now-automated image.yaml build; image promotion
checklist updated for the known /health ingress bug (verify via
/v1/repos or /v1/federated instead).
14 new pytest cases, 173 total pass. Live-verified against production:
reuse-surface stats correctly showed composed_at/age_days for the real
federated index. Separately discovered and confirmed (via a live signed
webhook test) that reuse-surface-env moving to ExternalSecret/OpenBao
custody (railiance-apps commit 706f6c7, found while updating these docs)
did not break the T02/T03 webhook -- the synced value still matches what
the hub actually uses.
REUSE-WP-0019 is now fully complete (T01-T06). SCOPE.md and
docs/IntentScopeGapAnalysis.md updated to reflect closure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 00:09:58 +02:00
|
|
|
assert "publish pass ratio" in text
|
|
|
|
|
|
|
|
|
|
# --- T06: hub freshness (composed_at age + stale flag) ---
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
|
|
|
|
|
|
from reuse_surface.stats import _composed_at_age_days, _freshness_days, _hub_summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hub_summary_includes_freshness_fields(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_URL", "http://example.invalid")
|
|
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_list",
|
|
|
|
|
lambda base_url=None: (200, {"count": 3, "repos": [{"enabled": True}] * 3}),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_federated",
|
|
|
|
|
lambda base_url=None: (200, {"composed_at": now, "stale": False}),
|
|
|
|
|
)
|
|
|
|
|
summary = _hub_summary(None)
|
|
|
|
|
assert summary["composed_at"] == now
|
|
|
|
|
assert summary["stale"] is False
|
|
|
|
|
assert summary["age_days"] is not None
|
|
|
|
|
assert summary["age_days"] < 1
|
|
|
|
|
assert summary["stale_warning"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hub_summary_stale_warning_from_age(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_URL", "http://example.invalid")
|
|
|
|
|
old = (datetime.now(timezone.utc) - timedelta(days=10)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_list",
|
|
|
|
|
lambda base_url=None: (200, {"count": 1, "repos": [{"enabled": True}]}),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_federated",
|
|
|
|
|
lambda base_url=None: (200, {"composed_at": old, "stale": False}),
|
|
|
|
|
)
|
|
|
|
|
summary = _hub_summary(None)
|
|
|
|
|
assert summary["age_days"] > 7
|
|
|
|
|
assert summary["stale_warning"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hub_summary_stale_warning_from_flag(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_URL", "http://example.invalid")
|
|
|
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_list",
|
|
|
|
|
lambda base_url=None: (200, {"count": 1, "repos": [{"enabled": True}]}),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_federated",
|
|
|
|
|
lambda base_url=None: (200, {"composed_at": now, "stale": True}),
|
|
|
|
|
)
|
|
|
|
|
summary = _hub_summary(None)
|
|
|
|
|
assert summary["stale_warning"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hub_summary_handles_null_composed_at(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_URL", "http://example.invalid")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_list",
|
|
|
|
|
lambda base_url=None: (200, {"count": 0, "repos": []}),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_federated",
|
|
|
|
|
lambda base_url=None: (200, {"composed_at": None, "stale": False}),
|
|
|
|
|
)
|
|
|
|
|
summary = _hub_summary(None)
|
|
|
|
|
assert summary["age_days"] is None
|
|
|
|
|
assert summary["stale_warning"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_hub_summary_degrades_when_federated_unreachable(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_URL", "http://example.invalid")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"reuse_surface.hub_client.hub_list",
|
|
|
|
|
lambda base_url=None: (200, {"count": 1, "repos": [{"enabled": True}]}),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _raise(base_url=None):
|
|
|
|
|
import urllib.error
|
|
|
|
|
|
|
|
|
|
raise urllib.error.URLError("no route")
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr("reuse_surface.hub_client.hub_federated", _raise)
|
|
|
|
|
summary = _hub_summary(None)
|
|
|
|
|
assert summary["configured"] is True
|
|
|
|
|
assert "composed_at" not in summary
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_composed_at_age_days_none_when_missing():
|
|
|
|
|
assert _composed_at_age_days(None) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_composed_at_age_days_none_when_malformed():
|
|
|
|
|
assert _composed_at_age_days("not-a-timestamp") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_freshness_days_default():
|
|
|
|
|
assert _freshness_days() == 7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_freshness_days_env_override(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_FRESHNESS_DAYS", "3")
|
|
|
|
|
assert _freshness_days() == 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_freshness_days_env_invalid_falls_back(monkeypatch):
|
|
|
|
|
monkeypatch.setenv("REUSE_SURFACE_FRESHNESS_DAYS", "not-a-number")
|
|
|
|
|
assert _freshness_days() == 7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_format_stats_markdown_shows_stale_warning():
|
|
|
|
|
from reuse_surface.stats import format_stats_markdown
|
|
|
|
|
|
|
|
|
|
stats = {
|
|
|
|
|
"repo_root": "/tmp/x",
|
|
|
|
|
"capability_count": 0,
|
|
|
|
|
"registry_present": True,
|
|
|
|
|
"index_present": True,
|
|
|
|
|
"sources_present": True,
|
|
|
|
|
"reliability": {"r0_r2": 0, "r3_plus": 0},
|
|
|
|
|
"histograms": {},
|
|
|
|
|
"vector_drift": [],
|
|
|
|
|
"hub": {
|
|
|
|
|
"configured": True,
|
|
|
|
|
"registration_count": 1,
|
|
|
|
|
"enabled_count": 1,
|
|
|
|
|
"composed_at": "2020-01-01T00:00:00Z",
|
|
|
|
|
"stale": False,
|
|
|
|
|
"age_days": 100.0,
|
|
|
|
|
"freshness_threshold_days": 7,
|
|
|
|
|
"stale_warning": True,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
text = format_stats_markdown(stats)
|
|
|
|
|
assert "STALE" in text
|