feat: catch-up de decisiones previas (Webpage→Url, anti-bot, UI 2-col, tests cross-platform)
Bloque de cambios revisados y validados con el usuario en sesiones previas que no habian aterrizado en commits propios. Lista por tema: * enrichers: web_search ahora usa lite.duckduckgo.com como endpoint primario (mas tolerante con bot detection desde IP residencial), con fallback al endpoint html. Detecta pagina captcha y emite error claro si ambos fallan. Anyade _DDGLiteParser para el formato lite + auto-pick de parser por contenido. * enrichers: tipo Webpage unificado en Url (campos de cuerpo cacheado viven en metadata del Url). Manifests actualizados (applies_to: [Url]). fetch_webpage ya no convierte Url->Webpage. * enrichers/manifest: campo `params` parseado a EnricherSpec.params (name, type, default_value, description). UI puede renderizar dialog de configuracion. * jobs: fix de path conversion para Python embebido nativo Windows (no convertir a /mnt/c/... cuando el subproceso es Windows-native; solo cuando es bash o python via WSL). * main.cpp: ventana ImGui (no modal) "Run enricher" con layout 2-col (label izq, input der). Inserta job con JSON tipado. Layout clustering apretado: hijos del mismo anchor en un solo anillo alrededor del padre, sin desperdigar por anillos crecientes. * views: inspector con layout 2-col via BeginTable (Identity, Schema fields, Extras). Description full-width debajo de su label. * tests: portable conftest (auto-detecta REGISTRY_ROOT, PYTHON_BIN, ENRICHERS_DIR para WSL y Windows portable). _runner.py trampoline inyecta stub via sys.path porque embedded Python ignora PYTHONPATH. Tests bash-only (vendor_script, freeze, dispatcher bash, resolver Linux-binary) skipean en Windows. Tests existentes adaptados a Webpage->Url. Resultado actual: 32 passed WSL, 21 passed + 11 skipped Windows.
This commit is contained in:
+93
-5
@@ -23,24 +23,106 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
||||
REGISTRY_ROOT = Path(__file__).resolve().parents[5]
|
||||
APP_DIR_SRC = Path(__file__).resolve().parents[1] # graph_explorer/
|
||||
ENRICHERS_DIR = APP_DIR_SRC / "enrichers"
|
||||
TESTS_DIR = Path(__file__).resolve().parent
|
||||
STUBS_DIR = TESTS_DIR / "_stubs"
|
||||
PYTHON_BIN = REGISTRY_ROOT / "python" / ".venv" / "bin" / "python3"
|
||||
|
||||
# Los enrichers viven en `<app>/enrichers/` en el repo dev y en
|
||||
# `<app>/assets/enrichers/` en la carpeta portable de Windows
|
||||
# (convencion `assets/` desde el ADR de feb-2026). Detectar cual
|
||||
# existe y usar ese.
|
||||
def _resolve_enrichers_dir() -> Path:
|
||||
cands = [
|
||||
APP_DIR_SRC / "enrichers",
|
||||
APP_DIR_SRC / "assets" / "enrichers",
|
||||
]
|
||||
for c in cands:
|
||||
if c.is_dir():
|
||||
return c
|
||||
# Default a la primera para mensajes de error consistentes con el dev layout.
|
||||
return cands[0]
|
||||
|
||||
|
||||
ENRICHERS_DIR = _resolve_enrichers_dir()
|
||||
|
||||
|
||||
def _resolve_registry_root() -> Path:
|
||||
"""Sube desde el directorio de tests buscando un marker del registry.
|
||||
|
||||
En el repo: APP_DIR/projects/osint_graph/apps/graph_explorer/tests
|
||||
-> 5 niveles arriba esta fn_registry/. En la carpeta de Windows
|
||||
(Desktop/apps/graph_explorer/tests) NO hay registry — usamos el
|
||||
propio app dir como fallback. Los tests no leen registry.db; solo
|
||||
se pasa registry_root via ctx por compatibilidad con run.py.
|
||||
"""
|
||||
# Marker fiable: fichero `cmd/fn/main.go` o `registry.db`.
|
||||
p = APP_DIR_SRC
|
||||
for _ in range(8):
|
||||
if (p / "cmd" / "fn" / "main.go").exists() or \
|
||||
(p / "registry.db").exists():
|
||||
return p
|
||||
if p.parent == p:
|
||||
break
|
||||
p = p.parent
|
||||
# Sin registry: usa el app dir como pseudo-root. Los tests funcionan
|
||||
# igual mientras no haya un test que importe paquetes del registry.
|
||||
return APP_DIR_SRC
|
||||
|
||||
|
||||
REGISTRY_ROOT = _resolve_registry_root()
|
||||
|
||||
|
||||
def _resolve_python_bin() -> Path:
|
||||
"""Elige el Python con el que ejecutar los enrichers.
|
||||
|
||||
Prioridad (cubre Linux/WSL dev y Windows portable instalado):
|
||||
1. $FN_TEST_PYTHON env override
|
||||
2. <app>/assets/runtime/python/python.exe (Windows portable, solo Windows)
|
||||
3. <app>/runtime/python/python.exe (legacy, solo Windows)
|
||||
4. <registry>/python/.venv/bin/python3 (WSL dev venv)
|
||||
5. sys.executable (whatever runs pytest)
|
||||
|
||||
Los candidatos `python.exe` solo se aceptan si corremos en Windows
|
||||
nativo. En WSL/Linux pueden existir vendored en el repo (los
|
||||
distribuibles), pero no son ejecutables en este OS.
|
||||
"""
|
||||
env = os.environ.get("FN_TEST_PYTHON")
|
||||
if env and Path(env).exists():
|
||||
return Path(env)
|
||||
is_windows = sys.platform.startswith("win")
|
||||
cands: list[Path] = []
|
||||
if is_windows:
|
||||
cands += [
|
||||
APP_DIR_SRC / "assets" / "runtime" / "python" / "python.exe",
|
||||
APP_DIR_SRC / "runtime" / "python" / "python.exe",
|
||||
]
|
||||
cands += [REGISTRY_ROOT / "python" / ".venv" / "bin" / "python3"]
|
||||
for c in cands:
|
||||
if c.exists():
|
||||
return c
|
||||
return Path(sys.executable)
|
||||
|
||||
|
||||
PYTHON_BIN = _resolve_python_bin()
|
||||
|
||||
|
||||
def stub_requests(tmp_path: Path, plan: dict) -> dict:
|
||||
"""Escribe el plan de respuestas y devuelve el env que activa el stub.
|
||||
|
||||
El stub vive en tests/_stubs/requests.py y se activa via PYTHONPATH.
|
||||
Devuelve dos vias por las que `_runner.py` y un Python no-embedded
|
||||
pueden inyectar el stub:
|
||||
- `PYTHONPATH`: la ruta estandar; respeta el orden y el resto del
|
||||
entorno. Funciona en Linux y en Python full instalado (no-embed).
|
||||
- `_STUB_PATHS`: lo lee `_runner.py` y hace `sys.path.insert(0, ...)`.
|
||||
Necesario en el Python embebido de Windows, que ignora
|
||||
PYTHONPATH (lo controla `python312._pth`).
|
||||
Plan acepta `default` y/o `match` (lista de {contains, status, text}).
|
||||
"""
|
||||
plan_file = tmp_path / "_stub_plan.json"
|
||||
plan_file.write_text(json.dumps(plan), encoding="utf-8")
|
||||
return {
|
||||
"PYTHONPATH": str(STUBS_DIR) + os.pathsep + os.environ.get("PYTHONPATH", ""),
|
||||
"_STUB_PATHS": str(STUBS_DIR),
|
||||
"_STUB_REQUESTS_PLAN": str(plan_file),
|
||||
}
|
||||
|
||||
@@ -189,17 +271,23 @@ def run_enricher(enricher_id: str, ctx: dict, *, env: dict | None = None,
|
||||
timeout: int = 30) -> tuple[int, dict | None, str]:
|
||||
"""Lanza enrichers/<id>/run.py con el wire protocol estandar.
|
||||
|
||||
Usa siempre el trampoline `_runner.py` para que el stub de
|
||||
requests se inyecte tanto con PYTHONPATH (Python normal) como con
|
||||
`_STUB_PATHS` (Python embebido de Windows que ignora PYTHONPATH).
|
||||
|
||||
Returns: (exit_code, stdout_json_or_None, stderr_text)
|
||||
"""
|
||||
run_py = ENRICHERS_DIR / enricher_id / "run.py"
|
||||
assert run_py.exists(), f"no existe {run_py}"
|
||||
runner = TESTS_DIR / "_runner.py"
|
||||
assert runner.exists(), f"no existe {runner}"
|
||||
|
||||
full_env = os.environ.copy()
|
||||
if env:
|
||||
full_env.update(env)
|
||||
|
||||
proc = subprocess.run(
|
||||
[str(PYTHON_BIN), str(run_py)],
|
||||
[str(PYTHON_BIN), str(runner), str(run_py)],
|
||||
input=json.dumps(ctx),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
|
||||
Reference in New Issue
Block a user