REUSE-WP-0019-T04: reuse telemetry store and recording
Implements the hub side of the shared reuse-event schema (already drafted in WP-0018-T01, schemas/reuse-event.schema.json): a SQLite reuse_events table, POST /v1/reuse-events (token-auth), GET /v1/reuse-events?capability_id= (read-only). reuse_surface/plan_check.py: refactored record_outcome around a new shared post_or_fallback_reuse_event() helper -- tries the hub first, falls back to the local JSONL only on failure/unreachability, never both. New record_manual_reuse_event() backs a new CLI command, reuse-surface record-reuse, for retroactive facts recorded outside plan-check. Privacy/scope (repo slugs and capability ids only, no code, no secrets) is enforced structurally via the schema's additionalProperties: false, not just by convention. 21 new pytest cases, 145 total pass. Live-verified against a real running hub instance: POST/GET /v1/reuse-events directly, record-reuse and plan-check --record-outcome both posting successfully to the hub, and -- after actually killing the hub process -- confirmed the fallback path writes correctly to the local JSONL instead of erroring. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2fcc91f2aa
commit
d181043717
12 changed files with 562 additions and 32 deletions
|
|
@ -311,4 +311,86 @@ def test_webhook_accepts_gitea_signature_header(webhook_client):
|
|||
content=body,
|
||||
headers={"X-Gitea-Signature": _sign(body), "Content-Type": "application/json"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# --- T04: reuse telemetry store ---
|
||||
|
||||
VALID_REUSE_EVENT = {
|
||||
"ts": "2026-07-08T00:00:00Z",
|
||||
"consumer_repo": "some-repo",
|
||||
"capability_id": "capability.infotech.issue-tracking",
|
||||
"verdict": "reuse",
|
||||
"outcome": "reused",
|
||||
"source": "plan-check",
|
||||
}
|
||||
|
||||
|
||||
def test_record_reuse_event_requires_auth(hub_client):
|
||||
response = hub_client.post("/v1/reuse-events", json=VALID_REUSE_EVENT)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_record_reuse_event_and_list(hub_client):
|
||||
response = hub_client.post(
|
||||
"/v1/reuse-events",
|
||||
json=VALID_REUSE_EVENT,
|
||||
headers={"Authorization": "Bearer test-token"},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
listed = hub_client.get("/v1/reuse-events")
|
||||
assert listed.status_code == 200
|
||||
assert listed.json()["count"] == 1
|
||||
assert listed.json()["events"][0]["consumer_repo"] == "some-repo"
|
||||
|
||||
|
||||
def test_record_reuse_event_rejects_invalid_verdict(hub_client):
|
||||
bad = {**VALID_REUSE_EVENT, "verdict": "not-a-verdict"}
|
||||
response = hub_client.post(
|
||||
"/v1/reuse-events",
|
||||
json=bad,
|
||||
headers={"Authorization": "Bearer test-token"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_record_reuse_event_rejects_extra_fields(hub_client):
|
||||
bad = {**VALID_REUSE_EVENT, "unexpected_field": "nope"}
|
||||
response = hub_client.post(
|
||||
"/v1/reuse-events",
|
||||
json=bad,
|
||||
headers={"Authorization": "Bearer test-token"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_list_reuse_events_filters_by_capability_id(hub_client):
|
||||
other = {**VALID_REUSE_EVENT, "capability_id": "capability.infotech.other"}
|
||||
hub_client.post("/v1/reuse-events", json=VALID_REUSE_EVENT, headers={"Authorization": "Bearer test-token"})
|
||||
hub_client.post("/v1/reuse-events", json=other, headers={"Authorization": "Bearer test-token"})
|
||||
|
||||
listed = hub_client.get("/v1/reuse-events?capability_id=capability.infotech.other")
|
||||
assert listed.status_code == 200
|
||||
assert listed.json()["count"] == 1
|
||||
assert listed.json()["events"][0]["capability_id"] == "capability.infotech.other"
|
||||
|
||||
|
||||
def test_list_reuse_events_empty_by_default(hub_client):
|
||||
listed = hub_client.get("/v1/reuse-events")
|
||||
assert listed.status_code == 200
|
||||
assert listed.json() == {"count": 0, "events": []}
|
||||
|
||||
|
||||
def test_store_record_reuse_event_and_list(tmp_path):
|
||||
store = HubStore(tmp_path / "hub.db")
|
||||
store.record_reuse_event(VALID_REUSE_EVENT)
|
||||
events = store.list_reuse_events()
|
||||
assert len(events) == 1
|
||||
assert events[0]["consumer_repo"] == "some-repo"
|
||||
|
||||
|
||||
def test_store_record_reuse_event_rejects_invalid(tmp_path):
|
||||
store = HubStore(tmp_path / "hub.db")
|
||||
with pytest.raises(ValueError):
|
||||
store.record_reuse_event({**VALID_REUSE_EVENT, "verdict": "nope"})
|
||||
|
|
@ -10,6 +10,7 @@ from reuse_surface.plan_check import (
|
|||
load_query_from_workplan,
|
||||
maybe_file_capability_request,
|
||||
match_query,
|
||||
record_manual_reuse_event,
|
||||
record_outcome,
|
||||
request_rerank,
|
||||
run_plan_check,
|
||||
|
|
@ -133,6 +134,83 @@ def test_record_outcome_appends_jsonl(tmp_path, monkeypatch):
|
|||
assert event["source"] == "plan-check"
|
||||
|
||||
|
||||
def test_record_outcome_posts_to_hub_when_reachable(tmp_path, monkeypatch):
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
monkeypatch.setattr(
|
||||
"reuse_surface.hub_client.hub_record_reuse_event",
|
||||
lambda event, base_url=None: (201, event),
|
||||
)
|
||||
result = {"verdict": "reuse", "matches": [{"id": "capability.infotech.issue-tracking"}]}
|
||||
recorded = record_outcome(result, "reused", consumer_repo="some-repo")
|
||||
assert recorded["recorded_to"] == "hub"
|
||||
assert not telemetry_path.exists()
|
||||
|
||||
|
||||
def test_record_outcome_falls_back_when_hub_rejects(tmp_path, monkeypatch):
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
monkeypatch.setattr(
|
||||
"reuse_surface.hub_client.hub_record_reuse_event",
|
||||
lambda event, base_url=None: (500, {"error": "boom"}),
|
||||
)
|
||||
result = {"verdict": "reuse", "matches": [{"id": "capability.infotech.issue-tracking"}]}
|
||||
recorded = record_outcome(result, "reused", consumer_repo="some-repo")
|
||||
assert recorded["recorded_to"] == "local"
|
||||
assert telemetry_path.exists()
|
||||
|
||||
|
||||
def test_record_outcome_falls_back_when_hub_unconfigured(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
result = {"verdict": "reuse", "matches": [{"id": "capability.infotech.issue-tracking"}]}
|
||||
recorded = record_outcome(result, "reused", consumer_repo="some-repo")
|
||||
assert recorded["recorded_to"] == "local"
|
||||
|
||||
|
||||
def test_record_manual_reuse_event_local_fallback(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
recorded = record_manual_reuse_event(
|
||||
consumer_repo="some-repo",
|
||||
capability_id="capability.infotech.issue-tracking",
|
||||
verdict="reuse",
|
||||
outcome="reused",
|
||||
)
|
||||
assert recorded["recorded_to"] == "local"
|
||||
event = json.loads(telemetry_path.read_text().splitlines()[0])
|
||||
assert event["source"] == "manual"
|
||||
assert event["consumer_repo"] == "some-repo"
|
||||
|
||||
|
||||
def test_record_manual_reuse_event_posts_to_hub(tmp_path, monkeypatch):
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
monkeypatch.setattr(
|
||||
"reuse_surface.hub_client.hub_record_reuse_event",
|
||||
lambda event, base_url=None: (201, event),
|
||||
)
|
||||
recorded = record_manual_reuse_event(
|
||||
consumer_repo="some-repo", capability_id=None, verdict="new", outcome="new",
|
||||
)
|
||||
assert recorded["recorded_to"] == "hub"
|
||||
assert recorded["event"]["source"] == "manual"
|
||||
assert recorded["event"]["capability_id"] is None
|
||||
|
||||
|
||||
def test_record_manual_reuse_event_rejects_invalid_verdict(monkeypatch):
|
||||
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||
try:
|
||||
record_manual_reuse_event(
|
||||
consumer_repo="some-repo", capability_id=None, verdict="not-a-verdict",
|
||||
)
|
||||
assert False, "expected ValueError"
|
||||
except ValueError as exc:
|
||||
assert "schema validation failed" in str(exc)
|
||||
|
||||
|
||||
def test_maybe_file_capability_request_only_on_new_verdict(monkeypatch):
|
||||
called = []
|
||||
monkeypatch.setattr(
|
||||
|
|
@ -349,3 +427,59 @@ def test_run_plan_check_integrates_successful_rerank(monkeypatch):
|
|||
assert llm_entries[0]["score"] == 0.77
|
||||
# deterministic top match is still first and unaffected
|
||||
assert result["matches"][0]["kind"] == "deterministic"
|
||||
|
||||
|
||||
def test_cmd_record_reuse_cli_local_fallback(tmp_path, monkeypatch, capsys):
|
||||
from reuse_surface.cli import main
|
||||
|
||||
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
|
||||
exit_code = main([
|
||||
"record-reuse",
|
||||
"--consumer-repo", "some-repo",
|
||||
"--capability-id", "capability.infotech.issue-tracking",
|
||||
"--verdict", "reuse",
|
||||
"--outcome", "reused",
|
||||
])
|
||||
assert exit_code == 0
|
||||
out = capsys.readouterr().out
|
||||
assert "local" in out
|
||||
event = json.loads(telemetry_path.read_text().splitlines()[0])
|
||||
assert event["consumer_repo"] == "some-repo"
|
||||
assert event["source"] == "manual"
|
||||
|
||||
|
||||
def test_cmd_record_reuse_cli_rejects_invalid_verdict():
|
||||
import pytest
|
||||
|
||||
from reuse_surface.cli import main
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main([
|
||||
"record-reuse",
|
||||
"--consumer-repo", "some-repo",
|
||||
"--verdict", "not-a-verdict",
|
||||
])
|
||||
assert exc_info.value.code == 2 # argparse choices rejection
|
||||
|
||||
|
||||
def test_cmd_record_reuse_cli_json_format(tmp_path, monkeypatch, capsys):
|
||||
from reuse_surface.cli import main
|
||||
|
||||
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||
|
||||
exit_code = main([
|
||||
"record-reuse",
|
||||
"--consumer-repo", "some-repo",
|
||||
"--verdict", "new",
|
||||
"--format", "json",
|
||||
])
|
||||
assert exit_code == 0
|
||||
out = capsys.readouterr().out
|
||||
payload = json.loads(out)
|
||||
assert payload["recorded_to"] == "local"
|
||||
assert payload["event"]["capability_id"] is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue