e206778a5c
Cluster B, milestone #99. Backend for the allowlist tuning dashboard. #7a: AllowlistService.coverage(tag_id, threshold) counts distinct images with a prediction resolving to the tag (raw_name==tag.name OR (raw_name,category) in the tag's aliases) scoring >= threshold — the gross candidate pool, mirroring tasks.ml._confidence_for_tag resolution. list_all now carries applied_count (grouped image_tag count) + coverage_count (at the row's threshold). New GET /api/tags/<id>/allowlist/coverage?threshold= for the live what-if number. #7b: /suggestions/accept + /alias return {allowlisted, tag_id, tag_name, projected_count} (projection at the tag's threshold) instead of 204, so the UI can show a non-blocking 'auto-applying to ~N images' toast. Apply still runs async via apply_allowlist_tags — projected_count is an estimate. Tests: coverage by threshold (direct + alias-with-category), list applied vs coverage, coverage route (explicit/default/bad threshold), accept/alias payload (newly-allowlisted vs already-on-list). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XCUHUGQLrBrkgyk1t49kpX
89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
import pytest
|
|
|
|
from backend.app.models import ImagePrediction, ImageRecord, TagAllowlist, TagKind
|
|
from backend.app.services.tag_service import TagService
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_and_patch_and_delete(client, db):
|
|
tag = await TagService(db).find_or_create("AL", TagKind.character)
|
|
db.add(TagAllowlist(tag_id=tag.id, min_confidence=0.95))
|
|
await db.commit()
|
|
|
|
resp = await client.get("/api/allowlist")
|
|
assert resp.status_code == 200
|
|
assert any(r["tag_id"] == tag.id for r in await resp.get_json())
|
|
|
|
resp = await client.patch(
|
|
f"/api/tags/{tag.id}/allowlist", json={"min_confidence": 0.80}
|
|
)
|
|
assert resp.status_code == 204
|
|
|
|
resp = await client.get(f"/api/tags/{tag.id}/allowlist")
|
|
assert (await resp.get_json())["min_confidence"] == pytest.approx(0.80)
|
|
|
|
resp = await client.delete(f"/api/tags/{tag.id}/allowlist")
|
|
assert resp.status_code == 204
|
|
resp = await client.get(f"/api/tags/{tag.id}/allowlist")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_patch_rejects_out_of_range(client, db):
|
|
tag = await TagService(db).find_or_create("AL2", TagKind.character)
|
|
db.add(TagAllowlist(tag_id=tag.id))
|
|
await db.commit()
|
|
resp = await client.patch(
|
|
f"/api/tags/{tag.id}/allowlist", json={"min_confidence": 1.5}
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_coverage_endpoint(client, db):
|
|
tag = await TagService(db).find_or_create("Cover", TagKind.general)
|
|
db.add(TagAllowlist(tag_id=tag.id, min_confidence=0.90))
|
|
for i, score in enumerate((0.95, 0.60)):
|
|
img = ImageRecord(
|
|
path=f"/images/cov{i}.jpg", sha256=f"cv{i:062d}", size_bytes=1,
|
|
mime="image/jpeg", width=1, height=1,
|
|
origin="imported_filesystem", integrity_status="unknown",
|
|
)
|
|
db.add(img)
|
|
await db.flush()
|
|
db.add(ImagePrediction(
|
|
image_record_id=img.id, raw_name="Cover",
|
|
category="general", score=score,
|
|
))
|
|
await db.commit()
|
|
|
|
# Explicit threshold.
|
|
resp = await client.get(
|
|
f"/api/tags/{tag.id}/allowlist/coverage?threshold=0.90"
|
|
)
|
|
assert resp.status_code == 200
|
|
assert (await resp.get_json())["count"] == 1
|
|
# Lower what-if threshold widens coverage.
|
|
resp = await client.get(
|
|
f"/api/tags/{tag.id}/allowlist/coverage?threshold=0.50"
|
|
)
|
|
assert (await resp.get_json())["count"] == 2
|
|
# No threshold → uses the stored min_confidence (0.90).
|
|
resp = await client.get(f"/api/tags/{tag.id}/allowlist/coverage")
|
|
body = await resp.get_json()
|
|
assert body["count"] == 1
|
|
assert body["threshold"] == pytest.approx(0.90)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_coverage_rejects_bad_threshold(client, db):
|
|
tag = await TagService(db).find_or_create("Cover2", TagKind.general)
|
|
db.add(TagAllowlist(tag_id=tag.id))
|
|
await db.commit()
|
|
resp = await client.get(
|
|
f"/api/tags/{tag.id}/allowlist/coverage?threshold=2.0"
|
|
)
|
|
assert resp.status_code == 400
|