merge issue/0118: agent_runs_timeline C++ ImGui + helpers
# Conflicts: # cpp/tests/CMakeLists.txt
This commit is contained in:
@@ -304,3 +304,7 @@ add_dependencies(test_visual primitives_gallery)
|
||||
# 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)
|
||||
|
||||
# --- 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)
|
||||
|
||||
@@ -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 <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using fn_viz::AgentRun;
|
||||
using fn_viz::TimelineFilter;
|
||||
using namespace fn_viz::timeline;
|
||||
|
||||
static std::vector<AgentRun> make_mock() {
|
||||
std::vector<AgentRun> 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");
|
||||
}
|
||||
Reference in New Issue
Block a user