diff --git a/src/backend/services/vision_service.py b/src/backend/services/vision_service.py index 8b74e2d..400472f 100644 --- a/src/backend/services/vision_service.py +++ b/src/backend/services/vision_service.py @@ -261,7 +261,15 @@ def store_recipe_folder( is left untouched (immutable: other task versions may reference it). """ canonical = json.dumps(recipe, sort_keys=True, separators=(",", ":")) - digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16] + # Il digest copre anche i file (path ordinato + sha256 del contenuto): la + # stessa ricetta con file diversi (es. primo upload senza il DXF) deve dare + # una cartella diversa, altrimenti quella incompleta resterebbe per sempre + # perche' immutabile (#46). + h = hashlib.sha256(canonical.encode("utf-8")) + for rel in sorted(files): + h.update(b"\0" + rel.encode("utf-8") + b"\0") + h.update(hashlib.sha256(files[rel]).digest()) + digest = h.hexdigest()[:16] relative = f"vision/recipes/{task_id}/{digest}" folder = settings.upload_path / relative if folder.exists(): diff --git a/src/backend/tests/test_vision_recipe_upload.py b/src/backend/tests/test_vision_recipe_upload.py index ed922fb..6a7262e 100644 --- a/src/backend/tests/test_vision_recipe_upload.py +++ b/src/backend/tests/test_vision_recipe_upload.py @@ -263,3 +263,20 @@ async def test_copy_on_write_carries_recipe_dir(client, db_session, maker_user): version, new_task, _ = await _current_task_and_subtask(db_session, recipe.id) assert version.version_number == 2 assert new_task.vision_recipe_dir == "vision/recipes/1/abc" and new_task.vision_json == RECIPE + + +@pytest.mark.asyncio +async def test_retry_with_missing_file_then_complete_succeeds( + client, db_session, maker_user, monkeypatch, +): + # worker finto: ok solo se il DXF esiste nella cartella della ricetta + async def fake(graph, recipe_dir): + present = (settings.upload_path / recipe_dir / "dxf" / "part.dxf").is_file() + return {"ok": present, "errors": [] if present else ["dxf missing"], "outputs": OUTPUTS} + monkeypatch.setattr("src.backend.services.vision_service.validate_recipe", fake) + task, _ = await make_task(db_session, maker_user) + + first = await upload(client, maker_user, task, files=[]) + assert first.status_code == 422 + second = await upload(client, maker_user, task, files=GOOD_FILES) + assert second.status_code == 200, second.text