feat(allowlist): coverage projection + applied-count + post-accept projection (#7a/#7b)
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
This commit is contained in:
@@ -14,11 +14,11 @@ def eager():
|
||||
celery.conf.task_always_eager = False
|
||||
|
||||
|
||||
async def _img(db, preds):
|
||||
async def _img(db, preds, sha="s" * 64):
|
||||
from tests._prediction_helpers import seed_predictions
|
||||
|
||||
img = ImageRecord(
|
||||
path="/images/s.jpg", sha256="s" * 64, size_bytes=1,
|
||||
path=f"/images/{sha[:8]}.jpg", sha256=sha, size_bytes=1,
|
||||
mime="image/jpeg", width=1, height=1,
|
||||
origin="imported_filesystem", integrity_status="unknown",
|
||||
)
|
||||
@@ -57,7 +57,31 @@ async def test_accept_then_applied(client, db):
|
||||
resp = await client.post(
|
||||
f"/api/images/{img.id}/suggestions/accept", json={"tag_id": tag.id}
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
# #7b: a fresh accept newly-allowlists → projection payload for the toast.
|
||||
assert body["allowlisted"] is True
|
||||
assert body["tag_id"] == tag.id
|
||||
assert body["tag_name"] == "AcceptMe"
|
||||
assert "projected_count" in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_accept_already_allowlisted_reports_not_new(client, db):
|
||||
img1 = await _img(db, {}, sha="c" * 64)
|
||||
img2 = await _img(db, {}, sha="d" * 64)
|
||||
tag = await TagService(db).find_or_create("Twice", TagKind.character)
|
||||
await db.commit()
|
||||
first = await client.post(
|
||||
f"/api/images/{img1.id}/suggestions/accept", json={"tag_id": tag.id}
|
||||
)
|
||||
assert (await first.get_json())["allowlisted"] is True
|
||||
second = await client.post(
|
||||
f"/api/images/{img2.id}/suggestions/accept", json={"tag_id": tag.id}
|
||||
)
|
||||
body = await second.get_json()
|
||||
assert body["allowlisted"] is False # already on the allowlist
|
||||
assert "projected_count" not in body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -127,7 +151,8 @@ async def test_alias_roundtrip_resolves_by_raw_key(client, db):
|
||||
"canonical_tag_id": canonical.id,
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 204
|
||||
assert resp.status_code == 200
|
||||
assert (await resp.get_json())["allowlisted"] is True
|
||||
|
||||
# (b) A DIFFERENT image with the same prediction now resolves via the alias
|
||||
# (image A's tag is applied, so it's filtered there). Had the alias been
|
||||
|
||||
Reference in New Issue
Block a user