af26c78e70
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>
184 lines
6.2 KiB
C++
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");
|
|
}
|