Files
shelfmark/tests/core/test_status_messages.py
T
splitsec2 371ac75954 feat(activity): say in plain words what a failed download means and what happens next (#1617)
A failed or cancelled card showed the log line, things like `Peer
transfer failed: Errored` or a 500 with a URL in it, so a red card meant
going to the logs to find out whether anything was coming next. They now
say what happened and what happens next.

For a download that belongs to a request, the card says another copy
will be tried in about N minutes when the quick retry from #1535 is on,
and points at Retry or reopening the request when it isn't. A manual
download is never promised an automatic retry. The Soulseek cases each
get their own sentence: the peer dropped the transfer, the peer could
not be reached, slskd refused to queue it, and the peer never answered.
The "tried twice" wording leans on the retry from #1590.

The saved message is left alone because auto-download reads it to tell a
failed release from an interrupted one. Only what the activity list
shows is reworded, on a copy, and anything the new code doesn't
recognise comes back as it was.

It matches on how the message starts, so it is only as current as the
wording the rest of the code uses. A message that gets reworded
elsewhere falls back to the original line instead of breaking, which
seemed fine for something that only changes the display, but it is the
part I would look at first.

It has been running on my instance since mid-October. Ten new tests
cover each message and the next step, and the activity route tests still
pass. Ruff and basedpyright are clean on the changed files.
2026-11-08 14:51:52 -07:00

145 lines
5.5 KiB
Python

"""What the activity list says about a download that failed, and what happens next."""
from __future__ import annotations
import pytest
from shelfmark.core.status_messages import explain, explain_buckets
def _explain(message, *, linked=True, minutes=20):
return explain(message, request_linked=linked, retry_minutes=minutes)
@pytest.mark.parametrize(
("raw", "expected"),
[
(
"Peer transfer failed: Errored",
"The Soulseek peer dropped the transfer (Errored). "
"Another copy will be tried in about 20 minutes.",
),
(
"Failed to add to slskd: 500 Server Error: Internal Server Error for url: http://x",
"The Soulseek peer did not answer slskd's queue request (tried twice). "
"Another copy will be tried in about 20 minutes.",
),
(
"Download stalled (no activity for 300s)",
"Nothing was downloading, so it was stopped. "
"Another copy will be tried in about 20 minutes.",
),
(
"No seeders: 0 found, 1 needed, for 5 min (12% downloaded)",
"No seeders: 0 found, 1 needed, for 5 min (12% downloaded); the torrent was removed. "
"Another copy will be tried in about 20 minutes.",
),
(
"All download sources failed",
"None of the download sources could provide this book. "
"Another copy will be tried in about 20 minutes.",
),
],
)
def test_a_failure_says_what_happened_and_what_comes_next(raw, expected):
assert _explain(raw) == expected
def test_interrupted_says_a_restart_did_it():
assert _explain("Interrupted") == "Interrupted by a restart. Use Retry to run it again."
def test_a_superseded_row_says_the_book_was_delivered():
assert (
_explain("Superseded by a completed download")
== "This book was already delivered by another download."
)
def test_without_the_quick_retry_a_request_is_pointed_at_retry_or_reopen():
assert _explain("Peer transfer failed: TimedOut", minutes=0).endswith(
"Use Retry to try again, or reopen the request to try another copy."
)
def test_a_manual_download_is_never_promised_an_automatic_retry():
text = _explain("Peer transfer failed: Errored", linked=False)
assert "will be tried" not in text
assert text.endswith("Use Retry to try again, or pick another copy.")
def test_an_unknown_message_and_an_empty_one_are_left_as_they_are():
assert _explain("Something new broke") == "Something new broke"
assert _explain(None) is None
assert _explain(" ") == " "
def test_the_buckets_are_rewritten_without_touching_the_original_payload():
original = {"id": "t", "request_id": 5, "status_message": "Peer transfer failed: Errored"}
status = {"error": {"t": original}, "cancelled": {}, "complete": {"c": {"status_message": "x"}}}
explain_buckets(status)
assert status["error"]["t"]["status_message"].startswith("The Soulseek peer dropped")
assert original["status_message"] == "Peer transfer failed: Errored"
assert status["complete"]["c"]["status_message"] == "x"
@pytest.mark.parametrize(
("raw", "expected"),
[
(
'Failed to add to slskd: 500 Server Error: Internal Server Error for url: http://x/batches ("Failed to connect to user u: Failed to establish a direct or indirect message connection to u (1.2.3.4:5)")',
"Could not reach the Soulseek peer, directly or through the Soulseek server (tried twice). "
"Another copy will be tried in about 20 minutes.",
),
(
'Failed to add to slskd: 500 Server Error for url: http://x/batches ("The wait timed out after 5000 milliseconds")',
"The Soulseek peer did not answer slskd's queue request (tried twice). "
"Another copy will be tried in about 20 minutes.",
),
(
'Failed to add to slskd: 500 Server Error for url: http://x/batches ("The peer is banned")',
"slskd would not queue the download (tried twice). "
"Another copy will be tried in about 20 minutes.",
),
(
"Download failed: TypeError",
"The download hit an internal error (TypeError). "
"Another copy will be tried in about 20 minutes.",
),
(
"Retrying after a 20-minute cooldown (failed; delivery was queued)",
"Retrying automatically after a failed download.",
),
],
)
def test_slskd_and_internal_failures_say_what_went_wrong(raw, expected):
assert _explain(raw) == expected
def test_a_request_reopened_after_a_restart_says_it_will_be_tried_again():
from shelfmark.core.status_messages import explain_request_rows
out = explain_request_rows([{"id": 1, "last_failure_reason": "Interrupted by a restart"}])
assert out[0]["last_failure_reason"] == "Interrupted by a restart; it will be tried again."
def test_request_failure_reasons_are_explained_on_copies():
from shelfmark.core.status_messages import explain_request_rows
rows = [
{"id": 1, "last_failure_reason": "Download failed: TypeError"},
{"id": 2, "last_failure_reason": None},
{"id": 3},
]
out = explain_request_rows(rows)
assert out[0]["last_failure_reason"].startswith("The download hit an internal error")
assert out[1]["last_failure_reason"] is None
assert "last_failure_reason" not in out[2]
assert rows[0]["last_failure_reason"] == "Download failed: TypeError" # untouched