#!/usr/bin/env python3 """DiDi LOT 1 — runner automat de teste end-to-end. Rulează un test funcțional pe fiecare componentă și scrie rezultatele într-un raport JSON cu timestamp (reports/test_report_.json). Fiecare intrare conține: componentă, endpoint, http_code, ok (verdict funcțional), latență, extras din răspuns + request-ul folosit — ca dovadă vizibilă. Rulare: python3 run_tests.py (HOST_IP din .env sau 10.11.10.18) HOST_IP=1.2.3.4 python3 run_tests.py """ from __future__ import annotations import json, os, subprocess, sys, time, datetime, pathlib HERE = pathlib.Path(__file__).resolve().parent ASSETS = HERE / "test_assets" REPORTS = HERE / "reports" # --- config din .env (fallback la defaults) --------------------------------- def load_env() -> dict: env = {} f = HERE / ".env" if f.exists(): for line in f.read_text().splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) env[k.strip()] = v.strip() return env ENV = load_env() H = os.environ.get("HOST_IP") or ENV.get("HOST_IP") or "10.11.10.18" TOKEN = ENV.get("GATEWAY_API_TOKEN", "didi-local-dev-token-123") WHISPER = ENV.get("WHISPER_MODEL", "large-v3-turbo") # --- docker shim (pt pregatirea asset-urilor; merge si prin sg docker) ------- def docker(args: list[str]) -> subprocess.CompletedProcess: if subprocess.run(["docker", "ps"], capture_output=True).returncode == 0: return subprocess.run(["docker", *args], capture_output=True, text=True) return subprocess.run(["sg", "docker", "-c", "docker " + " ".join(args)], capture_output=True, text=True) def ensure_assets() -> None: ASSETS.mkdir(exist_ok=True) mp4 = ASSETS / "sample.mp4" if not mp4.exists(): import glob cands = sorted(glob.glob(str(HERE.parent.parent / "artefacte_lot1/runs_deepfake/**/*.mp4"), recursive=True), key=lambda p: os.path.getsize(p)) if cands: subprocess.run(["cp", cands[0], str(mp4)]) if not (ASSETS / "frame.jpg").exists() or not (ASSETS / "audio.wav").exists(): # genereaza un cadru + audio din containerul extractors (are ffmpeg) docker(["cp", str(mp4), "didiAI-extractors:/tmp/s.mp4"]) docker(["exec", "didiAI-extractors", "sh", "-c", "ffmpeg -y -i /tmp/s.mp4 -frames:v 1 /tmp/frame.jpg -vn -ar 16000 -ac 1 -t 8 /tmp/audio.wav"]) docker(["cp", "didiAI-extractors:/tmp/frame.jpg", str(ASSETS / "frame.jpg")]) docker(["cp", "didiAI-extractors:/tmp/audio.wav", str(ASSETS / "audio.wav")]) # --- helper request prin curl ----------------------------------------------- def run_curl(args: list[str], timeout: int) -> tuple[int, str, float]: """Return (http_code, body, latency_ms).""" full = ["curl", "-s", "-m", str(timeout), "-w", "\n%{http_code}", *args] t0 = time.monotonic() try: p = subprocess.run(full, capture_output=True, text=True, timeout=timeout + 10) out = p.stdout except subprocess.TimeoutExpired: return (0, "TIMEOUT", round((time.monotonic() - t0) * 1000, 1)) lat = round((time.monotonic() - t0) * 1000, 1) code = 0; body = out if "\n" in out: body, _, last = out.rpartition("\n") try: code = int(last.strip()) except ValueError: code = 0 return (code, body, lat) def jparse(body: str): try: return json.loads(body) except Exception: return None # --- definitia testelor ------------------------------------------------------ def t_llm(): c, b, l = run_curl([f"http://{H}:14011/v1/chat/completions", "-H", "Content-Type: application/json", "-d", json.dumps({"model":"qwen3.5","messages":[{"role":"user","content":"Ce este un deepfake? O propozitie."}],"max_tokens":120})], 90) d = jparse(b); txt = (d or {}).get("choices",[{}])[0].get("message",{}).get("content","") if d else "" return c, l, bool(txt) and not txt.lower().startswith("thinking"), {"reply": txt[:300]} def t_embeddings(): c,b,l = run_curl([f"http://{H}:14100/v1/embeddings","-H","Content-Type: application/json","-d",json.dumps({"model":"bge-m3","input":"test"})],30) d=jparse(b); dim=len((d or {}).get("data",[{}])[0].get("embedding",[])) if d else 0 return c,l,dim==1024,{"dim":dim} def t_rerank(): c,b,l=run_curl([f"http://{H}:14200/v1/rerank","-H","Content-Type: application/json","-d",json.dumps({"model":"bge-reranker-v2-m3","query":"deepfake detection","documents":["o pisica","sistem detectie deepfake video"]})],30) d=jparse(b); r=(d or {}).get("results",[]) ; top=r[0] if r else {} return c,l,top.get("index")==1,{"top_index":top.get("index"),"score":round(top.get("relevance_score",0),4)} def t_audio(): c,b,l=run_curl([f"http://{H}:54300/v1/audio/transcriptions","-F",f"file=@{ASSETS}/audio.wav","-F",f"model={WHISPER}"],120) d=jparse(b); txt=(d or {}).get("text","") if d else "" return c,l,bool(txt.strip()),{"text":txt[:200],"language":(d or {}).get("language")} def t_video(): c,b,l=run_curl([f"http://{H}:54600/analyze/video","-F",f"file=@{ASSETS}/sample.mp4"],180) d=jparse(b); v=(d or {}).get("verdict") return c,l,v in ("REAL","FAKE","UNCERTAIN"),{"verdict":v,"frames_analyzed":(d or {}).get("frames_analyzed")} def t_metadata(): c,b,l=run_curl([f"http://{H}:54400/v1/metadata","-F",f"file=@{ASSETS}/sample.mp4"],40) d=jparse(b); ok=bool((d or {}).get("analyses",{}).get("integrity",{}).get("ok")) return c,l,ok,{"media_type":(d or {}).get("media_type"),"sha256":(d or {}).get("sha256","")[:16]} def t_ner(): c,b,l=run_curl([f"http://{H}:54400/v1/ner","-H","Content-Type: application/json","-d",json.dumps({"text":"Klaus Iohannis s-a intalnit cu Emmanuel Macron la Bucuresti."})],60) d=jparse(b); ents=(d or {}).get("results",{}).get("entities",[]) return c,l,len(ents)>=2,{"entities":[e.get("text")+":"+e.get("label","") for e in ents][:5]} def t_detect(): c,b,l=run_curl([f"http://{H}:54400/v1/detect","-F",f"file=@{ASSETS}/frame.jpg"],90) d=jparse(b); det=(d or {}).get("results",{}).get("detections",[]) return c,l,len(det)>=1,{"objects":len(det),"labels":[x.get("label") for x in det][:5]} def t_ocr(): c,b,l=run_curl([f"http://{H}:54400/v1/ocr","-F",f"file=@{ASSETS}/frame.jpg"],90) d=jparse(b); ok=bool((d or {}).get("ok")); txt=(d or {}).get("results",{}).get("text","") return c,l,ok,{"text":txt[:150]} def t_sentiment(): c,b,l=run_curl([f"http://{H}:54400/v1/sentiment","-H","Content-Type: application/json","-d",json.dumps({"text":"Produsul este excelent, sunt foarte multumit!"})],60) d=jparse(b); ok=bool((d or {}).get("ok")) return c,l,ok,{"label":(d or {}).get("results",{}).get("label"),"score":(d or {}).get("results",{}).get("score")} def t_forensic(): c,b,l=run_curl([f"http://{H}:8085/api/forensic-modules"],15) d=jparse(b); m=(d or {}).get("available_modules",[]) return c,l,len(m)>=3,{"modules":[x.get("id") for x in m]} def t_web(): c,b,l=run_curl([f"http://{H}:51100/v1/search","-H","Content-Type: application/json","-d",json.dumps({"queries":["deepfake detection"],"max_results":3})],40) d=jparse(b); r=(d or {}).get("results",[]) return c,l,len(r)>0,{"results":len(r),"first":(r[0].get("title","")[:60] if r else None)} def t_catalog(): c,b,l=run_curl([f"http://{H}:11000/catalog/v1/status","-H",f"Authorization: Bearer {TOKEN}"],15) d=jparse(b); st=(d or {}).get("status") return c,l,st in ("healthy","degraded"),{"status":st,"components":[x.get("component_id") for x in (d or {}).get("components",[])]} def t_gateway(): c,b,l=run_curl([f"http://{H}:11000/health"],10) c2,_,_=run_curl([f"http://{H}:11000/web/v1/info"],10) # fara token -> 401 return c,l,(c==200 and c2==401),{"health":c,"web_no_auth":c2} def t_brain_atoms(): c,b,l=run_curl([f"http://{H}:8090/v1/analysis_atom/stats/extended"],15) d=jparse(b) return c,l,(d is not None and "total_atoms" in d),{"total_atoms":(d or {}).get("total_atoms"),"by_tier":(d or {}).get("by_tier")} def t_brain_facts(): c,b,l=run_curl([f"http://{H}:8090/v1/fact_status/list?page=1&page_size=10"],15) d=jparse(b) return c,l,(d is not None and "total" in d),{"total":(d or {}).get("total")} def t_brain_vcache(): c,b,l=run_curl([f"http://{H}:8090/v1/verification_cache/list?page=1&page_size=10"],15) d=jparse(b) return c,l,(d is not None and "total" in d),{"total":(d or {}).get("total")} def t_llm_completions(): c,b,l=run_curl([f"http://{H}:14011/v1/completions","-H","Content-Type: application/json","-d",json.dumps({"model":"qwen3.5","prompt":"Capitala Romaniei este","max_tokens":10})],60) d=jparse(b); txt=(d or {}).get("choices",[{}])[0].get("text","") if d else "" return c,l,bool(txt.strip()),{"text":txt[:120]} def t_llm_info(): c,b,l=run_curl([f"http://{H}:14011/v1/info"],15); d=jparse(b) return c,l,isinstance(d,dict) and len(d)>0,{"keys":list(d.keys())[:6] if isinstance(d,dict) else None} def t_video_semantic(): c,b,l=run_curl([f"http://{H}:54600/analyze/video/semantic","-F",f"file=@{ASSETS}/sample.mp4"],180) d=jparse(b); ok=bool(d) and ("final_summary" in d or "chunk_results" in d) return c,l,ok,{"num_chunks":(d or {}).get("num_chunks"),"summary":(str((d or {}).get("final_summary",""))[:120])} def t_web_fetch(): c,b,l=run_curl([f"http://{H}:51100/v1/fetch","-H","Content-Type: application/json","-d",json.dumps({"urls":["https://en.wikipedia.org/wiki/Deepfake"]})],60) ok=(c==200 and len(b)>1500 and ("title" in b or "content" in b or "text" in b)) return c,l,ok,{"bytes":len(b)} def t_web_gather(): c,b,l=run_curl([f"http://{H}:51100/v1/gather","-H","Content-Type: application/json","-d",json.dumps({"claim":"Deepfakes can be detected by AI","max_search_results":5})],150) d=jparse(b); ev=(d or {}).get("evidence",[]); n=(d or {}).get("total_evidence_items",len(ev) if isinstance(ev,list) else 0) return c,l,bool(d) and (n>0 or (isinstance(ev,list) and len(ev)>0)),{"evidence_items":n,"urls_found":(d or {}).get("total_urls_found")} def t_forensic_evidence(): c,b,l=run_curl([f"http://{H}:8085/api/forensic-evidence","-F",f"video=@{ASSETS}/sample.mp4","-F","modules=m29","-F","every_n_frames=30"],150) d=jparse(b); mr=(d or {}).get("modules_run",[]) return c,l,bool(mr),{"modules_run":mr,"frames":(d or {}).get("n_frames_extracted")} def t_catalog_components(): c,b,l=run_curl([f"http://{H}:11000/catalog/v1/components","-H",f"Authorization: Bearer {TOKEN}"],15) d=jparse(b); comp=(d or {}).get("components",d if isinstance(d,list) else []) return c,l,c==200 and len(comp)>0,{"components":len(comp)} def t_catalog_models(): c,b,l=run_curl([f"http://{H}:11000/catalog/v1/models","-H",f"Authorization: Bearer {TOKEN}"],15) d=jparse(b) return c,l,c==200 and d is not None,{"models":len((d or {}).get("models",d if isinstance(d,list) else []))} def t_brain_search(): c,b,l=run_curl([f"http://{H}:8090/v1/search","-H","Content-Type: application/json","-d",json.dumps({"queries":["deepfake"],"max_results":2})],60) d=jparse(b) return c,l,bool(d) and "results" in d,{"total_results":(d or {}).get("total_results"),"cache":(d or {}).get("brain_meta",{}).get("cache_status")} def t_brain_gather(): c,b,l=run_curl([f"http://{H}:8090/v1/gather","-H","Content-Type: application/json","-d",json.dumps({"claim":"Deepfakes can be detected by AI"})],120) d=jparse(b) return c,l,bool(d) and "evidence" in d,{"evidence_items":(d or {}).get("total_evidence_items"),"urls":(d or {}).get("total_urls_found")} def t_dashboard_health(): c,b,l=run_curl([f"http://{H}:51300/health"],10); d=jparse(b) return c,l,(d or {}).get("status") in ("healthy","ok"),{"status":(d or {}).get("status"),"db":(d or {}).get("db")} def t_dashboard_monitoring(): c,b,l=run_curl([f"http://{H}:51300/api/monitoring/services"],20); d=jparse(b) s=d if isinstance(d,list) else (d or {}).get("services",[]) h=sum(1 for x in s if x.get("status")=="healthy") return c,l,len(s)>0 and h==len(s),{"healthy":f"{h}/{len(s)}"} def t_cloak(): c,b,l=run_curl([f"http://{H}:8770/v1/search","-H","Content-Type: application/json","-d",json.dumps({"queries":["deepfake"],"engines":["ddg"],"max_results_per_engine":3})],45) d=jparse(b); r=(d or {}).get("results",[]) return c,l,len(r)>0,{"results":len(r),"first":(r[0].get("title","")[:55] if r else None)} TESTS = [ ("llm","LLM (Qwen3.5-35B-A3B)","POST /v1/chat/completions",t_llm), ("llm_completions","LLM · completions","POST /v1/completions",t_llm_completions), ("llm_info","LLM · info","GET /v1/info",t_llm_info), ("embeddings","Embeddings (bge-m3)","POST /v1/embeddings",t_embeddings), ("rerank","Rerank (bge-reranker-v2-m3)","POST /v1/rerank",t_rerank), ("audio","Audio (Whisper large-v3-turbo)","POST /v1/audio/transcriptions",t_audio), ("video","Video/BusterX (deepfake)","POST /analyze/video",t_video), ("video_semantic","Video · semantic","POST /analyze/video/semantic",t_video_semantic), ("extractors_metadata","Extractors · metadata","POST /v1/metadata",t_metadata), ("extractors_ner","Extractors · NER (GLiNER)","POST /v1/ner",t_ner), ("extractors_detect","Extractors · detect (YOLOv8)","POST /v1/detect",t_detect), ("extractors_ocr","Extractors · OCR (LLM vision)","POST /v1/ocr",t_ocr), ("extractors_sentiment","Extractors · sentiment (LLM)","POST /v1/sentiment",t_sentiment), ("forensic","Forensic · module list","GET /api/forensic-modules",t_forensic), ("forensic_evidence","Forensic · analiza reala","POST /api/forensic-evidence",t_forensic_evidence), ("web","Web · search (SearXNG)","POST /v1/search",t_web), ("web_fetch","Web · fetch","POST /v1/fetch",t_web_fetch), ("web_gather","Web · gather (evidence)","POST /v1/gather",t_web_gather), ("catalog","Catalog · status","GET /catalog/v1/status",t_catalog), ("catalog_components","Catalog · components","GET /catalog/v1/components",t_catalog_components), ("catalog_models","Catalog · models","GET /catalog/v1/models",t_catalog_models), ("gateway","Gateway (auth+rutare)","GET /health + 401",t_gateway), ("brain_atoms","Brain · analysis_atom","GET /v1/analysis_atom/stats/extended",t_brain_atoms), ("brain_facts","Brain · fact_status","GET /v1/fact_status/list",t_brain_facts), ("brain_vcache","Brain · verification_cache","GET /v1/verification_cache/list",t_brain_vcache), ("brain_search","Brain · search (RAG)","POST /v1/search",t_brain_search), ("brain_gather","Brain · gather (RAG)","POST /v1/gather",t_brain_gather), ("dashboard","Dashboard · health","GET /health",t_dashboard_health), ("dashboard_monitoring","Dashboard · monitoring 11/11","GET /api/monitoring/services",t_dashboard_monitoring), ("cloak","Cloak · SERP scraper","POST /v1/search",t_cloak), ] def main() -> int: ensure_assets() ts = datetime.datetime.now().astimezone() stamp = ts.strftime("%Y%m%d_%H%M%S") results = [] print(f"\n DiDi LOT 1 — test run @ {ts.isoformat()} (host {H})\n") for tid, name, ep, fn in TESTS: try: code, lat, ok, extra = fn() except Exception as e: # noqa: BLE001 code, lat, ok, extra = 0, 0.0, False, {"error": str(e)} mark = "\033[32mPASS\033[0m" if ok else "\033[31mFAIL\033[0m" print(f" [{mark}] {name:34} {ep:38} HTTP {code:>3} {lat:>7.0f}ms {json.dumps(extra, ensure_ascii=False)[:80]}") results.append({"id":tid,"name":name,"endpoint":ep,"http_code":code, "ok":ok,"latency_ms":lat,"result":extra,"checked_at":ts.isoformat()}) passed = sum(1 for r in results if r["ok"]) report = { "generated_at": ts.isoformat(), "host": H, "summary": {"total": len(results), "passed": passed, "failed": len(results) - passed}, "tests": results, } REPORTS.mkdir(exist_ok=True) out = REPORTS / f"test_report_{stamp}.json" out.write_text(json.dumps(report, indent=2, ensure_ascii=False)) latest = REPORTS / "latest.json" latest.write_text(json.dumps(report, indent=2, ensure_ascii=False)) print(f"\n === {passed}/{len(results)} PASS ===") print(f" Raport JSON: {out}") print(f" (si copie: {latest})\n") return 0 if passed == len(results) else 1 if __name__ == "__main__": sys.exit(main())