Skip to content
quarry Artifact [fbd98a5fa4]
Login

Artifact [fbd98a5fa4]

Artifact fbd98a5fa4c07d7142dded4582434f12fac6798af46657b673f7f19a01dd0a70:


package api

import (
	"context"
	"errors"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

const (
	stackBase = "bbbb1111bbbb1111bbbb1111bbbb1111"
	stackTop  = "cccc2222cccc2222cccc2222cccc2222"
)

// stackIndex folds a two-rung stack: base lands feature-a on trunk, top
// lands feature-b on feature-a. baseState settles the base ("" leaves
// it open), which is the fact the top's answer has to follow.
func stackIndex(t *testing.T, baseState string) *index.Index {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	open := func(hash, branch, target, when string) index.Parsed {
		return index.Parsed{Hash: hash, Art: &artifact.Artifact{
			Type: artifact.Forum, ThreadTitle: "land " + branch, Date: when, User: "bob",
			Body: []byte("zit-landing: 1\nbranch: " + branch + "\ntarget: " + target + "\n\nready\n"),
		}}
	}
	batch := []index.Parsed{
		open(stackBase, "feature-a", "trunk", "2026-01-01T10:00:00"),
		open(stackTop, "feature-b", "feature-a", "2026-01-01T10:30:00"),
	}
	if baseState != "" {
		batch = append(batch, index.Parsed{Hash: "dddd3333dddd3333dddd3333dddd3333",
			Art: &artifact.Artifact{Type: artifact.Forum, ThreadRoot: stackBase,
				Date: "2026-01-01T11:00:00", User: "carol",
				Body: []byte("zit-landing-state: " + baseState + "\n\ndone\n")}})
	}
	if err := ix.FoldNow(batch); err != nil {
		t.Fatal(err)
	}
	return ix
}

func stackRegistry(t *testing.T, baseState string) *ops.Registry {
	t.Helper()
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(stackIndex(t, baseState))}); err != nil {
		t.Fatal(err)
	}
	return r
}

func withI() context.Context {
	return ops.WithCaps(ops.WithUser(context.Background(), "bob"), oltp.CapSet("i"))
}

// The answer a client reads before pressing is the answer the press
// gives. landing.get on a rung above an open base says it may NOT land
// and why; landing.accept on it refuses with that same reason — and
// never reaches zit, whose refusal was a command line (e85bd39d).
func TestARungAboveAnOpenBaseSaysWhyItCannotLandAndAcceptAgrees(t *testing.T) {
	r := stackRegistry(t, "")
	got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackTop})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(landingGetOut)
	if !out.CanAccept {
		t.Fatalf("FAIL: canAccept = false; it means PERMISSION and the caller holds i")
	}
	if out.MayLand {
		t.Fatalf("FAIL: mayLand = true on a rung above an open base")
	}
	if !strings.Contains(out.MayLandReason, short(stackBase)) || !strings.Contains(out.MayLandReason, "first") {
		t.Fatalf("FAIL: mayLandReason does not name the base or the remedy: %q", out.MayLandReason)
	}
	t.Logf("ok: get: canAccept=%v mayLand=%v reason=%q", out.CanAccept, out.MayLand, out.MayLandReason)

	_, err = r.Invoke(withI(), "landing.accept", landingSayIn{Repo: "test", ID: stackTop})
	if !errors.Is(err, ops.ErrInvalid) {
		t.Fatalf("FAIL: accept err = %v, want ErrInvalid", err)
	}
	if !strings.Contains(err.Error(), out.MayLandReason) {
		t.Fatalf("FAIL: accept refused with a different reason than get reported:\n  get:    %q\n  accept: %v", out.MayLandReason, err)
	}
	if strings.Contains(err.Error(), "zit landing") {
		t.Fatalf("FAIL: the API's refusal is zit's command line: %v", err)
	}
	t.Logf("ok: accept: %v", err)
}

// A rung on a REJECTED base is sitting on work nobody will land. The
// remedy is different — retarget, not wait — and both surfaces say so.
func TestARungOnARejectedBaseSaysRetarget(t *testing.T) {
	r := stackRegistry(t, "rejected")
	got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackTop})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(landingGetOut)
	if out.MayLand || !strings.Contains(out.MayLandReason, "rejected") || !strings.Contains(out.MayLandReason, "trunk") {
		t.Fatalf("FAIL: mayLand=%v reason=%q; want false with 'rejected' and the base's target", out.MayLand, out.MayLandReason)
	}
	_, err = r.Invoke(withI(), "landing.accept", landingSayIn{Repo: "test", ID: stackTop})
	if !errors.Is(err, ops.ErrInvalid) || !strings.Contains(err.Error(), out.MayLandReason) {
		t.Fatalf("FAIL: accept = %v; want ErrInvalid carrying %q", err, out.MayLandReason)
	}
	t.Logf("ok: %q on both surfaces", out.MayLandReason)
}

// The base itself, and a standalone landing, are not blocked by any of
// this: the reason is empty and mayLand follows permission alone.
func TestTheBaseOfAStackIsNotBlockedByTheStack(t *testing.T) {
	r := stackRegistry(t, "")
	got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackBase})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(landingGetOut)
	if !out.MayLand || out.MayLandReason != "" {
		t.Fatalf("FAIL: the base reads mayLand=%v reason=%q; the bottom rung is the one accept acts on", out.MayLand, out.MayLandReason)
	}
	t.Log("ok: the base may land, with no reason attached")
}

const stackThird = "eeee4444eeee4444eeee4444eeee4444"

// threeRungIndex: base (feature-a -> trunk) REJECTED, A (feature-b ->
// feature-a) and B (feature-c -> feature-b) both open.
func threeRungRegistry(t *testing.T) *ops.Registry {
	t.Helper()
	ix := stackIndex(t, "rejected")
	if err := ix.FoldNow([]index.Parsed{{Hash: stackThird, Art: &artifact.Artifact{
		Type: artifact.Forum, ThreadTitle: "land feature-c", Date: "2026-01-01T12:00:00", User: "bob",
		Body: []byte("zit-landing: 1\nbranch: feature-c\ntarget: feature-b\n\nready\n"),
	}}}); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(ix)}); err != nil {
		t.Fatal(err)
	}
	return r
}

// On R <- A <- B with R rejected, each rung is told the thing IT can
// do. A, the bottom open rung, retargets onto R's target. B waits for A
// — it is not told A was rejected, which is what the first version of
// stackBlock said, because it took "the landing I target" for the base.
func TestEachRungAboveARejectedBaseIsToldItsOwnNextStep(t *testing.T) {
	r := threeRungRegistry(t)
	for _, c := range []struct{ id, want string }{
		{stackTop, "base " + short(stackBase) + " was rejected; retarget this landing to trunk"},
		{stackThird, "stacked on " + short(stackTop) + "; land it first"},
	} {
		got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: c.id})
		if err != nil {
			t.Fatal(err)
		}
		out := got.(landingGetOut)
		if out.MayLand || out.MayLandReason != c.want {
			t.Errorf("FAIL: %s: mayLand=%v reason=%q, want %q", short(c.id), out.MayLand, out.MayLandReason, c.want)
			continue
		}
		t.Logf("ok: %s -> %q", short(c.id), out.MayLandReason)
	}
}

// stack.base names the REJECTED landing, agreeing with the refusal
// beside it — not the segment itself, which is what jva saw (#1038):
// two fields answering one question, disagreeing.
func TestStackBaseNamesTheRejectedLandingNotItself(t *testing.T) {
	r := stackRegistry(t, "rejected")
	got, err := r.Invoke(withI(), "landing.get", landingGetIn{Repo: "test", ID: stackTop})
	if err != nil {
		t.Fatal(err)
	}
	out := got.(landingGetOut)
	if out.Stack.Base != stackBase {
		t.Fatalf("FAIL: stack.base = %q, want the rejected base %q", out.Stack.Base, stackBase)
	}
	if out.Stack.Broken != "base rejected" {
		t.Fatalf("FAIL: stack.broken = %q", out.Stack.Broken)
	}
	t.Logf("ok: stack.base = %s, broken = %q", short(out.Stack.Base), out.Stack.Broken)
}