diff --git a/tradein-mvp/backend/tests/test_3297_avito_detail_browser_status.py b/tradein-mvp/backend/tests/test_3297_avito_detail_browser_status.py
new file mode 100644
index 00000000..e6de2270
--- /dev/null
+++ b/tradein-mvp/backend/tests/test_3297_avito_detail_browser_status.py
@@ -0,0 +1,154 @@
+"""#3297: browser-ветка fetch_detail не смотрела на HTTP-статус ответа.
+
+Прод-замер 31.08.2026 (24 попытки через 3 узла): 11 отказов площадки (8×403, 2×429,
+1×439), из них 3 пришли страницей-заглушкой (8172 байта, title «Доска объявлений от
+частных лиц и компаний на Авито»), которая не содержит НИ ОДНОГО известного на тот
+момент маркера — ни firewall, ни soft-block. До фикса browser-ветка `fetch_detail`
+(providers/avito/detail.py) НИКОГДА не читала `browser_fetcher.last_response_status`
+и такие страницы уходили прямиком в `parse_detail_html`, который падал
+`ValueError("Cannot extract item_id")` — отказ ПЛОЩАДКИ считался обычной ошибкой
+парсинга, узел не ротировался (зеркало давно рабочей curl-ветки того же файла,
+`if sc in (403, 439) or is_firewall`).
+
+Фальсификация: без правки (a) поднимает ValueError вместо AvitoBlockedError.
+"""
+
+from __future__ import annotations
+
+import os
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+
+os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost/test_db")
+
+from scraper_kit.avito_exceptions import (
+ AvitoBlockedError,
+ AvitoListingGoneError,
+ AvitoRateLimitedError,
+)
+from scraper_kit.providers.avito.detail import (
+ _is_detail_soft_block,
+ fetch_detail,
+)
+
+_MIN_HTML = '
№ 1234567890
'
+
+# Ровно страница из прод-замера 31.08: нет item-view, нет старого soft-block-маркера
+# ("объявления на сайте авито") — до фикса такая страница проваливалась мимо ВСЕХ
+# известных детекторов прямиком в parse_detail_html/ValueError.
+_UNRECOGNIZED_BLOCK_PAGE = (
+ "Доска объявлений от частных лиц и компаний на Авито"
+ "антибот-заглушка без известных текстовых маркеров"
+)
+
+_NOT_FOUND_HTML = (
+ "Ошибка 404Страница не найдена"
+)
+
+
+def _bf(html: str, *, status: int | None) -> MagicMock:
+ bf = MagicMock()
+ bf.fetch = AsyncMock(return_value=html)
+ bf.last_response_status = status
+ return bf
+
+
+# ── (a) 403/439 -> AvitoBlockedError, 429 -> AvitoRateLimitedError, не ValueError ──
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("status", [403, 439])
+async def test_browser_block_status_raises_avito_blocked_not_valueerror(status: int) -> None:
+ bf = _bf(_UNRECOGNIZED_BLOCK_PAGE, status=status)
+
+ with pytest.raises(AvitoBlockedError):
+ await fetch_detail(
+ "https://www.avito.ru/ekaterinburg/kvartiry/x_1234567890", browser_fetcher=bf
+ )
+
+
+@pytest.mark.asyncio
+async def test_browser_429_raises_rate_limited_not_blocked() -> None:
+ """429 — не блок, а просьба сбавить темп; тип решает судьбу тёплого контекста.
+
+ На AvitoBlockedError оркестратор один раз за прогон зовёт request_context_reset()
+ (avito_detail_backfill.py, #3251) и выбрасывает пройденный QRATOR-PoW. При
+ rate-limit контекст ЦЕЛ, сбрасывать его — значит проходить проверку заново с того
+ же IP. Curl-ветка того же файла разводит эти случаи так же: 429 → короткий retry,
+ при исчерпании AvitoRateLimitedError, а не AvitoBlockedError.
+
+ Проверяем ИМЕННО невхождение в AvitoBlockedError: AvitoRateLimitedError ему не
+ родственник, так что pytest.raises(AvitoRateLimitedError) прошёл бы и на блоке,
+ будь иерархия обратной — поэтому ассертим тип точно.
+ """
+ bf = _bf(_UNRECOGNIZED_BLOCK_PAGE, status=429)
+
+ with pytest.raises(AvitoRateLimitedError) as exc_info:
+ await fetch_detail(
+ "https://www.avito.ru/ekaterinburg/kvartiry/x_1234567890", browser_fetcher=bf
+ )
+
+ assert not isinstance(exc_info.value, AvitoBlockedError)
+
+
+# ── (b) None-статус ничего не ломает — старое поведение сохранено ───────────────
+
+
+@pytest.mark.asyncio
+async def test_browser_none_status_does_not_break_normal_parse() -> None:
+ """Сайдкар старой версии / goto без статуса — фетч ведёт себя как ДО фикса."""
+ bf = _bf(_MIN_HTML, status=None)
+
+ enrichment = await fetch_detail(
+ "https://www.avito.ru/ekaterinburg/kvartiry/x_1234567890", browser_fetcher=bf
+ )
+
+ assert enrichment.item_id == "1234567890"
+
+
+@pytest.mark.asyncio
+async def test_browser_none_status_still_falls_back_to_html_heuristics() -> None:
+ """None-статус НЕ подавляет остальные проверки — soft-block-страница всё ещё блок."""
+ bf = _bf(_UNRECOGNIZED_BLOCK_PAGE, status=None)
+
+ with pytest.raises(AvitoBlockedError):
+ await fetch_detail(
+ "https://www.avito.ru/ekaterinburg/kvartiry/x_1234567890", browser_fetcher=bf
+ )
+
+
+# ── (c) 404 остаётся AvitoListingGoneError, даже если статус — блокирующий ──────
+
+
+@pytest.mark.asyncio
+async def test_browser_404_page_wins_over_block_status() -> None:
+ """Проверка статуса стоит ПОСЛЕ _is_detail_not_found: 404-текст не должен
+
+ маскироваться под блок, даже если last_response_status совпал с одним из
+ блокирующих кодов (площадка вполне может отдать 404-текст с любым HTTP-кодом).
+ """
+ bf = _bf(_NOT_FOUND_HTML, status=403)
+
+ with pytest.raises(AvitoListingGoneError):
+ await fetch_detail(
+ "https://www.avito.ru/ekaterinburg/kvartiry/x_1234567890", browser_fetcher=bf
+ )
+
+
+# ── (d) новый title-маркер опознаётся _is_detail_soft_block ─────────────────────
+
+
+def test_new_title_marker_is_recognized_as_soft_block() -> None:
+ """Маркер снят вживую 31.08.2026 — старый ('объявления на сайте авито')
+
+ в этом title НЕ встречается подстрокой, поэтому нужен отдельный маркер, а не
+ расширение старого.
+ """
+ assert _is_detail_soft_block(_UNRECOGNIZED_BLOCK_PAGE) is True
+
+
+def test_old_title_marker_still_recognized() -> None:
+ """Старый маркер не удалён — площадка может отдавать обе формы заглушки."""
+ old_style = "Авито — Объявления на сайте Авито