Files
fn_registry/cpp/tests/test_agent_runs_timeline.cpp
egutierrez ecd864f2d3 test(viz): test_agent_runs_timeline — 17 cases / 53 assertions
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) <noreply@anthropic.com>
2026-05-18 18:31:37 +02:00

184 lines
6.2 KiB
C++

// 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");
}