git-subtree-dir: fiscal git-subtree-mainline:d95165fc8fgit-subtree-split:102ba84500
138 lines
7.9 KiB
Python
138 lines
7.9 KiB
Python
"""E2E of the enrichment across REAL HTTP: the real Agent worker (real Firebird COPY) <-> the real Hub container (dev PostgreSQL, real datasets, real providers).
|
|
|
|
docker compose up -d --build fiscal-hub # Hub at http://localhost:8000
|
|
python scripts/enrichment_local_hub_e2e.py
|
|
|
|
Registers a throw-away dev Agent (AGENT_DEV_TOKEN), flips its per-installation features straight in the dev database (the panel route is covered by tests), seeds the KB of the dev Hub with a VERIFIED NCM for
|
|
one test GTIN (two independent provider families), then walks: DISABLED -> DRY_RUN -> ENABLED -> DISABLED, an unknown GTIN (real providers, internet) and an invalid GTIN (no provider call).
|
|
Writes go ONLY to scratch_rtc_e2e/TESTESOFICIAIS_RTC_COPY.FDB (guarded by rtc_copy_write_e2e.guard). The dev database receives test rows only.
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts")); sys.path.insert(0, str(ROOT / "agent" / "src"))
|
|
from dotenv import load_dotenv # noqa: E402
|
|
|
|
load_dotenv(ROOT / ".env")
|
|
import enrichment_copy_e2e as ce # noqa: E402 (also patches base.connect with the brief retry)
|
|
from gti_fiscal_agent import enrichment as E # noqa: E402
|
|
from gti_fiscal_agent.client import HubClient # noqa: E402
|
|
|
|
base = ce.base
|
|
HUB = "http://127.0.0.1:8000" # NOT "localhost": on Windows it resolves ::1 first and adds ~500 ms per request
|
|
TOKEN = os.environ["AGENT_DEV_TOKEN"]
|
|
CONTAINER = "projeto-postgres-1"
|
|
|
|
|
|
def sql(query):
|
|
done = subprocess.run(["docker", "exec", CONTAINER, "psql", "-U", "gti", "-d", "gti_fiscal", "-At", "-c", query], capture_output=True, text=True, timeout=60)
|
|
if done.returncode != 0:
|
|
raise RuntimeError(done.stderr[-300:])
|
|
return done.stdout.strip()
|
|
|
|
|
|
def seed_kb(gtin14, description, ncm):
|
|
code = f'''
|
|
from central.app.db import SessionLocal
|
|
from central.app.enrichment import kb
|
|
with SessionLocal() as s:
|
|
p = kb.upsert_product(s, "{gtin14}", "{description}")
|
|
kb.add_evidence(s, p, "ncm", "{ncm}", "provider:cosmos", family="PROVIDER:cosmos")
|
|
kb.add_evidence(s, p, "ncm", "{ncm}", "provider:other", family="PROVIDER:other")
|
|
s.commit()
|
|
print("seeded")
|
|
'''
|
|
done = subprocess.run(["docker", "exec", "-i", "projeto-fiscal-hub-1", "python", "-"], input=code, capture_output=True, text=True, timeout=120, cwd=str(ROOT))
|
|
assert "seeded" in done.stdout, done.stderr[-400:]
|
|
|
|
|
|
def set_features(installation_id, mode, corrector=True):
|
|
sql(f"UPDATE installations SET enrichment_mode='{mode}', corrector_enabled={'true' if corrector else 'false'}, features_configured=true WHERE installation_id='{installation_id}'")
|
|
|
|
|
|
def main():
|
|
checks = {}
|
|
base.restore_copy()
|
|
client = HubClient(HUB, TOKEN)
|
|
try:
|
|
installation_id = client.identity().get("installation_id") or str(uuid.uuid4()) # the dev Agent may already own an installation
|
|
except Exception: # noqa: BLE001
|
|
installation_id = str(uuid.uuid4())
|
|
client.heartbeat_metadata({"installation_id": installation_id, "status": "IDLE", "agent_version": "1.3.0", "display_name": "E2E-ENRICH", "capabilities": {"PRODUCT_ENRICHMENT_WRITE_V1": True}})
|
|
set_features(installation_id, "DISABLED") # start every run from the default
|
|
checks["REGISTERED_DEFAULT_DISABLED"] = client.authorizations()["features"]["enrichment_mode"] == "DISABLED"
|
|
gtin_ok = ce.gtin("789500000001")
|
|
seed_kb("0" + gtin_ok, "REFRIGERANTE COLA LATA 350ML", "22021000")
|
|
for suffix in ("", "-wal", "-shm"):
|
|
try:
|
|
(ROOT / "scratch_enrich" / f"enrich_hub_e2e.db{suffix}").unlink()
|
|
except FileNotFoundError:
|
|
pass
|
|
store = E.EnrichmentStore(ROOT / "scratch_enrich" / "enrich_hub_e2e.db")
|
|
worker = E.EnrichmentWorker(hub=client, store=store, read_conn=lambda: base.connect(False), write_conn=lambda: base.connect(True), create_reprocess=lambda codes: client.create_job("REPROCESS_PRODUCTS", {"codigos": list(codes), "reason": "ENRICHMENT"}))
|
|
|
|
class Stop:
|
|
stop = False
|
|
stopper = Stop()
|
|
threading.Thread(target=worker.run_forever, args=(stopper,), daemon=True).start()
|
|
|
|
def refresh():
|
|
worker._features_at = -999
|
|
worker._auth_cache = (None, 0.0)
|
|
|
|
time.sleep(4)
|
|
checks["DISABLED_NO_ACTIVITY"] = store.meta("baseline_done") is None
|
|
set_features(installation_id, "DRY_RUN"); refresh(); time.sleep(6)
|
|
checks["BASELINE_ON_ACTIVATION"] = store.meta("baseline_done") == "1"
|
|
ce.insert_product("HD001", "REFRIGERANTE COLA LATA 350ML", gtin_ok); time.sleep(8)
|
|
checks["DRY_RUN_ZERO_WRITES"] = ce.row("HD001") == (None, None, None)
|
|
audits = int(sql(f"SELECT count(*) FROM product_field_audits a JOIN installations i ON i.id=a.installation_id WHERE i.installation_id='{installation_id}' AND a.outcome='DRY_RUN' AND a.product_code='HD001'"))
|
|
checks["DRY_RUN_AUDITED_IN_HUB"] = audits >= 1
|
|
set_features(installation_id, "ENABLED"); refresh()
|
|
ce.insert_product("HE001", "REFRIGERANTE COLA LATA 350ML", gtin_ok)
|
|
t0 = time.monotonic()
|
|
while time.monotonic() - t0 < 25 and ce.row("HE001")[0] is None:
|
|
time.sleep(0.2)
|
|
latency = round(time.monotonic() - t0, 2)
|
|
r = ce.row("HE001")
|
|
checks["ENABLED_NCM_WRITTEN"] = r[0] == "22021000"
|
|
time.sleep(3) # the Agent reports the audit right after the write
|
|
applied = sql(f"SELECT field||':'||coalesce(new_value,'') FROM product_field_audits a JOIN installations i ON i.id=a.installation_id WHERE i.installation_id='{installation_id}' AND a.outcome='APPLIED' AND a.product_code='HE001' ORDER BY a.id")
|
|
checks["WRITTEN_FIELDS_MATCH_HUB_AUDIT"] = ("CLASSIFICACAO_FISCAL:22021000" in applied) and all(v in applied for v in ([f"CEST:{r[1]}"] if r[1] else []) + ([f"IPI:{r[2]}"] if r[2] not in (None,) else []))
|
|
time.sleep(6)
|
|
hd = ce.row("HD001")
|
|
checks["EARLIER_DRY_RUN_PRODUCT_STILL_UNTOUCHED"] = hd == (None, None, None)
|
|
# unknown GTIN goes to the REAL providers (internet); nothing may be written, a human blocker must appear
|
|
unknown = ce.gtin("789600000009")
|
|
ce.insert_product("HE002", "PRODUTO INEXISTENTE ESPECIAL XYZ 123", unknown); time.sleep(14)
|
|
checks["UNKNOWN_GTIN_NO_WRITE"] = ce.row("HE002") == (None, None, None)
|
|
blockers = sql("SELECT reason_code FROM blocked_products WHERE field='ENRICHMENT' AND product_code='HE002'")
|
|
checks["UNKNOWN_GTIN_BLOCKER_PT"] = blockers in ("NOT_FOUND", "SINGLE_WEAK_SOURCE", "PENDING_PROVIDER_QUOTA", "PENDING_PROVIDERS_OFFLINE") or blockers.startswith("NOT_FOUND")
|
|
bad = unknown[:-1] + str((int(unknown[-1]) + 1) % 10)
|
|
before_req = sql("SELECT coalesce(sum(requests),0) FROM provider_usage_daily")
|
|
ce.insert_product("HE003", "PRODUTO GTIN INVALIDO", bad); time.sleep(8)
|
|
checks["INVALID_GTIN_NO_PROVIDER_CALL"] = sql("SELECT coalesce(sum(requests),0) FROM provider_usage_daily") == before_req and ce.row("HE003") == (None, None, None)
|
|
# feature switched off centrally: the next new product is not touched
|
|
set_features(installation_id, "DISABLED"); refresh(); time.sleep(4)
|
|
ce.insert_product("HE004", "REFRIGERANTE COLA LATA 350ML", gtin_ok); time.sleep(8)
|
|
checks["DISABLED_AGAIN_ZERO_WRITES"] = ce.row("HE004") == (None, None, None)
|
|
# corrector off + enrichment on: independent switches
|
|
set_features(installation_id, "ENABLED", corrector=False); refresh(); time.sleep(3)
|
|
checks["CORRECTOR_OFF_NO_JOBS"] = client.next_job() is None
|
|
stopper.stop = True
|
|
result = {"copy_only": True, "hub": HUB, "detect_to_written_seconds_enabled": latency, "checks": checks, "ALL_PASS": all(checks.values())}
|
|
(ROOT / "docs" / "runs" / "enrichment_local_hub_e2e.json").write_text(json.dumps(result, indent=1), encoding="utf-8")
|
|
print(json.dumps(result, indent=1))
|
|
sys.exit(0 if result["ALL_PASS"] else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|