Files
TieMeasureFlow/src/backend/tests/test_vision_graph.py
T
Adriano 54bc384650 fix(vision): task_id di vision_results/vision_reference_images in CASCADE (#38.6)
Migrazione 014 (batch_alter_table, nome del vincolo esistente letto con
sa.inspect invece di indovinato) e stessi ondelete nei modelli ORM.
Cancellare un task con esecuzioni o immagini di riferimento non fallisce
più su un vincolo FK MySQL.

MANCA: i file delle immagini di riferimento su disco non vengono
rimossi alla cancellazione del task - tasks.py::delete_task (NON
TOCCARE da questo lotto) cancella con `db.delete(task)` diretto, senza
un punto unico in vision_service da agganciare. `_copy_tasks_to_version`
(recipe_service.py) non copia le immagini di riferimento nel
copy-on-write: nemmeno questo si tocca qui.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 22:44:55 +02:00

81 lines
3.1 KiB
Python

"""Il grafo di visione vive sul task, e la quota sa quale uscita la riempie."""
import pytest
from sqlalchemy import select
from src.backend.models.orm.recipe import RecipeVersion
from src.backend.models.orm.task import TASK_TYPES, RecipeSubtask, RecipeTask
from src.backend.models.orm.vision import VisionReferenceImage, VisionResult
from src.backend.tests.conftest import create_test_recipe
async def recipe_parts(db_session, recipe_id: int):
"""`create_test_recipe` restituisce la sola ricetta: qui si scompone.
Versione corrente, il suo unico task, la sua prima quota. È l'idioma che
`test_out_of_tolerance.py` usa già, ripetuto perché ogni file di prova si
legge da solo.
"""
version = (await db_session.execute(
select(RecipeVersion).where(
RecipeVersion.recipe_id == recipe_id,
RecipeVersion.is_current == True, # noqa: E712
)
)).scalar_one()
task = (await db_session.execute(
select(RecipeTask).where(RecipeTask.version_id == version.id)
)).scalars().first()
subtask = (await db_session.execute(
select(RecipeSubtask).where(RecipeSubtask.task_id == task.id)
)).scalars().first()
return version, task, subtask
@pytest.mark.asyncio
async def test_task_carries_a_vision_graph(db_session, maker_user):
"""Il grafo si salva e si rilegge identico: il server non lo interpreta."""
recipe = await create_test_recipe(db_session, maker_user.id, code="VIS-1")
version, task, subtask = await recipe_parts(db_session, recipe.id)
graph = {
"schema_version": 1,
"name": "diametro del foro",
"tools": [{"id": "t1", "kind": "DetectCircle", "name": "foro"}],
}
task.vision_json = graph
task.task_type = "camera_measure"
await db_session.flush()
db_session.expunge_all()
reloaded = (await db_session.execute(
select(RecipeTask).where(RecipeTask.id == task.id)
)).scalar_one()
assert reloaded.vision_json == graph
@pytest.mark.asyncio
async def test_subtask_names_the_output_that_fills_it(db_session, maker_user):
"""La quota dichiara quale uscita del grafo le compete."""
recipe = await create_test_recipe(db_session, maker_user.id, code="VIS-2")
version, task, subtask = await recipe_parts(db_session, recipe.id)
subtask.vision_output = "t1.diameter"
await db_session.flush()
db_session.expunge_all()
reloaded = (await db_session.execute(
select(RecipeSubtask).where(RecipeSubtask.id == subtask.id)
)).scalar_one()
assert reloaded.vision_output == "t1.diameter"
def test_the_typo_is_gone():
"""`xf_compare` era un refuso: si corregge finché nessuna riga lo usa."""
assert "dxf_compare" in TASK_TYPES
assert "xf_compare" not in TASK_TYPES
@pytest.mark.parametrize("model", [VisionResult, VisionReferenceImage])
def test_task_id_cascades_when_the_task_is_deleted(model):
"""#38.6: cancellare un task con esecuzioni/immagini non deve fallire su
un vincolo FK MySQL - la riga di visione deve seguirlo."""
(fk,) = model.__table__.columns["task_id"].foreign_keys
assert fk.ondelete == "CASCADE"