Combined approved and candidate view with actions

This commit is contained in:
2026-04-29 13:19:58 +02:00
parent 8bd22dab1b
commit 142812e7f2
5 changed files with 942 additions and 43 deletions

View File

@@ -804,6 +804,59 @@ def test_approve_candidate_graph_publishes_ability_map_once(tmp_path):
assert decisions[0].notes == "Looks good for the first pass."
def test_accept_candidate_feature_promotes_parent_context_once(tmp_path):
source = tmp_path / "repo"
source.mkdir()
(source / "README.md").write_text(
"# Feature Accept\nReports health over HTTP.\n",
encoding="utf-8",
)
(source / "app.py").write_text(
"from fastapi import FastAPI\n"
"app = FastAPI()\n"
'@app.get("/health")\n'
"def health():\n"
" return {}\n",
encoding="utf-8",
)
service = make_service(tmp_path)
repository = service.register_repository(name="Feature Accept", url=str(source))
summary = service.analyze_repository(repository.id)
graph = service.candidate_graph(repository.id, summary.analysis_run.id)
candidate_feature = graph.abilities[0].capabilities[0].features[0]
ability_map = service.accept_candidate_feature(
repository.id,
summary.analysis_run.id,
candidate_feature.id,
)
graph_after_feature_accept = service.candidate_graph(
repository.id,
summary.analysis_run.id,
)
assert len(ability_map.abilities) == 1
assert ability_map.abilities[0].capabilities[0].features[0].name == "GET /health"
assert graph_after_feature_accept.abilities[0].capabilities[0].features[0].status == (
"approved"
)
final_map = service.approve_candidate_graph(repository.id, summary.analysis_run.id)
assert len(final_map.abilities) == 1
interface_capabilities = [
capability
for capability in final_map.abilities[0].capabilities
if capability.name == "Expose Repository Interface"
]
assert len(interface_capabilities) == 1
assert len(interface_capabilities[0].features) == 1
decisions = service.list_review_decisions(repository.id, summary.analysis_run.id)
assert {decision.action for decision in decisions} >= {
"accept_candidate_feature",
"approve_candidate_graph",
}
def test_analysis_run_diff_keeps_approved_map_stable_until_change_approval(tmp_path):
source = tmp_path / "repo"
source.mkdir()

View File

@@ -1194,6 +1194,17 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "Use OpenRouter Models" in run_detail.text
assert "Expected from provider docs." in run_detail.text
pending_candidate_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={
"scope": "candidate",
"analysis_run_id": first_run_id,
"type": "features",
},
)
assert pending_candidate_listing.status_code == 200
assert "Accept" in pending_candidate_listing.text
approve_response = client.post(
f"{run_path}/candidate-graph/approve",
follow_redirects=False,
@@ -1215,14 +1226,14 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
assert "Search Profile" in approved_detail.text
assert "Discovery" in approved_detail.text
assert "Export" in approved_detail.text
assert "Approved Elements" in approved_detail.text
assert "Elements" in approved_detail.text
assert "q=Report+Service+Status" in approved_detail.text
assert (
f"/ui/repos/{repository_id}/elements?scope=approved&type=abilities"
f"/ui/repos/{repository_id}/elements?scope=all&entry_filter=approved&type=abilities"
in approved_detail.text
)
assert (
f"/ui/repos/{repository_id}/elements?scope=candidate&analysis_run_id={first_run_id}&type=features"
f"/ui/repos/{repository_id}/elements?scope=all&entry_filter=candidate&analysis_run_id={first_run_id}&type=features"
in approved_detail.text
)
assert (
@@ -1237,11 +1248,49 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
approved_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={"scope": "approved", "type": "capabilities"},
params={"scope": "all", "entry_filter": "approved", "type": "capabilities"},
)
assert approved_listing.status_code == 200
assert "Approved Capabilities" in approved_listing.text
assert "Registry Capabilities" in approved_listing.text
assert "Entry" in approved_listing.text
assert "Approved only" in approved_listing.text
assert "Expose Repository Interface" in approved_listing.text
assert "Save" in approved_listing.text
assert "Delete" in approved_listing.text
combined_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={
"scope": "all",
"analysis_run_id": first_run_id,
"type": "features",
},
)
assert combined_listing.status_code == 200
assert "Registry Features" in combined_listing.text
assert "Approved and candidate" in combined_listing.text
assert ">approved<" in combined_listing.text
assert ">candidate: approved<" in combined_listing.text
approved_map = client.get(f"/repos/{repository_id}/ability-map").json()
approved_capability = approved_map["abilities"][0]["capabilities"][0]
tune_response = client.post(
f"/ui/repos/{repository_id}/capabilities/{approved_capability['id']}/edit",
data={
"name": "Expose Tuned Repository Interface",
"description": approved_capability["description"],
"inputs": ", ".join(approved_capability["inputs"]),
"outputs": ", ".join(approved_capability["outputs"]),
"confidence": str(approved_capability["confidence"]),
},
follow_redirects=False,
)
assert tune_response.status_code == 303
tuned_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={"scope": "all", "entry_filter": "approved", "type": "capabilities"},
)
assert "Expose Tuned Repository Interface" in tuned_listing.text
candidate_listing = client.get(
f"/ui/repos/{repository_id}/elements",
@@ -1359,6 +1408,95 @@ def test_ui_register_analyze_and_approve_loop(tmp_path):
)
assert filtered_search_response.status_code == 200
assert "UI Repo" in filtered_search_response.text
final_map = client.get(f"/repos/{repository_id}/ability-map").json()
feature_id = final_map["abilities"][0]["capabilities"][0]["features"][0]["id"]
delete_feature_response = client.post(
f"/ui/repos/{repository_id}/features/{feature_id}/delete",
follow_redirects=False,
)
assert delete_feature_response.status_code == 303
deleted_feature_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={"scope": "approved", "type": "features"},
)
assert f"/ui/repos/{repository_id}/features/{feature_id}/delete" not in (
deleted_feature_listing.text
)
finally:
app.dependency_overrides.clear()
def test_ui_element_listing_hides_rejected_candidates_by_default(tmp_path):
source = tmp_path / "repo"
source.mkdir()
(source / "requirements.txt").write_text("fastapi\n", encoding="utf-8")
(source / "app.py").write_text(
"from fastapi import FastAPI\n"
"app = FastAPI()\n"
'@app.get("/status")\n'
"def status():\n"
" return {}\n",
encoding="utf-8",
)
def override_settings():
return Settings(
database_path=str(tmp_path / "ui-rejected.sqlite3"),
checkout_root=str(tmp_path / "ui-rejected-checkouts"),
)
app.dependency_overrides[get_settings] = override_settings
client = TestClient(app)
try:
repository_response = client.post(
"/repos",
json={"name": "Rejected UI", "url": str(source)},
)
repository_id = repository_response.json()["id"]
run_response = client.post(f"/repos/{repository_id}/analysis-runs", json={})
run_id = run_response.json()["analysis_run"]["id"]
candidate_graph = client.get(
f"/repos/{repository_id}/analysis-runs/{run_id}/candidate-graph"
).json()
candidate_feature = candidate_graph["abilities"][0]["capabilities"][0][
"features"
][0]
candidate_feature_id = candidate_feature["id"]
candidate_feature_name = candidate_feature["name"]
reject_response = client.post(
f"/ui/repos/{repository_id}/analysis-runs/{run_id}"
f"/candidate-features/{candidate_feature_id}/reject",
follow_redirects=False,
)
assert reject_response.status_code == 303
default_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={
"scope": "all",
"analysis_run_id": run_id,
"type": "features",
},
)
assert default_listing.status_code == 200
assert "Hide rejected" in default_listing.text
assert candidate_feature_name not in default_listing.text
rejected_listing = client.get(
f"/ui/repos/{repository_id}/elements",
params={
"scope": "all",
"analysis_run_id": run_id,
"type": "features",
"candidate_status_filter": "all",
},
)
assert rejected_listing.status_code == 200
assert candidate_feature_name in rejected_listing.text
assert "candidate: rejected" in rejected_listing.text
assert "Accept" in rejected_listing.text
finally:
app.dependency_overrides.clear()