feat(5.19): auto-send toggle per cont + tinere manuala randuri (held)
Comutator accounts.auto_send_enabled per cont: Auto OFF (default) tine randurile
la ingestie (submissions.held=1), worker-ul (claim_one AND held=0) le sare pana la
eliberare umana (per rand/bulk/auto-release OFF->ON). Snapshot held prin chokepoint
unic held_for_account pe toate caile de ingestie (API, import, reresolve, reactivare).
- schema/migrare: coloana held + index partial idx_submissions_held; auto_send_enabled
- API: echo onest held+motiv (US-010), ruta /prezentari/{id}/trimite-acum
- web: toggle header, modal confirmare tipata, buton Trimite per rand + Trimite toate,
banner coada tinuta imbatranita (L.142), contor "In asteptare (manual)"
- worker: expire_held (US-008, inchide gaura retentie PII), metrics held gauges
- ops: tools/carantina_held + runbook rollback (R4)
Nota review (/code-review high): re-snapshot held lipseste pe caile repune/corectie
(requeue_submission, post_corectie, bulk-fix) — de aliniat separat cu create_prezentari.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
204
tests/test_worker_held.py
Normal file
204
tests/test_worker_held.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""Teste PRD 5.19 US-003 + US-008 — worker respecta flag `held`.
|
||||
|
||||
US-003: claim_one sare peste randurile `queued AND held=1` (tinute manual).
|
||||
US-008: expire_held expira randurile tinute mai vechi de `held_retention_days`
|
||||
(default 90) -> status 'error', rar_error='TINUT_EXPIRAT', purge_after
|
||||
DIRECT la momentul expirarii (NU +30z retentie blocati -> nu 120z total),
|
||||
apoi purge_expired le sterge (PII purjat).
|
||||
|
||||
TDD: testele se scriu INAINTE de modificarea worker-ului.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# --- Fixture DB (pattern din test_worker_active_gate.py) ---
|
||||
|
||||
@pytest.fixture()
|
||||
def env(monkeypatch):
|
||||
tmp = tempfile.mkdtemp()
|
||||
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "t.db"))
|
||||
from app.config import get_settings
|
||||
get_settings.cache_clear()
|
||||
from app.db import get_connection, init_db
|
||||
init_db()
|
||||
conn = get_connection()
|
||||
yield conn, get_settings()
|
||||
conn.close()
|
||||
get_settings.cache_clear()
|
||||
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
_CONTENT = {
|
||||
"vin": "WVWZZZ1KZAW000123", "nr_inmatriculare": "B999TST",
|
||||
"data_prestatie": "2026-06-15", "odometru_final": "123456",
|
||||
"prestatii": [{"cod_prestatie": "OE-1"}], "sistem_reparat": "null",
|
||||
}
|
||||
|
||||
|
||||
def _insert(conn, account_id=None, status="queued", held=0, content=None, created_at=None):
|
||||
content = content or _CONTENT
|
||||
cur = conn.execute(
|
||||
"INSERT INTO submissions (idempotency_key, status, payload_json, account_id, held) "
|
||||
"VALUES (?, ?, ?, ?, ?)",
|
||||
(f"key-{os.urandom(4).hex()}", status, json.dumps(content), account_id, held),
|
||||
)
|
||||
sid = int(cur.lastrowid)
|
||||
if created_at is not None:
|
||||
conn.execute("UPDATE submissions SET created_at=? WHERE id=?", (created_at, sid))
|
||||
return sid
|
||||
|
||||
|
||||
def _row(conn, sid):
|
||||
return conn.execute("SELECT * FROM submissions WHERE id=?", (sid,)).fetchone()
|
||||
|
||||
|
||||
def _row_status(conn, sid):
|
||||
r = _row(conn, sid)
|
||||
return r["status"] if r else None
|
||||
|
||||
|
||||
# --- US-003: claim_one sare peste randurile tinute ---
|
||||
|
||||
def test_claim_sare_rand_tinut(env):
|
||||
"""queued, held=1, cont activ, send on -> claim_one intoarce None; ramane queued."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import claim_one
|
||||
|
||||
conn, _ = env
|
||||
acct_id = create_account(conn, "Service Tinut", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=1)
|
||||
|
||||
result = claim_one(conn)
|
||||
|
||||
assert result is None, "claim_one trebuia sa sara peste randul tinut (held=1)"
|
||||
assert _row_status(conn, sid) == "queued", "randul tinut trebuia sa ramana queued"
|
||||
|
||||
|
||||
def test_claim_ia_rand_neted(env):
|
||||
"""Acelasi rand cu held=0 -> claim_one il ia si il marcheaza sending."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import claim_one
|
||||
|
||||
conn, _ = env
|
||||
acct_id = create_account(conn, "Service Liber", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=0)
|
||||
|
||||
result = claim_one(conn)
|
||||
|
||||
assert result is not None, "claim_one trebuia sa ridice randul held=0"
|
||||
assert result["id"] == sid
|
||||
assert _row_status(conn, sid) == "sending"
|
||||
|
||||
|
||||
def test_eliberarea_deblocheaza_claim(env):
|
||||
"""held=1 -> claim_one None; dupa held=0 -> claim_one il ridica."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import claim_one
|
||||
|
||||
conn, _ = env
|
||||
acct_id = create_account(conn, "Service Eliberat", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=1)
|
||||
|
||||
assert claim_one(conn) is None
|
||||
assert _row_status(conn, sid) == "queued"
|
||||
|
||||
conn.execute("UPDATE submissions SET held=0 WHERE id=?", (sid,))
|
||||
result = claim_one(conn)
|
||||
assert result is not None and result["id"] == sid
|
||||
assert _row_status(conn, sid) == "sending"
|
||||
|
||||
|
||||
# --- US-008: expirare randuri tinute ---
|
||||
|
||||
def test_expire_held_expira_rand_vechi(env):
|
||||
"""queued, held=1, created_at vechi (>90z) -> error + TINUT_EXPIRAT + purge_after ~now."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import expire_held
|
||||
|
||||
conn, settings = env
|
||||
acct_id = create_account(conn, "Service Vechi", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=1,
|
||||
created_at="datetime('now','-100 days')")
|
||||
# created_at trebuie sa fie o valoare literala, nu expresie SQL text -> setam explicit.
|
||||
conn.execute("UPDATE submissions SET created_at=datetime('now','-100 days') WHERE id=?", (sid,))
|
||||
|
||||
n = expire_held(conn, settings)
|
||||
|
||||
assert n == 1, "expire_held trebuia sa expire exact 1 rand"
|
||||
row = _row(conn, sid)
|
||||
assert row["status"] == "error"
|
||||
assert row["rar_error"] == "TINUT_EXPIRAT"
|
||||
assert row["purge_after"] is not None
|
||||
# purge_after ~ now (NU +30z): verificam ca e <= acum + o mica marja (sub 1 zi).
|
||||
aproape_acum = conn.execute(
|
||||
"SELECT purge_after <= datetime('now', '+1 day') AS ok FROM submissions WHERE id=?",
|
||||
(sid,),
|
||||
).fetchone()["ok"]
|
||||
assert aproape_acum == 1, "purge_after trebuia setat ~now, nu +30z"
|
||||
assert row["sending_since"] is None
|
||||
|
||||
|
||||
def test_expire_held_ignora_rand_recent(env):
|
||||
"""queued, held=1 recent (created_at now) -> neatins de expire_held."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import expire_held
|
||||
|
||||
conn, settings = env
|
||||
acct_id = create_account(conn, "Service Recent", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=1)
|
||||
|
||||
n = expire_held(conn, settings)
|
||||
|
||||
assert n == 0
|
||||
assert _row_status(conn, sid) == "queued"
|
||||
|
||||
|
||||
def test_expire_held_ignora_rand_neted_vechi(env):
|
||||
"""queued, held=0, vechi -> NU e tinut -> neatins de expire_held."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import expire_held
|
||||
|
||||
conn, settings = env
|
||||
acct_id = create_account(conn, "Service NetedVechi", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=0)
|
||||
conn.execute("UPDATE submissions SET created_at=datetime('now','-100 days') WHERE id=?", (sid,))
|
||||
|
||||
n = expire_held(conn, settings)
|
||||
|
||||
assert n == 0
|
||||
assert _row_status(conn, sid) == "queued"
|
||||
|
||||
|
||||
def test_expire_held_apoi_purge_sterge_randul(env):
|
||||
"""Lant complet: expire_held (purge_after ~now) -> purge_expired sterge randul (PII purjat)."""
|
||||
from app.accounts import create_account
|
||||
from app.worker.__main__ import expire_held, purge_expired
|
||||
|
||||
conn, settings = env
|
||||
acct_id = create_account(conn, "Service Purjat", active=True)
|
||||
sid = _insert(conn, account_id=acct_id, held=1)
|
||||
conn.execute("UPDATE submissions SET created_at=datetime('now','-100 days') WHERE id=?", (sid,))
|
||||
|
||||
assert expire_held(conn, settings) == 1
|
||||
# purge_after=datetime('now') (moment expirarii). La URMATORUL ciclu de purjare
|
||||
# (o data pe ora) purge_after < now -> se sterge. Simulam trecerea timpului
|
||||
# (>= 1s) verificand ca purge_after e deja in trecutul acelui ciclu ulterior.
|
||||
assert conn.execute(
|
||||
"SELECT purge_after < datetime('now','+1 second') AS ok FROM submissions WHERE id=?",
|
||||
(sid,),
|
||||
).fetchone()["ok"] == 1
|
||||
conn.execute(
|
||||
"UPDATE submissions SET purge_after=datetime('now','-1 second') WHERE id=?", (sid,)
|
||||
)
|
||||
stats = purge_expired(conn)
|
||||
|
||||
assert stats["submissions_purged"] >= 1
|
||||
assert _row(conn, sid) is None, "randul expirat trebuia sters de purge_expired la ciclul ulterior"
|
||||
Reference in New Issue
Block a user