fix(mera): живой тест витрины гоняется на полной схеме, а не только на пустой (#3469)
All checks were successful
CI / changes (pull_request) Successful in 12s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m52s
CI Trade-In / frontend-checks (pull_request) Successful in 1m17s
All checks were successful
CI / changes (pull_request) Successful in 12s
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m52s
CI Trade-In / frontend-checks (pull_request) Successful in 1m17s
Тест применял 015/051/052 безусловно, и на базе, прошедшей всю цепочку миграций (CI и прод), повтор 015 падал: psycopg.errors.UndefinedColumn: column "returning_count" of relation "scrape_runs" does not exist Файл 015 идемпотентен относительно себя, но не относительно схемы, прошедшей 214 (DROP COLUMN IF EXISTS returning_count): CREATE TABLE IF NOT EXISTS — no-op, а COMMENT ON COLUMN в конце того же файла обращается к снесённой колонке. На чистой базе, где 015 ложится с нуля, этого не видно по построению — потому прогон и был зелёным там, где его гонял я, и красным там, где его гоняет CI. Зависимости теперь применяются только когда scrape_schedules ещё нет; в докстроке — рецепт прогона на ПОЛНОЙ схеме, тем же путём, что у CI. Заодно закрыты три дыры, которые находились мутациями: - окно расписания и повторное применение проверяются на живой БД (до этого 6,7 → 6,23 и DELETE+INSERT вместо ON CONFLICT проходили насквозь); идемпотентность меряется created_at строки, а не числом строк — замена «удалить и вставить» тоже оставляет ровно одну строку, но стирает last_run_at/next_run_at на каждом деплое; - next_run_at в будущем — утверждение стояло в приёмке и ничем не проверялось; - handler сравнивается по САМОМУ job'у, а не по log_name: имя — второй литерал конструктора Handler, и чужое тело под верным ключом (_job_landing_stats) проходило проверку по имени. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
59482cae85
commit
e6e7a8db1c
1 changed files with 49 additions and 11 deletions
|
|
@ -25,14 +25,23 @@
|
|||
повторное применение её не задваивает, и настоящий запрос сводки
|
||||
`_STALE_SOURCES_SQL` видит эту строку и отдаёт витрину просроченной.
|
||||
|
||||
ЖИВОЙ ТЕСТ НЕ УДАЛЯЕТ И НЕ ПРАВИТ НИЧЕГО ЧУЖОГО: он применяет те же идемпотентные
|
||||
файлы, что применяет деплой (CREATE TABLE IF NOT EXISTS + ON CONFLICT DO NOTHING).
|
||||
В CI он ИДЁТ — ci-tradein.yml поднимает свой Postgres и кладёт DSN в DATABASE_URL;
|
||||
на машине без базы само-скипается (запись в tests/skip_allowlist.txt). Поднять
|
||||
локально:
|
||||
ЖИВОЙ ТЕСТ НЕ УДАЛЯЕТ И НЕ ПРАВИТ НИЧЕГО ЧУЖОГО: он применяет ту же идемпотентную
|
||||
миграцию, что применяет деплой (ON CONFLICT DO NOTHING). В CI он ИДЁТ —
|
||||
ci-tradein.yml поднимает свой Postgres и кладёт DSN в DATABASE_URL; на машине без
|
||||
базы само-скипается (запись в tests/skip_allowlist.txt).
|
||||
|
||||
docker exec tradein-postgres psql -U tradein -c 'CREATE DATABASE t3469'
|
||||
DATABASE_URL="postgresql+psycopg://tradein:tradein@127.0.0.1:5433/t3469" \
|
||||
ГОНЯТЬ ЕГО НАДО НА ПОЛНОЙ СХЕМЕ, А НЕ НА ПУСТОЙ БАЗЕ. На чистой базе он был
|
||||
зелёным и при этом падал в CI: повтор `015_scrape_runs.sql` (его комментарий к
|
||||
колонке, снесённой миграцией 214) на полной схеме валится, а на пустой — нет.
|
||||
Поэтому зависимости применяются только когда таблицы ещё нет, а проверять надо
|
||||
тем же путём, каким гоняет CI:
|
||||
|
||||
docker exec tradein-postgres psql -U tradein -d postgres -c 'CREATE DATABASE t3469full'
|
||||
docker exec -i tradein-postgres psql -U tradein -d t3469full -c \\
|
||||
'CREATE EXTENSION postgis; CREATE EXTENSION pg_trgm; CREATE ROLE gendesign_reader;'
|
||||
for f in $(ls -1 data/sql/*.sql | sort); do docker exec -i tradein-postgres \\
|
||||
psql -U tradein -d t3469full -v ON_ERROR_STOP=on -q < "$f"; done
|
||||
DATABASE_URL="postgresql+psycopg://tradein:tradein@127.0.0.1:5433/t3469full" \\
|
||||
uv run python -m pytest tests/test_3469_showcase_schedule.py -q
|
||||
"""
|
||||
|
||||
|
|
@ -51,7 +60,7 @@ os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:
|
|||
|
||||
from scraper_kit.orchestration import scheduler as sched
|
||||
|
||||
from app.services.product_handlers import build_product_handlers
|
||||
from app.services.product_handlers import _job_landing_showcase_deals, build_product_handlers
|
||||
|
||||
SOURCE = "landing_showcase_deals"
|
||||
|
||||
|
|
@ -92,6 +101,13 @@ def test_handler_resolves_for_scheduler() -> None:
|
|||
registry = build_product_handlers(ctx=None) # type: ignore[arg-type]
|
||||
handler = sched.resolve_handler(SOURCE, registry)
|
||||
assert handler is not None, f"{SOURCE} не резолвится реестром — задача невидима"
|
||||
# СРАВНИВАЕМ САМ JOB, А НЕ `log_name`: имя — второй литерал конструктора
|
||||
# Handler, и правильный ключ с чужим телом (`_job_landing_stats` под ключом
|
||||
# витрины) проходил проверку по имени насквозь. Резолв ведёт к пересчёту
|
||||
# витрины или не ведёт — это свойство функции, а не подписи в логе.
|
||||
assert handler.job is _job_landing_showcase_deals, (
|
||||
f"под ключом {SOURCE} стоит чужой job: {handler.job.__name__}"
|
||||
)
|
||||
assert handler.log_name == SOURCE
|
||||
|
||||
|
||||
|
|
@ -250,14 +266,29 @@ def test_live_migration_puts_showcase_into_schedules_and_digest() -> None:
|
|||
db = _live_session()
|
||||
assert db is not None
|
||||
try:
|
||||
for dep in _DEPS:
|
||||
_apply(db, dep)
|
||||
# Зависимости — ТОЛЬКО на пустой базе. На базе, прошедшей всю цепочку
|
||||
# (CI и прод), повтор 015 падает: `CREATE TABLE IF NOT EXISTS` — no-op,
|
||||
# а `COMMENT ON COLUMN scrape_runs.returning_count` внизу того же файла
|
||||
# обращается к колонке, которую снесла 214. Файл идемпотентен
|
||||
# относительно себя, но не относительно схемы, прошедшей 214, — и
|
||||
# прогон на чистой базе этого не видит по построению.
|
||||
if db.execute(text("SELECT to_regclass('public.scrape_schedules')")).scalar() is None:
|
||||
for dep in _DEPS:
|
||||
_apply(db, dep)
|
||||
_apply(db, _MIGRATION)
|
||||
# ИДЕМПОТЕНТНОСТЬ МЕРЯЕТСЯ ПО СОСТОЯНИЮ СТРОКИ, А НЕ ПО ЧИСЛУ СТРОК.
|
||||
# «DELETE + INSERT» тоже оставляет ровно одну строку, но на КАЖДОМ
|
||||
# деплое стирает last_run_at/next_run_at и взводит расписание заново —
|
||||
# счёт строк такую замену не отличает, а created_at отличает.
|
||||
first_created_at = db.execute(
|
||||
text("SELECT created_at FROM scrape_schedules WHERE source = :s"), {"s": SOURCE}
|
||||
).scalar()
|
||||
_apply(db, _MIGRATION)
|
||||
_apply(db, _MIGRATION) # идемпотентность: второй прогон не задваивает
|
||||
|
||||
rows = db.execute(
|
||||
text(
|
||||
"SELECT enabled, window_start_hour, window_end_hour, created_at, "
|
||||
" (next_run_at > now()) AS next_run_ahead, "
|
||||
" default_params->>'interval_days' AS interval_days "
|
||||
"FROM scrape_schedules WHERE source = :s"
|
||||
),
|
||||
|
|
@ -265,9 +296,16 @@ def test_live_migration_puts_showcase_into_schedules_and_digest() -> None:
|
|||
).fetchall()
|
||||
assert len(rows) == 1, f"ожидалась одна строка расписания, получено {len(rows)}"
|
||||
row = rows[0]
|
||||
assert row.created_at == first_created_at, (
|
||||
"повторное применение пересоздало строку расписания — на каждом деплое "
|
||||
"это стирало бы состояние прогонов (last_run_at/next_run_at)"
|
||||
)
|
||||
assert row.enabled is True
|
||||
assert (row.window_start_hour, row.window_end_hour) == (6, 7)
|
||||
assert int(row.interval_days) == _seeded_interval_days()
|
||||
# next_run_at в БУДУЩЕМ: сев расписания не должен выстреливать прогоном
|
||||
# в момент деплоя (образец — 162/275). Стоит в приёмке, значит и здесь.
|
||||
assert row.next_run_ahead is True, "next_run_at в прошлом — прогон стартует на деплое"
|
||||
|
||||
# Сводка: прогонов у витрины нет, возраст считается от created_at строки.
|
||||
digest_rows = list(db.execute(sched._STALE_SOURCES_SQL).fetchall())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue