// 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 := ¬ifyCapture{}
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)
}
}
}