Skip to content

Commit c4cb8b0

Browse files
Aurelioloclaude
andcommitted
fix: correct event constant in rollback path, reorder delete operations
- Use PERSISTENCE_ARTIFACT_SAVE_FAILED (not _STORED) in PersistenceError handler -- success event must not be emitted on failure - Delete storage content before repo metadata so inconsistency is detectable (metadata present + no blob) rather than invisible (no metadata + orphaned blob) - Test: use asyncio.run(storage.store(...)) instead of internal dict Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b6cfd39 commit c4cb8b0

2 files changed

Lines changed: 13 additions & 7 deletions

File tree

src/synthorg/api/controllers/artifacts.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from synthorg.observability.events.persistence import (
2121
PERSISTENCE_ARTIFACT_CONTENT_MISSING,
2222
PERSISTENCE_ARTIFACT_DELETED,
23+
PERSISTENCE_ARTIFACT_SAVE_FAILED,
2324
PERSISTENCE_ARTIFACT_SAVED,
2425
PERSISTENCE_ARTIFACT_STORAGE_DELETE_FAILED,
2526
PERSISTENCE_ARTIFACT_STORAGE_ROLLBACK_FAILED,
@@ -219,15 +220,17 @@ async def delete_artifact(
219220
200 on success, 404 if not found.
220221
"""
221222
repo = state.app_state.persistence.artifacts
222-
deleted = await repo.delete(artifact_id)
223-
if not deleted:
223+
artifact = await repo.get(artifact_id)
224+
if artifact is None:
224225
return Response(
225226
content=ApiResponse[None](
226227
error=f"Artifact {artifact_id!r} not found",
227228
),
228229
status_code=404,
229230
)
230-
logger.info(PERSISTENCE_ARTIFACT_DELETED, artifact_id=artifact_id)
231+
# Delete storage content first -- if this fails, metadata still
232+
# exists so the inconsistency is detectable (vs. the reverse
233+
# order where metadata is gone but orphaned blob is invisible).
231234
storage = state.app_state.artifact_storage
232235
try:
233236
await storage.delete(artifact_id)
@@ -237,6 +240,8 @@ async def delete_artifact(
237240
artifact_id=artifact_id,
238241
error=str(exc),
239242
)
243+
await repo.delete(artifact_id)
244+
logger.info(PERSISTENCE_ARTIFACT_DELETED, artifact_id=artifact_id)
240245
return Response(
241246
content=ApiResponse[None](data=None),
242247
status_code=200,
@@ -306,7 +311,7 @@ async def upload_content(
306311
await repo.save(updated)
307312
except PersistenceError as exc:
308313
logger.warning(
309-
PERSISTENCE_ARTIFACT_STORED,
314+
PERSISTENCE_ARTIFACT_SAVE_FAILED,
310315
artifact_id=artifact_id,
311316
error=str(exc),
312317
note="metadata save failed, rolling back content",

tests/unit/api/controllers/test_artifacts.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ def test_delete_artifact_not_found(self, test_client: TestClient[Any]) -> None:
149149
assert resp.status_code == 404
150150

151151
def test_download_content(self, test_client: TestClient[Any]) -> None:
152-
"""Pre-populate storage directly to test download path."""
152+
"""Pre-populate storage via public API, then test download."""
153+
import asyncio
154+
153155
create_resp = test_client.post(
154156
"/api/v1/artifacts",
155157
json={
@@ -163,9 +165,8 @@ def test_download_content(self, test_client: TestClient[Any]) -> None:
163165
)
164166
artifact_id = create_resp.json()["data"]["id"]
165167
payload = b"hello world"
166-
# Pre-populate the fake storage dict to avoid multipart PUT.
167168
storage = test_client.app.state.app_state.artifact_storage
168-
storage._store[artifact_id] = payload
169+
asyncio.run(storage.store(artifact_id, payload))
169170
dl_resp = test_client.get(f"/api/v1/artifacts/{artifact_id}/content")
170171
assert dl_resp.status_code == 200
171172
assert dl_resp.content == payload

0 commit comments

Comments
 (0)