Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions api/src/learn_to_cloud/routes/htmx_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@
"Verification could not be started. This attempt was not counted — "
"please try again."
)
_DURABLE_TERMINAL_ERROR_MESSAGE = (
"Verification failed because the verification service hit an internal error. "
"This attempt was not counted. Please try again."
)

_ACTIVE_DURABLE_STATUSES = {"pending", "running", "continuedasnew"}
_TERMINAL_DURABLE_STATUSES = {"completed", "failed", "terminated", "canceled"}
Expand Down Expand Up @@ -155,7 +159,7 @@ async def _delete_terminal_job(
request: Request,
token_data: VerificationStatusToken,
status: str,
) -> None:
) -> bool:
"""Drop the verification_jobs row after Durable reaches terminal failure.

Replaces the older ``mark_server_error`` / ``mark_cancelled`` writes:
Expand All @@ -179,6 +183,7 @@ async def _delete_terminal_job(
"verification.poller.delete_skipped",
extra={"job_id": str(job_id), "runtime_status": status},
)
return deleted


async def _render_step_toggle(
Expand Down Expand Up @@ -570,8 +575,24 @@ async def htmx_verification_job_status(
)

if status in _DURABLE_FAILURE_STATUSES:
await _delete_terminal_job(request, token_data, status)
return HTMLResponse(_reload_verification_html())
deleted = await _delete_terminal_job(request, token_data, status)
if not deleted:
return HTMLResponse(_reload_verification_html())

requirement = await get_requirement_by_slug(db, token_data.requirement_slug)
if requirement is None:
return HTMLResponse(_reload_verification_html())

return templates.TemplateResponse(
request,
"partials/requirement_card.html",
build_requirement_card_context(
requirement=requirement,
github_username=current_user.github_username,
server_error=True,
server_error_message=_DURABLE_TERMINAL_ERROR_MESSAGE,
),
)

if status in _TERMINAL_DURABLE_STATUSES:
return HTMLResponse(_reload_verification_html())
Expand Down
23 changes: 20 additions & 3 deletions api/tests/routes/test_htmx_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,10 +656,12 @@ async def test_completed_status_returns_reload_trigger(self):
assert isinstance(result, HTMLResponse)
assert "location.reload()" in bytes(result.body).decode()

async def test_failed_status_deletes_active_job(self):
async def test_failed_status_deletes_active_job_and_renders_error(
self,
_patch_templates,
):
"""Durable terminal failure deletes the row instead of marking
a server-error status. Frees the partial unique index for retry
without copying Durable's terminal state into Postgres."""
a server-error status, then shows a retryable service error."""
request = _mock_request()
mock_session = AsyncMock()
request.app.state.session_maker.return_value.__aenter__.return_value = (
Expand Down Expand Up @@ -688,6 +690,10 @@ async def test_failed_status_deletes_active_job(self):
"learn_to_cloud.routes.htmx_routes.VerificationJobRepository",
autospec=True,
) as mock_repository_class,
patch(
"learn_to_cloud.routes.htmx_routes.get_requirement_by_slug",
return_value=MagicMock(),
),
):
mock_repository = mock_repository_class.return_value
mock_repository.delete_active = AsyncMock(return_value=True)
Expand All @@ -702,6 +708,13 @@ async def test_failed_status_deletes_active_job(self):
assert isinstance(result, HTMLResponse)
mock_repository.delete_active.assert_awaited_once_with(job_id)
mock_session.commit.assert_awaited_once()
_, _, context = _patch_templates.TemplateResponse.call_args.args
assert context["server_error"] is True
assert (
context["server_error_message"]
== "Verification failed because the verification service hit an internal "
"error. This attempt was not counted. Please try again."
)

async def test_canceled_status_also_deletes_active_job(self):
"""``Canceled`` and ``Terminated`` are handled the same way as
Expand Down Expand Up @@ -734,6 +747,10 @@ async def test_canceled_status_also_deletes_active_job(self):
"learn_to_cloud.routes.htmx_routes.VerificationJobRepository",
autospec=True,
) as mock_repository_class,
patch(
"learn_to_cloud.routes.htmx_routes.get_requirement_by_slug",
return_value=MagicMock(),
),
):
mock_repository = mock_repository_class.return_value
mock_repository.delete_active = AsyncMock(return_value=True)
Expand Down