feat(vision): immagini di riferimento non orfane, GET/DELETE (#38.5)
save_reference_image scrive il file dopo il flush riuscito; il router
lo cancella se il commit successivo fallisce. Aggiunti GET
/api/vision/reference-images?task_id= (lista, Maker) e DELETE
/api/vision/reference-images/{id} (cancella riga e file, Maker).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,113 @@ async def test_a_reference_image_keeps_its_provenance(
|
||||
assert saved.path
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_file_survives_a_failed_commit_as_nothing(
|
||||
client, db_session, maker_user, monkeypatch,
|
||||
):
|
||||
"""#38.5: the file is written only after a successful flush, and removed
|
||||
if the commit that follows still fails - never an orphan on disk."""
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
version, task, subtask = await recipe_parts(db_session, recipe.id)
|
||||
await db_session.commit()
|
||||
|
||||
original_commit = db_session.commit
|
||||
|
||||
async def boom(*args, **kwargs):
|
||||
raise RuntimeError("simulated commit failure")
|
||||
monkeypatch.setattr(db_session, "commit", boom)
|
||||
|
||||
try:
|
||||
# The ASGI transport re-raises an unhandled app exception rather
|
||||
# than turning it into a response - the same way it would surface
|
||||
# in production logs. What matters here is what is left on disk.
|
||||
with pytest.raises(RuntimeError, match="simulated commit failure"):
|
||||
await client.post(
|
||||
"/api/vision/reference-images",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("ref.png", an_image(), "image/png")},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
finally:
|
||||
# The db_session fixture commits again on its own teardown - restore
|
||||
# the real commit so that does not also blow up.
|
||||
monkeypatch.setattr(db_session, "commit", original_commit)
|
||||
|
||||
folder = settings.upload_path / "vision" / "reference" / str(task.id)
|
||||
assert not folder.exists() or list(folder.iterdir()) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_reference_images_are_listed_for_their_task(
|
||||
client, db_session, maker_user,
|
||||
):
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
version, task, subtask = await recipe_parts(db_session, recipe.id)
|
||||
await db_session.commit()
|
||||
|
||||
for _ in range(2):
|
||||
response = await client.post(
|
||||
"/api/vision/reference-images",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("ref.png", an_image(), "image/png")},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
listing = await client.get(
|
||||
"/api/vision/reference-images",
|
||||
headers=auth_headers(maker_user),
|
||||
params={"task_id": task.id},
|
||||
)
|
||||
|
||||
assert listing.status_code == 200
|
||||
body = listing.json()
|
||||
assert len(body) == 2
|
||||
assert {row["task_id"] for row in body} == {task.id}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deleting_a_reference_image_removes_the_row_and_the_file(
|
||||
client, db_session, maker_user,
|
||||
):
|
||||
recipe = await create_test_recipe(db_session, maker_user.id)
|
||||
version, task, subtask = await recipe_parts(db_session, recipe.id)
|
||||
await db_session.commit()
|
||||
|
||||
created = await client.post(
|
||||
"/api/vision/reference-images",
|
||||
headers=auth_headers(maker_user),
|
||||
files={"image": ("ref.png", an_image(), "image/png")},
|
||||
data={"task_id": str(task.id)},
|
||||
)
|
||||
reference_id = created.json()["id"]
|
||||
file_path = settings.upload_path / created.json()["path"]
|
||||
assert file_path.exists()
|
||||
|
||||
response = await client.delete(
|
||||
f"/api/vision/reference-images/{reference_id}",
|
||||
headers=auth_headers(maker_user),
|
||||
)
|
||||
|
||||
assert response.status_code == 204
|
||||
assert (await db_session.execute(
|
||||
select(VisionReferenceImage).where(VisionReferenceImage.id == reference_id)
|
||||
)).scalar_one_or_none() is None
|
||||
assert not file_path.exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deleting_an_unknown_reference_image_is_404(
|
||||
client, db_session, maker_user,
|
||||
):
|
||||
response = await client.delete(
|
||||
"/api/vision/reference-images/999999",
|
||||
headers=auth_headers(maker_user),
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_preview_runs_the_graph_and_saves_no_measurement(
|
||||
client, db_session, maker_user, worker_says,
|
||||
|
||||
Reference in New Issue
Block a user