Artifact 8fdc1e780b950b242b8c8e0cedc93e60721c520cf4f04b495b2e90d6bf200979:
- File
internal/harness/write_test.go
— part of check-in
[ac80214995]
at
2026-08-24 02:38:12
on branch notes
— notes: one row per technote, not one per edit
Fossil registers one timeline event per technote; quarry registered one per artifact. libfossil has 50 technote artifacts under 26 event ids, so its timeline showed a superseded title five rows above its own correction. The collapse is in the event trigger — insert, then evict every loser for the event id — which is order-independent, as the fold requires: a clone delivers artifacts in wire order.
Notes are no longer a tab. Fossil's built-in menu has none, and libfossil, which uses them as a milestone feed, overrode mainmenu to add TODOs and FIXMEs without adding one. They are reached from the About panel, the docs page and the timeline filter instead, and the listing is a feed with excerpts rather than a list of links.
Every edit stays readable, as a wiki page's revisions are.
Fixes an authoring bug found on the way: zitcli never passed --time, so an edit re-filed the note at the moment it was edited and it moved in the timeline. The filing time is now a field on both forms. (user: dmestas, size: 56629) [annotate] [blame] [check-ins using] [more...]
package harness import ( "context" "encoding/json" "io" "net/http" "net/url" "path/filepath" "regexp" "strings" "sync" "testing" "time" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/oltp" "craftdesign.group/quarry/internal/ops" ) // TestWriteOps drives the wiki/ticket write path the way a browser // does: form POSTs through the web handler, gated by Basic auth, with // read-your-writes (the op syncs before answering). func TestWriteOps(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "writeseed")} dataDir := t.TempDir() srv := ingestAndServeAuth(t, stores, dataDir, func(repo, user, pass string, _ ops.Auth) api.Decision { if repo == "alpha" && user == "dan" && pass == "hunter2" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(path string, form url.Values, auth bool) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if auth { req.SetBasicAuth("dan", "hunter2") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(path string) string { resp, err := client.Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } // Unauthenticated write: challenged, nothing recorded. if resp := post("/r/alpha/wiki/Guide/edit", url.Values{"body": {"nope"}}, false); resp.StatusCode != http.StatusUnauthorized { t.Fatalf("unauthenticated wiki save: status %d", resp.StatusCode) } // Authenticated wiki save: redirect, then the page serves the body // and the history records the author. if resp := post("/r/alpha/wiki/Guide/edit", url.Values{"body": {"# Guide\n\nwritten from the web\n"}}, true); resp.StatusCode != http.StatusSeeOther { t.Fatalf("wiki save: status %d", resp.StatusCode) } if body := get("/r/alpha/wiki/Guide"); !strings.Contains(body, "written from the web") { t.Fatalf("saved wiki not served:\n%s", body) } if body := get("/r/alpha/wiki/Guide/history"); !strings.Contains(body, "dan") { t.Fatalf("author missing from history:\n%s", body) } // A second save supersedes the first: zit's millisecond D card // (zit#75) gives same-second writes real submission order, so // last-write-wins holds and the history records both revisions. post("/r/alpha/wiki/Guide/edit", url.Values{"body": {"revised from the web\n"}}, true) hist := get("/r/alpha/wiki/Guide/history") if n := strings.Count(hist, "<li>"); n < 2 { t.Fatalf("second revision not recorded (%d in history):\n%s", n, hist) } if body := get("/r/alpha/wiki/Guide"); !strings.Contains(body, "revised from the web") { t.Fatalf("latest save is not current:\n%s", body) } // Ticket: new, then set, both visible immediately. resp := post("/r/alpha/ticket/new", url.Values{ "title": {"web-born ticket"}, "comment": {"filed from the form"}, }, true) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("ticket new: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") if body := get(loc); !strings.Contains(body, "web-born ticket") || !strings.Contains(body, "Open") { t.Fatalf("new ticket page:\n%s", body) } id := strings.TrimPrefix(loc, "/r/alpha/ticket/") if resp := post("/r/alpha/ticket/"+id[:10]+"/edit", url.Values{ "status": {"Closed"}, "resolution": {"Fixed"}, "priority": {"High"}, "severity": {"Severe"}, "type": {"Code_Defect"}, }, true); resp.StatusCode != http.StatusSeeOther { t.Fatalf("ticket set: status %d", resp.StatusCode) } if body := get(loc); !strings.Contains(body, "Closed") || !strings.Contains(body, "Fixed") { t.Fatalf("ticket change not folded:\n%s", body) } // The triage loop must be closable by a human with nothing but the // forms: the fields authored above appear on the detail page and the // edit form offers them back for the next change. If either half // breaks, triage silently becomes a CLI-only capability. if body := get(loc); !strings.Contains(body, "High") || !strings.Contains(body, "Severe") { t.Fatalf("triage fields not on the detail page:\n%s", body) } freq, _ := http.NewRequest("GET", srv.URL+"/r/alpha/ticket/"+id[:10]+"/edit", nil) freq.SetBasicAuth("dan", "hunter2") fresp, err := client.Do(freq) if err != nil { t.Fatal(err) } fb, _ := io.ReadAll(fresp.Body) fresp.Body.Close() editForm := string(fb) for _, want := range []string{ `name="priority" value="High"`, `name="severity" value="Severe"`, `name="type" value="Code_Defect"`, `name="subsystem"`, } { if !strings.Contains(editForm, want) { t.Fatalf("edit form is missing %s:\n%s", want, editForm) } } // Replying must APPEND. Fossil's schema puts a change's text under // "icomment" and the ticket's description under "comment"; writing a // reply into the latter silently replaces the description, which // reads as a successful post and destroys the report it answers. if resp := post("/r/alpha/ticket/"+id[:10]+"/edit", url.Values{ "comment": {"a reply, which must not eat the description"}, }, true); resp.StatusCode != http.StatusSeeOther { t.Fatalf("ticket comment: status %d", resp.StatusCode) } body := get(loc) if !strings.Contains(body, "a reply, which must not eat the description") { t.Fatalf("reply did not appear:\n%s", body) } if !strings.Contains(body, "filed from the form") { t.Fatalf("the reply replaced the description instead of appending:\n%s", body) } // The timeline defaults to check-ins now; tickets are one pill away. if body := get("/r/alpha/timeline?y=t"); !strings.Contains(body, "web-born ticket") && !strings.Contains(body, "Ticket") { t.Fatalf("ticket missing from timeline:\n%s", body) } } // TestForumTechnoteAuthoring drives the v2 write plane for the two // record types zit#76 unlocked: open a thread, reply to it, add a // technote, then edit that technote — all through the forms, gated. func TestForumTechnoteAuthoring(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "authorseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(path string, form url.Values, auth bool) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if auth { req.SetBasicAuth("dan", "pw") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(path string) string { resp, err := client.Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } // Unauthenticated thread creation is challenged. if resp := post("/r/alpha/forum-new", url.Values{"title": {"nope"}, "body": {"x"}}, false); resp.StatusCode != http.StatusUnauthorized { t.Fatalf("unauthenticated forum post: status %d", resp.StatusCode) } // Open a thread; the redirect names it. resp := post("/r/alpha/forum-new", url.Values{ "title": {"Web-born thread"}, "body": {"opening from the form\n"}, }, true) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("forum-new: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") if body := get(loc); !strings.Contains(body, "opening from the form") { t.Fatalf("thread page:\n%s", body) } if body := get("/r/alpha/forum"); !strings.Contains(body, "Web-born thread") { t.Fatalf("thread missing from list:\n%s", body) } // Reply in that thread: two posts, and the reply names its parent. if resp := post(loc+"/reply", url.Values{"body": {"answering from the form\n"}}, true); resp.StatusCode != http.StatusSeeOther { t.Fatalf("reply: status %d", resp.StatusCode) } body := get(loc) if !strings.Contains(body, "answering from the form") || !strings.Contains(body, "replying to") { t.Fatalf("reply not threaded:\n%s", body) } if n := strings.Count(body, "<article"); n != 2 { t.Fatalf("thread has %d posts, want 2", n) } // Add a note, then edit it: the edit supersedes, and the event id is // stable across the edit (one note, not two). The note is FILED at a // moment the author chose — back-dated here, which is the case that // separates a note from a forum post. resp = post("/r/alpha/technote-new", url.Values{ "body": {"# Note\n\nfirst version\n"}, "comment": {"Original title"}, "time": {"2019-04-05T09:30"}, }, true) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("technote-new: status %d", resp.StatusCode) } noteLoc := resp.Header.Get("Location") if b := get(noteLoc); !strings.Contains(b, "first version") { t.Fatalf("technote page:\n%s", b) } // An edit that names no time KEEPS the filing time. Without this the // note silently moves to the moment it was edited, which is a // different place in the timeline than its author put it. if resp := post(noteLoc+"/edit", url.Values{ "body": {"# Note\n\nsecond version\n"}, "comment": {"Corrected title"}, }, true); resp.StatusCode != http.StatusSeeOther { t.Fatalf("technote edit: status %d", resp.StatusCode) } nb := get(noteLoc) if !strings.Contains(nb, "second version") || strings.Contains(nb, "first version") { t.Fatalf("edit did not supersede:\n%s", nb) } if !strings.Contains(nb, "Corrected title") { t.Fatalf("edit did not retitle:\n%s", nb) } if !strings.Contains(nb, "2019") { t.Fatalf("edit moved the note off its filing date:\n%s", nb) } // One note on the listing, and its history holds both versions. list := get("/r/alpha/technote") if n := strings.Count(list, "class=\"tn-item\""); n != 1 { t.Fatalf("listing shows %d notes, want 1 — an edit is not a new note:\n%s", n, list) } if hist := get(noteLoc + "/history"); strings.Count(hist, "class=\"tn-hrow\"") != 2 { t.Fatalf("history lost a version:\n%s", hist) } } // TestAccountRolesGateWrites drives the real OLTP-backed authorization // end to end: a read-only account must not be able to write, a write // account must, and the site admin must reach every repo without a // per-repo grant. The gate under test is the one the composition root // builds, not a stub. func TestAccountRolesGateWrites(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "roleseed")} dataDir := t.TempDir() db, err := oltp.Open(filepath.Join(dataDir, "oltp.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) for _, a := range []struct { name, pw string admin bool }{{"reader", "pw1", false}, {"writer", "pw2", false}, {"root", "pw3", true}} { if err := db.AddAccount(a.name, a.pw, a.admin); err != nil { t.Fatal(err) } } if err := db.Grant("reader", "alpha", oltp.Read); err != nil { t.Fatal(err) } if err := db.Grant("writer", "alpha", oltp.Write); err != nil { t.Fatal(err) } // The same policy the composition root installs. verify := func(repo, user, pass string, need ops.Auth) api.Decision { if !db.HasAccounts() { return api.Allowed } acct, ok := db.Authenticate(user, pass) if !ok { return api.Denied } var want oltp.Role switch need { case ops.Admin: want = oltp.RepoAdmin case ops.Token: want = oltp.Write } if db.RoleOf(acct, repo) >= want { return api.Allowed } return api.Insufficient } srv := ingestAndServeAuth(t, stores, dataDir, verify) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } save := func(user, pw, page, body string) int { form := url.Values{"body": {body}} req, _ := http.NewRequest("POST", srv.URL+"/r/alpha/wiki/"+page+"/edit", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if user != "" { req.SetBasicAuth(user, pw) } resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp.StatusCode } if got := save("", "", "R1", "anon"); got != http.StatusUnauthorized { t.Errorf("anonymous write: status %d, want 401", got) } if got := save("writer", "wrong", "R2", "bad password"); got != http.StatusUnauthorized { t.Errorf("wrong password: status %d, want 401", got) } // Read is a real account with a real role — and still must not write. // 403, not 401: the credentials were correct and the capability was // not, and 401 would tell this client to retry the same password // forever. The two refusals above are 401 because those really are // credential failures. if got := save("reader", "pw1", "R3", "read-only tried to write"); got != http.StatusForbidden { t.Errorf("read-only account wrote: status %d, want 403", got) } if got := save("writer", "pw2", "R4", "written by writer"); got != http.StatusSeeOther { t.Errorf("writer blocked: status %d, want 303", got) } // The site admin holds every repo without any repo_role row. if got := save("root", "pw3", "R5", "written by site admin"); got != http.StatusSeeOther { t.Errorf("site admin blocked: status %d, want 303", got) } } // TestAuthoredCardsReachTheArtifact pins zit#77's cards to the write // path: the technote C card (a title zit refuses to attach later, so it // is write-once here) and the forum N card (an omitted one reads back as // text/x-fossil-wiki, which would render a markdown post as wiki markup). func TestAuthoredCardsReachTheArtifact(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "cardseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), nil) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) if err != nil { t.Fatal(err) } return resp } get := func(path string) string { resp, err := client.Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } // A technote authored with a title shows that title, not its event id. resp := post("/r/alpha/technote-new", url.Values{ "comment": {"Release notes for 1.0"}, "body": {"# Notes\n\nthe first release\n"}, }) resp.Body.Close() if resp.StatusCode != http.StatusSeeOther { t.Fatalf("technote save: status %d", resp.StatusCode) } if list := get("/r/alpha/technote"); !strings.Contains(list, "Release notes for 1.0") { t.Fatalf("technote title missing from the list:\n%s", list) } // A forum post typed as markdown must render as markdown. Without the // N card fossil's rule makes it text/x-fossil-wiki, where a leading # // is not a heading. resp = post("/r/alpha/forum-new", url.Values{ "title": {"Markdown thread"}, "body": {"# Heading\n\nbody text\n"}, }) resp.Body.Close() if resp.StatusCode != http.StatusSeeOther { t.Fatalf("forum post: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") if page := get(loc); !strings.Contains(page, "<h2 id=\"heading\">Heading</h2>") && !strings.Contains(page, "<h2>Heading</h2>") { t.Fatalf("forum body did not render as markdown (no N card?):\n%s", page) } } // Every authoring box in quarry is labelled Markdown, so what quarry // stores must SAY it is markdown. An absent mimetype means fossil wiki // (renderBody dispatches exactly as fossil does), so a description // typed with headings and fenced code came back with its "##" and // backticks as literal characters — the card lied by omission and the // renderer believed it. func TestAuthoredTextIsRecordedAsMarkdown(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "mdseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if repo == "alpha" && user == "dan" && pass == "hunter2" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(path string, form url.Values, auth bool) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if auth { req.SetBasicAuth("dan", "hunter2") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(path string) string { resp, err := client.Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } const body = "## A heading\n\nSome `code` and **bold** text.\n" resp := post("/r/alpha/ticket/new", url.Values{ "title": {"markdown ticket"}, "comment": {body}, }, true) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("ticket new: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") page := get(loc) // A heading element, not specifically h2: the renderer demotes // levels so a document's headings cannot outrank the page's own h1. if !strings.Contains(page, `id="a-heading"`) { t.Errorf("the description's heading did not render as one:\n%s", page) } if strings.Contains(page, "## A heading") { t.Error("the description rendered its markdown as literal text") } if !strings.Contains(page, "<code>code</code>") { t.Error("inline code did not render") } if !strings.Contains(page, "<strong>bold</strong>") { t.Error("emphasis did not render") } // And a comment appended later carries its own mimetype field, // which fossil spells differently from the description's. id := strings.TrimPrefix(loc, "/r/alpha/ticket/") if resp := post("/r/alpha/ticket/"+id[:10]+"/edit", url.Values{ "comment": {"a **later** remark"}, }, true); resp.StatusCode != http.StatusSeeOther { t.Fatalf("ticket set: status %d", resp.StatusCode) } page = get(loc) if !strings.Contains(page, "<strong>later</strong>") { t.Errorf("an appended comment did not render as markdown:\n%s", page) } } // TestForumVotes drives the vote plane through the forms: two accounts // vote, flip, and retract, and every page renders the DERIVED score — // there is no counter to disagree with the vote rows. func TestForumVotes(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "voteseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if (user == "dan" || user == "eve") && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(user, path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if user != "" { req.SetBasicAuth(user, "pw") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(user, path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) if user != "" { req.SetBasicAuth(user, "pw") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } resp := post("dan", "/r/alpha/forum-new", url.Values{ "title": {"Votable thread"}, "body": {"cast away\n"}, }) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("forum-new: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") root := strings.TrimPrefix(loc, "/r/alpha/forum/") // Anonymous readers see no arrows and no zero score. if body := get("", loc); strings.Contains(body, `class="vote-pill"`) { t.Fatalf("anonymous page shows vote chrome:\n%s", body) } // A signed-in (Basic) writer sees the arrows. if body := get("dan", loc); !strings.Contains(body, `class="vote-pill"`) { t.Fatalf("writer page shows no arrows:\n%s", body) } // Voting without credentials is challenged, and records nothing. if resp := post("", loc+"/vote", url.Values{"post": {root}, "dir": {"up"}}); resp.StatusCode != http.StatusUnauthorized { t.Fatalf("anonymous vote: status %d", resp.StatusCode) } // dan and eve upvote: score 2, dan's arrow is lit (retract offered). for _, u := range []string{"dan", "eve"} { if resp := post(u, loc+"/vote", url.Values{"post": {root}, "dir": {"up"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("%s vote: status %d", u, resp.StatusCode) } } if body := get("", loc); !strings.Contains(body, `data-score="2"`) { t.Fatalf("anonymous page missing derived score:\n%s", body) } if body := get("dan", loc); !strings.Contains(body, `class="vote-cast"`) { t.Fatalf("cast arrow not lit for dan:\n%s", body) } if body := get("", "/r/alpha/forum"); !strings.Contains(body, `data-score="2"`) { t.Fatalf("thread list missing score:\n%s", body) } // eve flips to a downvote — one account, one vote: net 0, and a // zero score renders as nothing rather than a meaningless "0". if resp := post("eve", loc+"/vote", url.Values{"post": {root}, "dir": {"down"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("eve flip: status %d", resp.StatusCode) } if body := get("", loc); strings.Contains(body, `class="vote-pill"`) { t.Fatalf("net-zero score rendered:\n%s", body) } // eve retracts: dan's upvote stands alone again. if resp := post("eve", loc+"/vote", url.Values{"post": {root}, "dir": {"none"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("eve retract: status %d", resp.StatusCode) } if body := get("", loc); !strings.Contains(body, `data-score="1"`) { t.Fatalf("score after retract:\n%s", body) } // A direction outside the vocabulary is refused, not recorded. if resp := post("dan", loc+"/vote", url.Values{"post": {root}, "dir": {"sideways"}}); resp.StatusCode == http.StatusSeeOther { t.Fatal("invalid direction accepted") } if body := get("", loc); !strings.Contains(body, `data-score="1"`) { t.Fatalf("invalid direction changed the score:\n%s", body) } } // TestForumHotTab: votes reorder the hot view while recent keeps // arrival order, and the page prints the formula it ranked by. func TestForumHotTab(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "hotseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth("dan", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(path string) string { resp, err := client.Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } first := post("/r/alpha/forum-new", url.Values{"title": {"First thread"}, "body": {"a\n"}}) post("/r/alpha/forum-new", url.Values{"title": {"Second thread"}, "body": {"b\n"}}) firstLoc := first.Header.Get("Location") // Upvote the FIRST (older) thread; ages are near-equal so the vote decides. if resp := post(firstLoc+"/vote", url.Values{ "post": {strings.TrimPrefix(firstLoc, "/r/alpha/forum/")}, "dir": {"up"}, }); resp.StatusCode != http.StatusSeeOther { t.Fatalf("vote: status %d", resp.StatusCode) } recent := get("/r/alpha/forum") if strings.Index(recent, "Second thread") > strings.Index(recent, "First thread") { t.Fatalf("recent view lost arrival order:\n%s", recent) } hot := get("/r/alpha/forum?sort=hot") if strings.Index(hot, "First thread") > strings.Index(hot, "Second thread") { t.Fatalf("vote did not lift thread in hot view:\n%s", hot) } if !strings.Contains(hot, `class="hot-formula"`) { t.Fatalf("hot view does not print its formula:\n%s", hot) } if strings.Contains(recent, `class="hot-formula"`) { t.Fatalf("recent view prints the hot formula:\n%s", recent) } } // TestForumAnchorsCheckin: a thread that mentions a check-in by hash // prefix surfaces as "discussed in" on that check-in's page — and only // there. Anchors extract at fold, resolve at render. func TestForumAnchorsCheckin(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "anchorseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } get := func(path string) string { resp, err := client.Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } // Two real check-ins from the store, via the API. var tl struct { Entries []struct { Kind string `json:"kind"` Hash string `json:"hash"` } `json:"entries"` } if err := json.Unmarshal([]byte(get("/api/v1/r/alpha/timeline")), &tl); err != nil { t.Fatal(err) } var checkins []string for _, e := range tl.Entries { if e.Kind == "ci" { checkins = append(checkins, e.Hash) } } if len(checkins) < 2 { t.Fatalf("timeline has %d check-ins", len(checkins)) } mentioned, unmentioned := checkins[0], checkins[1] form := url.Values{"title": {"About that change"}, "body": {"I think [" + mentioned[:10] + "] broke the build\n"}} req, _ := http.NewRequest("POST", srv.URL+"/r/alpha/forum-new", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth("dan", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusSeeOther { t.Fatalf("forum-new: status %d", resp.StatusCode) } if body := get("/r/alpha/checkin/" + mentioned); !strings.Contains(body, "discussed in") || !strings.Contains(body, "About that change") { t.Fatalf("mentioned check-in has no backlink:\n%s", body) } // The honesty gate: a check-in nobody mentioned shows nothing. if body := get("/r/alpha/checkin/" + unmentioned); strings.Contains(body, "discussed in") { t.Fatalf("unmentioned check-in grew a backlink:\n%s", body) } } // TestForumAnnouncements: announcement-as-a-type — starting one takes // Admin, replying stays open to Token, the strip pins above the forum // and the repo home surfaces the newest one. func TestForumAnnouncements(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "annseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, level ops.Auth) api.Decision { switch { case user == "dan" && pass == "pw": // maintainer return api.Allowed case user == "eve" && pass == "pw": // ordinary account if level >= ops.Admin { return api.Insufficient } return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(user, path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(user, "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(user, path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) if user != "" { req.SetBasicAuth(user, "pw") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } form := url.Values{"title": {"v2.0 released"}, "body": {"it shipped\n"}} // An ordinary account cannot start an announcement... if resp := post("eve", "/r/alpha/announce-new", form); resp.StatusCode != http.StatusForbidden { t.Fatalf("eve announce: status %d, want 403", resp.StatusCode) } // ...a maintainer can. resp := post("dan", "/r/alpha/announce-new", form) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("dan announce: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") list := get("", "/r/alpha/forum") if !strings.Contains(list, `class="f-pinned"`) || !strings.Contains(list, "v2.0 released") { t.Fatalf("forum list has no pinned strip:\n%s", list) } if strings.Count(list, "v2.0 released") != 1 { t.Fatalf("announcement rendered twice (strip AND list):\n%s", list) } // The maintainer sees the announce control; an ordinary account // and anonymous readers do not. if s := get("dan", "/r/alpha/forum"); !strings.Contains(s, "announce-new") { t.Fatal("maintainer missing announce control") } for _, u := range []string{"eve", ""} { if s := get(u, "/r/alpha/forum"); strings.Contains(s, "announce-new") { t.Fatalf("%q sees the announce control", u) } } // Top-level visibility: the repo home carries the newest announcement. if home := get("", "/r/alpha"); !strings.Contains(home, "v2.0 released") { t.Fatalf("repo home has no announcement:\n%s", home) } // Anyone-can-reply is untouched. if resp := post("eve", loc+"/reply", url.Values{"body": {"congrats\n"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("eve reply on announcement: status %d", resp.StatusCode) } } // TestForumPolls: the definition is markdown in the post (fossil- // compatible by degradation), ballots are forge state — one per // account, changeable until close, option range enforced from the body. func TestForumPolls(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "pollseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if (user == "dan" || user == "eve") && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(user, path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(user, "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(user, path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) if user != "" { req.SetBasicAuth(user, "pw") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } body := "Time to pick.\n\n```poll\nquestion: Which default branch?\n- trunk\n- main\n```\n" resp := post("dan", "/r/alpha/forum-new", url.Values{"title": {"Branch poll"}, "body": {body}}) if resp.StatusCode != http.StatusSeeOther { t.Fatalf("forum-new: status %d", resp.StatusCode) } loc := resp.Header.Get("Location") root := strings.TrimPrefix(loc, "/r/alpha/forum/") page := get("dan", loc) if !strings.Contains(page, "Which default branch?") || strings.Contains(page, "```poll") { t.Fatalf("widget did not replace the block:\n%s", page) } if !strings.Contains(page, "Time to pick.") { t.Fatalf("prose around the poll lost:\n%s", page) } // Two ballots, one changed mind: trunk 1, main 1. for _, c := range []struct{ user, choice string }{ {"dan", "0"}, {"eve", "0"}, {"eve", "1"}, } { if resp := post(c.user, loc+"/poll", url.Values{"post": {root}, "choice": {c.choice}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("%s ballot %s: status %d", c.user, c.choice, resp.StatusCode) } } page = get("dan", loc) if !strings.Contains(page, "trunk · 1") || !strings.Contains(page, "main · 1") || !strings.Contains(page, "2 ballots") { t.Fatalf("counts wrong after change of mind:\n%s", page) } // dan's own ballot is marked. if !strings.Contains(page, "◉") { t.Fatalf("cast ballot not marked:\n%s", page) } // An out-of-range choice is refused by the BODY's option count. if resp := post("dan", loc+"/poll", url.Values{"post": {root}, "choice": {"7"}}); resp.StatusCode == http.StatusSeeOther { t.Fatal("out-of-range choice accepted") } // Anonymous readers see results, no ballot buttons. anon := get("", loc) if !strings.Contains(anon, "trunk · 1") || strings.Contains(anon, "◉") || strings.Contains(anon, "○") { t.Fatalf("anonymous poll view wrong:\n%s", anon) } } // TestForumModeration drives the whole gate: an untrusted author's post // stages (no artifact), the queue shows it with the hook's advisory // verdict, approval births the artifact under the original author, // rejection leaves nothing, hides collapse with a public reason, and // every action lands in the public log. func TestForumModeration(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "modseed")} hooked := make(chan struct{}, 4) srv := ingestAndServeAuthMod(t, stores, t.TempDir(), func(repo, user, pass string, level ops.Auth) api.Decision { switch { case user == "dan" && pass == "pw": return api.Allowed case user == "eve" && pass == "pw": if level >= ops.Admin { return api.Insufficient } return api.Allowed } return api.Denied }, func(account, repo string) bool { return account == "dan" }, func(_ context.Context, s oltp.Staged) (api.HookVerdict, error) { defer func() { hooked <- struct{}{} }() if strings.Contains(s.Body, "buy cheap") { return api.HookVerdict{Action: "reject", Score: 0.98, Reason: "reads as spam"}, nil } return api.HookVerdict{Action: "hold", Score: 0.4, Reason: "uncertain"}, nil }, nil) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(user, path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(user, "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(user, path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) if user != "" { req.SetBasicAuth(user, "pw") } resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } // dan switches the gate on (logged). if resp := post("dan", "/r/alpha/forum/queue", url.Values{"toggle": {"on"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("toggle: status %d", resp.StatusCode) } // The queue is admin-only; the log is public. if resp := post("eve", "/r/alpha/forum/queue", url.Values{"toggle": {"off"}}); resp.StatusCode != http.StatusForbidden { t.Fatalf("eve toggled moderation: status %d", resp.StatusCode) } // eve (no role) posts twice: both stage, neither becomes an artifact. resp := post("eve", "/r/alpha/forum-new", url.Values{"title": {"Genuine question"}, "body": {"how do I build?\n"}}) if resp.StatusCode != http.StatusSeeOther || !strings.Contains(resp.Header.Get("Location"), "staged=1") { t.Fatalf("stage redirect: %d %s", resp.StatusCode, resp.Header.Get("Location")) } post("eve", "/r/alpha/forum-new", url.Values{"title": {"Deals"}, "body": {"buy cheap watches\n"}}) <-hooked <-hooked if list := get("", "/r/alpha/forum"); strings.Contains(list, "Genuine question") || strings.Contains(list, "Deals") { t.Fatalf("staged post visible before approval:\n%s", list) } // dan (trusted) posts directly, no staging. if resp := post("dan", "/r/alpha/forum-new", url.Values{"title": {"Direct thread"}, "body": {"hi\n"}}); strings.Contains(resp.Header.Get("Location"), "staged") { t.Fatal("trusted author was staged") } // The queue shows both with the hook's advisory verdicts. queue := get("dan", "/r/alpha/forum/queue") if !strings.Contains(queue, "Genuine question") || !strings.Contains(queue, "reads as spam") || !strings.Contains(queue, "uncertain") { t.Fatalf("queue missing entries or verdicts:\n%s", queue) } // Approve the genuine one, reject the spam (reason required). approveID, rejectID := "1", "2" if strings.Index(queue, "Genuine question") > strings.Index(queue, "Deals") { approveID, rejectID = "2", "1" } if resp := post("dan", "/r/alpha/forum/queue", url.Values{"id": {approveID}, "action": {"approve"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("approve: status %d", resp.StatusCode) } if resp := post("dan", "/r/alpha/forum/queue", url.Values{"id": {rejectID}, "action": {"reject"}}); resp.StatusCode == http.StatusSeeOther { t.Fatal("reject without a reason accepted") } if resp := post("dan", "/r/alpha/forum/queue", url.Values{"id": {rejectID}, "action": {"reject"}, "reason": {"spam"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("reject: status %d", resp.StatusCode) } // The approved post is live, authored by eve; the rejected one never existed. list := get("", "/r/alpha/forum") if !strings.Contains(list, "Genuine question") || !strings.Contains(list, "eve") { t.Fatalf("approved post missing or misattributed:\n%s", list) } if strings.Contains(list, "Deals") { t.Fatalf("rejected post exists:\n%s", list) } // Hide the direct thread's post with a public reason; it collapses. dq := get("", "/r/alpha/forum") i := strings.Index(dq, "/forum/") _ = i // find the Direct thread link var directLoc string for _, line := range strings.Split(dq, "\n") { if strings.Contains(line, "Direct thread") { s := strings.SplitN(line, `href="`, 2)[1] directLoc = strings.SplitN(s, `"`, 2)[0] } } if directLoc == "" { t.Fatalf("direct thread not found:\n%s", dq) } root := strings.TrimPrefix(directLoc, "/r/alpha/forum/") if resp := post("dan", directLoc+"/hide", url.Values{"post": {root}, "action": {"hide"}, "reason": {"off-topic"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("hide: status %d", resp.StatusCode) } page := get("", directLoc) if !strings.Contains(page, `class="f-hidden"`) || !strings.Contains(page, "off-topic") { t.Fatalf("hidden post not collapsed:\n%s", page) } // The public log carries the whole story, anonymously readable. log := get("", "/r/alpha/forum/modlog") for _, want := range []string{"enable", "approve", "reject", "spam", "hide", "off-topic", "ai-hold", "ai-reject", "hook"} { if !strings.Contains(log, want) { t.Fatalf("modlog missing %q:\n%s", want, log) } } } type notifyCapture struct { mu sync.Mutex events []api.NotifyEvent } func (n *notifyCapture) Trigger(_ context.Context, e api.NotifyEvent) error { n.mu.Lock() defer n.mu.Unlock() n.events = append(n.events, e) return nil } func (n *notifyCapture) count(workflow, to string) int { n.mu.Lock() defer n.mu.Unlock() c := 0 for _, e := range n.events { if e.Workflow == workflow && e.To == to { c++ } } return c } func (n *notifyCapture) waitFor(t *testing.T, workflow, to string, want int) { t.Helper() for i := 0; i < 100; i++ { if n.count(workflow, to) >= want { return } time.Sleep(20 * time.Millisecond) } t.Fatalf("never saw %d %s → %s events", want, workflow, to) } // TestForumSubscriptions: authoring auto-subscribes, replies notify // subscribers (never the actor), @mentions route to their people, and // an explicit unsubscribe is honored — and survives posting again. func TestForumSubscriptions(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "subseed")} capture := ¬ifyCapture{} srv := ingestAndServeAuthMod(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if (user == "dan" || user == "eve") && pass == "pw" { return api.Allowed } return api.Denied }, nil, nil, capture) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(user, path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(user, "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(user, path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) req.SetBasicAuth(user, "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } resp := post("dan", "/r/alpha/forum-new", url.Values{ "title": {"Subscribable"}, "body": {"opening, cc @carol\n"}}) loc := resp.Header.Get("Location") // The mention routed to carol; the author got nothing. capture.waitFor(t, "quarry-mention", "carol", 1) if capture.count("quarry-forum-reply", "dan") != 0 { t.Fatal("author notified about their own opener") } // The author auto-subscribed: the page offers unsubscribe. if page := get("dan", loc); !strings.Contains(page, `name="state" value="unsub"`) { t.Fatalf("author not auto-subscribed:\n%s", page) } // eve replies: dan (subscriber) hears about it, eve does not. post("eve", loc+"/reply", url.Values{"body": {"first answer\n"}}) capture.waitFor(t, "quarry-forum-reply", "dan", 1) if capture.count("quarry-forum-reply", "eve") != 0 { t.Fatal("actor notified about their own reply") } // dan unsubscribes; the next reply stays silent for dan... if resp := post("dan", loc+"/subscribe", url.Values{"state": {"unsub"}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("unsubscribe: status %d", resp.StatusCode) } post("eve", loc+"/reply", url.Values{"body": {"second answer\n"}}) // eve auto-subscribed by her first reply: she hears about nothing of // her own, but her subscription proves the fanout ran for this post. capture.waitFor(t, "quarry-forum-reply", "dan", 1) // still exactly 1 // ...and dan REPLYING AGAIN does not quietly re-subscribe him. post("dan", loc+"/reply", url.Values{"body": {"my own follow-up\n"}}) capture.waitFor(t, "quarry-forum-reply", "eve", 1) // eve hears dan's reply post("eve", loc+"/reply", url.Values{"body": {"third answer\n"}}) time.Sleep(200 * time.Millisecond) if got := capture.count("quarry-forum-reply", "dan"); got != 1 { t.Fatalf("explicit unsub did not survive dan's later reply: %d events", got) } if page := get("dan", loc); !strings.Contains(page, `name="state" value="sub"`) { t.Fatalf("unsubscribed page does not offer subscribe:\n%s", page) } } // TestForumVoteDatastarPatch: a datastar-flagged vote answers with an // SSE element patch of the widget (no redirect, no page flash); the // plain form path keeps its redirect — the enhancement is a layer. func TestForumVoteDatastarPatch(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "dsseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } form := url.Values{"title": {"DS thread"}, "body": {"x\n"}} req, _ := http.NewRequest("POST", srv.URL+"/r/alpha/forum-new", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth("dan", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() loc := resp.Header.Get("Location") root := strings.TrimPrefix(loc, "/r/alpha/forum/") vote := url.Values{"post": {root}, "dir": {"up"}} req, _ = http.NewRequest("POST", srv.URL+loc+"/vote", strings.NewReader(vote.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("datastar-request", "true") req.SetBasicAuth("dan", "pw") resp, err = client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK || !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/event-stream") { t.Fatalf("datastar vote: status %d type %s", resp.StatusCode, resp.Header.Get("Content-Type")) } b, _ := io.ReadAll(resp.Body) body := string(b) if !strings.Contains(body, "datastar-patch-elements") || !strings.Contains(body, `id="vote-`) || !strings.Contains(body, `class="vote-cast"`) || !strings.Contains(body, `data-score="1"`) { t.Fatalf("patch missing widget state:\n%s", body) } // The plain path is untouched: same POST without the header redirects. req, _ = http.NewRequest("POST", srv.URL+loc+"/vote", strings.NewReader(url.Values{"post": {root}, "dir": {"down"}}.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth("dan", "pw") resp, err = client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != http.StatusSeeOther { t.Fatalf("plain vote: status %d, want redirect", resp.StatusCode) } } // TestComposerPreviewAndForumHashLinks: the preview endpoint renders // through the production pipeline (SSE patch), and a forum post that // mentions another post's hash links to its thread, anchored — with a // mentioned CHECK-IN carrying its comment as excerpt chrome. func TestComposerPreviewAndForumHashLinks(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "prevseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "pw" { return api.Allowed } return api.Denied }) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(path string, form url.Values, ds bool) (*http.Response, string) { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if ds { req.Header.Set("datastar-request", "true") } req.SetBasicAuth("dan", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp, string(b) } get := func(path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) req.SetBasicAuth("dan", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } // A check-in hash for the preview body. var tl struct { Entries []struct { Kind string `json:"kind"` Hash string `json:"hash"` } `json:"entries"` } json.Unmarshal([]byte(get("/api/v1/r/alpha/timeline")), &tl) var ci string for _, e := range tl.Entries { if e.Kind == "ci" { ci = e.Hash break } } // Preview: markdown renders, the mentioned check-in gets its // comment as title + visible excerpt — production-pipeline parity. resp, body := post("/r/alpha/markup-preview", url.Values{ "body": {"**bold** and [" + ci[:12] + "]"}}, true) // A bare fragment: composer.js sets innerHTML for both the live // pane and the Preview tab, so no SSE envelope. if resp.StatusCode != 200 || strings.Contains(body, "datastar-patch-elements") || !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") { t.Fatalf("preview: status %d\n%s", resp.StatusCode, body) } if !strings.Contains(body, "<strong>bold</strong>") || !strings.Contains(body, `class="cref"`) || !strings.Contains(body, "/r/alpha/checkin/") { t.Fatalf("preview render wrong:\n%s", body) } // A post mentioning another POST's hash links to its thread+anchor. resp, _ = post("/r/alpha/forum-new", url.Values{"title": {"First"}, "body": {"origin\n"}}, false) first := strings.TrimPrefix(resp.Header.Get("Location"), "/r/alpha/forum/") post("/r/alpha/forum-new", url.Values{"title": {"Second"}, "body": {"see [" + first + "] for context\n"}}, false) // Find the second thread and check the rendered link. list := get("/r/alpha/forum") var secondLoc string for _, line := range strings.Split(list, "\n") { if strings.Contains(line, "Second") && strings.Contains(line, "/forum/") { secondLoc = strings.SplitN(strings.SplitN(line, `href="`, 2)[1], `"`, 2)[0] } } page := get(secondLoc) if !strings.Contains(page, `href="/r/alpha/forum/`+first[:16]+`#p-`) { t.Fatalf("forum hash not linked to thread anchor:\n%s", page) } } // TestLivePreviewPlainFragment: composer.js's live pane endpoint — a // bare sanitized fragment, and a poll block previews as the widget it // will become, not a code fence. func TestLivePreviewPlainFragment(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "liveseed")} srv := ingestAndServeAuth(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "pw" { return api.Allowed } return api.Denied }) body := "Intro prose.\n\n```poll\nquestion: Ship it <now>?\n- yes\n- no\n```\n" form := url.Values{"body": {body}} req, _ := http.NewRequest("POST", srv.URL+"/r/alpha/markup-preview?plain=1", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth("dan", "pw") resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) out := string(b) if !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") || strings.Contains(out, "datastar") { t.Fatalf("plain fragment wrong shape: %s\n%s", resp.Header.Get("Content-Type"), out) } if !strings.Contains(out, `class="f-pollprev"`) || strings.Contains(out, "```") { t.Fatalf("poll block did not preview as widget:\n%s", out) } // Draft content is escaped — it is the least trusted input there is. if !strings.Contains(out, "Ship it <now>?") { t.Fatalf("question not escaped:\n%s", out) } if !strings.Contains(out, "<p>Intro prose.</p>") { t.Fatalf("prose around the poll lost:\n%s", out) } } // TestBranchRootKeepsAboutAside: switching branches at the repo root // must not drop the About column — but the doc TABS stay default-ref // only (they render default-branch docs), and the branch's own README // renders plainly below. func TestBranchRootKeepsAboutAside(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "aboutseed")} srv := ingestAndServe(t, stores, t.TempDir()) get := func(path string) string { resp, err := srv.Client().Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } home := get("/r/alpha") if !strings.Contains(home, `class="about-card"`) { t.Fatalf("default root lost the About aside:\n%s", home) } branch := get("/r/alpha/tree/trunk/") if !strings.Contains(branch, `class="about-card"`) { t.Fatalf("branch root has no About aside:\n%s", branch) } // The branch view keeps its own plain README, not the tab panel. if !strings.Contains(branch, "store aboutseed") { t.Fatalf("branch README missing:\n%s", branch) } // Deeper paths stay working views: no About aside. if deep := get("/r/alpha/tree/trunk/src/"); strings.Contains(deep, `class="about-card"`) { t.Fatalf("deep path grew the About aside:\n%s", deep) } } // TestTopicPillsFilterTheFleet: a topic pill links to /?q=<topic>; the // landing filter must FIND repositories by their topics, not only by // name substring. func TestTopicPillsFilterTheFleet(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "topicseed"), "beta": buildStore(t, "topicbeta")} dataDir := t.TempDir() srv := ingestAndServe(t, stores, dataDir) // Register alpha with topics the way settings would store them. odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db")) if err != nil { t.Fatal(err) } defer odb.Close() if err := odb.EnsureRepo("alpha", stores["alpha"]); err != nil { t.Fatal(err) } if err := odb.SetRepoAbout("alpha", "", "forge zig"); err != nil { t.Fatal(err) } get := func(path string) string { resp, err := srv.Client().Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } page := get("/?q=zig") if !strings.Contains(page, `href="/r/alpha"`) { t.Fatalf("topic did not find its repo:\n%s", page) } if strings.Contains(page, `href="/r/beta"`) { t.Fatalf("untagged repo matched the topic:\n%s", page) } // Topics match as whole words, not substrings. if page := get("/?q=zi"); strings.Contains(page, `href="/r/alpha"`) { t.Fatal("topic matched by prefix") } } // TestReplyDAGAndDirectNotification: replies target SPECIFIC posts // (fossil's reply DAG, not a linear fake), the page joins each reply to // its parent's author, and the answered author is notified even after // unsubscribing from the thread. func TestReplyDAGAndDirectNotification(t *testing.T) { stores := map[string]string{"alpha": buildStore(t, "dagseed")} capture := ¬ifyCapture{} srv := ingestAndServeAuthMod(t, stores, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if (user == "dan" || user == "eve") && pass == "pw" { return api.Allowed } return api.Denied }, nil, nil, capture) client := srv.Client() client.CheckRedirect = func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse } post := func(user, path string, form url.Values) *http.Response { req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.SetBasicAuth(user, "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } resp.Body.Close() return resp } get := func(path string) string { req, _ := http.NewRequest("GET", srv.URL+path, nil) req.SetBasicAuth("dan", "pw") resp, err := client.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return string(b) } resp := post("dan", "/r/alpha/forum-new", url.Values{"title": {"DAG"}, "body": {"root\n"}}) loc := resp.Header.Get("Location") post("eve", loc+"/reply", url.Values{"body": {"first answer\n"}}) // dan finds eve's reply and answers IT, not the root. page := get(loc) m := regexp.MustCompile(`id="p-([0-9a-f]{12})"`).FindAllStringSubmatch(page, -1) if len(m) < 2 { t.Fatalf("thread has %d posts", len(m)) } evePost := m[1][1] // The aimed composer names its target post. aimed := get(loc + "?reply-to=" + evePost) if !strings.Contains(aimed, `name="to" value="`+evePost+`"`) { t.Fatalf("composer not aimed at the post:\n%s", aimed) } // dan unsubscribes — the DIRECT reply must still reach eve... and // eve unsubscribes too, proving the replied-to path alone delivers. post("eve", loc+"/subscribe", url.Values{"state": {"unsub"}}) before := capture.count("quarry-forum-reply", "eve") if resp := post("dan", loc+"/reply", url.Values{"body": {"answering eve directly\n"}, "to": {evePost}}); resp.StatusCode != http.StatusSeeOther { t.Fatalf("aimed reply: status %d", resp.StatusCode) } capture.waitFor(t, "quarry-forum-reply", "eve", before+1) // The page joins the reply to its PARENT: a link to eve's post // carrying eve's name, not the root's. page = get(loc) if !strings.Contains(page, `href="#p-`+evePost+`"`) { t.Fatalf("reply not linked to its parent post:\n%s", page) } last := page[strings.LastIndex(page, `href="#p-`+evePost+`"`):] if !strings.Contains(last[:200], "eve") { t.Fatalf("parent link does not name its author:\n%s", last[:200]) } }