merge issue/0117: dod_evidence_panel C++ ImGui + helpers
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
#include "dod_evidence_panel.h"
|
||||
#include "core/icons_tabler.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
# ifndef WIN32_LEAN_AND_MEAN
|
||||
# define WIN32_LEAN_AND_MEAN
|
||||
# endif
|
||||
# include <windows.h>
|
||||
# include <shellapi.h>
|
||||
#endif
|
||||
|
||||
namespace fn_viz {
|
||||
namespace {
|
||||
|
||||
// Mapea icon_id (helpers) -> macro TI_*.
|
||||
const char* ti_for(const std::string& icon_id) {
|
||||
if (icon_id == "circle-dot") return TI_CIRCLE_DOT;
|
||||
if (icon_id == "circle-check") return TI_CIRCLE_CHECK;
|
||||
if (icon_id == "circle-x") return TI_CIRCLE_X;
|
||||
return TI_CIRCLE_DASHED;
|
||||
}
|
||||
|
||||
// Color por token (0=neutral, 1=info, 2=success, 3=danger).
|
||||
ImVec4 color_for_token(int tok) {
|
||||
using namespace fn_tokens;
|
||||
switch (tok) {
|
||||
case 1: return colors::info;
|
||||
case 2: return colors::success;
|
||||
case 3: return colors::error;
|
||||
default: return colors::text_muted;
|
||||
}
|
||||
}
|
||||
|
||||
// Abre una URL en el browser por defecto. Stub en plataformas no soportadas.
|
||||
void open_url(const std::string& url) {
|
||||
#ifdef _WIN32
|
||||
ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
||||
#else
|
||||
// xdg-open con argumento quoteado minimo (sin caracteres especiales no
|
||||
// se gana mucho con quoting completo). Si el caller pasa una URL
|
||||
// controlada por usuario, esto deberia validarse antes.
|
||||
std::string cmd = "xdg-open '" + url + "' >/dev/null 2>&1 &";
|
||||
int rc = std::system(cmd.c_str());
|
||||
(void)rc;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Lee primeras N lineas de un archivo log. Devuelve "" si no se puede abrir.
|
||||
std::string read_first_lines(const std::string& path, int max_lines) {
|
||||
std::ifstream f(path);
|
||||
if (!f.is_open()) return "";
|
||||
std::ostringstream out;
|
||||
std::string line;
|
||||
int n = 0;
|
||||
while (n < max_lines && std::getline(f, line)) {
|
||||
out << line << "\n";
|
||||
++n;
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// Carga entero el archivo. Truncado a 64KiB para popup.
|
||||
std::string read_full_capped(const std::string& path, size_t cap = 64 * 1024) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f.is_open()) return "";
|
||||
std::string s;
|
||||
f.seekg(0, std::ios::end);
|
||||
std::streamsize sz = f.tellg();
|
||||
if (sz < 0) return "";
|
||||
size_t n = static_cast<size_t>(sz);
|
||||
if (n > cap) n = cap;
|
||||
s.resize(n);
|
||||
f.seekg(0, std::ios::beg);
|
||||
f.read(&s[0], static_cast<std::streamsize>(n));
|
||||
return s;
|
||||
}
|
||||
|
||||
void render_preview_screenshot(const DodEvidence& ev) {
|
||||
// Sin GL context cacheado ni stb_image load aqui: stub textual. La carga
|
||||
// real requiere mantener un map<string, GLuint> que el panel no
|
||||
// posee. Las apps consumidoras pueden override esto sustituyendo la
|
||||
// funcion (issue futura: callback render_preview).
|
||||
ImGui::TextDisabled("[screenshot] %s", ev.payload_path.c_str());
|
||||
}
|
||||
|
||||
void render_preview_log(const DodEvidence& ev, int row_idx) {
|
||||
if (ev.payload_path.empty()) {
|
||||
ImGui::TextDisabled("(no log path)");
|
||||
return;
|
||||
}
|
||||
std::string head = read_first_lines(ev.payload_path, 5);
|
||||
if (head.empty()) {
|
||||
ImGui::TextDisabled("(log not readable)");
|
||||
} else {
|
||||
ImGui::TextWrapped("%s", head.c_str());
|
||||
}
|
||||
char btn_id[64];
|
||||
std::snprintf(btn_id, sizeof(btn_id), "Show full##log_%d", row_idx);
|
||||
if (ImGui::SmallButton(btn_id)) {
|
||||
ImGui::OpenPopup(btn_id);
|
||||
}
|
||||
if (ImGui::BeginPopup(btn_id)) {
|
||||
std::string full = read_full_capped(ev.payload_path);
|
||||
ImGui::InputTextMultiline("##log_full", full.data(), full.size() + 1,
|
||||
ImVec2(600, 400), ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void render_preview_url(const DodEvidence& ev) {
|
||||
ImGui::TextUnformatted(TI_EXTERNAL_LINK);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(ev.payload_url.c_str())) {
|
||||
open_url(ev.payload_url);
|
||||
}
|
||||
}
|
||||
|
||||
void render_preview_cmd(const DodEvidence& ev, const DodItem& item) {
|
||||
using namespace fn_tokens;
|
||||
const bool match = (ev.payload_text == item.expected);
|
||||
ImVec4 ok = colors::success;
|
||||
ImVec4 bad = colors::error;
|
||||
if (ImGui::BeginTable("##cmd_diff", 2, ImGuiTableFlags_SizingStretchSame)) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
ImGui::TextDisabled("expected");
|
||||
ImGui::TextWrapped("%s", item.expected.c_str());
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextDisabled("actual");
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, match ? ok : bad);
|
||||
ImGui::TextWrapped("%s", ev.payload_text.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndTable();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void render_dod_evidence_panel(DodPanelState& state) {
|
||||
using namespace fn_tokens;
|
||||
namespace P = ::fn_viz::dod_panel;
|
||||
|
||||
P::StatusCounts c = P::count_status(state);
|
||||
|
||||
// Header.
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted);
|
||||
ImGui::Text("run_id:");
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextUnformatted(state.run_id.empty() ? "(none)" : state.run_id.c_str());
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(" | ");
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("total %d ", c.total);
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::info);
|
||||
ImGui::Text("done %d ", c.done);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::success);
|
||||
ImGui::Text("validated %d ", c.validated);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
ImGui::Text("failed %d", c.failed);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
if (c.missing_required > 0) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled(" | ");
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
ImGui::Text("required missing: %d", c.missing_required);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Tabla principal.
|
||||
const ImGuiTableFlags flags =
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp;
|
||||
if (!ImGui::BeginTable("##dod_table", 6, flags)) return;
|
||||
|
||||
ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 24.0f);
|
||||
ImGui::TableSetupColumn("id", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
||||
ImGui::TableSetupColumn("kind", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
||||
ImGui::TableSetupColumn("expected", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("evidence", ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn("actions", ImGuiTableColumnFlags_WidthFixed, 130.0f);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
int row_idx = 0;
|
||||
for (const auto& item : state.items) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(row_idx);
|
||||
|
||||
// Col 0: status icon.
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
const std::string icon_id = P::status_icon_id(item.status);
|
||||
const int tok = P::status_color_token(item.status);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, color_for_token(tok));
|
||||
ImGui::TextUnformatted(ti_for(icon_id));
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
// Col 1: id (+ required badge si missing).
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::TextUnformatted(item.id.c_str());
|
||||
const DodEvidence* ev = P::find_evidence(state, item.id);
|
||||
const bool resolved = (item.status == "done" || item.status == "validated");
|
||||
if (item.required && ev == nullptr && !resolved) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
ImGui::TextUnformatted("required, missing");
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Col 2: kind.
|
||||
ImGui::TableSetColumnIndex(2);
|
||||
ImGui::TextUnformatted(item.kind.c_str());
|
||||
|
||||
// Col 3: expected.
|
||||
ImGui::TableSetColumnIndex(3);
|
||||
ImGui::TextWrapped("%s", item.expected.c_str());
|
||||
|
||||
// Col 4: evidence preview.
|
||||
ImGui::TableSetColumnIndex(4);
|
||||
if (ev == nullptr) {
|
||||
ImGui::TextDisabled("(no evidence)");
|
||||
} else if (item.kind == "screenshot") {
|
||||
render_preview_screenshot(*ev);
|
||||
} else if (item.kind == "log") {
|
||||
render_preview_log(*ev, row_idx);
|
||||
} else if (item.kind == "url") {
|
||||
render_preview_url(*ev);
|
||||
} else if (item.kind == "cmd") {
|
||||
render_preview_cmd(*ev, item);
|
||||
} else {
|
||||
ImGui::TextDisabled("(unknown kind: %s)", item.kind.c_str());
|
||||
}
|
||||
|
||||
// Col 5: actions.
|
||||
ImGui::TableSetColumnIndex(5);
|
||||
if (ev != nullptr) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::success);
|
||||
const bool clicked_v = ImGui::SmallButton(TI_CHECK " Validate");
|
||||
ImGui::PopStyleColor();
|
||||
if (clicked_v && state.on_validate) state.on_validate(item.id);
|
||||
|
||||
ImGui::SameLine();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, colors::error);
|
||||
const bool clicked_r = ImGui::SmallButton(TI_X " Reject");
|
||||
ImGui::PopStyleColor();
|
||||
if (clicked_r && state.on_reject) state.on_reject(item.id);
|
||||
} else {
|
||||
ImGui::TextDisabled("—");
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
++row_idx;
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
} // namespace fn_viz
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
// dod_evidence_panel — ImGui panel reutilizable para validar DoD evidence.
|
||||
//
|
||||
// Renderiza una tabla con items DoD (Definition of Done) + evidencias
|
||||
// adjuntas. Cada fila: icono de status, id, kind, expected, preview de
|
||||
// evidence (screenshot path / log preview / url clickable / cmd output con
|
||||
// diff vs expected), y botones Validate/Reject que invocan callbacks.
|
||||
//
|
||||
// La logica pura (contar status, buscar evidence, mapear icon/color) vive en
|
||||
// dod_evidence_panel_helpers.{h,cpp} y se testea sin ImGui.
|
||||
//
|
||||
// Uso:
|
||||
// #include "viz/dod_evidence_panel.h"
|
||||
// fn_viz::DodPanelState state;
|
||||
// state.run_id = "run_abc123";
|
||||
// state.items = load_dod_items(...);
|
||||
// state.evidences = load_dod_evidences(...);
|
||||
// state.on_validate = [](const std::string& id) { db.mark_validated(id); };
|
||||
// state.on_reject = [](const std::string& id) { db.mark_failed(id); };
|
||||
// fn_viz::render_dod_evidence_panel(state);
|
||||
|
||||
#include "dod_evidence_panel_helpers.h"
|
||||
|
||||
namespace fn_viz {
|
||||
|
||||
// Renderiza el panel inline en el current ImGui window.
|
||||
// Debe llamarse dentro de un frame ImGui activo (entre NewFrame/Render).
|
||||
void render_dod_evidence_panel(DodPanelState& state);
|
||||
|
||||
} // namespace fn_viz
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: dod_evidence_panel
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: viz
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "void fn_viz::render_dod_evidence_panel(DodPanelState& state)"
|
||||
description: "Panel ImGui que renderiza items DoD + evidencias (screenshot/log/url/cmd) con botones validate/reject por item y badge required-missing"
|
||||
tags: [agents, dod, evidence, imgui, viz, panel, cpp-dashboard-viz]
|
||||
uses_functions: [tokens_cpp_core]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [imgui]
|
||||
tested: true
|
||||
tests:
|
||||
- "count_status: total = items.size"
|
||||
- "count_status: unknown status counts as pending"
|
||||
- "count_status: missing_required only when required+no evidence+unresolved"
|
||||
- "find_evidence: returns matching evidence"
|
||||
- "find_evidence: empty state returns nullptr"
|
||||
- "status_icon_id: mapping per status"
|
||||
- "status_color_token: mapping per status"
|
||||
test_file_path: "cpp/tests/test_dod_evidence_panel.cpp"
|
||||
file_path: "cpp/functions/viz/dod_evidence_panel.cpp"
|
||||
framework: imgui
|
||||
params:
|
||||
- name: state
|
||||
desc: "DodPanelState con items (DodItem[]), evidences (DodEvidence[]), run_id y callbacks on_validate/on_reject (std::function<void(string item_id)>)"
|
||||
output: "void — renderiza header (run_id + counts) + tabla 6-col (status icon | id | kind | expected | evidence preview | actions) inline en current ImGui window"
|
||||
---
|
||||
|
||||
# dod_evidence_panel
|
||||
|
||||
Panel ImGui reutilizable para validar la **Definition of Done** de un agente o run. Compone:
|
||||
|
||||
- Header con `run_id` y counts por status (`total / done / validated / failed`) + badge rojo si hay items required sin evidence.
|
||||
- Tabla con icono de status (TI_CIRCLE_DASHED/DOT/CHECK/X), id, kind, expected, preview de la evidence segun kind, y botones Validate/Reject.
|
||||
|
||||
Logica pura (count, find, status mappers) en `dod_evidence_panel_helpers.{h,cpp}` — testeada sin ImGui. El `.cpp` del panel hace render real y se compila SOLO cuando una app lo lista en su CMakeLists.
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```cpp
|
||||
#include "viz/dod_evidence_panel.h"
|
||||
|
||||
fn_viz::DodPanelState state;
|
||||
state.run_id = "run_abc123";
|
||||
|
||||
fn_viz::DodItem it1;
|
||||
it1.id = "screenshot_home";
|
||||
it1.kind = "screenshot";
|
||||
it1.expected = "Landing page con KPI cards visibles";
|
||||
it1.required = true;
|
||||
it1.status = "done";
|
||||
state.items.push_back(it1);
|
||||
|
||||
fn_viz::DodEvidence ev1;
|
||||
ev1.item_id = "screenshot_home";
|
||||
ev1.kind = "screenshot";
|
||||
ev1.payload_path = "/tmp/screenshots/home.png";
|
||||
state.evidences.push_back(ev1);
|
||||
|
||||
state.on_validate = [&](const std::string& id) { db.mark_validated(id); };
|
||||
state.on_reject = [&](const std::string& id) { db.mark_failed(id); };
|
||||
|
||||
// En el render callback de fn::run_app:
|
||||
ImGui::Begin("DoD evidence");
|
||||
fn_viz::render_dod_evidence_panel(state);
|
||||
ImGui::End();
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Usala cuando una app necesita mostrar al humano los items pendientes de validacion de un run de agente (DoD) con sus evidencias para aprobar/rechazar a mano. Encaja en dashboards de orquestador (`fn-orquestador`), kanban de issues con sub-checks visuales, o paneles "human-in-the-loop" donde se requiere firma manual antes de marcar `done`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Screenshot preview es stub textual**. La carga real PNG requiere GL context y un cache `map<string, GLuint>` que el panel no posee. Si necesitas thumbnails reales, mantenlos en la app consumidora y pasalos a un callback custom (issue futura).
|
||||
- **URL open**: en Linux usa `system("xdg-open '<url>' &")`, en Windows `ShellExecuteA`. NO valida la URL antes — si la URL viene de input no confiable, sanitizar antes de poner en `payload_url`.
|
||||
- **Log preview** lee primeras 5 lineas con `std::ifstream` por frame. Para logs muy grandes o cuando el panel se redibuja a 60fps, considera cachear externamente.
|
||||
- **Cmd diff** compara strings exactamente (`==`). No hace trimming ni normalizacion de whitespace — el caller decide.
|
||||
- **Callbacks pueden mutar `state`**: las apps suelen mover el item a `validated`/`failed` desde el `on_validate`/`on_reject`. El panel re-renderiza con el nuevo status en el siguiente frame.
|
||||
- **`required` + `missing_required`**: solo se cuenta `missing_required` cuando el item es required, NO tiene evidence, y el status NO es `done`/`validated`. Un item required marcado `done` sin evidence no aparece como missing (la app ya lo cerro a mano).
|
||||
- Llamar dentro de un frame ImGui activo (entre `NewFrame`/`Render`). Fuera, `ImGui::BeginTable` aborta.
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "dod_evidence_panel_helpers.h"
|
||||
|
||||
namespace fn_viz {
|
||||
namespace dod_panel {
|
||||
|
||||
StatusCounts count_status(const DodPanelState& state) {
|
||||
StatusCounts c;
|
||||
c.total = static_cast<int>(state.items.size());
|
||||
for (const auto& it : state.items) {
|
||||
if (it.status == "pending") ++c.pending;
|
||||
else if (it.status == "done") ++c.done;
|
||||
else if (it.status == "validated") ++c.validated;
|
||||
else if (it.status == "failed") ++c.failed;
|
||||
else ++c.pending; // unknown treated as pending
|
||||
|
||||
if (it.required) {
|
||||
const DodEvidence* ev = find_evidence(state, it.id);
|
||||
const bool resolved = (it.status == "done" || it.status == "validated");
|
||||
if (ev == nullptr && !resolved) ++c.missing_required;
|
||||
}
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
const DodEvidence* find_evidence(const DodPanelState& state, const std::string& item_id) {
|
||||
for (const auto& ev : state.evidences) {
|
||||
if (ev.item_id == item_id) return &ev;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string status_icon_id(const std::string& status) {
|
||||
if (status == "done") return "circle-dot";
|
||||
if (status == "validated") return "circle-check";
|
||||
if (status == "failed") return "circle-x";
|
||||
return "circle-dashed"; // pending / unknown
|
||||
}
|
||||
|
||||
int status_color_token(const std::string& status) {
|
||||
if (status == "done") return 1;
|
||||
if (status == "validated") return 2;
|
||||
if (status == "failed") return 3;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace dod_panel
|
||||
} // namespace fn_viz
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
// dod_evidence_panel_helpers — logica pura, sin ImGui.
|
||||
// Tests (cpp/tests/test_dod_evidence_panel.cpp) linkan SOLO este archivo
|
||||
// para validar conteos, lookup y mapeo de status -> icon/color.
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
namespace fn_viz {
|
||||
|
||||
struct DodItem {
|
||||
std::string id;
|
||||
std::string kind; // screenshot|log|url|cmd
|
||||
std::string expected;
|
||||
bool required = true;
|
||||
std::string status; // pending|done|validated|failed
|
||||
};
|
||||
|
||||
struct DodEvidence {
|
||||
std::string item_id;
|
||||
std::string kind;
|
||||
std::string payload_path; // screenshot/log
|
||||
std::string payload_url; // url
|
||||
std::string payload_text; // cmd output
|
||||
int64_t attached_at = 0;
|
||||
bool validated = false;
|
||||
std::string validated_by;
|
||||
};
|
||||
|
||||
struct DodPanelState {
|
||||
std::vector<DodItem> items;
|
||||
std::vector<DodEvidence> evidences;
|
||||
std::string run_id;
|
||||
std::function<void(const std::string& item_id)> on_validate;
|
||||
std::function<void(const std::string& item_id)> on_reject;
|
||||
};
|
||||
|
||||
namespace dod_panel {
|
||||
|
||||
struct StatusCounts {
|
||||
int total = 0;
|
||||
int pending = 0;
|
||||
int done = 0;
|
||||
int validated = 0;
|
||||
int failed = 0;
|
||||
int missing_required = 0; // required + no evidence + status != done/validated
|
||||
};
|
||||
|
||||
// Cuenta items por status. Si un item required NO tiene evidence y su status
|
||||
// no es done/validated, suma missing_required.
|
||||
StatusCounts count_status(const DodPanelState& state);
|
||||
|
||||
// Busca la primera evidence cuyo item_id == item_id. nullptr si no existe.
|
||||
const DodEvidence* find_evidence(const DodPanelState& state, const std::string& item_id);
|
||||
|
||||
// Status -> icon key (uso interno; en render se mapea a TI_*).
|
||||
// pending -> "circle-dashed"
|
||||
// done -> "circle-dot"
|
||||
// validated -> "circle-check"
|
||||
// failed -> "circle-x"
|
||||
// otro -> "circle-dashed"
|
||||
std::string status_icon_id(const std::string& status);
|
||||
|
||||
// Status -> color token.
|
||||
// 0 = neutral (pending / desconocido)
|
||||
// 1 = info (done)
|
||||
// 2 = success (validated)
|
||||
// 3 = danger (failed)
|
||||
int status_color_token(const std::string& status);
|
||||
|
||||
} // namespace dod_panel
|
||||
} // namespace fn_viz
|
||||
@@ -297,3 +297,10 @@ target_compile_definitions(test_visual PRIVATE
|
||||
"FN_TEST_REPO_ROOT=\"${CMAKE_SOURCE_DIR}/..\"")
|
||||
# Asegura que primitives_gallery existe antes de correr el test.
|
||||
add_dependencies(test_visual primitives_gallery)
|
||||
|
||||
# --- Issue 0117 — dod_evidence_panel helpers: logica pura para panel DoD ----
|
||||
# Solo helpers (count_status, find_evidence, status_icon_id, status_color_token).
|
||||
# El render con ImGui (dod_evidence_panel.cpp) NO se compila aqui: requiere
|
||||
# imgui + tokens + icons_tabler — cubierto en builds de apps consumidoras.
|
||||
add_fn_test(test_dod_evidence_panel test_dod_evidence_panel.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/dod_evidence_panel_helpers.cpp)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
// Tests para la logica pura de dod_evidence_panel (issue 0117).
|
||||
//
|
||||
// El render real con ImGui no se testea aqui (requiere context). Cubrimos:
|
||||
// - count_status: tally por status + missing_required.
|
||||
// - find_evidence: lookup por item_id.
|
||||
// - status_icon_id: mapeo a icon key.
|
||||
// - status_color_token: mapeo a token semantico.
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch_amalgamated.hpp"
|
||||
|
||||
#include "viz/dod_evidence_panel_helpers.h"
|
||||
|
||||
using namespace fn_viz;
|
||||
using namespace fn_viz::dod_panel;
|
||||
|
||||
namespace {
|
||||
|
||||
DodItem mk_item(std::string id, std::string kind, std::string status, bool required = true) {
|
||||
DodItem it;
|
||||
it.id = std::move(id);
|
||||
it.kind = std::move(kind);
|
||||
it.status = std::move(status);
|
||||
it.required = required;
|
||||
it.expected = "expected_" + it.id;
|
||||
return it;
|
||||
}
|
||||
|
||||
DodEvidence mk_evidence(std::string item_id, std::string kind = "screenshot") {
|
||||
DodEvidence ev;
|
||||
ev.item_id = std::move(item_id);
|
||||
ev.kind = std::move(kind);
|
||||
ev.payload_path = "/tmp/" + ev.item_id + ".png";
|
||||
return ev;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("count_status: total = items.size", "[dod_panel]") {
|
||||
DodPanelState s;
|
||||
s.items.push_back(mk_item("a", "screenshot", "pending"));
|
||||
s.items.push_back(mk_item("b", "log", "done"));
|
||||
s.items.push_back(mk_item("c", "url", "validated"));
|
||||
s.items.push_back(mk_item("d", "cmd", "failed"));
|
||||
|
||||
auto c = count_status(s);
|
||||
REQUIRE(c.total == 4);
|
||||
REQUIRE(c.pending == 1);
|
||||
REQUIRE(c.done == 1);
|
||||
REQUIRE(c.validated == 1);
|
||||
REQUIRE(c.failed == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("count_status: unknown status counts as pending", "[dod_panel]") {
|
||||
DodPanelState s;
|
||||
s.items.push_back(mk_item("a", "log", "weird"));
|
||||
s.items.push_back(mk_item("b", "log", "pending"));
|
||||
|
||||
auto c = count_status(s);
|
||||
REQUIRE(c.total == 2);
|
||||
REQUIRE(c.pending == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("count_status: missing_required only when required+no evidence+unresolved", "[dod_panel]") {
|
||||
DodPanelState s;
|
||||
// required + no evidence + pending -> missing
|
||||
s.items.push_back(mk_item("missing_one", "screenshot", "pending", /*required*/ true));
|
||||
// required + no evidence + done -> resolved, no missing
|
||||
s.items.push_back(mk_item("done_no_ev", "log", "done", true));
|
||||
// required + evidence + pending -> no missing
|
||||
s.items.push_back(mk_item("has_ev", "url", "pending", true));
|
||||
s.evidences.push_back(mk_evidence("has_ev", "url"));
|
||||
// optional + no evidence + pending -> no missing
|
||||
s.items.push_back(mk_item("optional", "log", "pending", /*required*/ false));
|
||||
|
||||
auto c = count_status(s);
|
||||
REQUIRE(c.missing_required == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("find_evidence: returns matching evidence", "[dod_panel]") {
|
||||
DodPanelState s;
|
||||
s.items.push_back(mk_item("a", "log", "done"));
|
||||
s.evidences.push_back(mk_evidence("a", "log"));
|
||||
s.evidences.push_back(mk_evidence("b", "screenshot"));
|
||||
|
||||
const DodEvidence* a = find_evidence(s, "a");
|
||||
REQUIRE(a != nullptr);
|
||||
REQUIRE(a->item_id == "a");
|
||||
REQUIRE(a->kind == "log");
|
||||
|
||||
const DodEvidence* b = find_evidence(s, "b");
|
||||
REQUIRE(b != nullptr);
|
||||
REQUIRE(b->kind == "screenshot");
|
||||
|
||||
REQUIRE(find_evidence(s, "missing") == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("find_evidence: empty state returns nullptr", "[dod_panel]") {
|
||||
DodPanelState s;
|
||||
REQUIRE(find_evidence(s, "anything") == nullptr);
|
||||
}
|
||||
|
||||
TEST_CASE("status_icon_id: mapping per status", "[dod_panel]") {
|
||||
REQUIRE(status_icon_id("pending") == "circle-dashed");
|
||||
REQUIRE(status_icon_id("done") == "circle-dot");
|
||||
REQUIRE(status_icon_id("validated") == "circle-check");
|
||||
REQUIRE(status_icon_id("failed") == "circle-x");
|
||||
REQUIRE(status_icon_id("") == "circle-dashed");
|
||||
REQUIRE(status_icon_id("garbage") == "circle-dashed");
|
||||
}
|
||||
|
||||
TEST_CASE("status_color_token: mapping per status", "[dod_panel]") {
|
||||
REQUIRE(status_color_token("pending") == 0);
|
||||
REQUIRE(status_color_token("done") == 1);
|
||||
REQUIRE(status_color_token("validated") == 2);
|
||||
REQUIRE(status_color_token("failed") == 3);
|
||||
REQUIRE(status_color_token("") == 0);
|
||||
REQUIRE(status_color_token("garbage") == 0);
|
||||
}
|
||||
Reference in New Issue
Block a user