Artifact 805794ff9c163637325527b75eb784fe6fe6b33276bc0155358a0efd306d7ebf:
- File
internal/harness/landing_flow_test.go
— part of check-in
[f102c4c7ec]
at
2026-08-25 13:04:39
on branch trunk
— Settling a landing is idempotent
Accepting an already-accepted landing wrote another state reply, and four stacked up on one record before it was noticed.
Two causes. The guard read state from the INDEX, which lags the store by an ingest round, so a second press inside that window saw 'open' and passed. And zit does not refuse the write either — an already-contained branch still gets a reply saying 'accepted', with 'nothing to merge' in the body.
So: fold first, re-read, then decide. A settle is deliberate and rare and can afford one ingest round to know what it is settling. And already-settled now succeeds writing nothing, because that is the end state the caller asked for; an error made a double press look broken when the landing was exactly as they wanted.
Proven against a running server: three accepts in a row leave one 'accepted this' reply and no 'nothing to merge'. (user: dmestas, size: 5988) [annotate] [blame] [check-ins using]
package harness import ( "encoding/json" "io" "net/http" "os" "os/exec" "path/filepath" "strings" "testing" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/ops" ) // The whole landing lifecycle through quarry's own surfaces, against a real // store and a real zit: open a branch for landing, discuss it, and settle it. // Every step mints ordinary fossil artifacts, so the proof that this works is // the same proof that a stock fossil clone would still read the repository. func TestLandingLifecycleThroughTheForge(t *testing.T) { if _, err := exec.LookPath("zit"); err != nil { t.Skip("zit not on PATH") } store := buildStore(t, "flowseed") // A branch worth landing: a real edit, on its own branch. if err := os.WriteFile(filepath.Join(store, "feature.txt"), []byte("the work\n"), 0o644); err != nil { t.Fatal(err) } zrun(t, store, "add", "feature.txt") zrun(t, store, "commit", "-m", "work on the branch", "--branch", "feat") srv := ingestAndServeAuth(t, map[string]string{"alpha": store}, t.TempDir(), func(repo, user, pass string, _ ops.Auth) api.Decision { if user == "dan" && pass == "hunter2" { return api.Allowed } return api.Denied }) post := func(path, body string) (int, string) { t.Helper() req, _ := http.NewRequest("POST", srv.URL+path, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.SetBasicAuth("dan", "hunter2") resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(b) } code, body := post("/api/v1/r/alpha/landing", `{"branch":"feat","target":"trunk","message":"land the feat work"}`) if code != http.StatusOK { t.Fatalf("opening a landing: %d %s", code, body) } var opened struct { ID string `json:"id"` } json.Unmarshal([]byte(body), &opened) if opened.ID == "" { t.Fatalf("no record id came back: %s", body) } // It is a forum thread, so it reads as one — the compatibility // promise, checked rather than asserted. req, _ := http.NewRequest("GET", srv.URL+"/api/v1/r/alpha/forum/"+opened.ID, nil) req.SetBasicAuth("dan", "hunter2") resp, _ := srv.Client().Do(req) if resp != nil { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Errorf("the record is not readable as an ordinary forum thread: %d", resp.StatusCode) } } if code, body := post("/api/v1/r/alpha/landing/"+opened.ID[:12]+"/comment", `{"message":"looks right to me"}`); code != http.StatusOK { t.Fatalf("commenting: %d %s", code, body) } // Settling it. reject writes no check-in, so it needs nothing more // than the capability to speak. if code, body := post("/api/v1/r/alpha/landing/"+opened.ID[:12]+"/reject", `{"message":"not this week"}`); code != http.StatusOK { t.Fatalf("rejecting: %d %s", code, body) } // A settled record does not reopen, and settling it again is a // NO-OP rather than an error: already-rejected is the end state the // caller asked for, so reporting failure would make a double press // look broken when the landing is exactly as they wanted. // // What must not happen is a second state reply. That is the bug this // pins — the guard used to read index state that lagged the store, // so a second press inside one ingest round wrote another reply, and // zit does not refuse those either. reCode, reBody := post("/api/v1/r/alpha/landing/"+opened.ID[:12]+"/reject", `{"message":"again"}`) if reCode != http.StatusOK { t.Errorf("re-settling a closed record: %d %s, want 200", reCode, reBody) } if !strings.Contains(reBody, `"state":"rejected"`) { t.Errorf("re-settle should report the settled state, got %s", reBody) } if !strings.Contains(reBody, `"hash":""`) { t.Errorf("re-settle wrote a reply rather than doing nothing: %s", reBody) } // Accept: the one op that MERGES. It writes a check-in to the // target, which is why it takes `i` and why nothing else here does. // Against a bare store this is zit#131's path — the forge has no // checkout, and until that shipped this could not run at all. if err := os.WriteFile(filepath.Join(store, "second.txt"), []byte("more work\n"), 0o644); err != nil { t.Fatal(err) } zrun(t, store, "update", "trunk") zrun(t, store, "add", "second.txt") zrun(t, store, "commit", "-m", "second branch work", "--branch", "feat2") code, body = post("/api/v1/r/alpha/landing", `{"branch":"feat2","target":"trunk","message":"land the second lot"}`) if code != http.StatusOK { t.Fatalf("opening the second landing: %d %s", code, body) } var two struct { ID string `json:"id"` } json.Unmarshal([]byte(body), &two) if code, body := post("/api/v1/r/alpha/landing/"+two.ID[:12]+"/accept", `{"message":"landing it"}`); code != http.StatusOK { t.Fatalf("accepting: %d %s", code, body) } // The branch is on trunk now — the whole point of the ceremony. merged := zout(t, store, "timeline", "-n", "5") if !strings.Contains(merged, "second branch work") && !strings.Contains(merged, "feat2") { t.Errorf("the accepted branch does not appear on trunk:\n%s", merged) } // And the listing agrees about where it ended up. req, _ = http.NewRequest("GET", srv.URL+"/api/v1/r/alpha/landing?state=rejected", nil) req.SetBasicAuth("dan", "hunter2") resp, err := srv.Client().Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) if !strings.Contains(string(b), opened.ID) { t.Errorf("the rejected record is not listed as rejected: %s", b) } } // zout runs a zit verb and returns its output, for the assertions that have // to ask the STORE what happened rather than trusting the forge's own report. func zout(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("zit", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("zit %v: %v\n%s", args, err, out) } return string(out) }