From af26c78e706772b05148328bfe7621df6d4adb3a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Mon, 18 May 2026 18:31:37 +0200 Subject: [PATCH] =?UTF-8?q?test(viz):=20test=5Fagent=5Fruns=5Ftimeline=20?= =?UTF-8?q?=E2=80=94=2017=20cases=20/=2053=20assertions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue 0118. Catch2 coverage de los helpers puros: - passes_filter: filtro vacio, filter por app, app+status combinado, since_ts - filter_and_sort: orden started_at DESC, combina filtro + sort - format_duration: running, segundos, minutos, horas, timestamps invertidos - status_color_token: mapping conocido + fallback neutral - status_icon_id: no-empty + distinto entre statuses - app_chip_hex: apps conocidas + fallback gris Mock fixture de 5 AgentRun mixtos en make_mock(). Co-Authored-By: Claude Opus 4.7 (1M context) --- cpp/tests/CMakeLists.txt | 4 + cpp/tests/test_agent_runs_timeline.cpp | 183 +++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 cpp/tests/test_agent_runs_timeline.cpp diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 9fa887bb..0b9ac6cd 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -297,3 +297,7 @@ 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 0118 — agent_runs_timeline: helpers puros ---------- +add_fn_test(test_agent_runs_timeline test_agent_runs_timeline.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../functions/viz/agent_runs_timeline_helpers.cpp) diff --git a/cpp/tests/test_agent_runs_timeline.cpp b/cpp/tests/test_agent_runs_timeline.cpp new file mode 100644 index 00000000..ca1a856b --- /dev/null +++ b/cpp/tests/test_agent_runs_timeline.cpp @@ -0,0 +1,183 @@ +// Tests for the pure helpers of fn_viz::agent_runs_timeline (issue 0118). +// +// Render path (ImGui) is intentionally NOT tested here — only filter / sort / +// duration / status mapping logic. + +#define CATCH_CONFIG_MAIN +#include "catch_amalgamated.hpp" + +#include "viz/agent_runs_timeline_helpers.h" + +#include +#include +#include + +using fn_viz::AgentRun; +using fn_viz::TimelineFilter; +using namespace fn_viz::timeline; + +static std::vector make_mock() { + std::vector runs; + AgentRun r; + + r = {}; r.id = "r1"; r.app = "kanban_cpp"; r.issue_id = "0118"; r.card_id = "c1"; + r.branch = "issue/0118"; r.status = "running"; r.started_at = 1000; + r.finished_at = 0; r.dod_total = 5; r.dod_done = 2; r.dod_validated = 0; + runs.push_back(r); + + r = {}; r.id = "r2"; r.app = "skill_tree"; r.issue_id = "0107"; r.card_id = "c2"; + r.branch = "issue/0107"; r.status = "done"; r.started_at = 800; + r.finished_at = 860; r.dod_total = 3; r.dod_done = 3; r.dod_validated = 0; + runs.push_back(r); + + r = {}; r.id = "r3"; r.app = "kanban_cpp"; r.issue_id = "0100"; r.card_id = "c3"; + r.branch = "issue/0100"; r.status = "merged"; r.started_at = 600; + r.finished_at = 4200; r.dod_total = 4; r.dod_done = 4; r.dod_validated = 4; + runs.push_back(r); + + r = {}; r.id = "r4"; r.app = "skill_tree"; r.issue_id = "0090"; r.card_id = "c4"; + r.branch = "quick/x"; r.status = "failed"; r.started_at = 1200; + r.finished_at = 1205; r.dod_total = 2; r.dod_done = 0; r.dod_validated = 0; + runs.push_back(r); + + r = {}; r.id = "r5"; r.app = "graph_explorer"; r.issue_id = "0080"; r.card_id = "c5"; + r.branch = "issue/0080"; r.status = "validated"; r.started_at = 1500; + r.finished_at = 5000; r.dod_total = 6; r.dod_done = 6; r.dod_validated = 5; + runs.push_back(r); + + return runs; +} + +TEST_CASE("passes_filter — empty filter matches everything", "[timeline]") { + auto runs = make_mock(); + TimelineFilter f; + for (const auto& r : runs) REQUIRE(passes_filter(r, f)); +} + +TEST_CASE("passes_filter — app filter", "[timeline]") { + auto runs = make_mock(); + TimelineFilter f; + f.apps = {"kanban_cpp"}; + int n = 0; + for (const auto& r : runs) if (passes_filter(r, f)) ++n; + REQUIRE(n == 2); + + f.apps = {"kanban_cpp", "graph_explorer"}; + n = 0; + for (const auto& r : runs) if (passes_filter(r, f)) ++n; + REQUIRE(n == 3); +} + +TEST_CASE("passes_filter — status filter combined with app", "[timeline]") { + auto runs = make_mock(); + TimelineFilter f; + f.apps = {"kanban_cpp"}; + f.statuses = {"merged"}; + int n = 0; + AgentRun match{}; + for (const auto& r : runs) { + if (passes_filter(r, f)) { ++n; match = r; } + } + REQUIRE(n == 1); + REQUIRE(match.id == "r3"); +} + +TEST_CASE("passes_filter — since_ts lower bound", "[timeline]") { + auto runs = make_mock(); + TimelineFilter f; + f.since_ts = 1000; // keep r1 (1000), r4 (1200), r5 (1500) + int n = 0; + for (const auto& r : runs) if (passes_filter(r, f)) ++n; + REQUIRE(n == 3); +} + +TEST_CASE("filter_and_sort — sorts by started_at desc", "[timeline]") { + auto runs = make_mock(); + TimelineFilter f; // no filter + auto out = filter_and_sort(runs, f); + REQUIRE(out.size() == 5); + // expected order by started_at desc: r5(1500), r4(1200), r1(1000), r2(800), r3(600) + REQUIRE(out[0].id == "r5"); + REQUIRE(out[1].id == "r4"); + REQUIRE(out[2].id == "r1"); + REQUIRE(out[3].id == "r2"); + REQUIRE(out[4].id == "r3"); +} + +TEST_CASE("filter_and_sort — combines filter and sort", "[timeline]") { + auto runs = make_mock(); + TimelineFilter f; + f.apps = {"kanban_cpp"}; + auto out = filter_and_sort(runs, f); + REQUIRE(out.size() == 2); + REQUIRE(out[0].id == "r1"); // 1000 + REQUIRE(out[1].id == "r3"); // 600 +} + +TEST_CASE("format_duration — running when finished_at=0", "[timeline]") { + REQUIRE(format_duration(1000, 0) == "running"); +} + +TEST_CASE("format_duration — seconds", "[timeline]") { + REQUIRE(format_duration(1000, 1045) == "45s"); + REQUIRE(format_duration(1000, 1000) == "0s"); +} + +TEST_CASE("format_duration — minutes + seconds", "[timeline]") { + REQUIRE(format_duration(0, 65) == "1m05s"); + REQUIRE(format_duration(0, 305) == "5m05s"); + REQUIRE(format_duration(0, 3599) == "59m59s"); +} + +TEST_CASE("format_duration — hours + minutes", "[timeline]") { + REQUIRE(format_duration(0, 3600) == "1h00m"); + REQUIRE(format_duration(0, 3600 + 720) == "1h12m"); + REQUIRE(format_duration(0, 7200 + 1800) == "2h30m"); +} + +TEST_CASE("format_duration — inverted timestamps", "[timeline]") { + REQUIRE(format_duration(2000, 1000) == "—"); +} + +TEST_CASE("status_color_token — known statuses", "[timeline]") { + REQUIRE(status_color_token("pending") == 1); + REQUIRE(status_color_token("running") == 1); + REQUIRE(status_color_token("done") == 2); + REQUIRE(status_color_token("validated") == 2); + REQUIRE(status_color_token("merged") == 3); + REQUIRE(status_color_token("aborted") == 5); + REQUIRE(status_color_token("failed") == 6); +} + +TEST_CASE("status_color_token — unknown falls back to neutral", "[timeline]") { + REQUIRE(status_color_token("") == 0); + REQUIRE(status_color_token("zombie") == 0); +} + +TEST_CASE("status_icon_id — non-empty for known statuses", "[timeline]") { + for (const std::string s : {"pending", "running", "done", "validated", + "merged", "aborted", "failed"}) { + REQUIRE_FALSE(status_icon_id(s).empty()); + } + // unknown still returns a glyph (hourglass), never empty + REQUIRE_FALSE(status_icon_id("nope").empty()); +} + +TEST_CASE("status_icon_id — distinct for distinct statuses", "[timeline]") { + auto a = status_icon_id("running"); + auto b = status_icon_id("done"); + auto c = status_icon_id("failed"); + REQUIRE(a != b); + REQUIRE(b != c); + REQUIRE(a != c); +} + +TEST_CASE("app_chip_hex — known apps", "[timeline]") { + REQUIRE(app_chip_hex("kanban_cpp") == "#a855f7"); + REQUIRE(app_chip_hex("skill_tree") == "#0ea5e9"); +} + +TEST_CASE("app_chip_hex — unknown falls back to gray", "[timeline]") { + REQUIRE(app_chip_hex("never_seen") == "#5C5F66"); + REQUIRE(app_chip_hex("") == "#5C5F66"); +}