From 42c853e9810bc3eed2a9570ed0fd748ede275be2 Mon Sep 17 00:00:00 2001 From: Matthew Robert Wesney <157447210+dovvnloading@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:36:10 -0400 Subject: [PATCH] fix(execution): fail an attachment job when the disk does, instead of escaping A full disk while staging an attachment left the job non-terminal for good and answered the route with HTTP 500. `_publish_bytes` caught only `ExecutionRepositoryError`: except ExecutionRepositoryError: raise ArtifactBoundaryError("artifact_publish_failed") from None But `publish_artifact` re-raises: its handler removes the partial files and then `raise`s the original, so a full disk, a permission error or an antivirus lock arrives unwrapped as `OSError`. That escaped the boundary, so `AttachmentStagingService`'s `except ArtifactBoundaryError` did not match, `_fail` never ran, and the job it had already created stayed at `queued` forever while the exception surfaced as an unhandled 500. Reproduced by making `publish_artifact` raise `OSError(28)`: before: OSError escaped the boundary job status left as: queued after: AttachmentStagingError raised job status left as: failed `publish_outputs`, a few lines down in the same class, already catches `ExecutionRepositoryError` and `OSError` and rolls back for both -- so this was an inconsistency between two sibling paths rather than a deliberate distinction. Co-Authored-By: Claude Opus 5 --- .../execution/artifact_boundary.py | 10 +++++- tests/test_attachment_staging.py | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/backend/cortex_backend/execution/artifact_boundary.py b/backend/cortex_backend/execution/artifact_boundary.py index e994380..3b0b5db 100644 --- a/backend/cortex_backend/execution/artifact_boundary.py +++ b/backend/cortex_backend/execution/artifact_boundary.py @@ -354,7 +354,15 @@ def _publish_bytes( mime_type=mime_type, retention_seconds=retention_seconds, ) - except ExecutionRepositoryError: + except (ExecutionRepositoryError, OSError): + # OSError as well, because publish_artifact re-raises it: its own + # handler removes the partial files and then `raise`s the original, + # so a full disk, a permission error or an antivirus lock arrives + # here unwrapped. Without this the exception escaped the boundary + # entirely -- the caller's `except ArtifactBoundaryError` missed + # it, so the staging job was never failed and stayed non-terminal + # for good, and the route answered 500 instead of a stable code. + # publish_outputs below already treats both the same way. raise ArtifactBoundaryError("artifact_publish_failed") from None def stage_bytes( diff --git a/tests/test_attachment_staging.py b/tests/test_attachment_staging.py index eb5d802..71ed611 100644 --- a/tests/test_attachment_staging.py +++ b/tests/test_attachment_staging.py @@ -128,3 +128,35 @@ def test_stage_bytes_duplicate_terminal_result_is_revalidated(tmp_path: Path): with pytest.raises(AttachmentStagingError) as error: service.stage(owner=OWNER, request_id="attach-integrity", content=_image_bytes()) assert error.value.code in {"attachment_artifact_unavailable", "attachment_artifact_invalid"} + + +def test_a_disk_failure_fails_the_job_instead_of_escaping(tmp_path: Path, monkeypatch): + """A full disk must produce a stable code and a terminal job. + + `publish_artifact` removes its partial files and then re-raises the + original exception, so an OSError -- a full disk, a permission error, an + antivirus lock -- reached `_publish_bytes` unwrapped. That method caught + only `ExecutionRepositoryError`, so the exception escaped the boundary + entirely: the caller's `except ArtifactBoundaryError` missed it, `_fail` + never ran, the job stayed non-terminal for the rest of the installation's + life, and the route answered HTTP 500 rather than a stable code. + + `publish_outputs` in the same class already treats both the same way. + """ + repository, service = _service(tmp_path) + + def full_disk(*args, **kwargs): + raise OSError(28, "No space left on device") + + monkeypatch.setattr(ExecutionRepository, "publish_artifact", full_disk) + + with pytest.raises(AttachmentStagingError): + service.stage(owner=OWNER, request_id="attach-disk-full", content=_image_bytes()) + + with repository.connect() as connection: + statuses = [ + str(row["status"]) + for row in connection.execute("SELECT status FROM execution_jobs").fetchall() + ] + + assert statuses == ["failed"], "the staging job was left non-terminal"