Skip to content
quarry Artifact [cf6dd56b99]
Login

Artifact [cf6dd56b99]

Artifact cf6dd56b9959203ac26fdb06c45a1cac383510566bf7d14bb5c103c9da2a88a5:


package web

import (
	"io"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
)

// A landing is a forum thread with a marker body (zit#127), so the pages are
// tested the way the store actually presents one: fold the artifacts, then
// read the surfaces. No zit CLI is involved, because quarry reads its index.
func landingIndex(t *testing.T, withTransition bool) *index.Index {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	root := strings.Repeat("a1", 20)
	batch := []index.Parsed{
		{Hash: root, Art: &artifact.Artifact{Type: artifact.Forum,
			ThreadTitle: "land the caps work", Date: "2026-08-20T09:00:00", User: "dan",
			Mimetype: "text/x-markdown",
			Body: []byte("zit-landing: 1\nbranch: fossil-caps\ntarget: trunk\n\n" +
				"Capability model, phases one through five.\n")}},
		{Hash: strings.Repeat("b2", 20), Art: &artifact.Artifact{Type: artifact.Forum,
			ThreadRoot: root, InReplyTo: root, Date: "2026-08-20T10:00:00", User: "eve",
			Mimetype: "text/x-markdown", Body: []byte("Reads well. One question about `e`.\n")}},
	}
	if withTransition {
		batch = append(batch, index.Parsed{Hash: strings.Repeat("c3", 20),
			Art: &artifact.Artifact{Type: artifact.Forum, ThreadRoot: root, InReplyTo: root,
				Date: "2026-08-20T11:00:00", User: "dan", Mimetype: "text/x-markdown",
				Body: []byte("zit-landing-state: accepted\n")}})
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	return ix
}

func landingServer(t *testing.T, ix *index.Index, caps string) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"})}))
	t.Cleanup(srv.Close)
	return srv
}

func landingGet(t *testing.T, srv *httptest.Server, path string) string {
	t.Helper()
	req, _ := http.NewRequest("GET", srv.URL+path, nil)
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	resp, err := srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		t.Fatalf("GET %s: %d", path, resp.StatusCode)
	}
	b, _ := io.ReadAll(resp.Body)
	return string(b)
}

// The listing answers one question — what is awaiting a decision — and a
// landing's identity is the pair of branch names, so both must be on the row.
func TestLandingListShowsWhatAwaitsDecision(t *testing.T) {
	srv := landingServer(t, landingIndex(t, false), "")
	html := landingGet(t, srv, "/r/alpha/landing")
	for _, want := range []string{"land the caps work", "fossil-caps", "trunk",
		`href="/r/alpha/landing/`} {
		if !strings.Contains(html, want) {
			t.Errorf("the listing does not show %q:\n%s", want, html)
		}
	}
	// A settled record is history; the default view is the queue.
	settled := landingGet(t, landingServer(t, landingIndex(t, true), ""), "/r/alpha/landing")
	if strings.Contains(settled, "land the caps work") {
		t.Error("an accepted landing is still listed as awaiting decision")
	}
	if !strings.Contains(landingGet(t, landingServer(t, landingIndex(t, true), ""),
		"/r/alpha/landing?state=accepted"), "land the caps work") {
		t.Error("an accepted landing cannot be found under accepted")
	}
}

// zit records a state change as a reply, because a reply is an artifact
// fossil understands. A reader wants it as an EVENT — "dan accepted this" —
// not as a comment reading "zit-landing-state: accepted", which is the
// encoding leaking through the interface that exists to hide it.
func TestLandingDetailLiftsTransitionsOutOfTheDiscussion(t *testing.T) {
	srv := landingServer(t, landingIndex(t, true), "")
	html := landingGet(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))

	if strings.Contains(html, "zit-landing-state") || strings.Contains(html, "zit-landing: 1") {
		t.Errorf("the record's encoding reached the page:\n%s", html)
	}
	if !strings.Contains(html, `class="l-events"`) || !strings.Contains(html, "accepted this") {
		t.Errorf("the state change is not on the record's timeline:\n%s", html)
	}
	// The opener is the proposal, not a comment on it.
	if !strings.Contains(html, "Capability model") {
		t.Error("the proposal's own body is missing")
	}
	// The remark IS a comment.
	if !strings.Contains(html, "One question about") {
		t.Error("the discussion is missing")
	}
	// A settled record offers no decision controls.
	if strings.Contains(html, "Accept and land") {
		t.Error("an already-accepted landing still offers to be accepted")
	}
}

// The accept control is the one that merges, so it appears only for a viewer
// who could actually land it. With no capability plane configured there is no
// caps answer at all, and a control that cannot work must not be drawn.
func TestLandingAcceptControlFollowsCapability(t *testing.T) {
	srv := landingServer(t, landingIndex(t, false), "")
	html := landingGet(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if strings.Contains(html, "Accept and land") {
		t.Errorf("the accept control was drawn without the check-in capability:\n%s", html)
	}
	// Rejecting writes no check-in and rides the forum capability, so an
	// ungated forge — which has no strangers — may still settle a record.
	if !strings.Contains(html, "Reject") {
		t.Error("no way to settle a landing without merging it")
	}
}