Skip to content
quarry Artifact [37ab8dc236]
Login

Artifact [37ab8dc236]

Artifact 37ab8dc23671297a1db83c88e6594607025fb5a6a4d685b121aaae011b078694:


package harness

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/events"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ingest"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/web"
	"craftdesign.group/quarry/internal/xfer"
	"craftdesign.group/quarry/internal/zitcli"
)

// RFC 0007 §Testing: the matrix is the spec. alice owns trunk; bob
// holds `i` and owns nothing. Every row below is one line of the RFC's
// table, driven through the forge the way a person or a client would
// drive it — the forge's accept through the API, a clone's accept
// through the sync gate, a stock fossil client through the same gate.
//
// Three rows are proved elsewhere and named here so the matrix reads
// complete: the gate always sets its own header and strips a client's
// (xfer: TestGateAlwaysSetsItsOwnProtectHeader); a control artifact
// retagging a check-in onto trunk is refused (zit#151's own unit
// tests, over the three violation shapes); a stacked N+1 landing after
// N lands on trunk shows a disabled control, because MayLand is
// computed at read time against the CURRENT target and that is row 3.

const protectPW = "pw12345678"

type protectForge struct {
	srv   *httptest.Server
	odb   *oltp.DB
	store string
	code  string
	sync  func()
	wipe  func()
}

func newProtectForge(t *testing.T) *protectForge {
	t.Helper()
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	store := buildStore(t, "protectseed")
	dataDir := t.TempDir()
	ev, err := events.Start(filepath.Join(dataDir, "nats"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	odb, err := oltp.Open(filepath.Join(dataDir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { odb.Close() })

	code, err := xfer.ProjectCode(store)
	if err != nil {
		t.Fatal(err)
	}
	// Two accounts, both able to land in fossil's terms (`i`), and two
	// sync users with the same passwords, so the CLI rows authenticate
	// as the same people the API rows do.
	for _, name := range []string{"alice", "bob"} {
		if err := odb.AddAccount(name, protectPW, false); err != nil {
			t.Fatal(err)
		}
		if err := odb.SetCaps(name, "alpha", "kfw3C5aiz"); err != nil {
			t.Fatal(err)
		}
		if err := odb.SetSyncUser("alpha", name, xfer.SharedSecret(code, name, protectPW)); err != nil {
			t.Fatal(err)
		}
	}
	if err := odb.SetProtection("alpha", "trunk", []string{"alice"}); err != nil {
		t.Fatal(err)
	}

	f := &protectForge{odb: odb, store: store, code: code}
	var ix *index.Index
	var pipe *ingest.Pipeline
	open := func() {
		var err error
		ix, err = index.Open(filepath.Join(dataDir, "alpha.db"))
		if err != nil {
			t.Fatal(err)
		}
		cli := zitcli.New(store)
		pipe = ingest.NewPipeline(cli, ev, ix, "alpha", "harness")
	}
	open()
	t.Cleanup(func() { ix.Close() })

	deps := api.Deps{Version: "0.0.0-harness", Repos: api.NewRepos(nil), Votes: odb, Forum: odb,
		Protect: odb}
	cli := zitcli.New(store)
	put := func() {
		deps.Repos.Put("alpha", &api.Repo{Index: ix, Zit: cli, Sync: pipe.Sync,
			Leaves: func() (map[string]bool, error) { return cli.Leaves(context.Background()) }})
	}
	put()
	f.sync = func() {
		t.Helper()
		if _, err := pipe.Sync(); err != nil {
			t.Fatal(err)
		}
	}
	f.sync()
	// The §11 wipe: derived data thrown away and rebuilt from the store.
	f.wipe = func() {
		t.Helper()
		ix.Close()
		if err := os.Remove(filepath.Join(dataDir, "alpha.db")); err != nil {
			t.Fatal(err)
		}
		open()
		put()
		f.sync()
	}

	r := ops.NewRegistry()
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	verify := func(repo, user, pass string, _ ops.Auth) api.Decision {
		if (user == "alice" || user == "bob") && pass == protectPW {
			return api.Allowed
		}
		return api.Denied
	}
	capsFor := func(account, repo string) api.CapSet { return odb.EffectiveCaps(account, repo) }
	apiRouter, err := api.RouterWithCaps(r, "0.0.0-harness", capsFor, deps.CanRead, verify)
	if err != nil {
		t.Fatal(err)
	}
	// The sync gate, wired exactly as serve wires it: identity from the
	// shared secret, the rule from Refused, the child --fronted.
	fr := xfer.New()
	t.Cleanup(fr.Close)
	fr.Register("alpha", store)
	fr.SetAuth(odb)
	fr.SetProtect(odb.Refused)
	mux := http.NewServeMux()
	mux.Handle("/api/", apiRouter)
	mux.Handle("POST /r/{repo}", fr)
	mux.Handle("/", web.HandlerWith(r, nil, nil,
		[]web.Option{web.WithCaps(harnessCaps{odb: odb}), web.WithProtect(odb)}, web.Verify(verify)))
	f.srv = httptest.NewServer(mux)
	t.Cleanup(f.srv.Close)
	return f
}

func (f *protectForge) call(t *testing.T, user, method, path, body string) (int, string) {
	t.Helper()
	req, _ := http.NewRequest(method, f.srv.URL+path, strings.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req.SetBasicAuth(user, protectPW)
	resp, err := f.srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	return resp.StatusCode, string(b)
}

type landingView struct {
	Landing struct {
		ID        string `json:"id"`
		State     string `json:"state"`
		SettledBy string `json:"settled_by"`
		ClaimedBy string `json:"claimed_by"`
	} `json:"landing"`
	CanAccept bool     `json:"canAccept"`
	MayLand   bool     `json:"mayLand"`
	Protected bool     `json:"protected"`
	Owners    []string `json:"owners"`
	Merge     struct {
		Checkin string `json:"checkin"`
	} `json:"merge"`
	Stack struct {
		Segments []struct {
			ID       string `json:"id"`
			Landable bool   `json:"landable"`
		} `json:"segments"`
	} `json:"stack"`
}

func (f *protectForge) landing(t *testing.T, user, id string) landingView {
	t.Helper()
	code, body := f.call(t, user, "GET", "/api/v1/r/alpha/landing/"+id[:12], "")
	if code != 200 {
		t.Fatalf("GET landing %s as %s: %d %s", id[:12], user, code, body)
	}
	var v landingView
	if err := json.Unmarshal([]byte(body), &v); err != nil {
		t.Fatal(err)
	}
	return v
}

// openLanding commits on a fresh branch in the store and proposes it.
func (f *protectForge) openLanding(t *testing.T, branch, file string) string {
	t.Helper()
	zrun(t, f.store, "update", "trunk")
	if err := os.WriteFile(filepath.Join(f.store, file), []byte(branch+"\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", file)
	zrun(t, f.store, "commit", "-m", "work on "+branch, "--branch", branch)
	f.sync()
	code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing",
		`{"branch":"`+branch+`","target":"trunk","message":"land `+branch+`"}`)
	if code != 200 {
		t.Fatalf("open landing for %s: %d %s", branch, code, body)
	}
	var out struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal([]byte(body), &out); err != nil || out.ID == "" {
		t.Fatalf("no landing id: %v %s", err, body)
	}
	f.sync()
	return out.ID
}

func (f *protectForge) clone(t *testing.T) string {
	t.Helper()
	work := t.TempDir()
	dir := filepath.Join(work, "clone")
	zrun(t, work, "clone", f.srv.URL+"/r/alpha", dir, "--once")
	// A clone is a store; a checkout is what a person works in, and
	// what `landing accept` and `commit --branch` need.
	zrun(t, dir, "update", "trunk")
	return dir
}

// zsay runs zit and returns what it said, for the rows where the
// message is the assertion or the failure needs explaining.
func zsay(t *testing.T, dir string, args ...string) (string, error) {
	t.Helper()
	cmd := exec.Command("zit", args...)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	return string(out), err
}

func (f *protectForge) push(t *testing.T, dir, user string) (string, error) {
	t.Helper()
	cmd := exec.Command("zit", "push", f.srv.URL+"/r/alpha", "--user", user, "--password", protectPW)
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	return string(out), err
}

// ── the matrix ──────────────────────────────────────────────────────

// Rows 1 and 3: the forge's own accept, non-owner then owner.
func TestProtectForgeAccept(t *testing.T) {
	f := newProtectForge(t)
	id := f.openLanding(t, "feat-a", "a.txt")

	v := f.landing(t, "bob", id)
	if !v.CanAccept || v.MayLand || !v.Protected || strings.Join(v.Owners, ",") != "alice" {
		t.Fatalf("bob's view: %+v — want CanAccept, not MayLand, protected by alice", v)
	}
	if code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/accept", `{}`); code != http.StatusForbidden {
		t.Fatalf("non-owner accept answered %d %s, want 403", code, body)
	} else if !strings.Contains(body, "alice") {
		t.Errorf("the refusal does not name the owners: %s", body)
	}
	f.sync()
	if v := f.landing(t, "bob", id); v.Landing.State != "open" {
		t.Fatalf("record is %q after a refused accept, want open", v.Landing.State)
	}
	if tl := zout(t, f.store, "timeline", "-n", "3"); strings.Contains(tl, "land feat-a") {
		t.Fatalf("a refused accept wrote a merge:\n%s", tl)
	}

	// The page's disabled control with the reason is proved in the web
	// package, where a viewer has an identity: this harness has no
	// sessions plane, so its pages see everyone as anonymous.
	if v := f.landing(t, "alice", id); !v.MayLand {
		t.Fatalf("the owner may not land: %+v", v)
	}
	if code, body := f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/accept", `{}`); code != 200 {
		t.Fatalf("owner accept: %d %s", code, body)
	}
	f.sync()
	v = f.landing(t, "alice", id)
	if v.Landing.State != "accepted" || v.Merge.Checkin == "" {
		t.Fatalf("after the owner's accept: %+v, want accepted with a merge", v)
	}
	if tl := zout(t, f.store, "timeline", "-n", "3"); !strings.Contains(tl, "trunk") {
		t.Fatalf("the merge is not on trunk:\n%s", tl)
	}

	// Row: merged, then the branch continues. The tip is no longer an
	// ancestor of trunk's tip, and the record must not un-settle.
	zrun(t, f.store, "update", "feat-a")
	if err := os.WriteFile(filepath.Join(f.store, "a2.txt"), []byte("more\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "a2.txt")
	zrun(t, f.store, "commit", "-m", "feat-a continues")
	f.sync()
	if v := f.landing(t, "alice", id); v.Landing.State != "accepted" {
		t.Errorf("continuing the branch past its merge un-settled the record: %q", v.Landing.State)
	}
}

// Rows 2 and 4: accept on a clone, then push. The gate turns the
// pusher into a rule and the child refuses the whole round.
func TestProtectCLIAcceptAndPush(t *testing.T) {
	f := newProtectForge(t)
	id := f.openLanding(t, "feat-b", "b.txt")

	// bob: accept locally (a merge onto trunk in the clone), push.
	bob := f.clone(t)
	if out, err := zsay(t, bob, "landing", "accept", id[:12], "--user", "bob", "-m", "bob lands it"); err != nil {
		t.Fatalf("zit landing accept in a clone: %v\n%s", err, out)
	}
	out, err := f.push(t, bob, "bob")
	if err == nil {
		t.Fatalf("non-owner's pushed accept was admitted:\n%s", out)
	}
	if !strings.Contains(out, "trunk") {
		t.Errorf("the refusal does not name the branch:\n%s", out)
	}
	f.sync()
	if v := f.landing(t, "bob", id); v.Landing.State != "open" {
		t.Fatalf("record is %q after a refused push, want open — the whole round must be refused", v.Landing.State)
	}
	if tl := zout(t, f.store, "timeline", "-n", "3"); strings.Contains(tl, "bob lands it") {
		t.Fatalf("part of the refused round reached the store:\n%s", tl)
	}

	// alice: the same, admitted — and with no reply needed, the merge
	// alone settles the record and names her as approver.
	alice := f.clone(t)
	zrun(t, alice, "update", "trunk")
	zrun(t, alice, "merge", "feat-b")
	zrun(t, alice, "commit", "-m", "alice merges by hand")
	if out, err := f.push(t, alice, "alice"); err != nil {
		t.Fatalf("owner's push refused: %v\n%s", err, out)
	}
	f.sync()
	v := f.landing(t, "alice", id)
	if v.Landing.State != "accepted" || v.Landing.SettledBy != "" || v.Landing.ClaimedBy != "" {
		t.Fatalf("after a by-hand merge with no reply: %+v, want accepted, nothing posted", v)
	}
}

// Rows 7 and 8: what protection does NOT touch. Branching off trunk is
// not joining it; an unprotected branch is anyone's.
func TestProtectLeavesOtherWritesAlone(t *testing.T) {
	f := newProtectForge(t)
	bob := f.clone(t)
	if err := os.WriteFile(filepath.Join(bob, "side.txt"), []byte("off trunk\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	if out, err := zsay(t, bob, "add", "side.txt"); err != nil {
		t.Fatalf("add in clone: %v\n%s", err, out)
	}
	if out, err := zsay(t, bob, "commit", "-m", "bob branches off trunk", "--branch", "side"); err != nil {
		t.Fatalf("commit --branch in clone: %v\n%s", err, out)
	}
	if out, err := f.push(t, bob, "bob"); err != nil {
		t.Fatalf("branching OFF a protected branch was refused: %v\n%s", err, out)
	}
	if err := os.WriteFile(filepath.Join(bob, "side2.txt"), []byte("more\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, bob, "add", "side2.txt")
	zrun(t, bob, "commit", "-m", "bob continues side")
	if out, err := f.push(t, bob, "bob"); err != nil {
		t.Fatalf("a push to an unprotected branch was refused: %v\n%s", err, out)
	}
	f.sync()
	if tl := zout(t, f.store, "timeline", "-n", "4"); !strings.Contains(tl, "bob continues side") {
		t.Fatalf("bob's side work did not land:\n%s", tl)
	}
}

// Row 5: a stock fossil client pushing a merge to trunk. The same
// gate, the same rule, and a refusal the client renders.
func TestProtectRefusesStockFossilMerge(t *testing.T) {
	f := newProtectForge(t)
	if _, err := exec.LookPath("fossil"); err != nil {
		t.Skip("fossil not on PATH")
	}
	f.openLanding(t, "feat-c", "c.txt")
	frun := func(dir string, args ...string) (string, error) {
		cmd := exec.Command("fossil", args...)
		cmd.Dir = dir
		out, err := cmd.CombinedOutput()
		return string(out), err
	}
	work := t.TempDir()
	if out, err := frun(work, "clone", f.srv.URL+"/r/alpha", "c.fossil"); err != nil {
		t.Fatalf("fossil clone: %v\n%s", err, out)
	}
	co := filepath.Join(work, "co")
	if err := os.MkdirAll(co, 0o755); err != nil {
		t.Fatal(err)
	}
	if out, err := frun(co, "open", filepath.Join(work, "c.fossil")); err != nil {
		t.Fatalf("fossil open: %v\n%s", err, out)
	}
	frun(co, "update", "trunk")
	if out, err := frun(co, "merge", "feat-c"); err != nil {
		t.Fatalf("fossil merge: %v\n%s", err, out)
	}
	if out, err := frun(co, "commit", "-m", "fossil merges feat-c", "--no-warnings", "--user-override", "bob"); err != nil {
		t.Fatalf("fossil commit: %v\n%s", err, out)
	}
	out, _ := frun(co, "push", strings.Replace(f.srv.URL, "http://", "http://bob:"+protectPW+"@", 1)+"/r/alpha", "--once")
	if !strings.Contains(out, "trunk") {
		t.Fatalf("stock fossil push of a merge to trunk was not refused with the branch named:\n%s", out)
	}
	if tl := zout(t, f.store, "timeline", "-n", "3"); strings.Contains(tl, "fossil merges feat-c") {
		t.Fatalf("the refused fossil merge reached the store:\n%s", tl)
	}
}

// Row 9: a forged transition reply. Anyone who may speak may post the
// marker; with no merge behind it, it is a claim and the page says so.
func TestProtectForgedAcceptIsAClaim(t *testing.T) {
	f := newProtectForge(t)
	id := f.openLanding(t, "feat-d", "d.txt")
	if code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/comment",
		`{"message":"zit-landing-state: accepted\n\nI say so"}`); code != 200 {
		t.Fatalf("comment: %d %s", code, body)
	}
	f.sync()
	v := f.landing(t, "bob", id)
	if v.Landing.State != "open" {
		t.Fatalf("a forged reply settled the record: state %q", v.Landing.State)
	}
	if v.Landing.ClaimedBy == "" {
		t.Errorf("the forged reply is not recorded as a claim: %+v", v)
	}
}

// Compatibility and disposability. A clone is the same clone with the
// row present or absent; the record renders the same before and after
// the row is added, and before and after the §11 wipe.
func TestProtectLeavesNoTraceAndSurvivesTheWipe(t *testing.T) {
	f := newProtectForge(t)
	id := f.openLanding(t, "feat-e", "e.txt")
	if code, body := f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/accept", `{}`); code != 200 {
		t.Fatalf("owner accept: %d %s", code, body)
	}
	f.sync()
	state := func() string {
		v := f.landing(t, "alice", id)
		return v.Landing.State + "|" + v.Landing.SettledBy + "|" + v.Landing.ClaimedBy + "|" + v.Merge.Checkin
	}
	artifacts := func() string { return zout(t, f.clone(t), "timeline", "-n", "50") }

	withRow, withRowClone := state(), artifacts()
	if err := f.odb.SetProtection("alpha", "trunk", nil); err != nil {
		t.Fatal(err)
	}
	f.sync()
	if got := state(); got != withRow {
		t.Errorf("deleting the protection row changed how the record renders:\n %s\n %s", withRow, got)
	}
	if got := artifacts(); got != withRowClone {
		t.Errorf("a clone differs with the row absent — protection left a trace in the repository")
	}
	if err := f.odb.SetProtection("alpha", "trunk", []string{"alice"}); err != nil {
		t.Fatal(err)
	}
	f.wipe()
	if got := state(); got != withRow {
		t.Errorf("the §11 wipe changed how the record renders:\n %s\n %s", withRow, got)
	}
}

// RFC 0006 build order, phase 4: a real strata through the forge with a
// real zit. Two landings stacked; the queue counts one decision; accept
// the base as the owner and zit#144 retargets the next onto trunk, so
// it becomes the landable end and the queue still counts one.
func TestStrataWalksUpAsTheBaseLands(t *testing.T) {
	f := newProtectForge(t)
	a := f.openLanding(t, "st-a", "sa.txt")
	// The second branch off the first, proposed onto it.
	zrun(t, f.store, "update", "st-a")
	if err := os.WriteFile(filepath.Join(f.store, "sb.txt"), []byte("b\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "sb.txt")
	zrun(t, f.store, "commit", "-m", "work on st-b", "--branch", "st-b")
	f.sync()
	code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing",
		`{"branch":"st-b","target":"st-a","message":"land st-b on st-a"}`)
	if code != 200 {
		t.Fatalf("open stacked landing: %d %s", code, body)
	}
	var b struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal([]byte(body), &b); err != nil {
		t.Fatal(err)
	}
	f.sync()

	list := func() (open int, stacks int) {
		code, body := f.call(t, "alice", "GET", "/api/v1/r/alpha/landing", "")
		if code != 200 {
			t.Fatalf("list: %d %s", code, body)
		}
		var out struct {
			Open     int `json:"open"`
			Landings []struct {
				Stack    string `json:"stack"`
				Position int    `json:"position"`
			} `json:"landings"`
		}
		if err := json.Unmarshal([]byte(body), &out); err != nil {
			t.Fatal(err)
		}
		for _, l := range out.Landings {
			if l.Stack != "" {
				stacks++
			}
		}
		return out.Open, stacks
	}
	if open, stacked := list(); open != 1 || stacked != 2 {
		t.Fatalf("two stacked landings: open=%d stacked=%d, want 1 decision over 2 rungs", open, stacked)
	}
	if v := f.landing(t, "alice", b.ID); v.Landing.State != "open" || len(v.Stack.Segments) != 2 {
		t.Fatalf("top rung before the base lands: %+v", v)
	}
	if code, body := f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+b.ID[:12]+"/accept", `{}`); code == 200 {
		t.Fatalf("accepting the TOP rung succeeded: %s", body)
	}

	if code, body := f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+a[:12]+"/accept", `{}`); code != 200 {
		t.Fatalf("accepting the base: %d %s", code, body)
	}
	f.sync()
	if v := f.landing(t, "alice", a); v.Landing.State != "accepted" {
		t.Fatalf("base after accept: %q", v.Landing.State)
	}
	// zit#144 retargeted st-b onto trunk: it is now a standalone,
	// landable landing, and the queue still holds one decision.
	if open, stacked := list(); open != 1 || stacked != 0 {
		t.Fatalf("after the base landed: open=%d stacked=%d, want 1 decision and no stack", open, stacked)
	}
	v := f.landing(t, "alice", b.ID)
	if v.Landing.State != "open" || len(v.Stack.Segments) != 0 || !v.MayLand {
		t.Fatalf("top rung after the base landed: %+v, want open, no stack, landable", v)
	}
}

// Land the strata: one op, one accept per rung bottom-up, zit retargeting
// between them. The first rung that will not merge stops the walk; what
// landed stays landed and the rest is still a strata.
func TestLandTheStrata(t *testing.T) {
	f := newProtectForge(t)
	// Three rungs: st-a on trunk, st-b on st-a, st-c on st-b.
	a := f.openLanding(t, "st-a", "sa.txt")
	stackOn := func(branch, on, file string) string {
		t.Helper()
		zrun(t, f.store, "update", on)
		if err := os.WriteFile(filepath.Join(f.store, file), []byte(branch+"\n"), 0o644); err != nil {
			t.Fatal(err)
		}
		zrun(t, f.store, "add", file)
		zrun(t, f.store, "commit", "-m", "work on "+branch, "--branch", branch)
		f.sync()
		code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing",
			`{"branch":"`+branch+`","target":"`+on+`","message":"land `+branch+`"}`)
		if code != 200 {
			t.Fatalf("open %s: %d %s", branch, code, body)
		}
		var out struct {
			ID string `json:"id"`
		}
		json.Unmarshal([]byte(body), &out)
		f.sync()
		return out.ID
	}
	b := stackOn("st-b", "st-a", "sb.txt")
	c := stackOn("st-c", "st-b", "sc.txt")

	land := func(user, id string) (int, map[string]any) {
		t.Helper()
		code, body := f.call(t, user, "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/land", `{}`)
		var out map[string]any
		json.Unmarshal([]byte(body), &out)
		return code, out
	}
	// A non-owner may not land any of it: trunk is protected, and the
	// base is what lands first.
	if code, out := land("bob", c); code != 200 || out["stopped"] == nil || len(out["landed"].([]any)) != 0 {
		t.Fatalf("non-owner landing the strata: %d %v — want a stop at the base with nothing landed", code, out)
	}
	f.sync()
	// The owner lands all three in one op.
	code, out := land("alice", c)
	if code != 200 {
		t.Fatalf("land the strata: %d %v", code, out)
	}
	if landed := out["landed"].([]any); len(landed) != 3 || out["stopped"] != nil {
		t.Fatalf("landed %v stopped %v, want all three and no stop", landed, out["stopped"])
	}
	f.sync()
	for _, id := range []string{a, b, c} {
		if v := f.landing(t, "alice", id); v.Landing.State != "accepted" {
			t.Errorf("%s: %q after landing the strata", id[:12], v.Landing.State)
		}
	}
	if tl := zout(t, f.store, "timeline", "-n", "8"); !strings.Contains(tl, "trunk") {
		t.Fatalf("nothing on trunk:\n%s", tl)
	}
}

// The mid-stack failure mode, answered: rung 2 conflicts with trunk, so
// the walk lands rung 1, stops at 2 with the reason, and rung 3 is
// still stacked on 2 — a shorter strata, not a broken one.
func TestLandTheStrataStopsAtTheFirstConflict(t *testing.T) {
	f := newProtectForge(t)
	a := f.openLanding(t, "cf-a", "ca.txt")
	zrun(t, f.store, "update", "cf-a")
	if err := os.WriteFile(filepath.Join(f.store, "shared.txt"), []byte("rung two's line\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "shared.txt")
	zrun(t, f.store, "commit", "-m", "work on cf-b", "--branch", "cf-b")
	f.sync()
	code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing", `{"branch":"cf-b","target":"cf-a","message":"land cf-b"}`)
	if code != 200 {
		t.Fatalf("open cf-b: %d %s", code, body)
	}
	var bb struct {
		ID string `json:"id"`
	}
	json.Unmarshal([]byte(body), &bb)
	// trunk moves under it with a different shared.txt.
	zrun(t, f.store, "update", "trunk")
	if err := os.WriteFile(filepath.Join(f.store, "shared.txt"), []byte("trunk's line\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "shared.txt")
	zrun(t, f.store, "commit", "-m", "trunk moves")
	f.sync()

	code, body = f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+bb.ID[:12]+"/land", `{}`)
	if code != 200 {
		t.Fatalf("land: %d %s", code, body)
	}
	var out struct {
		Landed  []string `json:"landed"`
		Stopped string   `json:"stopped"`
		Reason  string   `json:"reason"`
	}
	json.Unmarshal([]byte(body), &out)
	if len(out.Landed) != 1 || out.Landed[0] != a {
		t.Fatalf("landed %v, want just the base", out.Landed)
	}
	if out.Stopped != bb.ID || out.Reason == "" {
		t.Fatalf("stopped=%q reason=%q, want the conflicting rung with a reason", out.Stopped, out.Reason)
	}
	f.sync()
	if v := f.landing(t, "alice", a); v.Landing.State != "accepted" {
		t.Errorf("the base did not stay landed: %q", v.Landing.State)
	}
	if v := f.landing(t, "alice", bb.ID); v.Landing.State != "open" {
		t.Errorf("the conflicting rung was settled: %q", v.Landing.State)
	}
}

// zit#152 through the forge: the target moves, the page's update brings
// it into the branch on the store, the record reads clean and the reply
// is lifted as an event; a conflicting update is refused and nothing is
// written. Writes to the PROPOSED branch, so bob — who holds i but owns
// nothing on trunk — may do it.
func TestUpdateBringsTheTargetIn(t *testing.T) {
	f := newProtectForge(t)
	id := f.openLanding(t, "up-a", "ua.txt")
	// trunk moves on an unrelated file.
	zrun(t, f.store, "update", "trunk")
	if err := os.WriteFile(filepath.Join(f.store, "elsewhere.txt"), []byte("moved\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "elsewhere.txt")
	zrun(t, f.store, "commit", "-m", "trunk moves")
	f.sync()
	code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/update", `{"message":"bring trunk in"}`)
	if code != 200 {
		t.Fatalf("update: %d %s", code, body)
	}
	f.sync()
	code, body = f.call(t, "bob", "GET", "/api/v1/r/alpha/landing/"+id[:12], "")
	if code != 200 {
		t.Fatal(body)
	}
	var v struct {
		Mergeable string `json:"mergeable"`
		Posts     []struct {
			Updated string `json:"updated"`
		} `json:"posts"`
	}
	json.Unmarshal([]byte(body), &v)
	if v.Mergeable != "clean" {
		t.Errorf("after update the record reads %q, want clean", v.Mergeable)
	}
	lifted := false
	for _, p := range v.Posts {
		if p.Updated != "" {
			lifted = true
		}
	}
	if !lifted {
		t.Error("the update reply is not lifted as an event")
	}
	if tl := zout(t, f.store, "timeline", "-n", "3"); !strings.Contains(tl, "up-a") {
		t.Fatalf("no merge on the branch:\n%s", tl)
	}

	// Now a real conflict: both sides edit the same file.
	zrun(t, f.store, "update", "up-a")
	if err := os.WriteFile(filepath.Join(f.store, "same.txt"), []byte("branch\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "same.txt")
	zrun(t, f.store, "commit", "-m", "branch edits same")
	zrun(t, f.store, "update", "trunk")
	if err := os.WriteFile(filepath.Join(f.store, "same.txt"), []byte("trunk\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "same.txt")
	zrun(t, f.store, "commit", "-m", "trunk edits same")
	f.sync()
	before := zout(t, f.store, "timeline", "-n", "1")
	code, body = f.call(t, "bob", "POST", "/api/v1/r/alpha/landing/"+id[:12]+"/update", `{}`)
	if code == 200 {
		t.Fatalf("a conflicting update was accepted: %s", body)
	}
	if !strings.Contains(body, "same.txt") {
		t.Errorf("the refusal does not name the path: %s", body)
	}
	if after := zout(t, f.store, "timeline", "-n", "1"); after != before {
		t.Error("a refused update wrote something")
	}
}

// Through the forge with the real verb: land a branch, then open a
// second landing on the same (now contained) branch and accept it. zit
// settles it with nothing to merge; quarry must read it as accepted,
// not as a claim.
func TestAcceptingAContainedBranchReadsAccepted(t *testing.T) {
	f := newProtectForge(t)
	first := f.openLanding(t, "twice", "tw.txt")
	if code, body := f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+first[:12]+"/accept", `{}`); code != 200 {
		t.Fatalf("first accept: %d %s", code, body)
	}
	f.sync()
	code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing",
		`{"branch":"twice","target":"trunk","message":"land twice again"}`)
	if code != 200 {
		t.Fatalf("second open: %d %s", code, body)
	}
	var out struct {
		ID string `json:"id"`
	}
	json.Unmarshal([]byte(body), &out)
	f.sync()
	if code, body := f.call(t, "alice", "POST", "/api/v1/r/alpha/landing/"+out.ID[:12]+"/accept", `{}`); code != 200 {
		t.Fatalf("accept of a contained branch: %d %s", code, body)
	}
	f.sync()
	v := f.landing(t, "alice", out.ID)
	if v.Landing.State != "accepted" || v.Landing.ClaimedBy != "" {
		t.Fatalf("contained branch after accept: %+v, want accepted and no claim", v.Landing)
	}
}

// pushAnon pushes with NO credential at all — the shape every row of the
// matrix above is missing, because every one of them names a user.
func (f *protectForge) pushAnon(t *testing.T, dir string) (string, error) {
	t.Helper()
	cmd := exec.Command("zit", "push", f.srv.URL+"/r/alpha")
	cmd.Dir = dir
	out, err := cmd.CombinedOutput()
	return string(out), err
}

// THE ROW NOBODY WROTE DOWN.
//
// Every case in the matrix above has a pusher. Protection turns a
// pusher's identity into the branches they may NOT join, so the one
// question never asked was what it does with no identity at all.
//
// I inferred from the eleven-hour outage that an empty pusher owns
// nothing and is therefore refused everything. jva ran it instead, on a
// repository with anonymous push enabled, and got the inverse: an
// anonymous commit landed on protected trunk with "Artifacts sent: 2"
// and nothing refused it. Protection with no identity refuses NOTHING.
//
// That is a bypass wherever anonymous push is on, and it fails OPEN,
// which is the worst direction for a gate to fail.
func TestProtectionRefusesAnAnonymousPush(t *testing.T) {
	f := newProtectForge(t)
	// jva's configuration exactly: the letters permit an anonymous
	// push, so the AUTH gate steps aside and protection is the only
	// thing left between a stranger and a protected branch. Without
	// this the auth gate refuses first and protection is never asked —
	// which is why every existing row passes while the hole is open.
	if err := f.odb.SetCategoryCaps("alpha", "nobody", "gjorzi"); err != nil {
		t.Fatal(err)
	}
	if f.odb.Gated("alpha") {
		t.Fatal("premise broken: alpha is still gated, so this tests the auth gate rather than protection")
	}
	bob := f.clone(t)
	zrun(t, bob, "update", "trunk")
	if err := os.WriteFile(filepath.Join(bob, "anon.txt"), []byte("no credential\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, bob, "add", "anon.txt")
	zrun(t, bob, "commit", "-m", "anonymous commit straight onto protected trunk")

	out, err := f.pushAnon(t, bob)
	if err == nil {
		t.Fatalf("an ANONYMOUS push onto a protected branch was admitted:\n%s\n"+
			"protection turns an identity into a refusal list, and with no identity it "+
			"produced no list rather than the whole list — a bypass that fails OPEN", out)
	}
	if !strings.Contains(out, "trunk") {
		t.Errorf("the refusal does not name the branch:\n%s", out)
	}
	f.sync()
	if tl := zout(t, f.store, "timeline", "-n", "3"); strings.Contains(tl, "anonymous commit straight onto") {
		t.Fatalf("the anonymous commit reached the store:\n%s", tl)
	}
}