7a94160fd2
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.
195 lines
7.8 KiB
Python
195 lines
7.8 KiB
Python
"""Tests del dispatcher multi-lang (issue 0033 fase A).
|
|
|
|
Verifica que el parser del manifest lee `lang`/`exec` correctamente
|
|
y que el wire protocol (stdin JSON / stdout JSON / exit code)
|
|
funciona identico para enrichers bash y python.
|
|
|
|
No probamos `lang: go` aqui — eso vive en los tests Go nativos del
|
|
issue 0034. Esta suite cubre el dispatcher como tal: que la
|
|
ramificacion de argv funciona y el contrato es estable.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from conftest import (
|
|
PYTHON_BIN, base_ctx, list_entities, make_node,
|
|
)
|
|
|
|
|
|
def _write_dummy_enricher(tmp_path: Path, *, eid: str, lang: str,
|
|
exec_basename: str = "run",
|
|
applies_to: str = "[text]",
|
|
body: str = "") -> Path:
|
|
"""Crea un enricher dummy aislado en tmp_path/enrichers/<eid>/."""
|
|
enr_dir = tmp_path / "enrichers" / eid
|
|
enr_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
manifest = (
|
|
f"id: {eid}\n"
|
|
f"name: \"{eid}\"\n"
|
|
f"description: \"dummy {lang} enricher para tests\"\n"
|
|
f"applies_to: {applies_to}\n"
|
|
f"lang: {lang}\n"
|
|
f"exec: {exec_basename}\n"
|
|
)
|
|
(enr_dir / "manifest.yaml").write_text(manifest, encoding="utf-8")
|
|
|
|
if lang == "python":
|
|
ext = ".py"
|
|
full_body = body or (
|
|
"import json, sys\n"
|
|
"ctx = json.loads(sys.stdin.read())\n"
|
|
"sys.stderr.write('PROGRESS:1.0 done\\n')\n"
|
|
"print(json.dumps({'ok': True, 'lang': 'python', "
|
|
"'node_id': ctx.get('node_id', '')}))\n"
|
|
)
|
|
elif lang == "bash":
|
|
ext = ".sh"
|
|
full_body = body or (
|
|
"#!/usr/bin/env bash\n"
|
|
"ctx=$(cat)\n"
|
|
"echo 'PROGRESS:1.0 done' >&2\n"
|
|
'echo "{\\"ok\\": true, \\"lang\\": \\"bash\\"}"\n'
|
|
)
|
|
elif lang == "go":
|
|
# Para tests del loader que verifican el caso "binario
|
|
# ausente" — solo escribimos el manifest, sin script ni
|
|
# binario.
|
|
return enr_dir
|
|
else:
|
|
raise ValueError(f"lang {lang} no soportado en este test")
|
|
|
|
script = enr_dir / f"{exec_basename}{ext}"
|
|
script.write_text(full_body, encoding="utf-8")
|
|
if lang == "bash":
|
|
os.chmod(script, 0o755)
|
|
return enr_dir
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Parser del manifest — verifica que lang/exec se reconocen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_parser_default_lang_is_python_when_omitted(tmp_path):
|
|
"""Manifest sin `lang` se considera `python` por retrocompat."""
|
|
enr = _write_dummy_enricher(tmp_path, eid="legacy", lang="python")
|
|
# Quitamos las lineas lang/exec del manifest para emular un manifest viejo.
|
|
manifest = enr / "manifest.yaml"
|
|
text = manifest.read_text()
|
|
text = "\n".join(l for l in text.splitlines()
|
|
if not l.startswith("lang:") and not l.startswith("exec:"))
|
|
manifest.write_text(text + "\n", encoding="utf-8")
|
|
|
|
# Reusamos el binario graph_explorer indirectamente via un test
|
|
# caja-blanca: parseamos con yq + verificamos comportamiento via
|
|
# subprocess de run.py. El parser C++ no es directamente
|
|
# accesible desde pytest, por eso lo testeamos transitivamente:
|
|
# corremos el dummy python y verificamos que su run.py se
|
|
# encuentra. El loader C++ solo deja el spec con run_path no
|
|
# vacio si encuentra el archivo.
|
|
py_script = enr / "run.py"
|
|
assert py_script.exists()
|
|
|
|
|
|
def test_parser_reads_lang_bash(tmp_path):
|
|
enr = _write_dummy_enricher(tmp_path, eid="dummy_bash", lang="bash")
|
|
manifest = (enr / "manifest.yaml").read_text()
|
|
assert "lang: bash" in manifest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wire protocol — Python (regresion del comportamiento existente)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_python_dummy_enricher_obeys_wire_protocol(tmp_path):
|
|
enr = _write_dummy_enricher(tmp_path, eid="dummy_py", lang="python")
|
|
|
|
ctx = json.dumps({
|
|
"node_id": "n1", "node_name": "x", "node_type": "text",
|
|
"metadata": {}, "ops_db_path": "", "app_dir": str(tmp_path),
|
|
"cache_dir": str(tmp_path / "cache"),
|
|
"registry_root": "", "params": {},
|
|
})
|
|
proc = subprocess.run(
|
|
[str(PYTHON_BIN), str(enr / "run.py")],
|
|
input=ctx, capture_output=True, text=True, timeout=10,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
assert "PROGRESS:1.0" in proc.stderr
|
|
out = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
assert out == {"ok": True, "lang": "python", "node_id": "n1"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wire protocol — Bash (la ruta nueva)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform.startswith("win") or not shutil.which("bash"),
|
|
reason="test bash-only — saltado en Windows (el bash de WSL no acepta "
|
|
"rutas Windows nativas) y en sistemas sin bash",
|
|
)
|
|
def test_bash_dummy_enricher_obeys_wire_protocol(tmp_path):
|
|
enr = _write_dummy_enricher(tmp_path, eid="dummy_sh", lang="bash")
|
|
|
|
ctx = json.dumps({"node_id": "n1", "ops_db_path": "", "params": {}})
|
|
proc = subprocess.run(
|
|
[shutil.which("bash"), str(enr / "run.sh")],
|
|
input=ctx, capture_output=True, text=True, timeout=10,
|
|
)
|
|
assert proc.returncode == 0, proc.stderr
|
|
assert "PROGRESS:1.0" in proc.stderr
|
|
out = json.loads(proc.stdout.strip().splitlines()[-1])
|
|
assert out == {"ok": True, "lang": "bash"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Comportamiento del loader: enricher Go sin binario queda disabled
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_go_enricher_without_binary_is_disabled_in_load(tmp_path):
|
|
"""Un manifest con `lang: go` pero sin binario compilado debe
|
|
quedar disabled. El test es indirecto — solo confirmamos que el
|
|
layout esperado (manifest sin binario) es el caso real.
|
|
|
|
El loader C++ marcara el spec como disabled. Esto se valida en
|
|
integracion (smoke test del binario) pero no aqui — pytest no
|
|
ejecuta el loader C++ directamente.
|
|
"""
|
|
enr = _write_dummy_enricher(tmp_path, eid="dummy_go", lang="go")
|
|
# Un enricher Go necesita <run> (Linux) o <run>.exe (Windows).
|
|
# Como el dummy_go solo tiene manifest, no hay binario.
|
|
files = sorted(p.name for p in enr.iterdir())
|
|
assert files == ["manifest.yaml"], files
|
|
# Si en el futuro alguien anade el binario, este test debera
|
|
# actualizarse para verificar el flujo enabled tambien.
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Manifests con `lang: python` explicito leen igual que los implicitos
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_existing_enrichers_keep_working_after_dispatcher(ops_db, app_dir,
|
|
registry_root):
|
|
"""Regresion: un enricher real del proyecto (extract_domain,
|
|
`lang: python` por default) sigue funcionando con el flujo
|
|
estandar del wire protocol."""
|
|
make_node(ops_db, node_id="u1", name="ex", type_ref="Url",
|
|
metadata={"url": "https://www.test.example/x"})
|
|
from conftest import run_enricher
|
|
ctx = base_ctx(ops_db=ops_db, app_dir=app_dir, registry_root=registry_root,
|
|
node_id="u1", node_name="ex", node_type="Url",
|
|
metadata={"url": "https://www.test.example/x"})
|
|
rc, out, err = run_enricher("extract_domain", ctx)
|
|
assert rc == 0, err
|
|
domains = list_entities(ops_db, type_ref="Domain")
|
|
assert any(d["name"] == "www.test.example" for d in domains)
|