150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
"""Test e2e del LAND-simplified pipeline con escenario Vero Beach $7K.
|
|
|
|
Verifica:
|
|
1. detect_property_type_anomalies dispara should_bypass=True
|
|
2. analyze_deal route a _analyze_land_simplified
|
|
3. NO se ejecutan DealAnalyzer/LenderMatcher/ValueEstimator/OfferStrategist
|
|
4. NO se computa Cap Rate / DSCR (computed_scenarios queda {})
|
|
5. Briefing arranca con "🚨 ALERTA CRITICA: PROPIEDAD TIPO TERRENO DETECTADA"
|
|
6. Pipeline mode = land_simplified
|
|
"""
|
|
from __future__ import annotations
|
|
import io, sys, time
|
|
from pathlib import Path
|
|
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from orchestrator import DealInputs, BuyerProfile, analyze_deal # noqa: E402
|
|
|
|
|
|
def main() -> int:
|
|
print("=" * 70)
|
|
print("LAND bugfix test — Vero Beach $7K (user mistakenly declared SFR)")
|
|
print("=" * 70)
|
|
|
|
deal = DealInputs(
|
|
address="5800 32nd Ave, Vero Beach, FL 32966",
|
|
price=7_000,
|
|
rent=0,
|
|
property_tax=200,
|
|
insurance=0,
|
|
hoa=0,
|
|
sqft=0,
|
|
beds=0,
|
|
baths=0,
|
|
year_built=0,
|
|
arv=0,
|
|
property_type="sfr", # USER MISTAKENLY DECLARED SFR
|
|
listing_description="0.23 acre buildable lot, ready for construction. Cleared and graded.",
|
|
)
|
|
profile = BuyerProfile(
|
|
profile_class="C",
|
|
fico=720,
|
|
capital_available=50_000,
|
|
)
|
|
|
|
print(f"DEAL: {deal.address}")
|
|
print(f" price=${deal.price:,} | property_type DECLARED='{deal.property_type}'")
|
|
print(f" year_built={deal.year_built}, sqft={deal.sqft}, beds/baths={deal.beds}/{deal.baths}")
|
|
print(f" rent={deal.rent}, arv={deal.arv}")
|
|
print(f" description: {deal.listing_description}")
|
|
print()
|
|
|
|
def log(msg):
|
|
print(f" [{time.strftime('%H:%M:%S')}] {msg}")
|
|
|
|
t0 = time.perf_counter()
|
|
result = analyze_deal(deal, profile, photo_bytes=None, status_cb=log)
|
|
elapsed = time.perf_counter() - t0
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print(f"PIPELINE COMPLETED in {elapsed:.0f}s")
|
|
print("=" * 70)
|
|
|
|
# ─── Validaciones ──────────────────────────────────────────────────────
|
|
ptw = result.property_type_warning or {}
|
|
mode = result.pipeline_mode
|
|
|
|
print()
|
|
print("VALIDATIONS:")
|
|
print(f" pipeline_mode: {mode}")
|
|
assert mode == "land_simplified", f"FAIL: expected land_simplified, got {mode}"
|
|
print(" ✅ pipeline_mode == 'land_simplified'")
|
|
|
|
print(f" warnings detectados: {len(ptw.get('warnings', []))}")
|
|
assert len(ptw.get("warnings", [])) >= 4, f"FAIL: expected >=4 warnings"
|
|
print(" ✅ >= 4 warnings dispararon detection")
|
|
|
|
print(f" should_bypass_sfr_pipeline: {ptw.get('should_bypass_sfr_pipeline')}")
|
|
assert ptw.get("should_bypass_sfr_pipeline") is True
|
|
print(" ✅ should_bypass=True")
|
|
|
|
# SFR agents should be SKIPPED
|
|
da_out = (result.deal_analysis or {}).get("output", "")
|
|
le_out = (result.lender or {}).get("output", "")
|
|
ve_out = (result.value_estimate or {}).get("output", "")
|
|
os_out = (result.offer_strategy or {}).get("output", "")
|
|
print(f" DealAnalyzer output (should be SKIPPED): {da_out[:80]!r}")
|
|
print(f" LenderMatcher output (should be SKIPPED): {le_out[:80]!r}")
|
|
print(f" ValueEstimator output (should be SKIPPED): {ve_out[:80]!r}")
|
|
print(f" OfferStrategist output (should be SKIPPED): {os_out[:80]!r}")
|
|
assert "SKIPPED" in da_out and "land" in da_out.lower()
|
|
assert "SKIPPED" in le_out
|
|
assert "SKIPPED" in ve_out
|
|
assert "SKIPPED" in os_out
|
|
print(" ✅ Los 4 agentes SFR-specific NO se ejecutaron (SKIPPED)")
|
|
|
|
# computed_scenarios should be empty (no DSCR/Cap Rate)
|
|
cs = result.computed_scenarios or {}
|
|
print(f" computed_scenarios (should be empty for LAND): {len(cs)} keys")
|
|
assert cs == {}, f"FAIL: computed_scenarios should be empty for LAND, got: {cs}"
|
|
print(" ✅ NO se computó finance_calculator (no hay Cap Rate / DSCR inflado)")
|
|
|
|
# Briefing should start with land warning
|
|
briefing_out = (result.executive_briefing or {}).get("output", "")
|
|
print(f"\n Briefing first 300 chars:")
|
|
print(f" {briefing_out[:300]}")
|
|
briefing_lower = briefing_out.lower()
|
|
has_alert = any(kw in briefing_lower for kw in [
|
|
"alerta", "terreno", "land", "propiedad tipo terreno",
|
|
])
|
|
assert has_alert, "FAIL: briefing missing LAND alert"
|
|
print()
|
|
print(" ✅ Briefing destaca alerta LAND")
|
|
|
|
# Coordinator output should NOT have DSCR/Cap Rate inflados
|
|
coord_out = (result.final or {}).get("output", "")
|
|
coord_lower = coord_out.lower()
|
|
has_dscr = "dscr" in coord_lower
|
|
has_caprate = "cap rate" in coord_lower
|
|
has_coc = "coc" in coord_lower or "cash on cash" in coord_lower
|
|
print(f" Coordinator mentions DSCR: {has_dscr}")
|
|
print(f" Coordinator mentions Cap Rate: {has_caprate}")
|
|
print(f" Coordinator mentions CoC: {has_coc}")
|
|
# Es OK que el LLM las mencione para EXPLICAR que no aplican, pero NO debe presentarlas
|
|
# como metricas calculadas. Validamos que mencione "no aplica" o "land" si las menciona.
|
|
if has_dscr or has_caprate or has_coc:
|
|
explains_inapplicable = any(kw in coord_lower for kw in [
|
|
"no aplica", "no apply", "land", "terreno", "no se calcul",
|
|
])
|
|
if explains_inapplicable:
|
|
print(" ✅ Coordinator menciona metricas SFR pero CLARIFICA que no aplican")
|
|
else:
|
|
print(" ⚠️ Coordinator menciona DSCR/Cap Rate sin clarificar — review")
|
|
|
|
print()
|
|
print("=" * 70)
|
|
print("✅ ALL VALIDATIONS PASSED — LAND bug FIXED")
|
|
print("=" * 70)
|
|
|
|
# Save the result for inspection
|
|
print(f"\nFull result saved by orchestrator. Check analyses/*_LAND.json")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|