From af2b540ac2368848c45f98236bdcd3fc041d1530 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sat, 6 Jun 2026 22:00:42 +0300 Subject: [PATCH] =?UTF-8?q?fix(tradein):=20migration=20100=20=E2=80=94=20e?= =?UTF-8?q?nable=20deactivate=5Fstale=5Favito=20schedule=20on=20prod=20(#7?= =?UTF-8?q?59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: migration 090 used ON CONFLICT (source) DO NOTHING; prod already had a row source='deactivate_stale_avito' with enabled=false (inserted before 090 ran), so the INSERT was silently skipped. The scheduler's WHERE enabled=true filter meant the task never fired (last_run_id=NULL, next_run_at=2026-06-01 expired). Fix: add migration 100 — idempotent UPDATE SET enabled=true, next_run_at=NOW() WHERE source='deactivate_stale_avito' AND enabled=false. Safe to re-run: 0 rows matched once the row is already enabled. Auto-applied on next deploy via _schema_migrations tracking in deploy-tradein.yml. Migration 090 seed (enabled=true + ON CONFLICT DO NOTHING) is already correct for fresh installs — no change needed there. Tests: 7 new assertions in test_deactivate_stale_avito.py for migration 100 (exists, UPDATE source, SET enabled=true, idempotent guard, BEGIN/COMMIT, no psycopg trap, next_run_at reset). 39 tests pass total. Refs #759 --- .../sql/100_enable_deactivate_stale_avito.sql | 19 +++++++ .../tests/test_deactivate_stale_avito.py | 50 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 tradein-mvp/backend/data/sql/100_enable_deactivate_stale_avito.sql diff --git a/tradein-mvp/backend/data/sql/100_enable_deactivate_stale_avito.sql b/tradein-mvp/backend/data/sql/100_enable_deactivate_stale_avito.sql new file mode 100644 index 00000000..989b0044 --- /dev/null +++ b/tradein-mvp/backend/data/sql/100_enable_deactivate_stale_avito.sql @@ -0,0 +1,19 @@ +-- #759: deactivate_stale_avito schedule seeded enabled=false → task never ran on prod. +-- +-- Root cause: migration 090 (ON CONFLICT (source) DO NOTHING) was a no-op because +-- the row 'deactivate_stale_avito' already existed in scrape_schedules with +-- enabled=false when 090 ran on prod (inserted by a pre-release iteration or manual +-- seed). The scheduler's WHERE enabled=true filter excluded it on every tick, so +-- last_run_id stayed NULL and next_run_at=2026-06-01 expired without any runs. +-- +-- Fix: idempotent UPDATE that only fires when the row is still disabled. +-- Re-running this migration after the row is already enabled → 0 rows matched, safe. +-- +-- No bind params → no CAST(:x AS type) needed; BEGIN/COMMIT per sql.md conventions. +BEGIN; +UPDATE scrape_schedules + SET enabled = true, + next_run_at = NOW() + WHERE source = 'deactivate_stale_avito' + AND enabled = false; +COMMIT; diff --git a/tradein-mvp/backend/tests/test_deactivate_stale_avito.py b/tradein-mvp/backend/tests/test_deactivate_stale_avito.py index aed99581..ee1da4ab 100644 --- a/tradein-mvp/backend/tests/test_deactivate_stale_avito.py +++ b/tradein-mvp/backend/tests/test_deactivate_stale_avito.py @@ -252,3 +252,53 @@ def test_failure_path_rollback_and_mark_failed(monkeypatch: pytest.MonkeyPatch) assert db.rolled_back is True assert failed["run_id"] == 7 + + +# ── Migration 100: prod hot-fix — flip enabled=false → true ────────────────── + +_MIGRATION_100 = _SQL_DIR / "100_enable_deactivate_stale_avito.sql" + + +def test_migration_100_exists() -> None: + """Prod hot-fix migration must be present so deploy auto-applies it.""" + assert _MIGRATION_100.is_file(), f"missing migration: {_MIGRATION_100}" + + +def test_migration_100_updates_correct_source() -> None: + sql = _MIGRATION_100.read_text("utf-8") + assert "'deactivate_stale_avito'" in sql + + +def test_migration_100_sets_enabled_true() -> None: + sql = _MIGRATION_100.read_text("utf-8") + # UPDATE … SET enabled = true + assert re.search( + r"enabled\s*=\s*true", sql, re.IGNORECASE + ), "migration 100 must SET enabled = true" + + +def test_migration_100_is_idempotent() -> None: + """WHERE enabled = false ensures re-running after fix → 0 rows matched.""" + sql = _MIGRATION_100.read_text("utf-8") + assert re.search( + r"enabled\s*=\s*false", sql, re.IGNORECASE + ), "migration 100 must guard with AND enabled = false for idempotency" + + +def test_migration_100_is_transactional() -> None: + sql = _MIGRATION_100.read_text("utf-8") + assert "BEGIN;" in sql + assert "COMMIT;" in sql + + +def test_migration_100_no_psycopg_trap() -> None: + """Plain DML — no bind params — guard :x::type trap anyway.""" + sql = _MIGRATION_100.read_text("utf-8") + assert not re.search(r":\w+::", sql) + + +def test_migration_100_also_resets_next_run_at() -> None: + """next_run_at = NOW() ensures the scheduler fires on the next tick.""" + sql = _MIGRATION_100.read_text("utf-8") + assert "next_run_at" in sql + assert "NOW()" in sql