There is no test target. Verification is done by driving the TUI under script -qec with piped
keystrokes and reading the last rendered frame, which works but is manual, slow, and only ever run
against the thing being changed at that moment.
This is recorded as an open question in progress.md ("Test strategy: unit tests against fixture
repos created at test time? Which framework?") and as a known limit in the 2026.08.0 changelog. It
is the largest single gap in the project.
Why it is worth doing now rather than later
Three bugs fixed recently were all invariant violations that a test would have caught immediately
and that manual driving nearly missed:
- The CI job drill-down indexed the unfiltered
pipelines_.runs with a cursor that indexes the
visible list, so with a / filter active, enter opened a different run than the one
highlighted.
RebuildFilter() was missing from EnsureHistory, CollectPipelines and CollectPulls, so a
filtered view kept drawing the response before the one that just landed.
- Two hardcoded glyphs (
"❯" in app.cpp, "…" in signin_panel.cpp) bypassed the glyph table
and would render as tofu on a terminal that had been given the ascii set.
None of these needed a UI to find. All three are assertions about pure functions.
Where the value is concentrated
The codebase is unusually well set up for this, because the expensive parts are already pure:
- Reads return snapshots and touch no UI state —
ReadStatus, ReadHistory, FetchRepoInfo,
FetchPipelines, FetchPulls. Each is a function from a fixture to a value.
- Provider normalization is the highest-value target.
model::RunStatus folds GitHub's split
status/conclusion and GitLab's eleven-value single field into one type in
remote/pipelines.cpp. That is a table of cases, it is exactly where a provider change will break
things silently, and it needs no repository and no network — just recorded JSON.
- Text measurement.
TextWidth, Truncate, Fit, Rjust are cell-accurate and the whole
point is that they do not tear a double-width glyph. Trivially testable, currently untested.
- The config reader/writer round-trips a strict TOML subset and reports every rejection by name.
- Filtering.
RebuildFilter and the Visible* mirrors, which is where two of the three bugs
above lived.
- Lane assignment in
graph.cpp.
Proposed
- Pick a framework. Catch2 or doctest — both are header-only and both drop into the existing
FetchContent arrangement without a new system dependency, which matters given the build currently
needs only libcurl and zlib.
- Fixture repositories built at test time with libgit2 rather than checked in, so they are readable
as code and cannot rot into a binary blob nobody can regenerate.
- Recorded provider JSON for the normalization tests. The stub-API approach already described in
CLAUDE.md is the right shape; the responses it serves should become fixtures.
- Wire it to CTest and run it in the release workflow before packaging. The workflow currently
builds and packages without ever executing a test.
Not in scope: driving the full TUI in CI. The manual approach stays useful for render checks; this
issue is about the layer underneath it.
There is no test target. Verification is done by driving the TUI under
script -qecwith pipedkeystrokes and reading the last rendered frame, which works but is manual, slow, and only ever run
against the thing being changed at that moment.
This is recorded as an open question in
progress.md("Test strategy: unit tests against fixturerepos created at test time? Which framework?") and as a known limit in the 2026.08.0 changelog. It
is the largest single gap in the project.
Why it is worth doing now rather than later
Three bugs fixed recently were all invariant violations that a test would have caught immediately
and that manual driving nearly missed:
pipelines_.runswith a cursor that indexes thevisible list, so with a
/filter active,enteropened a different run than the onehighlighted.
RebuildFilter()was missing fromEnsureHistory,CollectPipelinesandCollectPulls, so afiltered view kept drawing the response before the one that just landed.
"❯"inapp.cpp,"…"insignin_panel.cpp) bypassed the glyph tableand would render as tofu on a terminal that had been given the ascii set.
None of these needed a UI to find. All three are assertions about pure functions.
Where the value is concentrated
The codebase is unusually well set up for this, because the expensive parts are already pure:
ReadStatus,ReadHistory,FetchRepoInfo,FetchPipelines,FetchPulls. Each is a function from a fixture to a value.model::RunStatusfolds GitHub's splitstatus/conclusionand GitLab's eleven-value single field into one type inremote/pipelines.cpp. That is a table of cases, it is exactly where a provider change will breakthings silently, and it needs no repository and no network — just recorded JSON.
TextWidth,Truncate,Fit,Rjustare cell-accurate and the wholepoint is that they do not tear a double-width glyph. Trivially testable, currently untested.
RebuildFilterand theVisible*mirrors, which is where two of the three bugsabove lived.
graph.cpp.Proposed
FetchContent arrangement without a new system dependency, which matters given the build currently
needs only libcurl and zlib.
as code and cannot rot into a binary blob nobody can regenerate.
CLAUDE.mdis the right shape; the responses it serves should become fixtures.builds and packages without ever executing a test.
Not in scope: driving the full TUI in CI. The manual approach stays useful for render checks; this
issue is about the layer underneath it.