Files
reuse-surface/tests/test_maintain.py
tegwick b24ec507aa
Some checks failed
ci / validate-registry (push) Has been cancelled
WP-0016 finished: interactive registry maintain with llm-connect automation
Closes the registry maintenance loop from inside each domain repo:
interactive prompting for judgment calls, full automation for safe and
high-confidence changes, both backed by the llm-connect HTTP bridge.

- New modules: maintain.py, maintain_llm.py, patches.py, interactive.py
- Schema: schemas/registry-patch.schema.json
- CLI: reuse-surface maintain; establish --scaffold --hook
- Sibling templates: Makefile fragment, pre-commit hook
- Deterministic signal collectors extended; validate cwd auto-detect
- Docs, gap priority 28, SCOPE update
- Tests: test_maintain.py, test_interactive.py (59 pytest total)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:00:39 +02:00

188 lines
5.8 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from unittest.mock import patch
import yaml
from reuse_surface.establish import scaffold_registry
from reuse_surface.maintain import run_maintain
from reuse_surface.maintain_llm import build_maintain_prompt, load_patch_schema
from reuse_surface.patches import (
apply_patches,
evidence_gate,
filter_auto_patches,
patches_from_suggestions,
promotion_delta_gate,
suggestion_to_patch,
)
from reuse_surface.registry import load_index_at, registry_paths
from reuse_surface.registry_update import collect_deterministic_suggestions
def _seed_repo(tmp_path: Path) -> str:
scaffold_registry(tmp_path)
cap_id = "capability.demo.sample"
rel = "registry/capabilities/capability-demo-sample.md"
front_matter = {
"id": cap_id,
"name": "Sample",
"summary": "Sample",
"owner": "demo",
"status": "draft",
"domain": "helix_forge",
"tags": ["demo"],
"maturity": {
"discovery": {"current": "D2", "target": "D5", "confidence": "low"},
"availability": {"current": "A0", "target": "A3", "confidence": "low"},
},
"external_evidence": {
"completeness": {"level": "C0", "confidence": "low"},
"reliability": {"level": "R0", "confidence": "low"},
},
"discovery": {"intent": "demo", "includes": [], "excludes": []},
"availability": {
"current_level": "A0",
"target_level": "A3",
"current_artifacts": [],
"consumption_modes": ["informational"],
},
"relations": {"depends_on": [], "supports": [], "related_to": []},
"evidence": {"documentation": [], "tests": []},
"consumer_guidance": {
"recommended_for": [],
"not_recommended_for": [],
"known_limitations": [],
},
}
entry = tmp_path / rel
entry.parent.mkdir(parents=True, exist_ok=True)
entry.write_text("---\n" + yaml.safe_dump(front_matter, sort_keys=False) + "---\n")
index_path = registry_paths(tmp_path)["index"]
index = load_index_at(index_path)
index["capabilities"] = [
{
"id": cap_id,
"name": "Sample",
"summary": "Sample",
"vector": "D3 / A0 / C0 / R0",
"domain": "helix_forge",
"status": "draft",
"owner": "demo",
"path": rel,
"tags": ["demo"],
"consumption_modes": ["informational"],
}
]
index_path.write_text(yaml.safe_dump(index, sort_keys=False), encoding="utf-8")
return cap_id
def test_patch_schema_loads():
schema = load_patch_schema()
assert "patches" in schema["properties"]
def test_build_maintain_prompt(tmp_path: Path):
cap_id = _seed_repo(tmp_path)
prompt = build_maintain_prompt(tmp_path, cap_id, git_since=None)
assert cap_id in prompt
assert "Return ONLY JSON" in prompt
def test_suggestion_to_patch_vector_sync():
patch = suggestion_to_patch(
{
"capability_id": "capability.demo.sample",
"kind": "vector_drift",
"detail": "drift",
"apply_patch": {"field": "index.vector", "value": "D2 / A0 / C0 / R0"},
}
)
assert patch is not None
assert patch["kind"] == "vector_sync"
def test_evidence_gate_requires_files(tmp_path: Path):
evidence = (tmp_path / "tests" / "test_x.py")
evidence.parent.mkdir(parents=True)
evidence.write_text("def test_x(): pass\n")
patch = {
"kind": "maturity_promote",
"evidence_citations": ["tests/test_x.py"],
}
assert evidence_gate(tmp_path, patch)
patch["evidence_citations"] = ["tests/missing.py"]
assert not evidence_gate(tmp_path, patch)
def test_promotion_delta_gate():
patch = {
"kind": "maturity_promote",
"dimension": "availability",
"from_level": "A2",
"to_level": "A3",
}
assert promotion_delta_gate(patch, 1)
patch["to_level"] = "A5"
assert not promotion_delta_gate(patch, 1)
def test_apply_patches_vector_sync(tmp_path: Path):
cap_id = _seed_repo(tmp_path)
suggestions = collect_deterministic_suggestions(tmp_path, capability_id=cap_id)
patches = patches_from_suggestions(suggestions)
changed = apply_patches(tmp_path, patches)
assert changed
index = load_index_at(registry_paths(tmp_path)["index"])
assert index["capabilities"][0]["vector"] == "D2 / A0 / C0 / R0"
def test_filter_auto_patches(tmp_path: Path):
cap_id = _seed_repo(tmp_path)
suggestions = collect_deterministic_suggestions(tmp_path, capability_id=cap_id)
patches = patches_from_suggestions(suggestions)
selected = filter_auto_patches(patches, tmp_path)
assert selected
def test_run_maintain_auto_no_llm(tmp_path: Path):
_seed_repo(tmp_path)
def _validate() -> tuple[int, list[str], list[str]]:
return 0, [], []
result = run_maintain(
tmp_path,
all_capabilities=True,
auto=True,
no_llm=True,
validate_fn=_validate,
)
assert result.exit_code == 0
assert result.selected_count >= 1
def test_request_maintain_patches_mock(tmp_path: Path):
cap_id = _seed_repo(tmp_path)
payload = {
"patches": [
{
"capability_id": cap_id,
"kind": "consumer_feedback",
"confidence": "medium",
"rationale": "note",
"append": "helpful",
}
],
"notes": [],
}
with patch(
"reuse_surface.maintain_llm.execute_prompt",
return_value=json.dumps(payload),
):
from reuse_surface.maintain_llm import request_maintain_patches
result = request_maintain_patches(tmp_path, cap_id, llm_url="http://example")
assert len(result["patches"]) == 1