spelunkai/labeling/backend/tests/test_labels.py
Jonas 4c9deda66a Implement labeling backend: FastAPI + SQLAlchemy data model and CRUD API
Add the core labeling data model (label sets, ad-hoc Main->Sub class
hierarchy, frames, bounding-box labels, per-frame/per-set label
status) behind a FastAPI app, with SQLite as the default swappable
DATABASE_URL. Multi-user support is attribution-only for now
(get-or-create by username, no login flow yet). Dataset
versioning/promotion is intentionally deferred - it needs its own
design pass around snapshot semantics.

Each test gets a fully isolated app+DB via create_app(database_url=...)
rather than relying on process-global state. 13/13 tests pass; also
verified live end-to-end against a running uvicorn instance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 11:24:57 +02:00

76 lines
2.8 KiB
Python

def _make_set_with_subclass(client, name="Enemy", sub_name="Bat"):
set_id = client.post("/sets", json={"name": name}).json()["id"]
main_class_id = client.post(f"/sets/{set_id}/main-classes", json={"name": name}).json()["id"]
sub_class_id = client.post(f"/main-classes/{main_class_id}/sub-classes", json={"name": sub_name}).json()["id"]
return set_id, sub_class_id
def _make_frame(client, session_name="run01", frame_index=0):
payload = {"session_name": session_name, "frame_index": frame_index, "image_path": "x.png", "width": 1280, "height": 720}
return client.post("/frames", json=payload).json()["id"]
def test_create_list_update_delete_label(client):
set_id, sub_class_id = _make_set_with_subclass(client)
frame_id = _make_frame(client)
resp = client.post(
f"/frames/{frame_id}/sets/{set_id}/labels",
json={"sub_class_id": sub_class_id, "x": 10, "y": 20, "width": 30, "height": 40, "created_by": "jonas"},
)
assert resp.status_code == 201
label_id = resp.json()["id"]
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/labels")
assert len(resp.json()) == 1
resp = client.patch(f"/labels/{label_id}", json={"x": 15})
assert resp.status_code == 200
assert resp.json()["x"] == 15
resp = client.delete(f"/labels/{label_id}")
assert resp.status_code == 204
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/labels")
assert resp.json() == []
def test_label_rejects_sub_class_from_another_set(client):
_, sub_class_id = _make_set_with_subclass(client, name="Enemy", sub_name="Bat")
other_set_id, _ = _make_set_with_subclass(client, name="Items", sub_name="Gold")
frame_id = _make_frame(client)
resp = client.post(
f"/frames/{frame_id}/sets/{other_set_id}/labels",
json={"sub_class_id": sub_class_id, "x": 0, "y": 0, "width": 1, "height": 1},
)
assert resp.status_code == 422
def test_labels_require_existing_frame_and_set(client):
_, sub_class_id = _make_set_with_subclass(client)
resp = client.post(
"/frames/999/sets/999/labels",
json={"sub_class_id": sub_class_id, "x": 0, "y": 0, "width": 1, "height": 1},
)
assert resp.status_code == 404
def test_frame_set_status_defaults_and_updates(client):
set_id, _ = _make_set_with_subclass(client)
frame_id = _make_frame(client)
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/status")
assert resp.status_code == 200
assert resp.json()["status"] == "unlabeled"
resp = client.put(
f"/frames/{frame_id}/sets/{set_id}/status",
json={"status": "reviewed", "updated_by": "jonas"},
)
assert resp.status_code == 200
assert resp.json()["status"] == "reviewed"
resp = client.get(f"/frames/{frame_id}/sets/{set_id}/status")
assert resp.json()["status"] == "reviewed"