Ricetta: il digest della cartella copre anche i file di meta (#46)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -261,7 +261,15 @@ def store_recipe_folder(
|
|||||||
is left untouched (immutable: other task versions may reference it).
|
is left untouched (immutable: other task versions may reference it).
|
||||||
"""
|
"""
|
||||||
canonical = json.dumps(recipe, sort_keys=True, separators=(",", ":"))
|
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}"
|
relative = f"vision/recipes/{task_id}/{digest}"
|
||||||
folder = settings.upload_path / relative
|
folder = settings.upload_path / relative
|
||||||
if folder.exists():
|
if folder.exists():
|
||||||
|
|||||||
@@ -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)
|
version, new_task, _ = await _current_task_and_subtask(db_session, recipe.id)
|
||||||
assert version.version_number == 2
|
assert version.version_number == 2
|
||||||
assert new_task.vision_recipe_dir == "vision/recipes/1/abc" and new_task.vision_json == RECIPE
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user