Files
anti-plagiarism/services/api/tests/test_progress.py
jze9 78e27806b9
All checks were successful
Deploy / test (push) Successful in 3m56s
Deploy / deploy (push) Successful in 31s
feat(admin): шкала загрузки источников, отладка и загрузка работ в корпус
Заливка корпуса была чёрным ящиком: у источника только last_status
(idle/running/done/error), без «сколько из скольки», без причины падения и без
способа остановить начатое. Теперь каждый запуск создаёт строку parse_runs,
куда воркер раз в ~2с пишет стадию, счётчики и журнал событий.

Админка:
- шкала загрузки у каждого источника (0→50% выборка, 50→100% индексация),
  раскрытая строка — журнал прогона по шагам с таймингами;
- «Запустить всё» / «Остановить всё» и остановка по одному источнику
  (кооперативная отмена: воркер останавливается сам, не рвя запись в базу);
- пакетное добавление источников (тип + список тем), тип pmc в форме;
- загрузка PDF/DOCX/TXT прямо в базу сравнения (index.ingest_upload);
- страница «Отладка»: воркеры Celery и их текущие таски, очереди RabbitMQ,
  покрытие корпуса эмбеддингами, зависшие и упавшие прогоны, конфиг бэкендов.

Защита от краш-лупа по consumer_timeout RabbitMQ (docs/DR-HA.md §6), без неё
массовый запуск 170+ источников гарантированно ронял воркер:
- PARSER_TIME_BUDGET_S (1500с) — прогон закругляется сам и помечается partial;
- worker_prefetch_multiplier=1 — таймаут считается от ДОСТАВКИ сообщения, и с
  дефолтным префетчем очередь долгих run_parser убивала канал на задачах,
  которые ещё не начинались.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 17:40:23 +05:00

42 lines
2.1 KiB
Python

"""Юнит-тесты шкалы загрузки источников (app.core.progress) — чистая логика."""
from app.core.progress import run_percent
def test_queued_run_shows_nothing_done():
assert run_percent("queued", "queued", target=100, fetched=0, processed=0) == 0.0
def test_fetch_stage_fills_first_half():
assert run_percent("running", "fetch", target=100, fetched=50, processed=0) == 25.0
assert run_percent("running", "fetch", target=100, fetched=100, processed=0) == 50.0
def test_index_stage_fills_second_half():
# Выборка закончена (50%), проиндексирована половина полученного → 75%
assert run_percent("running", "index", target=100, fetched=100, processed=50) == 75.0
assert run_percent("running", "index", target=100, fetched=100, processed=100) == 100.0
def test_partial_fetch_does_not_inflate_index_progress():
"""Источник отдал меньше лимита: выборка даёт свои 20%, индексация — свои."""
percent = run_percent("running", "index", target=100, fetched=40, processed=20)
assert percent == 45.0 # 20% за выборку + 25% за половину индексации
def test_unknown_target_gives_zero_instead_of_division_error():
assert run_percent("running", "fetch", target=0, fetched=17, processed=0) == 0.0
assert run_percent("running", "index", target=0, fetched=0, processed=0) == 0.0
def test_overshoot_is_clamped():
"""Источник может вернуть больше лимита — шкала не должна уезжать за 100%."""
assert run_percent("running", "fetch", target=10, fetched=99, processed=0) == 50.0
assert run_percent("running", "index", target=10, fetched=10, processed=99) == 100.0
def test_finished_runs_are_always_full():
"""Шкала показывает «работа окончена», исход виден по статусу рядом."""
for status in ("done", "partial", "error", "cancelled"):
assert run_percent(status, "finished", target=100, fetched=3, processed=1) == 100.0