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:
2026-05-03 14:41:28 +02:00
parent 4be5734ce5
commit 7a94160fd2
26 changed files with 973 additions and 241 deletions
+31 -11
View File
@@ -378,15 +378,20 @@ std::string read_entity_field(const char* db_path, const char* id,
return out;
}
// JSON entregado al subprocess. Todos los paths se normalizan a WSL en
// Windows; en POSIX los respeta tal cual.
// JSON entregado al subprocess. En Windows, los paths se normalizan a
// forma WSL solo cuando el subprocess corre dentro de WSL (lang=bash, o
// python con runtime registry_venv). Para subprocesses nativos Windows
// (lang=go, o python embedded/FN_PYTHON/system) se mantienen los paths
// Windows-nativos — pasarlos como /mnt/c/... haria que fallen al abrir.
// En POSIX la conversion es no-op y siempre se respetan los paths.
std::string build_stdin_json(const std::string& job_id,
const std::string& enricher_id,
const std::string& node_id,
const std::string& params_json,
const std::string& ops_db,
const std::string& app_dir,
const std::string& registry_root)
const std::string& registry_root,
const std::string& lang)
{
std::string node_type, node_name, node_metadata = "{}";
if (!node_id.empty()) {
@@ -420,10 +425,25 @@ std::string build_stdin_json(const std::string& job_id,
std::string app_dir_abs = absify(app_dir);
std::string root_abs = absify(registry_root);
std::string ops_db_wsl = to_wsl_path(ops_db_abs);
std::string app_dir_wsl = to_wsl_path(app_dir_abs);
std::string root_wsl = to_wsl_path(root_abs);
std::string cache_dir = app_dir_wsl + "/cache";
// Decidir si convertir paths a forma WSL. Solo se hace cuando el
// subprocess vive dentro de WSL — si no, los paths /mnt/c/... no
// existen para el proceso Windows-nativo.
bool use_wsl_paths = false;
#ifdef _WIN32
if (lang == "bash") {
use_wsl_paths = true;
} else if (lang == "python") {
use_wsl_paths = cached_python_runtime().needs_wsl;
}
// lang == "go": siempre nativo Windows.
#else
(void)lang;
#endif
std::string ops_db_out = use_wsl_paths ? to_wsl_path(ops_db_abs) : ops_db_abs;
std::string app_dir_out = use_wsl_paths ? to_wsl_path(app_dir_abs) : app_dir_abs;
std::string root_out = use_wsl_paths ? to_wsl_path(root_abs) : root_abs;
std::string cache_dir = app_dir_out + "/cache";
std::ostringstream o;
o << '{'
@@ -434,10 +454,10 @@ std::string build_stdin_json(const std::string& job_id,
<< "\"node_name\":\"" << json_escape(node_name) << "\","
<< "\"metadata\":" << (node_metadata.empty() ? "{}" : node_metadata) << ","
<< "\"params\":" << (params_json.empty() ? "{}" : params_json) << ","
<< "\"ops_db_path\":\"" << json_escape(ops_db_wsl) << "\","
<< "\"app_dir\":\"" << json_escape(app_dir_wsl) << "\","
<< "\"ops_db_path\":\"" << json_escape(ops_db_out) << "\","
<< "\"app_dir\":\"" << json_escape(app_dir_out) << "\","
<< "\"cache_dir\":\"" << json_escape(cache_dir) << "\","
<< "\"registry_root\":\"" << json_escape(root_wsl) << "\""
<< "\"registry_root\":\"" << json_escape(root_out) << "\""
<< '}';
return o.str();
}
@@ -1030,7 +1050,7 @@ void worker_loop() {
}
std::string stdin_payload = build_stdin_json(
ctx.id, ctx.enricher_id, ctx.node_id, ctx.params_json,
ops_db, g_state->app_dir, g_state->registry_root);
ops_db, g_state->app_dir, g_state->registry_root, lang);
ProcResult res = run_subprocess(job_id, run_path, lang,
stdin_payload, ctrl);