Artifact 80aa71c509965362882980977d7d84cecaf9ecc244994d7b8ade4e26a5b9b596:
- File
internal/web/landing_test.go
— part of check-in
[302ca7fae6]
at
2026-08-24 02:39:40
on branch trunk
— forum: landings are not forum content
A landing is a forum thread — that is what a stock fossil clone reads — but it is a decision record with its own surface. Off the listing and out of the count, which must agree. An existing thread URL redirects to the record rather than 404ing a link that used to work. (user: dmestas, size: 7917) [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") } } // A landing IS a forum thread — that is what lets a stock fossil clone read // one — but it is not forum CONTENT. It has its own surface, its own controls // and its own listing, and a reader scanning discussions has no use for a // thread whose first line is a marker. // // So: absent from the forum listing, absent from the count above it (a number // that promises a thread the list refuses to show is the same confirmation // leak in miniature), and an existing link redirected rather than refused. func TestLandingsAreNotForumContent(t *testing.T) { ix := landingIndex(t, false) // An ordinary thread, so the forum has something to show and the // test cannot pass by everything being empty. if err := ix.FoldNow([]index.Parsed{ {Hash: strings.Repeat("d4", 20), Art: &artifact.Artifact{Type: artifact.Forum, ThreadTitle: "an ordinary question", Date: "2026-08-21T09:00:00", User: "eve", Mimetype: "text/x-markdown", Body: []byte("how does this work?\n")}}, }); err != nil { t.Fatal(err) } srv := landingServer(t, ix, "") list := landingGet(t, srv, "/r/alpha/forum") if !strings.Contains(list, "an ordinary question") { t.Fatalf("the forum lost its ordinary threads:\n%s", list) } if strings.Contains(list, "land the caps work") { t.Error("a landing record was listed as a forum thread") } // The tab's count must agree with the list beneath it. counts := landingGet(t, srv, "/r/alpha/forum") if strings.Contains(counts, `title="2"`) { t.Error("the forum count includes a thread the listing hides") } // An existing link still resolves — to the surface that owns it. req, _ := http.NewRequest("GET", srv.URL+"/r/alpha/forum/"+strings.Repeat("a1", 20), nil) req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"}) srv.Client().CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusSeeOther { t.Fatalf("a landing's forum URL returned %d, want a redirect", resp.StatusCode) } if loc := resp.Header.Get("Location"); !strings.Contains(loc, "/landing/") { t.Errorf("redirected to %q, want the landing record", loc) } }