feat(stuck): stuck-state pipeline overlay (cad-atlas#11)
CD / Build & Import (push) Failing after 3s
CD / Detect unsubstituted template (push) Successful in 1s
CD / Lint / Test / Vet (push) Successful in 5s
CD / var-go/oath (push) Skipped
CD / Deploy via GitOps (push) Skipped

Consumes mathias/cad#14's cad-stuck query directly — cad's app/domain
packages are public Go modules, so this reuses the exact tested
classification logic (import, not re-implementation) rather than
duplicating the taxonomy across two repos.

ApplyStuckOverlay (internal/atlas/stuck.go, fixture-tested) maps each
finding's category onto the existing 9-stage pipeline: armed_never_fired to
the human dispatch gate, claimed_never_ran/ceiling_hit/refire_loop/
near_attempt_cap to the one Execute stage (there is no separate Claim
stage), done_not_closed to the loop-back stage. Adds a StuckCount/
AwaitingCount badge per stage plus one Node per finding, linking out to the
underlying issue/comment — read-only, no controls, matching cad#14's own
constraint.

Two visual classes, kept deliberately distinct per the issue's own framing:
a coral 'N stuck' badge (needs intervention) vs a green 'N awaiting' badge
(a functioning control waiting on a human) — done_not_closed can never
render as a bottleneck.

Verified against the live estate: STAGE 05 shows 2 stuck (cobalt-dingo#30
ceiling_hit, cad-atlas#10 near_attempt_cap), STAGE 08 shows 5 awaiting
(cobalt-dingo #3/#7/#12/#16/#19 done_not_closed) — screenshotted via a
koala k3s Playwright job against a live-token server, matching cad#14's own
proven query output exactly.

go.mod: added git.d-ma.be/mathias/cad v0.10.0 (app/domain/adapters-gitearead
only — no mcp-chassis pulled in, that's only imported by cad's own cmd/cad).
Dockerfile: added the same GOPRIVATE/insteadOf fetch config mathias/cad's
own Dockerfile uses, now needed for this new private-module dependency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 13:27:02 +02:00
co-authored by Claude Sonnet 5
parent 4ab65a3b1b
commit 97d0f56383
9 changed files with 327 additions and 8 deletions
+110
View File
@@ -0,0 +1,110 @@
package atlas_test
import (
"testing"
"git.d-ma.be/mathias/cad-atlas/internal/atlas"
caddomain "git.d-ma.be/mathias/cad/domain"
)
func stage(no, generate string) atlas.Stage {
return atlas.Stage{No: no, Title: "example", Generate: generate}
}
func finding(repo string, issue int, cat caddomain.StuckCategory, class caddomain.StuckClass) caddomain.StuckFinding {
return caddomain.StuckFinding{
Repo: repo, Issue: issue, Title: "example", Category: cat, Class: class,
Detail: "detail",
Locators: []string{"https://git.d-ma.be/" + repo + "/issues/1"},
}
}
func TestApplyStuckOverlay_BadgesOnlyOnGeneratedStages(t *testing.T) {
stages := []atlas.Stage{
stage("STAGE 04", ""), // no generate marker — must be untouched
stage("STAGE 04b", "stuck-badges"), // marked but no findings map here
}
out := atlas.ApplyStuckOverlay(stages, nil)
if len(out[0].Nodes) != 0 || out[0].StuckCount != 0 {
t.Fatalf("un-marked stage was touched: %+v", out[0])
}
}
func TestApplyStuckOverlay_ArmedNeverFiredGoesToStage04(t *testing.T) {
stages := []atlas.Stage{stage("STAGE 04", "stuck-badges")}
findings := []caddomain.StuckFinding{
finding("mathias/brain-gardener", 190, caddomain.CategoryArmedNeverFired, caddomain.ClassStuck),
}
out := atlas.ApplyStuckOverlay(stages, findings)
if out[0].StuckCount != 1 || out[0].AwaitingCount != 0 {
t.Fatalf("counts = stuck=%d awaiting=%d, want stuck=1 awaiting=0", out[0].StuckCount, out[0].AwaitingCount)
}
if len(out[0].Nodes) != 1 {
t.Fatalf("want 1 node, got %d", len(out[0].Nodes))
}
n := out[0].Nodes[0]
if n.Cls != "stuck" {
t.Errorf("Cls = %q, want %q", n.Cls, "stuck")
}
if n.URL == "" {
t.Error("node has no URL locator")
}
if n.Title != "#190 armed_never_fired" {
t.Errorf("Title = %q", n.Title)
}
}
func TestApplyStuckOverlay_DoneNotClosedIsAwaitingHumanAtStage08(t *testing.T) {
stages := []atlas.Stage{stage("STAGE 08", "stuck-badges")}
findings := []caddomain.StuckFinding{
finding("mathias/cobalt-dingo", 19, caddomain.CategoryDoneNotClosed, caddomain.ClassAwaitingHuman),
finding("mathias/cobalt-dingo", 16, caddomain.CategoryDoneNotClosed, caddomain.ClassAwaitingHuman),
}
out := atlas.ApplyStuckOverlay(stages, findings)
if out[0].StuckCount != 0 || out[0].AwaitingCount != 2 {
t.Fatalf("counts = stuck=%d awaiting=%d, want stuck=0 awaiting=2 — done_not_closed must never render as stuck", out[0].StuckCount, out[0].AwaitingCount)
}
for _, n := range out[0].Nodes {
if n.Cls != "awaiting-human" {
t.Errorf("node Cls = %q, want %q", n.Cls, "awaiting-human")
}
}
}
func TestApplyStuckOverlay_ExecutionCategoriesCollapseToStage05(t *testing.T) {
stages := []atlas.Stage{stage("STAGE 05", "stuck-badges")}
findings := []caddomain.StuckFinding{
finding("mathias/cobalt-dingo", 30, caddomain.CategoryCeilingHit, caddomain.ClassStuck),
finding("mathias/cad-atlas", 10, caddomain.CategoryNearAttemptCap, caddomain.ClassStuck),
finding("mathias/cobalt-dingo", 16, caddomain.CategoryReFireLoop, caddomain.ClassStuck),
finding("mathias/cobalt-dingo", 41, caddomain.CategoryClaimedNeverRan, caddomain.ClassStuck),
}
out := atlas.ApplyStuckOverlay(stages, findings)
if out[0].StuckCount != 4 {
t.Fatalf("StuckCount = %d, want 4", out[0].StuckCount)
}
}
func TestApplyStuckOverlay_NodesPrependedNotReplacingAuthoredContent(t *testing.T) {
s := stage("STAGE 04", "stuck-badges")
s.Nodes = []atlas.Node{{Title: "authored node"}}
out := atlas.ApplyStuckOverlay([]atlas.Stage{s}, []caddomain.StuckFinding{
finding("mathias/brain-gardener", 190, caddomain.CategoryArmedNeverFired, caddomain.ClassStuck),
})
if len(out[0].Nodes) != 2 {
t.Fatalf("want 2 nodes (1 live + 1 authored), got %d", len(out[0].Nodes))
}
if out[0].Nodes[len(out[0].Nodes)-1].Title != "authored node" {
t.Errorf("authored node was not preserved: %+v", out[0].Nodes)
}
}
func TestApplyStuckOverlay_DoesNotMutateInput(t *testing.T) {
stages := []atlas.Stage{stage("STAGE 04", "stuck-badges")}
_ = atlas.ApplyStuckOverlay(stages, []caddomain.StuckFinding{
finding("mathias/brain-gardener", 190, caddomain.CategoryArmedNeverFired, caddomain.ClassStuck),
})
if len(stages[0].Nodes) != 0 {
t.Fatalf("input slice was mutated: %+v", stages[0].Nodes)
}
}