Artifact cf6dd56b9959203ac26fdb06c45a1cac383510566bf7d14bb5c103c9da2a88a5:
- File
internal/web/landing_test.go
— part of check-in
[7d2a820f65]
at
2026-08-23 19:17:55
on branch trunk
— landings: the decision surface
A list of what awaits a decision and a page per record: proposal, diff against target computed at view time, discussion, and the controls. accept rides `i` because it merges; everything else is a forum cap. Transitions render as events — zit stores them as replies, which is encoding the interface exists to hide. (user: dmestas, size: 5666) [annotate] [blame] [check-ins using]
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") } }