feat(5.15+5.14): CLOSE — fix-uri code-review + embeddings functional

5.15 (propagare design + dashboard editare) si 5.14 (mapare LLM distilata)
inchise dupa /code-review high. 8 buguri reparate TDD:

- HIGH modal nu se deschidea pe randul slim (base.html: trimitere-slim)
- HIGH /repune trunchia prestatii (declaratie incompleta la RAR) -> iterare
  peste existing, codes pozitional
- HIGH embeddings incarca model ~230MB degeaba pe corpus gol -> poarta has_corpus()
- HIGH picker chips gol pe re-render eroare -> conn/account_id pe toate ramurile
- MED obs re-derivat dupa stergere explicita -> _merge_override pastreaza obs=''
- MED mapare salvata fara denumire poluă GOLD -> _record_gold_validation guard
- MED typo nome_prestatie -> nume_prestatie in select /repune
- MED bucketare timp +3h gresita iarna -> SQLite localtime + TZ=Europe/Bucharest

Embeddings WIRE-uit functional (PRD #15, decizie user): ensure_embeddings_corpus
construieste corpus din nomenclator, gated pe AUTOPASS_EMBEDDINGS_ENABLED (default
off). Marime model corectata ~50MB->~230MB (estimare PRD gresita).

Cleanup: hoist load_* din bucla bulk-fix; import re la top.
Regresie: 1256 passed, 1 deselected (live), 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-28 20:48:34 +00:00
parent 9e42e7ed6f
commit 3fc53534e2
53 changed files with 9684 additions and 384 deletions

152
tests/test_device_mix.py Normal file
View File

@@ -0,0 +1,152 @@
"""Teste US-012 (PRD 5.15): Analytics device-mix — validare premisa mobil, fara PII.
TDD: RED inainte de implementare.
Semnal: la acces dashboard -> eveniment 'device_mix' in app_events cu cod 'desktop'/'mobil'.
Zero PII: nu se stocheaza UA brut, IP sau VIN.
"""
from __future__ import annotations
import json
import os
import tempfile
import pytest
from fastapi.testclient import TestClient
# --------------------------------------------------------------------------- #
# Fixture #
# --------------------------------------------------------------------------- #
@pytest.fixture()
def client(monkeypatch):
tmp = tempfile.mkdtemp()
monkeypatch.setenv("AUTOPASS_DB_PATH", os.path.join(tmp, "device_mix.db"))
monkeypatch.setenv("AUTOPASS_LOG_DIR", os.path.join(tmp, "logs"))
monkeypatch.setenv("AUTOPASS_WEB_AUTH_REQUIRED", "false")
from app.config import get_settings
get_settings.cache_clear()
from app.main import app
with TestClient(app) as c:
yield c
get_settings.cache_clear()
def _events_device_mix():
from app.db import get_connection
conn = get_connection()
try:
return conn.execute(
"SELECT * FROM app_events WHERE tip='device_mix' ORDER BY id"
).fetchall()
finally:
conn.close()
# --------------------------------------------------------------------------- #
# test_device_mix_inregistrat #
# --------------------------------------------------------------------------- #
UA_DESKTOP = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0 Safari/537.36"
)
UA_MOBIL_ANDROID = (
"Mozilla/5.0 (Linux; Android 13; Pixel 7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/125.0 Mobile Safari/537.36"
)
UA_MOBIL_IPHONE = (
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
"AppleWebKit/605.1.15 (KHTML, like Gecko) "
"Version/17.0 Mobile/15E148 Safari/604.1"
)
def test_device_mix_inregistrat_desktop(client):
"""Acces dashboard cu UA desktop -> eveniment device_mix cod='desktop'."""
r = client.get("/", headers={"User-Agent": UA_DESKTOP})
assert r.status_code == 200
events = _events_device_mix()
assert len(events) >= 1, "Trebuie cel putin un eveniment device_mix dupa acces dashboard"
# ultimul eveniment clasificat ca desktop
ev = events[-1]
assert ev["tip"] == "device_mix"
assert ev["cod"] == "desktop", f"Clasificare gresita: {ev['cod']!r}"
def test_device_mix_inregistrat_mobil_android(client):
"""Acces dashboard cu UA Android Mobile -> eveniment device_mix cod='mobil'."""
r = client.get("/", headers={"User-Agent": UA_MOBIL_ANDROID})
assert r.status_code == 200
events = _events_device_mix()
assert len(events) >= 1
ev = events[-1]
assert ev["tip"] == "device_mix"
assert ev["cod"] == "mobil", f"Clasificare gresita Android: {ev['cod']!r}"
def test_device_mix_inregistrat_mobil_iphone(client):
"""Acces dashboard cu UA iPhone -> eveniment device_mix cod='mobil'."""
r = client.get("/", headers={"User-Agent": UA_MOBIL_IPHONE})
assert r.status_code == 200
events = _events_device_mix()
assert len(events) >= 1
ev = events[-1]
assert ev["tip"] == "device_mix"
assert ev["cod"] == "mobil", f"Clasificare gresita iPhone: {ev['cod']!r}"
# --------------------------------------------------------------------------- #
# test_device_mix_fara_pii #
# --------------------------------------------------------------------------- #
def test_device_mix_fara_pii(client):
"""Evenimentul device_mix nu contine UA brut, IP sau alte PII."""
r = client.get("/", headers={"User-Agent": UA_MOBIL_ANDROID})
assert r.status_code == 200
events = _events_device_mix()
assert len(events) >= 1
ev = events[-1]
# Campul mesaj: doar eticheta grosiera, nu UA brut
mesaj = ev["mesaj"] or ""
assert UA_MOBIL_ANDROID not in mesaj, "UA brut nu trebuie stocat in mesaj"
assert "Android" not in mesaj, "Fragment UA nu trebuie stocat in mesaj"
assert "Mozilla" not in mesaj, "Fragment UA nu trebuie stocat in mesaj"
# context_json: daca exista, nu contine UA brut / IP
ctx_raw = ev["context_json"]
if ctx_raw:
ctx = json.loads(ctx_raw)
ctx_str = json.dumps(ctx)
assert UA_MOBIL_ANDROID not in ctx_str, "UA brut nu trebuie in context_json"
assert "Mozilla" not in ctx_str, "Fragment UA nu trebuie in context_json"
# IP-uri tipice nu apar (testclient trimite 127.0.0.1/testclient)
for ip_fragment in ["127.0.0.1", "testclient", "192.168."]:
assert ip_fragment not in ctx_str, f"IP {ip_fragment!r} nu trebuie in context_json"
# codul este doar eticheta grosiera
assert ev["cod"] in ("desktop", "mobil"), f"Cod neasteptat: {ev['cod']!r}"
def test_device_mix_fara_pii_desktop(client):
"""Evenimentul device_mix pentru desktop nu contine UA brut."""
r = client.get("/", headers={"User-Agent": UA_DESKTOP})
assert r.status_code == 200
events = _events_device_mix()
assert len(events) >= 1
ev = events[-1]
mesaj = ev["mesaj"] or ""
assert UA_DESKTOP not in mesaj, "UA brut desktop nu trebuie in mesaj"
assert "Windows NT" not in mesaj, "Fragment UA nu trebuie in mesaj"
assert ev["cod"] == "desktop"