Skip to content
quarry Artifact [e69b77e836]
Login

Artifact [e69b77e836]

Artifact e69b77e836a526f60d5ca910aa1ac4adf831fa30a13fbd12c06098406bcbabdf:


package web

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

// fakeEngine answers the run page from fixtures.
type fakeEngine struct {
	run    ci.Run
	chunks map[string][]ci.LogChunk
	// attempts answers a read that names its attempt; chunks answers
	// the engine's default (the latest attempt).
	attempts map[string]map[int][]ci.LogChunk
	noLogs   bool
}

func (f fakeEngine) RunDetail(string) (ci.Run, error) { return f.run, nil }
func (f fakeEngine) StepLogs(_, step string, attempt, _ int, _ bool) ([]ci.LogChunk, error) {
	if f.noLogs {
		return nil, ci.ErrNoLogs
	}
	if tries, ok := f.attempts[step]; ok {
		if attempt == 0 {
			for n := range tries {
				attempt = max(attempt, n)
			}
		}
		return tries[attempt], nil
	}
	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, []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: "i8"})}
	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: "i8"}),
		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")
	}
}

// The list answers "what happened" on the row: both click targets open
// the run, a red row names the step and quotes the line, durations and
// the health strip come from the same window, and filters narrow the
// list without rewriting the truth.
// fakeGate answers the tripwire's findings from a fixture.
type fakeGate struct{ rows []oltp.GateBypass }

func (f fakeGate) RecordForgeAccept(string, string) error              { return nil }
func (f fakeGate) GateBypasses(string, int) ([]oltp.GateBypass, error) { return f.rows, nil }
func (f fakeGate) GateBypassHistory(string, int, int) ([]oltp.GateBypass, int, error) {
	return f.rows, len(f.rows), nil
}
func (f fakeGate) AckGateBypass(string, string) error     { return nil }
func (f fakeGate) AckAllGateBypasses(string) (int, error) { return len(f.rows), nil }

// A protected branch that moved without a forge accept is named on the
// CI page, above everything else — the tripwire's whole point.
func TestGateBypassIsNamedOnTheCIPage(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{},
		Gate: fakeGate{rows: []oltp.GateBypass{{Repo: "alpha", Branch: "trunk",
			Checkin: "r0gue00000000000", Seen: "2026-08-29T20:00:00Z", Who: "eve"}}}}
	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"})}))
	t.Cleanup(srv.Close)
	body := asDan(t, srv, "/r/alpha/ci")
	if !strings.Contains(body, "straight to <code>trunk</code>") || !strings.Contains(body, "r0gue0000000") {
		t.Fatalf("tripwire finding not on the page")
	}
	// The finding names a person, not just a hash.
	if !strings.Contains(body, "eve committed") {
		t.Errorf("the finding does not say who")
	}
	// A tripwire needs a reset: holders of "a" can acknowledge, others
	// only see. Both see the way to the history.
	if !strings.Contains(body, `action="/r/alpha/ci/gate/ack"`) || !strings.Contains(body, `name="all"`) {
		t.Errorf("an admin cannot acknowledge findings")
	}
	if !strings.Contains(body, `href="/r/alpha/ci/gate"`) {
		t.Errorf("no way to the gate history")
	}
	hist := asDan(t, srv, "/r/alpha/ci/gate")
	if !strings.Contains(hist, "r0gue0000000") || !strings.Contains(hist, "1 finding in all") {
		t.Errorf("the history page does not list the finding")
	}
	preq, _ := http.NewRequest("POST", srv.URL+"/r/alpha/ci/gate/ack",
		strings.NewReader(url.Values{"checkin": {"r0gue00000000000"}}.Encode()))
	preq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	preq.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	res, err := srv.Client().Do(preq)
	if err != nil {
		t.Fatal(err)
	}
	res.Body.Close()
	if res.Request.URL.Path != "/r/alpha/ci" {
		t.Errorf("acknowledging landed on %s, want back on the CI page", res.Request.URL.Path)
	}

	// Without "a": the finding shows, the buttons do not. A verifier is
	// wired so that capabilities mean something — an ungated forge waves
	// everyone through by design.
	viewer := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "o8"})},
		Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })))
	t.Cleanup(viewer.Close)
	body = asDan(t, viewer, "/r/alpha/ci")
	if !strings.Contains(body, "r0gue0000000") || strings.Contains(body, "/ci/gate/ack") {
		t.Errorf("a viewer sees acknowledge buttons, or lost the finding")
	}
}

// The CI page is a page, not the whole history: a full page links to
// the older one, and an older page links back to the newer.
func TestCIRunsArePaged(t *testing.T) {
	ix := landingIndex(t, false)
	r := ops.NewRegistry()
	var runs []api.CIRun
	for i := 0; i < ciPageSize; i++ {
		runs = append(runs, api.CIRun{RunID: fmt.Sprintf("run-%03d", i), Checkin: "c0ffee0000000000",
			Branch: "trunk", Workflow: "test", Pipeline: "test", Status: "ok", Started: "2026-08-29T20:00:00Z"})
	}
	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)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, nil))
	t.Cleanup(srv.Close)
	body := landingGet(t, srv, "/r/alpha/ci")
	if !strings.Contains(body, `href="/r/alpha/ci?offset=100"`) {
		t.Errorf("a full page does not link to older runs")
	}
	if strings.Contains(body, "newer runs") {
		t.Errorf("the first page links to newer runs")
	}
	body = landingGet(t, srv, "/r/alpha/ci?offset=100")
	if !strings.Contains(body, `href="/r/alpha/ci?offset=0"`) {
		t.Errorf("the second page does not link back to newer runs")
	}
}

func TestCIListAnswersWhatFailed(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: "runfail000000000", Checkin: "c1c1c1c1c1c1c1c1", Workflow: "quarry", Pipeline: "ci/q.json",
				Status: "failed", Started: "2026-08-29T10:00:00Z", Completed: "2026-08-29T10:02:30Z",
				Branch: "trunk", Comment: "break things\nlong body", User: "mel"},
			{RunID: "runpass000000000", Checkin: "c2c2c2c2c2c2c2c2", Workflow: "quarry", Pipeline: "ci/q.json",
				Status: "completed", Started: "2026-08-29T09:00:00Z", Completed: "2026-08-29T09:01:40Z",
				Branch: "side", Comment: "fine work", User: "dan"},
		}}}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	store := fakeLogStore{chunks: []ci.LogChunk{
		{Seq: 1, Stream: "out", Data: "--- FAIL: TestThing\n", Step: "test"},
		{Seq: 2, Stream: "marker", Data: "failed", Step: "test"},
	}}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i8"}),
			WithCILogs(store)}))
	t.Cleanup(srv.Close)

	body := asDan(t, srv, "/r/alpha/ci")
	for _, want := range []string{
		`href="/r/alpha/ci/run/runfail00000"`, // the row opens the RUN
		"step <strong>test</strong> failed",   // and names the break
		"--- FAIL: TestThing",                 // quoting the line
		"break things",                        // the check-in's own story
		"<code>trunk</code>",                  //
		"2m30s",                               // duration
		"is red on trunk",                     // the health strip's banner
		"50% pass over 2",                     //
		`name="state"`,                        // the controls exist
		"Re-run",                              // and so does the way back
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("list lacks %q", want)
		}
	}
	filtered := asDan(t, srv, "/r/alpha/ci?state=pass")
	if strings.Contains(filtered, "break things") || !strings.Contains(filtered, "fine work") {
		t.Fatal("the state filter did not narrow the list")
	}
	if !strings.Contains(filtered, "is red on trunk") {
		t.Fatal("a filtered view rewrote the health strip")
	}
}

// The editor: tracked content in, a branch and a landing out — and a
// spec that does not compile comes back worded, never committed.
func TestPipelineEditorValidatesAndSaves(t *testing.T) {
	ix := landingIndex(t, false)
	var saved struct{ branch, path, user string }
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		SavePipeline: func(_ lease.Token, repo, branch, user, message, path string, body []byte) (string, error) {
			saved.branch, saved.path, saved.user = branch, path, user
			return strings.Repeat("ab", 32), nil
		}}
	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"})}))
	t.Cleanup(srv.Close)

	page := asDan(t, srv, "/r/alpha/ci/pipeline?new=1")
	for _, want := range []string{"tracked content", `name="content"`, "my-test-task", "Validate"} {
		if !strings.Contains(page, want) {
			t.Fatalf("editor lacks %q", want)
		}
	}
	postDan := func(form url.Values) *http.Response {
		req, _ := http.NewRequest("POST", srv.URL+"/r/alpha/ci/pipeline", strings.NewReader(form.Encode()))
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
		req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		return resp
	}
	resp := postDan(url.Values{"path": {"ci/x.json"}, "content": {`{"steps":[]}`}, "action": {"validate"}})
	b, _ := io.ReadAll(resp.Body)
	resp.Body.Close()
	if !strings.Contains(string(b), "the workflow has no name") {
		t.Fatalf("diagnostics not rendered:\n%s", b)
	}
	resp2 := postDan(url.Values{"path": {"ci/x.json"},
		"content": {`{"name":"x","steps":[{"id":"t","task":"tt"}]}`}, "action": {"save"}})
	resp2.Body.Close()
	if saved.path != "ci/x.json" || !strings.HasPrefix(saved.branch, "ci-edit-x-") || saved.user != "dan" {
		t.Fatalf("save did not commit as expected: %+v", saved)
	}
}