Skip to content
quarry Artifact [d055b6b819]
Login

Artifact [d055b6b819]

Artifact d055b6b819162a8399582863b3e4f31aee57afc56eab19ff39d7aca6fc7ceae9:


package web

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

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

// fakeEngine answers the run page from fixtures.
type fakeEngine struct {
	run    ci.Run
	chunks map[string][]ci.LogChunk
	noLogs bool
}

func (f fakeEngine) RunDetail(string) (ci.Run, error) { return f.run, nil }
func (f fakeEngine) StepLogs(_, step string, _ int, _ bool) ([]ci.LogChunk, error) {
	if f.noLogs {
		return nil, ci.ErrNoLogs
	}
	return f.chunks[step], nil
}
func (f fakeEngine) Workers() ([]ci.Worker, error)           { return nil, nil }
func (f fakeEngine) WorkerTokens() ([]ci.WorkerToken, error) { return nil, nil }
func (f fakeEngine) MintWorkerToken(string, []string) (ci.WorkerToken, error) {
	return ci.WorkerToken{}, nil
}
func (f fakeEngine) RevokeWorkerToken(string) error { return nil }

func runPageServer(t *testing.T, eng EngineViews, runs []api.CIRun) *httptest.Server {
	t.Helper()
	ix := landingIndex(t, false)
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		CI:    fakeCI{runs: runs}}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	opts := []Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"})}
	if eng != nil {
		opts = append(opts, WithEngine(eng))
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, opts))
	t.Cleanup(srv.Close)
	return srv
}

// RFC 0009 §logs: the run page shows steps in order with durations,
// opens the failed step, tints stderr, records the failure marker, and
// prints the repro line.
func TestRunPageShowsStepsAndLogs(t *testing.T) {
	eng := fakeEngine{
		run: ci.Run{RunID: "run-1", Workflow: "quarry-e2e", Status: "failed",
			CreatedAt: "2026-08-29T01:00:00Z", CompletedAt: "2026-08-29T01:03:30Z",
			Input: map[string]any{"cloneUrl": "http://f/r/alpha", "checkin": "abc123"},
			Steps: map[string]ci.Step{
				"test":  {Status: "completed", Attempts: 1, StartedAt: "2026-08-29T01:00:01Z", CompletedAt: "2026-08-29T01:01:00Z"},
				"build": {Status: "failed", Attempts: 2, Error: "exit 1", StartedAt: "2026-08-29T01:01:01Z", CompletedAt: "2026-08-29T01:03:00Z"},
			}},
		chunks: map[string][]ci.LogChunk{
			"build": {{Seq: 1, Stream: "out", Data: "compiling"},
				{Seq: 2, Stream: "err", Data: "undefined: frob"},
				{Seq: 3, Stream: "marker", Data: "failed"}},
			"test": {{Seq: 1, Stream: "out", Data: "ok 12 tests"}},
		}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-1", Checkin: "abc123", Workflow: "quarry-e2e", Pipeline: "ci/test.json", Status: "failed", Started: "2026-08-29T01:00:00Z"}})
	body := asDan(t, srv, "/r/alpha/ci/run/run-1")
	for _, want := range []string{
		"took 3m30s",                      // run duration
		"<strong>test</strong>",           // steps present
		"<strong>build</strong>",          //
		"59s",                             // per-step duration (#626)
		"attempt 2",                       //
		"exit 1",                          //
		`runstep" open`,                   // failed step open
		"undefined: frob",                 // stderr chunk
		`class="ll ll-err"`,               // tinted
		`class="ll ll-mark"`,              // marker recorded
		"zit clone http://f/r/alpha work", // repro line
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("run page lacks %q", want)
		}
	}
	// The passed step is folded.
	if strings.Contains(body, `<details class="d-row runstep" open>
<summary><svg class="ic d-ic pass"`) {
		t.Fatal("a green step rendered open")
	}
}

// An engine that predates dagnats#624 says so instead of an empty pane.
func TestRunPageSaysWhenLogsAreNotServed(t *testing.T) {
	eng := fakeEngine{noLogs: true,
		run: ci.Run{RunID: "run-2", Status: "completed",
			Steps: map[string]ci.Step{"test": {Status: "completed", Attempts: 1}}}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-2", Checkin: "abc", Workflow: "w", Pipeline: "p", Status: "completed", Started: "2026-08-29T01:00:00Z"}})
	body := asDan(t, srv, "/r/alpha/ci/run/run-2")
	if !strings.Contains(body, "does not serve logs yet (dagnats#624)") {
		t.Fatal("the absence is not named")
	}
}

// No engine at all: the forge's own record, said as that.
func TestRunPageWithoutAnEngine(t *testing.T) {
	srv := runPageServer(t, nil, []api.CIRun{{RunID: "run-3", Checkin: "abc", Workflow: "w", Pipeline: "p", Status: "running", Started: "2026-08-29T01:00:00Z"}})
	body := asDan(t, srv, "/r/alpha/ci/run/run-3")
	if !strings.Contains(body, "No CI engine is configured") {
		t.Fatal("the absence is not named")
	}
}

// fakeLogStore is the forge's drained record in a map.
type fakeLogStore struct{ chunks []ci.LogChunk }

func (f fakeLogStore) DrainedLogs(string, string) ([]ci.LogChunk, bool) {
	return f.chunks, len(f.chunks) > 0
}

// RFC 0009 §logs: the drained record answers first — a run whose hot
// lane has expired (or whose engine is gone) still shows its logs.
func TestRunPageReadsDrainedLogs(t *testing.T) {
	ix := landingIndex(t, false)
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		CI: fakeCI{runs: []api.CIRun{{RunID: "run-9", Checkin: "abc", Workflow: "w",
			Pipeline: "ci/test.json", Status: "failed", Started: "2026-08-29T01:00:00Z"}}}}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	eng := fakeEngine{run: ci.Run{RunID: "run-9", Status: "failed",
		Steps: map[string]ci.Step{"build": {Status: "failed", Attempts: 1}}}, noLogs: true}
	store := fakeLogStore{chunks: []ci.LogChunk{
		{Seq: 1, Stream: "out", Data: "compiling", Step: "build"},
		{Seq: 2, Stream: "err", Data: "boom", Step: "build"}}}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{
		WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"}),
		WithEngine(eng), WithCILogs(store)}))
	t.Cleanup(srv.Close)
	body := asDan(t, srv, "/r/alpha/ci/run/run-9")
	if !strings.Contains(body, "boom") || strings.Contains(body, "does not serve logs yet") {
		t.Fatal("the drained record did not answer first")
	}
}

// A cancelled run was stopped, not broken — superseded by a newer
// check-in, or called off by hand. It must not wear the failure badge,
// must not offer a "failing task" repro, and must not follow itself.
func TestCancelledRunIsNotDressedAsAFailure(t *testing.T) {
	eng := fakeEngine{noLogs: true,
		run: ci.Run{RunID: "run-c", Workflow: "quarry-e2e", Status: "cancelled",
			Input: map[string]any{"cloneUrl": "http://f/r/alpha", "checkin": "abc123"},
			Steps: map[string]ci.Step{"build": {Status: "cancelled", Attempts: 1}}}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-c", Checkin: "abc123", Workflow: "quarry-e2e", Pipeline: "ci/test.json", Status: "cancelled", Started: "2026-08-29T01:00:00Z"}})
	body := asDan(t, srv, "/r/alpha/ci/run/run-c")
	if !strings.Contains(body, ">cancelled</span>") {
		t.Fatalf("no cancelled badge:\n%s", body)
	}
	// "ci-fail" alone lives in the stylesheet of every page; the chip
	// usage is what must be absent.
	for _, no := range []string{"tl-badge ci-fail", "Reproduce", "data-on-interval"} {
		if strings.Contains(body, no) {
			t.Errorf("cancelled run page contains %q", no)
		}
	}
}

// A step an earlier failure kept from dispatching reads "not run" and
// sinks below the steps that did — never "pending" on a finished run.
func TestUndispatchedStepOnAFinishedRun(t *testing.T) {
	eng := fakeEngine{noLogs: true,
		run: ci.Run{RunID: "run-f", Workflow: "quarry-e2e", Status: "failed",
			Steps: map[string]ci.Step{
				"build": {Status: "pending"},
				"test":  {Status: "failed", Attempts: 1, StartedAt: "2026-08-29T01:00:01Z"},
			}}}
	srv := runPageServer(t, eng, []api.CIRun{{RunID: "run-f", Checkin: "abc123", Workflow: "quarry-e2e", Pipeline: "ci/test.json", Status: "failed", Started: "2026-08-29T01:00:00Z"}})
	body := asDan(t, srv, "/r/alpha/ci/run/run-f")
	if !strings.Contains(body, "not run") || strings.Contains(body, "pending") {
		t.Fatalf("undispatched step not worded 'not run'")
	}
	if strings.Index(body, "<strong>test</strong>") > strings.Index(body, "<strong>build</strong>") {
		t.Fatal("not-run step did not sink below the executed step")
	}
	if !strings.Contains(body, "Not dispatched") {
		t.Fatal("missing the why")
	}
}