Skip to content
quarry Artifact [9476322582]
Login

Artifact [9476322582]

Artifact 9476322582e59d2a4dd6d2478ceba8a1a0fdba98e17ae148f0901784a3aef60a:


// landingping_test.go proves RFC 0011's landing family end to end: a
// settled landing reaches the person who opened it and the owners of
// the branch it settled onto, on BOTH halves — quarry's own attention
// stream and the notify companion — and reaches nobody else. The last
// case is the one that matters most: a reindex replays every artifact
// through the fold, and the fold must stay silent (RFC 0001 §5).
package harness

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

	"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/zitcli"
)

const pingPW = "pw12345678"

type pingForge struct {
	srv     *httptest.Server
	ev      *events.Server
	notify  *notifyCapture
	store   string
	sync    func()
	reindex func() (int, error)
}

// buildPingForge stands up a forge with BOTH planes wired: the local
// attention stream and a recording notify. bob opens; alice and carol
// own trunk; carol settles.
func buildPingForge(t *testing.T) *pingForge {
	t.Helper()
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	store := buildStore(t, "pingseed")
	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() })

	for _, name := range []string{"alice", "bob", "carol"} {
		if err := odb.AddAccount(name, pingPW, false); err != nil {
			t.Fatal(err)
		}
		if err := odb.SetCaps(name, "alpha", "kfw3C5aiz"); err != nil {
			t.Fatal(err)
		}
	}
	// alice owns trunk and never touches this landing; carol owns it
	// too, which is what lets her be the hand that settles.
	if err := odb.SetProtection("alpha", "trunk", []string{"alice", "carol"}); err != nil {
		t.Fatal(err)
	}

	ix, err := index.Open(filepath.Join(dataDir, "alpha.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	cli := zitcli.New(store)
	pipe := ingest.NewPipeline(cli, ev, ix, "alpha", "harness")

	nc := &notifyCapture{}
	deps := api.Deps{Version: "0.0.0-harness", Repos: api.NewRepos(nil),
		Protect: odb, Forum: odb, Votes: odb, Notify: nc,
		Attention: harnessAttention{ev: ev}, AttnRead: odb, Accounts: harnessAccounts{db: odb}}
	deps.CapsFor = func(account, repo string) api.CapSet { return odb.EffectiveCaps(account, repo) }
	deps.Repos.Put("alpha", &api.Repo{Index: ix, Zit: cli, Sync: pipe.Sync, Code: "alpha",
		Reindex: pipe.Reindex,
		Leaves:  func() (map[string]bool, error) { return cli.Leaves(context.Background()) }})

	f := &pingForge{ev: ev, notify: nc, store: store, reindex: pipe.Reindex}
	f.sync = func() {
		t.Helper()
		if _, err := pipe.Sync(); err != nil {
			t.Fatal(err)
		}
	}
	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 pass == pingPW && (user == "alice" || user == "bob" || user == "carol") {
			return api.Allowed
		}
		return api.Denied
	}
	mux, err := api.RouterWithCaps(r, "0.0.0-harness", deps.CapsFor, deps.CanRead, verify)
	if err != nil {
		t.Fatal(err)
	}
	f.srv = httptest.NewServer(mux)
	t.Cleanup(f.srv.Close)
	return f
}

func (f *pingForge) 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, pingPW)
	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)
}

// total is every trigger the fake notify has seen, read under its lock.
func (n *notifyCapture) total() int {
	n.mu.Lock()
	defer n.mu.Unlock()
	return len(n.events)
}

// attention returns the account's attention rows, waiting for want of
// them. want 0 waits the whole window, so "nothing arrived" is proved.
func (f *pingForge) attention(t *testing.T, who string, want int) []events.AttentionEvent {
	t.Helper()
	var out []events.AttentionEvent
	for i := 0; i < 60; i++ {
		got, err := f.ev.RecentAttention(who, 50)
		if err != nil {
			t.Fatal(err)
		}
		out = got
		if want > 0 && len(out) >= want {
			return out
		}
		time.Sleep(25 * time.Millisecond)
	}
	return out
}

// A landing OPENING reaches the target's owners at once; a settled
// landing reaches the opener and the owners, on both planes, and never
// the hand that settled it; a reindex afterwards reaches nobody at all.
// Owners here are alice and carol, bob opens, carol settles: alice hears
// twice (open, accept), bob once (accept), carol once (open, as an
// owner — the accept was her own hand).
func TestSettledLandingPingsOpenerAndOwnerNotTheActor(t *testing.T) {
	f := buildPingForge(t)

	zrun(t, f.store, "update", "trunk")
	if err := os.WriteFile(filepath.Join(f.store, "feature.txt"), []byte("work\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	zrun(t, f.store, "add", "feature.txt")
	zrun(t, f.store, "commit", "-m", "work on feature-x", "--branch", "feature-x")
	f.sync()

	code, body := f.call(t, "bob", "POST", "/api/v1/r/alpha/landing",
		`{"branch":"feature-x","target":"trunk","message":"land feature-x"}`)
	if code != http.StatusOK {
		t.Fatalf("landing.open: %d %s", code, body)
	}
	var opened struct {
		ID string `json:"id"`
	}
	if err := json.Unmarshal([]byte(body), &opened); err != nil || opened.ID == "" {
		t.Fatalf("no landing id: %v %s", err, body)
	}
	f.sync()

	if code, body := f.call(t, "carol", "POST",
		"/api/v1/r/alpha/landing/"+opened.ID[:12]+"/accept", `{"message":"landed"}`); code != http.StatusOK {
		t.Fatalf("landing.accept: %d %s", code, body)
	}

	// The notify half. Expected triggers per person, open + accept.
	want := map[string]int{"alice": 2, "bob": 1, "carol": 1}
	f.notify.waitFor(t, "quarry-landing", "alice", 2)
	f.notify.waitFor(t, "quarry-landing", "bob", 1)
	f.notify.waitFor(t, "quarry-landing", "carol", 1)
	for who, n := range want {
		if got := f.notify.count("quarry-landing", who); got != n {
			t.Fatalf("%s got %d landing triggers, want %d", who, got, n)
		}
	}
	// The one trigger carol has is the open, not her own accept.
	if n := f.notify.countTxn("quarry-landing", "carol", "landing:accept:"+opened.ID+":carol"); n != 0 {
		t.Fatalf("the actor was told about her own accept %d times", n)
	}

	// The attention half: the same people, keyed to the same events.
	for who, n := range want {
		evs := f.attention(t, who, n)
		if len(evs) != n {
			t.Fatalf("%s has %d attention rows, want %d: %+v", who, len(evs), n, evs)
		}
		for _, e := range evs {
			if !strings.HasPrefix(e.Kind, "landing.") {
				t.Fatalf("%s attention kind = %q, want the landing family", who, e.Kind)
			}
			if e.Repo != "alpha" {
				t.Fatalf("%s attention repo = %q", who, e.Repo)
			}
			if !strings.HasSuffix(e.Link, "/landing/"+opened.ID) {
				t.Fatalf("%s attention link = %q", who, e.Link)
			}
			switch e.TxnID {
			case "landing:open:" + opened.ID + ":" + who:
				if e.From != "bob" || who == "bob" {
					t.Fatalf("%s open row from %q", who, e.From)
				}
			case "landing:accept:" + opened.ID + ":" + who:
				if e.From != "carol" || who == "carol" {
					t.Fatalf("%s accept row from %q", who, e.From)
				}
			default:
				t.Fatalf("%s attention TxnID = %q, want an address-based open or accept key", who, e.TxnID)
			}
		}
	}

	// The fold is not a notifier. A reindex replays every artifact,
	// including the accept — and tells nobody.
	before := f.notify.total()
	if _, err := f.reindex(); err != nil {
		t.Fatal(err)
	}
	f.sync()
	time.Sleep(300 * time.Millisecond)
	if got := f.notify.total(); got != before {
		t.Fatalf("a reindex fired %d new notifications, want none", got-before)
	}
	for who, n := range want {
		if evs := f.attention(t, who, 0); len(evs) != n {
			t.Fatalf("after reindex %s has %d attention rows, want %d", who, len(evs), n)
		}
	}
}