Skip to content
quarry Artifact [e750978dcb]
Login

Artifact [e750978dcb]

Artifact e750978dcb0a3882dd0a6d1276340286cd9040d219bf87c22271f58c8bb597de:


package web

import (
	"net/http/httptest"
	"regexp"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/ci"
	"craftdesign.group/quarry/internal/ops"
)

// hookAttrs reads the data-* attributes of every element carrying
// data-hook="name", in page order.
func hookAttrs(body, name string) []map[string]string {
	tag := regexp.MustCompile(`<[a-z]+[^>]*data-hook="` + regexp.QuoteMeta(name) + `"[^>]*>`)
	attr := regexp.MustCompile(`(data-[a-z-]+)="([^"]*)"`)
	var out []map[string]string
	for _, t := range tag.FindAllString(body, -1) {
		m := map[string]string{}
		for _, a := range attr.FindAllStringSubmatch(t, -1) {
			m[a[1]] = a[2]
		}
		out = append(out, m)
	}
	return out
}

func stepHook(t *testing.T, body, step string) map[string]string {
	t.Helper()
	for _, h := range hookAttrs(body, "run-step") {
		if h["data-step"] == step {
			return h
		}
	}
	t.Fatalf("no run-step hook for %q", step)
	return nil
}

var failedThenRunning = map[int][]ci.LogChunk{
	1: {{Seq: 1, Stream: "out", Data: "--- FAIL: TestFlake"}, {Seq: 2, Stream: "marker", Data: "failed"}},
	2: {{Seq: 1, Stream: "out", Data: "=== RUN TestFlake"}},
}

// #283: a step on its second of three attempts says so, and says the
// one before it failed. The verdict is still open, so it still runs.
func TestRunPageNamesTheAttemptOfARetriedStep(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "run-a", Workflow: "quarry", Status: "running",
			Steps: map[string]ci.Step{"test": {Status: "running", Attempts: 2, StartedAt: "2026-08-29T01:00:01Z"}}},
		attempts: map[string]map[int][]ci.LogChunk{"test": failedThenRunning},
		limits:   map[string]int{"test": 3}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-a", Checkin: "abc", Workflow: "quarry", Pipeline: "ci/quarry.json", Status: "running", Started: "2026-08-29T01:00:00Z"}})
	h := stepHook(t, asDan(t, srv, "/r/alpha/ci/run/run-a"), "test")
	if h["data-attempt"] != "2" || h["data-attempts"] != "3" || h["data-prev-outcome"] != "fail" || h["data-state"] != "run" {
		t.Fatalf("step hook = %v, want attempt 2 of 3, previous fail, running", h)
	}
}

// #283: the last allowed attempt failed while the engine still reports
// the step — and another — running. Nothing can change the verdict, so
// nothing spins: the exhausted step is failed, the other is stopped.
func TestRunPageStopsTheSpinnerOnceTheVerdictIsIn(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "run-b", Workflow: "quarry", Status: "running",
			Steps: map[string]ci.Step{
				"test": {Status: "running", Attempts: 3, StartedAt: "2026-08-29T01:00:01Z"},
				"fmt":  {Status: "running", Attempts: 1, StartedAt: "2026-08-29T01:00:02Z"},
			}},
		attempts: map[string]map[int][]ci.LogChunk{
			"test": {
				1: {{Seq: 1, Stream: "marker", Data: "failed"}},
				2: {{Seq: 1, Stream: "marker", Data: "failed"}},
				3: {{Seq: 1, Stream: "out", Data: "--- FAIL: TestFlake"}, {Seq: 2, Stream: "marker", Data: "failed"}},
			},
			"fmt": {1: {{Seq: 1, Stream: "out", Data: "gofmt -l"}}},
		},
		limits: map[string]int{"test": 3, "fmt": 3}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-b", Checkin: "abc", Workflow: "quarry", Pipeline: "ci/quarry.json", Status: "running", Started: "2026-08-29T01:00:00Z"}})
	body := asDan(t, srv, "/r/alpha/ci/run/run-b")
	if h := stepHook(t, body, "test"); h["data-state"] != "fail" || h["data-attempt"] != "3" || h["data-attempts"] != "3" {
		t.Errorf("exhausted step = %v, want fail on attempt 3 of 3", h)
	}
	if h := stepHook(t, body, "fmt"); h["data-state"] == "run" {
		t.Errorf("a step that can no longer change the verdict still spins: %v", h)
	}
}

// #283: the engine has failed the run, but a lane still reports a step
// running. The run is over; the step does not spin.
func TestRunPageDoesNotSpinInAFinishedRun(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "run-c", Workflow: "quarry", Status: "failed",
			Steps: map[string]ci.Step{
				"fmt":  {Status: "failed", Attempts: 3, StartedAt: "2026-08-29T01:00:01Z"},
				"test": {Status: "running", Attempts: 2, StartedAt: "2026-08-29T01:00:02Z"},
			}},
		attempts: map[string]map[int][]ci.LogChunk{"test": failedThenRunning},
		chunks:   map[string][]ci.LogChunk{"fmt": {{Seq: 1, Stream: "marker", Data: "failed"}}},
		limits:   map[string]int{"test": 3, "fmt": 3}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-c", Checkin: "abc", Workflow: "quarry", Pipeline: "ci/quarry.json", Status: "failed", Started: "2026-08-29T01:00:00Z"}})
	if h := stepHook(t, asDan(t, srv, "/r/alpha/ci/run/run-c"), "test"); h["data-state"] == "run" {
		t.Fatalf("a finished run still spins a step: %v", h)
	}
}

func landingWithEngine(t *testing.T, eng EngineViews, status string) string {
	t.Helper()
	ix := landingIndex(t, false)
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos:  api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		Checks: fakeChecks{required: map[string][]string{"trunk": {"quarry"}}},
		CI: fakeCI{runs: []api.CIRun{
			{RunID: "runretry00000000", Workflow: "quarry", Pipeline: "ci/quarry.json", Status: status, Started: "2026-08-29"},
		}}}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i8"}), WithEngine(eng)}))
	t.Cleanup(srv.Close)
	return asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
}

var checksIcon = regexp.MustCompile(`<details class="d-row" id="l-checks"[^>]*>\s*<summary><svg class="ic d-ic (\w+)"`)

// #283, the landing card: a check whose step is mid-retry names the
// attempt and the failed one before it, and is still running.
func TestLandingCheckNamesTheAttempt(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "runretry00000000", Workflow: "quarry", Status: "running",
			Steps: map[string]ci.Step{"test": {Status: "running", Attempts: 2}}},
		attempts: map[string]map[int][]ci.LogChunk{"test": failedThenRunning},
		limits:   map[string]int{"test": 3}}
	body := landingWithEngine(t, eng, "running")
	steps := hookAttrs(body, "check-step")
	if len(steps) != 1 || steps[0]["data-attempt"] != "2" || steps[0]["data-attempts"] != "3" || steps[0]["data-prev-outcome"] != "fail" {
		t.Fatalf("check-step hooks = %v, want attempt 2 of 3, previous fail", steps)
	}
	if rows := hookAttrs(body, "check-row"); len(rows) != 1 || rows[0]["data-state"] != "running" {
		t.Fatalf("check-row hooks = %v, want one running", rows)
	}
}

// #283, the landing card's gate line: the last allowed attempt failed,
// so the check is red and the gate stops waiting, whatever the engine
// still says about the lane.
func TestLandingGateStopsWaitingOnceTheVerdictIsIn(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "runretry00000000", Workflow: "quarry", Status: "running",
			Steps: map[string]ci.Step{"test": {Status: "running", Attempts: 3}}},
		attempts: map[string]map[int][]ci.LogChunk{"test": {
			2: {{Seq: 1, Stream: "marker", Data: "failed"}},
			3: {{Seq: 1, Stream: "out", Data: "--- FAIL: TestFlake"}, {Seq: 2, Stream: "marker", Data: "failed"}},
		}},
		limits: map[string]int{"test": 3}}
	body := landingWithEngine(t, eng, "running")
	if rows := hookAttrs(body, "check-row"); len(rows) != 1 || rows[0]["data-state"] != "fail" {
		t.Fatalf("check-row hooks = %v, want one fail", rows)
	}
	if m := checksIcon.FindStringSubmatch(body); m == nil || m[1] != "fail" {
		t.Fatalf("gate line state = %v, want fail", m)
	}
	if strings.Contains(body, "/landing.sse/") {
		t.Error("the card still follows a run whose verdict is in")
	}
}