fix(i18n): hold every refusal reason by type, and refuse what the scan cannot read

Review round 1 of the refusal contract: it claimed more than it held.

- The other reason carriers get their Literal too (BlockedUrl, ClipError,
  FolderError, the playlist and sign-in refusal helpers), so the type
  checker refuses a reason passed by variable or an unknown literal, and
  the contract reads one shape.
- A literal reason in a detail dict built anywhere counts as a reason of
  its code; a code the scan cannot read, a refused() whose reason it
  cannot read, and a new reason passed by variable each fail the contract
  until listed with what holds them.
- A sign-in refusal key the backend no longer sends is caught too.
- localizeDetail's numbers (download cap in GB, upload cap in bytes) and
  the shared-sentence fallbacks are tested in both languages.
- One Literal parser in the contract; a stale comment fixed.
This commit is contained in:
2026-10-31 01:00:26 +01:00
parent 4379f76747
commit ceadf69e43
8 changed files with 213 additions and 82 deletions
+12 -1
View File
@@ -24,6 +24,7 @@ kept it — and its `ready_at`, the moment it finished (`stamp_done`).
from datetime import datetime, timedelta, timezone
from enum import StrEnum
from typing import Literal
from urllib.parse import urlsplit
from sqlalchemy import select
@@ -181,10 +182,20 @@ def stamp_done(db: Session, job: DownloadJob) -> None:
job.expiry_warned = False
# Every folder change the rules refuse, answered as a generic refusal; the UI names each one
# (`errors.refused.<reason>`, held by the frontend's backendIds contract, which reads this Literal).
FolderReason = Literal[
"file_folder_gone",
"file_folder_name_taken",
"file_folder_not_its_source",
"file_folder_not_own",
]
class FolderError(Exception):
"""A folder change the rules refuse; `reason` is the refusal the route answers with."""
def __init__(self, reason: str) -> None:
def __init__(self, reason: FolderReason) -> None:
super().__init__(reason)
self.reason = reason