Skip to content
go-libfossil Artifact [87195d5e3b]
Login

Artifact [87195d5e3b]

Artifact 87195d5e3b23b935e31628475f0cca510a69ce12d190acccacd67d926594ea25:


package conformance

import (
	"bytes"
	"context"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/danmestas/go-libfossil/db"
	"github.com/danmestas/go-libfossil/internal/blob"
	"github.com/danmestas/go-libfossil/internal/content"
	"github.com/danmestas/go-libfossil/internal/deck"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
	"github.com/danmestas/go-libfossil/internal/hash"
	"github.com/danmestas/go-libfossil/internal/manifest"
	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/internal/sync"
	"github.com/danmestas/go-libfossil/internal/verify"
	"github.com/danmestas/go-libfossil/internal/xfer"
	"github.com/danmestas/go-libfossil/simio"
)

// The submanifest suite checks go-libfossil's reader and writer against
// draft-fossil-submanifests-00; the writer's cases are in
// submanifest_writer_test.go. Card-level cases are byte strings given to
// deck.Parse, as in the artifact suite; tree cases store artifacts in a
// repository and read them back through the same entry points every consumer
// uses. fossil(1) reads no S card, so nothing here is checked against it;
// the binding byte vectors of Appendix A, as authored for zit, stand in.

// smCase is one submanifest case. run reports nil when go-libfossil does what
// the draft requires.
type smCase struct {
	Case
	run func(t *testing.T) error
}

func submanifestSuiteCases() []Case {
	var all []Case
	for _, c := range submanifestCases() {
		all = append(all, c.Case)
	}
	return all
}

func TestSubmanifest(t *testing.T) {
	for _, c := range submanifestCases() {
		check(t, c.Case, func() error { return c.run(t) })
	}
}

func submanifestCases() []smCase {
	var all []smCase
	for _, group := range [][]smCase{
		smCardCases(), smTypeCases(), smWalkCases(), smStateCases(),
		smTransferCases(), smVectorCases(), smWriterCases(),
	} {
		all = append(all, group...)
	}
	return all
}

// Hashes for card-level cases, where only their shape matters.
const (
	smH40 = "0123456789abcdef0123456789abcdef01234567"
	smH64 = "aa9d404c14fe4dadd2ab398961fb0dfe650a877c061f650d8233a5888e920acf"
	smTS  = "2026-07-26T00:00:00"
)

func smAccepts(id, req, body string, want func(*deck.Deck) error) smCase {
	return smCase{Case{ID: id, Req: req}, func(*testing.T) error {
		d, err := deck.Parse(withZ(body))
		if err != nil {
			return fmt.Errorf("rejected: %v", err)
		}
		if want != nil {
			return want(d)
		}
		return nil
	}}
}

func smRejects(id, req, body string) smCase {
	return smCase{Case{ID: id, Req: req}, func(*testing.T) error {
		if _, err := deck.Parse(withZ(body)); err == nil {
			return fmt.Errorf("accepted")
		}
		return nil
	}}
}

func smType(want deck.ArtifactType) func(*deck.Deck) error {
	return func(d *deck.Deck) error {
		if d.Type != want {
			return fmt.Errorf("type %d, want %d", d.Type, want)
		}
		return nil
	}
}

func smCardCases() []smCase {
	return []smCase{
		smAccepts("submanifest.3.1.s-defined", "SM-3.1-1", "S a "+smH64+"\n", nil),
		smRejects("submanifest.3.1.o-undefined", "SM-3.1-1", "D "+smTS+"\nO x\n"),
		smRejects("submanifest.3.1.v-undefined", "SM-3.1-1", "D "+smTS+"\nV x\n"),
		smRejects("submanifest.3.1.x-undefined", "SM-3.1-1", "D "+smTS+"\nX x\n"),
		smRejects("submanifest.3.1.y-undefined", "SM-3.1-1", "D "+smTS+"\nY x\n"),
		smRejects("submanifest.3.1.outside-a-z", "SM-3.1-1", "D "+smTS+"\na x\n"),
		smRejects("submanifest.3.2.third-token", "SM-3.2-1", "S a "+smH64+" x\n"),
		smRejects("submanifest.3.2.no-hash", "SM-3.2-1", "S a\n"),
		smAccepts("submanifest.3.3.filename-decoded", "SM-3.3-1", `S a\sb `+smH64+"\n",
			func(d *deck.Deck) error { return want("S[0].Name", d.S[0].Name, "a b") }),
		smRejects("submanifest.3.3.hash-not-decoded", "SM-3.3-1",
			`S a `+smH64[:62]+`\s`+"\n"),
		smAccepts("submanifest.3.4.ascending", "SM-3.4-1",
			"S a "+smH64+"\nS b "+smH40+"\n",
			func(d *deck.Deck) error { return want("len(S)", len(d.S), 2) }),
		smRejects("submanifest.3.4.equal", "SM-3.4-1", "S a "+smH64+"\nS a "+smH40+"\n"),
		smRejects("submanifest.3.4.descending", "SM-3.4-1", "S b "+smH64+"\nS a "+smH40+"\n"),
		// The comparator orders decoded names: "a b" (0x20) before "a\b" (0x5C).
		smAccepts("submanifest.3.4.decoded-order", "SM-3.4-1",
			`S a\sb `+smH64+"\n"+`S a\\b `+smH40+"\n", nil),
		smAccepts("submanifest.3.4.r-s-t", "SM-3.4-2",
			"D "+smTS+"\nR "+md5Token+"\nS a "+smH64+"\nT +x *\nU u\n", smType(deck.Checkin)),
		smRejects("submanifest.3.4.s-after-t", "SM-3.4-2",
			"D "+smTS+"\nT +x *\nS a "+smH64+"\nU u\n"),
		smAccepts("submanifest.3.5.sha1", "SM-3.5-1", "S a "+smH40+"\n", nil),
		smAccepts("submanifest.3.5.upper-case", "SM-3.5-1", "S a "+strings.ToUpper(smH64)+"\n", nil),
		smRejects("submanifest.3.5.short", "SM-3.5-1", "S a "+smH40[:39]+"\n"),
		smRejects("submanifest.3.5.non-hex", "SM-3.5-1", "S a "+smH40[:39]+"g\n"),
		smRejects("submanifest.3.6.dot-dot", "SM-3.6-1", "S ../a "+smH64+"\n"),
		smRejects("submanifest.3.6.absolute", "SM-3.6-1", "S /a "+smH64+"\n"),
	}
}

func smTypeCases() []smCase {
	tree := "C t\nD " + smTS + "\nN text/plain\nP " + smH40 + "\nQ +" + smH40 + "\nR " + md5Token +
		"\nS a " + smH64 + "\nT +x *\nU u\n"
	bases := map[string]deck.ArtifactType{
		baseManifest: deck.Checkin, baseDelta: deck.Checkin, baseCluster: deck.Cluster,
		baseControl: deck.Control, baseWiki: deck.Wiki, baseTicket: deck.Ticket,
		baseAttachment: deck.Attachment, baseEvent: deck.Event, baseForum: deck.ForumPost,
	}
	return []smCase{
		smAccepts("submanifest.4.2.tree-cards", "SM-4.2-1", tree, smType(deck.Checkin)),
		smRejects("submanifest.4.2.tree-without-z", "SM-4.2-1", "D "+smTS+"\nS a "+smH64+"\n"+
			"Z "+md5Token+"x\n"),
		smAccepts("submanifest.4.2.leaf", "SM-4.2-2", "F a "+smH64+"\n", smType(deck.Submanifest)),
		smRejects("submanifest.4.2.leaf-with-u", "SM-4.2-2", "F a "+smH64+"\nU u\n"),
		smRejects("submanifest.4.2.leaf-with-r", "SM-4.2-2", "F a "+smH64+"\nR "+md5Token+"\n"),
		smAccepts("submanifest.4.3.leaf-reassigned", "SM-4.3-1", "F a "+smH64+"\n",
			smType(deck.Submanifest)),
		smAccepts("submanifest.4.3.interior-reassigned", "SM-4.3-1", "S a "+smH64+"\n",
			smType(deck.Submanifest)),
		smAccepts("submanifest.4.3.s-sets-manifest", "SM-4.3-1", "D "+smTS+"\nS a "+smH64+"\n",
			smType(deck.Checkin)),
		smRejects("submanifest.4.3.d-less-comment", "SM-4.3-1", "C x\nF a "+smH64+"\n"),
		{Case{ID: "submanifest.4.3.base-types-unchanged", Req: "SM-4.3-2"}, func(*testing.T) error {
			for body, typ := range bases {
				d, err := deck.Parse(withZ(body))
				if err != nil {
					return fmt.Errorf("a base artifact is rejected: %v", err)
				}
				if d.Type != typ {
					return fmt.Errorf("type %d, want %d, for %.20q", d.Type, typ, body)
				}
			}
			return nil
		}},
		smRejects("submanifest.4.4.b-and-s", "SM-4.4-1", "B "+smH40+"\nD "+smTS+"\nS a "+smH64+"\n"),
		smRejects("submanifest.4.4.manifest-f-and-s", "SM-4.4-2",
			"D "+smTS+"\nF a "+smH40+"\nS b "+smH64+"\n"),
		smRejects("submanifest.4.4.sub-f-and-s", "SM-4.4-3", "F a "+smH40+"\nS b "+smH64+"\n"),
		smRejects("submanifest.4.4.sub-neither", "SM-4.4-3", "Z "+md5Token+"\n"),
		smRejects("submanifest.4.4.sub-delete-marker", "SM-4.4-4", "F a\n"),
		smAccepts("submanifest.4.5.interior-first-card-s", "SM-4.5-1", "S a "+smH64+"\n", nil),
		// F sets MANIFEST as it is read; only the post-pass, once every card
		// is in, finds no D card and makes the artifact a sub-manifest, and
		// the required-card check then asks for no D card.
		smAccepts("submanifest.9.1.type-after-cards", "SM-9.1-1",
			"F a "+smH64+"\nF b "+smH40+"\n", smType(deck.Submanifest)),
	}
}

// smFixture is one repository under the sha3 hash policy, the policy the
// Appendix A artifacts are named under.
type smFixture struct {
	r       *repo.Repo
	content map[string][]byte // file name to content
}

func newSMFixture(t *testing.T) (*smFixture, error) {
	r, err := repo.Create(filepath.Join(t.TempDir(), "sm.fossil"), "u", simio.CryptoRand{}, "")
	if err != nil {
		return nil, err
	}
	t.Cleanup(func() { r.Close() })
	if err := r.SetHashPolicy("sha3"); err != nil {
		return nil, err
	}
	return &smFixture{r: r, content: map[string][]byte{}}, nil
}

func (f *smFixture) store(data []byte) (string, error) {
	var uuid string
	err := f.r.WithTx(func(tx *db.Tx) error {
		var err error
		_, uuid, err = blob.Store(tx, data)
		return err
	})
	return uuid, err
}

func (f *smFixture) storeDeck(d *deck.Deck) (string, error) {
	data, err := d.Marshal()
	if err != nil {
		return "", err
	}
	return f.store(data)
}

func (f *smFixture) file(name string) (deck.FileCard, error) {
	data := []byte("content of " + name + "\n")
	f.content[name] = data
	uuid, err := f.store(data)
	return deck.FileCard{Name: name, UUID: uuid}, err
}

// leaves stores one leaf sub-manifest per group of file names, storing the
// files too, and returns the S cards that reference the leaves. Nothing is
// linked yet.
func (f *smFixture) leaves(groups ...[]string) ([]deck.SubmanifestCard, error) {
	var refs []deck.SubmanifestCard
	for _, names := range groups {
		var cards []deck.FileCard
		for _, name := range names {
			c, err := f.file(name)
			if err != nil {
				return nil, err
			}
			cards = append(cards, c)
		}
		uuid, err := f.storeDeck(&deck.Deck{F: cards})
		if err != nil {
			return nil, err
		}
		refs = append(refs, deck.SubmanifestCard{Name: names[0], UUID: uuid})
	}
	return refs, nil
}

func (f *smFixture) interior(refs ...deck.SubmanifestCard) (deck.SubmanifestCard, error) {
	uuid, err := f.storeDeck(&deck.Deck{S: refs})
	return deck.SubmanifestCard{Name: refs[0].Name, UUID: uuid}, err
}

var smTime = time.Date(2026, 7, 26, 0, 0, 0, 0, time.UTC)

// checkin stores a tree check-in over refs on trunk and crosslinks.
func (f *smFixture) checkin(at time.Time, refs []deck.SubmanifestCard, parents ...string) (libfossil.FslID, string, error) {
	d := &deck.Deck{C: "tree", D: at, S: refs, P: parents, U: deck.User("u")}
	if len(parents) == 0 {
		d.T = []deck.TagCard{
			{Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: "trunk"},
			{Type: deck.TagPropagating, Name: "sym-trunk", UUID: "*"},
		}
	}
	return f.link(d)
}

func (f *smFixture) link(d *deck.Deck) (libfossil.FslID, string, error) {
	uuid, err := f.storeDeck(d)
	if err != nil {
		return 0, "", err
	}
	if _, err := manifest.Crosslink(f.r); err != nil {
		return 0, "", err
	}
	rid, _ := blob.Exists(f.r.DB(), uuid)
	return rid, uuid, nil
}

func (f *smFixture) names(rid libfossil.FslID) (string, error) {
	files, err := manifest.ListFiles(f.r, rid)
	if err != nil {
		return "", err
	}
	var names []string
	for _, e := range files {
		names = append(names, e.Name)
	}
	return strings.Join(names, " "), nil
}

func (f *smFixture) count(query string, args ...any) int {
	var n int
	if err := f.r.DB().QueryRow(query, args...).Scan(&n); err != nil {
		return -1
	}
	return n
}

// smRepoCase builds a case that runs against a fresh fixture.
func smRepoCase(id, req string, run func(f *smFixture) error) smCase {
	return smCase{Case{ID: id, Req: req}, func(t *testing.T) error {
		f, err := newSMFixture(t)
		if err != nil {
			return err
		}
		return run(f)
	}}
}

// smFault builds a tree over refs and requires ListFiles to refuse it with a
// tree fault, never listing it in part.
func smFault(id, req string, build func(f *smFixture) ([]deck.SubmanifestCard, error)) smCase {
	return smRepoCase(id, req, func(f *smFixture) error {
		refs, err := build(f)
		if err != nil {
			return err
		}
		rid, _, err := f.checkin(smTime, refs)
		if err != nil {
			return err
		}
		if names, err := f.names(rid); err == nil {
			return fmt.Errorf("listed %q", names)
		}
		if n := f.count("SELECT count(*) FROM event WHERE objid=?", rid); n != 0 {
			return fmt.Errorf("a tree that cannot be walked was linked")
		}
		return nil
	})
}

func smWalkCases() []smCase {
	twoLevel := func(f *smFixture) (libfossil.FslID, error) {
		refs, err := f.leaves([]string{"README.md", "src/main.c"}, []string{"src/node11.c", "src/util.c"})
		if err != nil {
			return 0, err
		}
		rid, _, err := f.checkin(smTime, refs)
		return rid, err
	}
	return []smCase{
		smRepoCase("submanifest.5.tree-manifest-walked", "SM-5-1", func(f *smFixture) error {
			rid, err := twoLevel(f)
			if err != nil {
				return err
			}
			got, err := f.names(rid)
			return wantNoErr(err, "ListFiles", want("files", got, "README.md src/main.c src/node11.c src/util.c"))
		}),
		smRepoCase("submanifest.5.1.three-level", "SM-5.1-1", func(f *smFixture) error {
			refs, err := f.leaves([]string{"a", "b"}, []string{"c"}, []string{"d", "e"}, []string{"f"})
			if err != nil {
				return err
			}
			left, err := f.interior(refs[0], refs[1])
			if err != nil {
				return err
			}
			right, err := f.interior(refs[2], refs[3])
			if err != nil {
				return err
			}
			rid, _, err := f.checkin(smTime, []deck.SubmanifestCard{left, right})
			if err != nil {
				return err
			}
			got, err := f.names(rid)
			return wantNoErr(err, "ListFiles", want("files", got, "a b c d e f"))
		}),
		smFault("submanifest.5.1.siblings-overlap", "SM-5.1-2", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			refs, err := f.leaves([]string{"a", "c"}, []string{"b"})
			return refs, err
		}),
		smFault("submanifest.5.1.leaf-unsorted-against-s", "SM-5.1-2", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			refs, err := f.leaves([]string{"b"}, []string{"c"})
			if err != nil {
				return nil, err
			}
			// The second S card is named before the first leaf's last file.
			return []deck.SubmanifestCard{refs[0], {Name: "bb", UUID: refs[1].UUID}}, nil
		}),
		smFault("submanifest.5.1.absent", "SM-5.1-3", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			return []deck.SubmanifestCard{{Name: "a", UUID: hash.AlgSHA3.Hash([]byte("absent"))}}, nil
		}),
		smFault("submanifest.5.1.not-a-submanifest", "SM-5.1-4", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			c, err := f.file("a")
			return []deck.SubmanifestCard{{Name: "a", UUID: c.UUID}}, err
		}),
		// A reference cycle cannot be built from content-addressed
		// artifacts; what stops any path from looping is shown here by the
		// depth bound, and the repeated-artifact check itself by
		// internal/manifest's TestTreeWalkRefusesRepeatedArtifact.
		smFault("submanifest.5.1.no-unbounded-path", "SM-5.1-5", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			return smChain(f, 17)
		}),
		smFault("submanifest.5.1.depth-17", "SM-5.1-6", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			return smChain(f, 17)
		}),
		smRepoCase("submanifest.5.1.depth-16", "SM-5.1-6", func(f *smFixture) error {
			refs, err := smChain(f, 16)
			if err != nil {
				return err
			}
			rid, _, err := f.checkin(smTime, refs)
			if err != nil {
				return err
			}
			got, err := f.names(rid)
			return wantNoErr(err, "ListFiles", want("files", got, "a"))
		}),
		smFault("submanifest.5.1.leaf-first-name", "SM-5.1-7", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			refs, err := f.leaves([]string{"b"})
			return []deck.SubmanifestCard{{Name: "a", UUID: refs[0].UUID}}, err
		}),
		smFault("submanifest.5.1.interior-first-name", "SM-5.1-8", func(f *smFixture) ([]deck.SubmanifestCard, error) {
			refs, err := f.leaves([]string{"b"})
			if err != nil {
				return nil, err
			}
			node, err := f.interior(refs[0])
			return []deck.SubmanifestCard{{Name: "a", UUID: node.UUID}}, err
		}),
		smRepoCase("submanifest.5.1.mlink-like-flat", "SM-5.1-9", smMlinkLikeFlat),
		smRepoCase("submanifest.4.4.tokens-unaltered", "SM-4.4-5", func(f *smFixture) error {
			c, err := f.file("x.sh")
			if err != nil {
				return err
			}
			c.Perm, c.OldName = "x", "old.sh"
			leaf, err := f.storeDeck(&deck.Deck{F: []deck.FileCard{c}})
			if err != nil {
				return err
			}
			rid, _, err := f.checkin(smTime, []deck.SubmanifestCard{{Name: "x.sh", UUID: leaf}})
			if err != nil {
				return err
			}
			files, err := manifest.ListFiles(f.r, rid)
			if err != nil {
				return err
			}
			if len(files) != 1 || files[0].UUID != c.UUID || files[0].Perm != "x" {
				return fmt.Errorf("listed %+v", files)
			}
			if err := want("executable mlink rows",
				f.count("SELECT count(*) FROM mlink WHERE mid=? AND mperm=1", rid), 1); err != nil {
				return err
			}
			// The old-name token reaches crosslink: a tree child renaming
			// x.sh to y.sh records the prior name.
			_, parent, err := f.checkin(smTime, []deck.SubmanifestCard{{Name: "x.sh", UUID: leaf}})
			if err != nil {
				return err
			}
			renamed := deck.FileCard{Name: "y.sh", UUID: c.UUID, Perm: "x", OldName: "x.sh"}
			leaf2, err := f.storeDeck(&deck.Deck{F: []deck.FileCard{renamed}})
			if err != nil {
				return err
			}
			child, _, err := f.checkin(smTime.Add(time.Hour), []deck.SubmanifestCard{{Name: "y.sh", UUID: leaf2}}, parent)
			if err != nil {
				return err
			}
			return want("rename rows", f.count(`SELECT count(*) FROM mlink m
				JOIN filename p ON p.fnid=m.pfnid WHERE m.mid=? AND p.name='x.sh'`, child), 1)
		}),
		smRepoCase("submanifest.5.2.missing-is-fatal", "SM-5.2-1", smMissingIsFatal),
		smRepoCase("submanifest.5.2.gap-recorded-and-retried", "SM-5.2-2", smGapRetried),
		smRepoCase("submanifest.5.3.lookup", "SM-5.3-1", func(f *smFixture) error {
			rid, err := twoLevel(f)
			if err != nil {
				return err
			}
			for name, present := range map[string]bool{
				"README.md": true, "src/main.c": true, "src/util.c": true,
				"A": false, "src/m": false, "src/zzz": false,
			} {
				_, found, err := manifest.LookupFile(f.r, rid, name)
				if err != nil || found != present {
					return fmt.Errorf("LookupFile(%q) = %v, %v", name, found, err)
				}
			}
			return nil
		}),
		smRepoCase("submanifest.5.3.depth-bound", "SM-5.3-2", func(f *smFixture) error {
			refs, err := smChain(f, 17)
			if err != nil {
				return err
			}
			uuid, err := f.storeDeck(&deck.Deck{C: "t", D: smTime, S: refs, U: deck.User("u")})
			if err != nil {
				return err
			}
			rid, _ := blob.Exists(f.r.DB(), uuid)
			if _, _, err := manifest.LookupFile(f.r, rid, "a"); err == nil {
				return fmt.Errorf("a lookup descended past 16 sub-manifests")
			}
			return nil
		}),
		smRepoCase("submanifest.5.4.delta-over-tree", "SM-5.4-1", smDeltaOverTree),
		smRepoCase("submanifest.5.4.commit-on-tree-is-flat", "SM-5.4-2", smCommitOnTree),
		smRepoCase("submanifest.5.5.r-card", "SM-5.5-1", func(f *smFixture) error {
			refs, err := f.leaves([]string{"a", "b"}, []string{"c"})
			if err != nil {
				return err
			}
			d := &deck.Deck{C: "t", D: smTime, R: smR(f.content, "a", "b", "c"), S: refs, U: deck.User("u")}
			rid, _, err := f.link(d)
			if err != nil {
				return err
			}
			return f.r.WithTx(func(tx *db.Tx) error { return manifest.VerifyRCard(tx, nil, rid) })
		}),
		smRepoCase("submanifest.9.3.consumers", "SM-9.3-1", smConsumers),
		smRepoCase("submanifest.10.bounds", "SM-10-1", func(f *smFixture) error {
			refs, err := smChain(f, 17)
			if err != nil {
				return err
			}
			uuid, err := f.storeDeck(&deck.Deck{C: "t", D: smTime, S: refs, U: deck.User("u")})
			if err != nil {
				return err
			}
			rid, _ := blob.Exists(f.r.DB(), uuid)
			if _, err := manifest.ListFiles(f.r, rid); err == nil {
				return fmt.Errorf("the walk exceeded the depth bound")
			}
			return nil
		}),
	}
}

// smChain is a tree whose single leaf lies depth sub-manifests down.
func smChain(f *smFixture, depth int) ([]deck.SubmanifestCard, error) {
	refs, err := f.leaves([]string{"a"})
	if err != nil {
		return nil, err
	}
	ref := refs[0]
	for level := 1; level < depth; level++ {
		if ref, err = f.interior(ref); err != nil {
			return nil, err
		}
	}
	return []deck.SubmanifestCard{ref}, nil
}

// smR is the R card over the named files' content.
func smR(content map[string][]byte, names ...string) string {
	sort.Strings(names)
	h := md5.New()
	for _, name := range names {
		fmt.Fprintf(h, "%s %d\n", name, len(content[name]))
		h.Write(content[name])
	}
	return hex.EncodeToString(h.Sum(nil))
}

func wantNoErr(err error, what string, rest error) error {
	if err != nil {
		return fmt.Errorf("%s: %v", what, err)
	}
	return rest
}

// smMlinkLikeFlat: a tree child of a tree parent gets the file-change rows a
// flat child of a flat parent gets for the same change.
func smMlinkLikeFlat(f *smFixture) error {
	refs, err := f.leaves([]string{"a", "b"}, []string{"c"})
	if err != nil {
		return err
	}
	_, parent, err := f.checkin(smTime, refs)
	if err != nil {
		return err
	}
	b2, err := f.store([]byte("b two\n"))
	if err != nil {
		return err
	}
	aCard := deck.FileCard{Name: "a", UUID: mustUUIDOf(f, "a")}
	leaf, err := f.storeDeck(&deck.Deck{F: []deck.FileCard{aCard, {Name: "b", UUID: b2}}})
	if err != nil {
		return err
	}
	child, _, err := f.checkin(smTime.Add(time.Hour), []deck.SubmanifestCard{{Name: "a", UUID: leaf}}, parent)
	if err != nil {
		return err
	}
	rows, err := f.r.DB().Query(`SELECT fn.name, m.fid=0 FROM mlink m JOIN filename fn
		ON fn.fnid=m.fnid WHERE m.mid=? ORDER BY fn.name`, child)
	if err != nil {
		return err
	}
	defer rows.Close()
	var got []string
	for rows.Next() {
		var name string
		var deleted bool
		if err := rows.Scan(&name, &deleted); err != nil {
			return err
		}
		got = append(got, fmt.Sprintf("%s:%v", name, deleted))
	}
	return want("mlink rows", strings.Join(got, " "), "b:false c:true")
}

func mustUUIDOf(f *smFixture, name string) string {
	return hash.AlgSHA3.Hash(f.content[name])
}

func smMissingIsFatal(f *smFixture) error {
	refs, err := f.leaves([]string{"a"})
	if err != nil {
		return err
	}
	absent := deck.SubmanifestCard{Name: "b", UUID: hash.AlgSHA3.Hash([]byte("not stored"))}
	rid, _, err := f.checkin(smTime, append(refs, absent))
	if err != nil {
		return err
	}
	if _, err := manifest.ListFiles(f.r, rid); err == nil {
		return fmt.Errorf("ListFiles listed a tree with an absent sub-manifest")
	} else if got, ok := manifest.MissingSubmanifest(err); !ok || got != absent.UUID {
		return fmt.Errorf("ListFiles error %v does not name the absent sub-manifest", err)
	}
	if _, _, err := manifest.LookupFile(f.r, rid, "b"); err == nil {
		return fmt.Errorf("LookupFile resolved a name under an absent sub-manifest")
	}
	if n := f.count("SELECT count(*) FROM mlink WHERE mid=?", rid); n != 0 {
		return fmt.Errorf("%d file-change rows for an incomplete tree", n)
	}
	return nil
}

func smGapRetried(f *smFixture) error {
	file, err := f.file("b")
	if err != nil {
		return err
	}
	leaf, err := (&deck.Deck{F: []deck.FileCard{file}}).Marshal()
	if err != nil {
		return err
	}
	absent := deck.SubmanifestCard{Name: "b", UUID: hash.AlgSHA3.Hash(leaf)}
	rid, _, err := f.checkin(smTime, []deck.SubmanifestCard{absent})
	if err != nil {
		return err
	}
	phantom, ok := blob.Exists(f.r.DB(), absent.UUID)
	if !ok {
		return fmt.Errorf("no phantom for the absent sub-manifest")
	}
	gap := "SELECT count(*) FROM orphan WHERE rid=? AND baseline=?"
	if n := f.count(gap, rid, phantom); n != 1 {
		return fmt.Errorf("gap rows %d, want 1", n)
	}
	if _, err := manifest.Crosslink(f.r); err != nil {
		return err
	}
	if n := f.count(gap, rid, phantom); n != 1 {
		return fmt.Errorf("gap rows after a second sweep %d, want 1", n)
	}
	if _, err := f.store(leaf); err != nil {
		return err
	}
	manifest.AfterDephantomize(context.Background(), f.r, phantom)
	if n := f.count("SELECT count(*) FROM event WHERE objid=?", rid); n != 1 {
		return fmt.Errorf("the tree was not linked when its sub-manifest arrived")
	}
	return nil
}

func smDeltaOverTree(f *smFixture) error {
	refs, err := f.leaves([]string{"a", "b"}, []string{"c"})
	if err != nil {
		return err
	}
	_, base, err := f.checkin(smTime, refs)
	if err != nil {
		return err
	}
	a2, err := f.store([]byte("a two\n"))
	if err != nil {
		return err
	}
	f.content["a"] = []byte("a two\n")
	delta := &deck.Deck{B: base, C: "delta", D: smTime.Add(time.Hour), P: []string{base},
		F: []deck.FileCard{{Name: "a", UUID: a2}, {Name: "c"}}, U: deck.User("u")}
	delta.R = smR(f.content, "a", "b")
	rid, _, err := f.link(delta)
	if err != nil {
		return err
	}
	got, err := f.names(rid)
	if err := wantNoErr(err, "ListFiles", want("files", got, "a b")); err != nil {
		return err
	}
	e, found, err := manifest.LookupFile(f.r, rid, "b")
	if err != nil || !found || e.UUID != mustUUIDOf(f, "b") {
		return fmt.Errorf("LookupFile(b) through the tree baseline = %+v, %v, %v", e, found, err)
	}
	return f.r.WithTx(func(tx *db.Tx) error { return manifest.VerifyRCard(tx, nil, rid) })
}

func smCommitOnTree(f *smFixture) error {
	refs, err := f.leaves([]string{"a", "b"})
	if err != nil {
		return err
	}
	parent, _, err := f.checkin(smTime, refs)
	if err != nil {
		return err
	}
	rid, _, err := manifest.Checkin(f.r, manifest.CheckinOpts{
		Files:   []manifest.File{{Name: "a", Content: f.content["a"]}, {Name: "b", Content: []byte("b2\n")}},
		Comment: "on a tree", User: "u", Time: smTime.Add(time.Hour), Parent: parent, Delta: true,
	})
	if err != nil {
		return err
	}
	d, err := manifest.GetManifest(f.r, rid)
	if err != nil {
		return err
	}
	if d.B != "" || len(d.S) != 0 {
		return fmt.Errorf("commit on a tree wrote B=%q and %d S cards", d.B, len(d.S))
	}
	return nil
}

// smConsumers: checkout, diff, rebuild, annotate and commit all read a tree
// check-in's file set through the seam, so each sees all of its files.
func smConsumers(f *smFixture) error {
	refs, err := f.leaves([]string{"a.txt", "b.txt"}, []string{"c.txt"})
	if err != nil {
		return err
	}
	rid, _, err := f.checkin(smTime, refs)
	if err != nil {
		return err
	}
	// Commit on it, then rebuild, then compare the two file sets.
	next, _, err := manifest.Checkin(f.r, manifest.CheckinOpts{
		Files: []manifest.File{
			{Name: "a.txt", Content: f.content["a.txt"]},
			{Name: "b.txt", Content: []byte("b changed\n")},
			{Name: "c.txt", Content: f.content["c.txt"]},
		},
		Comment: "edit", User: "u", Time: smTime.Add(time.Hour), Parent: rid,
	})
	if err != nil {
		return fmt.Errorf("commit: %v", err)
	}
	if _, err := verify.Rebuild(f.r); err != nil {
		return fmt.Errorf("rebuild: %v", err)
	}
	if n := f.count("SELECT count(*) FROM mlink WHERE mid=? AND pmid=?", next, rid); n != 1 {
		return fmt.Errorf("rebuilt diff rows %d, want 1 (b.txt)", n)
	}
	got, err := f.names(rid)
	return wantNoErr(err, "ListFiles after rebuild", want("files", got, "a.txt b.txt c.txt"))
}

func smStateCases() []smCase {
	return []smCase{
		smRepoCase("submanifest.7.1.phantoms-private", "SM-7.1-1", func(f *smFixture) error {
			missing := deck.SubmanifestCard{Name: "z", UUID: hash.AlgSHA3.Hash([]byte("never"))}
			uuid, err := f.storeDeck(&deck.Deck{S: []deck.SubmanifestCard{missing}})
			if err != nil {
				return err
			}
			rid, _ := blob.Exists(f.r.DB(), uuid)
			if err := content.MakePrivate(f.r.DB(), int64(rid)); err != nil {
				return err
			}
			if _, err := manifest.Crosslink(f.r); err != nil {
				return err
			}
			prid, ok := blob.Exists(f.r.DB(), missing.UUID)
			if !ok {
				return fmt.Errorf("no phantom for the absent reference")
			}
			if !content.IsPrivate(f.r.DB(), int64(prid)) {
				return fmt.Errorf("the phantom of a private reference is public")
			}
			return nil
		}),
		smRepoCase("submanifest.7.1.dephantomize-retries", "SM-7.1-2", smGapRetried),
		smRepoCase("submanifest.7.3.deferred-like-delta", "SM-7.3-1", smMissingIsFatal),
		smRepoCase("submanifest.7.3.no-crosslink-effect", "SM-7.3-2", func(f *smFixture) error {
			refs, err := f.leaves([]string{"a"})
			if err != nil {
				return err
			}
			rid, _ := blob.Exists(f.r.DB(), refs[0].UUID)
			if _, err := manifest.Crosslink(f.r); err != nil {
				return err
			}
			for _, table := range []string{"event", "plink", "mlink", "tagxref"} {
				col := map[string]string{"event": "objid", "plink": "cid", "mlink": "mid", "tagxref": "rid"}[table]
				if n := f.count("SELECT count(*) FROM "+table+" WHERE "+col+"=?", rid); n != 0 {
					return fmt.Errorf("a sub-manifest wrote %d %s rows", n, table)
				}
			}
			return nil
		}),
		smRepoCase("submanifest.7.3.verify-reports-gap", "SM-7.3-2", func(f *smFixture) error {
			absent := deck.SubmanifestCard{Name: "a", UUID: hash.AlgSHA3.Hash([]byte("absent leaf"))}
			if _, _, err := f.checkin(smTime, []deck.SubmanifestCard{absent}); err != nil {
				return err
			}
			report, err := verify.Verify(f.r)
			if err != nil {
				return err
			}
			for _, issue := range report.Issues {
				if issue.Kind == verify.IssueEventMissing || issue.Kind == verify.IssuePlinkMissing {
					return fmt.Errorf("an incomplete tree is reported as damage: %s", issue.Message)
				}
			}
			return want("missing references", report.MissingRefs, 1)
		}),
		smRepoCase("submanifest.8.1.seen-indicator", "SM-8.1-2", func(f *smFixture) error {
			if seen, _ := manifest.SeenSubmanifest(f.r.DB()); seen {
				return fmt.Errorf("indicator set before any tree")
			}
			if _, err := f.leaves([]string{"a"}); err != nil {
				return err
			}
			if _, err := manifest.Crosslink(f.r); err != nil {
				return err
			}
			seen, err := manifest.SeenSubmanifest(f.r.DB())
			return wantNoErr(err, "SeenSubmanifest", want("seen", seen, true))
		}),
		smRepoCase("submanifest.9.2.sweep-marker", "SM-9.2-1", func(f *smFixture) error {
			refs, err := f.leaves([]string{"a"})
			if err != nil {
				return err
			}
			rid, _ := blob.Exists(f.r.DB(), refs[0].UUID)
			if _, err := manifest.Crosslink(f.r); err != nil {
				return err
			}
			if n := f.count("SELECT count(*) FROM crosslink_nonartifact_v2 WHERE rid=?", rid); n != 1 {
				return fmt.Errorf("no completion marker for the sub-manifest")
			}
			linked, err := manifest.Crosslink(f.r)
			return wantNoErr(err, "Crosslink", want("relinked", linked, 0))
		}),
		smRepoCase("submanifest.9.4.rediscovered", "SM-9.4-1", smGapRetried),
	}
}

// smServerTree is a fixture holding a tree, for the transfer cases.
func smServerTree(f *smFixture) (tree, leaf, file string, err error) {
	refs, err := f.leaves([]string{"a"})
	if err != nil {
		return "", "", "", err
	}
	_, tree, err = f.checkin(smTime, refs)
	return tree, refs[0].UUID, mustUUIDOf(f, "a"), err
}

// smGimme asks r for uuid as a client announcing version, carrying pragma
// send-submanifests after client-version when reads is set.
func smGimme(r *repo.Repo, version string, reads bool, uuid string) (*xfer.Message, error) {
	var pc string
	r.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&pc)
	cards := []xfer.Card{
		&xfer.PragmaCard{Name: "client-version", Values: []string{version, "20260101", "000000"}},
	}
	if reads {
		cards = append(cards, &xfer.PragmaCard{Name: "send-submanifests"})
	}
	cards = append(cards, &xfer.PullCard{ServerCode: "x", ProjectCode: pc}, &xfer.GimmeCard{UUID: uuid})
	return sync.HandleSync(context.Background(), r, &xfer.Message{Cards: cards})
}

// smRefusal is zit's wording of §7.2's explicit error.
const smRefusal = "Peer does not accept sub-manifests."

// smRefusedOnce checks a reply carries exactly one error, the §7.2 refusal.
func smRefusedOnce(resp *xfer.Message) error {
	errs := smCardsOf[*xfer.ErrorCard](resp)
	if len(errs) != 1 || errs[0].Message != smRefusal {
		return fmt.Errorf("refusal cards %v, want one %q", errs, smRefusal)
	}
	return nil
}

// smStrip removes every pragma named name from msg, as a peer that does not
// implement the draft would leave it out.
func smStrip(msg *xfer.Message, name string) {
	kept := msg.Cards[:0]
	for _, c := range msg.Cards {
		if p, ok := c.(*xfer.PragmaCard); ok && p.Name == name {
			continue
		}
		kept = append(kept, c)
	}
	msg.Cards = kept
}

// smWire is msg as the lines it goes out as.
func smWire(msg *xfer.Message) ([]string, error) {
	raw, err := msg.EncodeUncompressed()
	if err != nil {
		return nil, err
	}
	return strings.Split(strings.TrimSuffix(string(raw), "\n"), "\n"), nil
}

// smLeadingRun checks the leading pragma run of a sync and a clone reply,
// with and without server-version, under both settings: send-submanifests
// first, then avoid-submanifests while the setting is off, and neither
// elsewhere. avoid selects which of the two pragmas the case is about.
func smLeadingRun(f *smFixture, avoid bool) error {
	if _, err := f.r.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil {
		return err
	}
	var pc string
	f.r.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&pc)
	dated := &xfer.PragmaCard{Name: "client-version", Values: []string{"22800", "20260101", "000000"}}
	bare := &xfer.PragmaCard{Name: "client-version", Values: []string{"22800"}}
	for _, enable := range []string{"off", "on"} {
		if _, err := f.r.DB().Exec(`REPLACE INTO config(name,value,mtime) VALUES
			('enable-submanifests',?,1),('submanifest-k','3',1)`, enable); err != nil {
			return err
		}
		for _, cv := range []*xfer.PragmaCard{dated, bare} {
			for _, body := range []xfer.Card{
				&xfer.PullCard{ServerCode: "x", ProjectCode: pc},
				&xfer.CloneCard{Version: 3, SeqNo: 1, SeqNoIsDecimal: true},
			} {
				resp, err := sync.HandleSync(context.Background(), f.r, &xfer.Message{Cards: []xfer.Card{cv, body}})
				if err != nil {
					return err
				}
				lines, err := smWire(resp)
				if err != nil {
					return err
				}
				at := 0
				if cv == dated {
					if !strings.HasPrefix(lines[0], "pragma server-version ") {
						return fmt.Errorf("reply begins %q", lines[0])
					}
					at = 1
				}
				var run []string
				for _, line := range lines[at:] {
					if !strings.HasPrefix(line, "pragma ") {
						break
					}
					run = append(run, line)
				}
				wantRun := []string{"pragma send-submanifests"}
				if enable == "off" {
					wantRun = append(wantRun, "pragma avoid-submanifests")
				}
				name := "pragma send-submanifests"
				if avoid {
					name = "pragma avoid-submanifests"
				}
				if len(run) < len(wantRun) || strings.Join(run[:len(wantRun)], "|") != strings.Join(wantRun, "|") {
					return fmt.Errorf("enable %s, %T: leading run %q, want %q first", enable, body, run, wantRun)
				}
				count := 0
				for _, line := range lines {
					if line == name {
						count++
					}
				}
				wantCount := 1
				if avoid && enable == "on" {
					wantCount = 0
				}
				if count != wantCount {
					return fmt.Errorf("enable %s, %T: %q %d times", enable, body, name, count)
				}
			}
		}
	}
	return nil
}

// smRecorder is a transport to r that keeps every request's wire lines.
func smRecorder(r *repo.Repo, requests *[][]string) *sync.MockTransport {
	return &sync.MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		lines, err := smWire(req)
		if err != nil {
			return &xfer.Message{}
		}
		*requests = append(*requests, lines)
		resp, err := sync.HandleSync(context.Background(), r, req)
		if err != nil {
			return &xfer.Message{}
		}
		return resp
	}}
}

// smRequestPragma: every request of a sync and of a clone carries
// send-submanifests right after client-version, emission on or off.
func smRequestPragma(f *smFixture) error {
	if _, err := f.r.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil {
		return err
	}
	dir, err := os.MkdirTemp("", "sm-request")
	if err != nil {
		return err
	}
	defer os.RemoveAll(dir)
	var requests [][]string
	clone, _, err := sync.Clone(context.Background(), filepath.Join(dir, "c.fossil"), smRecorder(f.r, &requests), sync.CloneOpts{})
	if err != nil {
		return err
	}
	defer clone.Close()
	for _, enable := range []string{"off", "on"} {
		if _, err := clone.DB().Exec(`REPLACE INTO config(name,value,mtime) VALUES
			('enable-submanifests',?,1),('submanifest-k','3',1)`, enable); err != nil {
			return err
		}
		if _, err := sync.Sync(context.Background(), clone, smRecorder(f.r, &requests),
			sync.SyncOpts{Push: true, Pull: true}); err != nil {
			return err
		}
	}
	if len(requests) < 3 {
		return fmt.Errorf("only %d requests", len(requests))
	}
	for i, lines := range requests {
		found := false
		for j, line := range lines {
			if strings.HasPrefix(line, "pragma client-version ") {
				if j+1 >= len(lines) || lines[j+1] != "pragma send-submanifests" {
					return fmt.Errorf("request %d: client-version not followed by send-submanifests", i)
				}
				found = true
				break
			}
		}
		if !found {
			return fmt.Errorf("request %d has no client-version", i)
		}
	}
	return nil
}

// smClientRefusesStripped: a client pushing a tree to a server whose
// replies lost send-submanifests refuses the server's gimmes with the
// explicit error, and the server never stores the tree; nothing else
// changes, so the ordinary file still arrives.
func smClientRefusesStripped(f *smFixture) error {
	if _, err := f.r.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil {
		return err
	}
	dir, err := os.MkdirTemp("", "sm-stripped")
	if err != nil {
		return err
	}
	defer os.RemoveAll(dir)
	var discard [][]string
	clone, _, err := sync.Clone(context.Background(), filepath.Join(dir, "c.fossil"), smRecorder(f.r, &discard), sync.CloneOpts{})
	if err != nil {
		return err
	}
	defer clone.Close()
	if _, err := clone.DB().Exec(`REPLACE INTO config(name,value,mtime) VALUES
		('enable-submanifests','on',1),('submanifest-k','1',1)`); err != nil {
		return err
	}
	files := map[string]string{"README.md": "# Example\n", "src/main.c": "int main(void){return 0;}\n",
		"src/node11.c": "/* node11 */\n", "src/util.c": "/* util */\n"}
	rid, d, err := smCommit(clone, files, 0, smTime.Add(2*time.Hour), false)
	if err != nil {
		return err
	}
	if len(d.S) == 0 {
		return fmt.Errorf("fixture: the commit is not a tree")
	}
	entries, err := manifest.ListFiles(clone, rid)
	if err != nil {
		return err
	}
	var refusals []string
	transport := &sync.MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		for _, e := range smCardsOf[*xfer.ErrorCard](req) {
			refusals = append(refusals, e.Message)
		}
		resp, err := sync.HandleSync(context.Background(), f.r, req)
		if err != nil {
			return &xfer.Message{}
		}
		smStrip(resp, "send-submanifests")
		return resp
	}}
	if _, err := sync.Sync(context.Background(), clone, transport, sync.SyncOpts{Push: true, Pull: true}); err != nil {
		return err
	}
	if len(refusals) == 0 || refusals[0] != smRefusal {
		return fmt.Errorf("refusals sent %q", refusals)
	}
	for _, s := range d.S {
		if _, ok := content.AvailableByUUID(f.r.DB(), s.UUID); ok {
			return fmt.Errorf("the server stored leaf %s", s.UUID)
		}
	}
	for _, e := range entries {
		if _, ok := content.AvailableByUUID(f.r.DB(), e.UUID); !ok {
			return fmt.Errorf("the ordinary file %s did not arrive", e.Name)
		}
	}
	return nil
}

// smReqConfigAtClone: a clone asks for the settings once, in its cleanup
// round, with reqconfig /config; a pull afterwards asks for nothing.
func smReqConfigAtClone(f *smFixture) error {
	if _, err := f.r.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil {
		return err
	}
	dir, err := os.MkdirTemp("", "sm-reqconfig")
	if err != nil {
		return err
	}
	defer os.RemoveAll(dir)
	var requests [][]string
	clone, _, err := sync.Clone(context.Background(), filepath.Join(dir, "c.fossil"), smRecorder(f.r, &requests), sync.CloneOpts{})
	if err != nil {
		return err
	}
	defer clone.Close()
	count := func(from int) (n, last int) {
		for i, lines := range requests[from:] {
			for _, line := range lines {
				if strings.HasPrefix(line, "reqconfig ") {
					if line != "reqconfig /config" {
						return -1, i
					}
					n, last = n+1, from+i
				}
			}
		}
		return n, last
	}
	cloneRounds := len(requests)
	n, last := count(0)
	if n != 1 || last != cloneRounds-1 {
		return fmt.Errorf("clone sent %d reqconfig /config (last in request %d of %d)", n, last+1, cloneRounds)
	}
	if _, err := sync.Sync(context.Background(), clone, smRecorder(f.r, &requests),
		sync.SyncOpts{Pull: true}); err != nil {
		return err
	}
	if n, _ := count(cloneRounds); n != 0 {
		return fmt.Errorf("a pull sent %d reqconfig cards", n)
	}
	return nil
}

func smCardsOf[T xfer.Card](msg *xfer.Message) []T {
	var out []T
	for _, c := range msg.Cards {
		if v, ok := c.(T); ok {
			out = append(out, v)
		}
	}
	return out
}

func smTransferCases() []smCase {
	return []smCase{
		smRepoCase("submanifest.7.2.gate", "SM-7.2-1", func(f *smFixture) error {
			tree, leaf, file, err := smServerTree(f)
			if err != nil {
				return err
			}
			for _, uuid := range []string{tree, leaf} {
				old, err := smGimme(f.r, "22800", false, uuid)
				if err != nil {
					return err
				}
				if len(smCardsOf[*xfer.FileCard](old)) != 0 {
					return fmt.Errorf("a peer without send-submanifests was sent %s", uuid)
				}
				if err := smRefusedOnce(old); err != nil {
					return fmt.Errorf("%s: %v", uuid, err)
				}
				current, err := smGimme(f.r, "22800", true, uuid)
				if err != nil {
					return err
				}
				if len(smCardsOf[*xfer.FileCard](current)) != 1 {
					return fmt.Errorf("a peer sending send-submanifests was refused %s", uuid)
				}
			}
			plain, err := smGimme(f.r, "22800", false, file)
			if err != nil {
				return err
			}
			return want("file cards for ordinary content", len(smCardsOf[*xfer.FileCard](plain)), 1)
		}),
		smRepoCase("submanifest.7.2.fossil-2.29-refused", "SM-7.2-1", func(f *smFixture) error {
			tree, _, _, err := smServerTree(f)
			if err != nil {
				return err
			}
			// The announcements fossil 2.29 and a later release make: no
			// version reads as a tree reader.
			for _, version := range []string{"22900", "23000"} {
				resp, err := smGimme(f.r, version, false, tree)
				if err != nil {
					return err
				}
				if len(smCardsOf[*xfer.FileCard](resp)) != 0 {
					return fmt.Errorf("a fossil %s peer, which reads no tree, is sent one", version)
				}
				if err := smRefusedOnce(resp); err != nil {
					return err
				}
			}
			return nil
		}),
		smRepoCase("submanifest.7.2.client-refuses-stripped", "SM-7.2-1", smClientRefusesStripped),
		smRepoCase("submanifest.7.2.clone-delta-of-withheld-tree", "SM-7.2-1", smCloneDeltaOfTree),
		smRepoCase("submanifest.7.2.pushed-tree-withheld", "SM-7.2-1", smPushedTreeWithheld),
		smRepoCase("submanifest.7.2.avoid-pragma", "SM-7.2-2", func(f *smFixture) error {
			return smLeadingRun(f, true)
		}),
		smRepoCase("submanifest.7.2.send-pragma-reply", "SM-7.2-4", func(f *smFixture) error {
			return smLeadingRun(f, false)
		}),
		smRepoCase("submanifest.7.2.send-pragma-request", "SM-7.2-4", smRequestPragma),
		smRepoCase("submanifest.7.2.base-version", "SM-7.2-4", func(f *smFixture) error {
			resp, err := smGimme(f.r, "22800", true, mustUUIDOf(f, "a"))
			if err != nil {
				return err
			}
			for _, p := range smCardsOf[*xfer.PragmaCard](resp) {
				if p.Name == "server-version" {
					return want("announced version", p.Values[0], "22800")
				}
			}
			return fmt.Errorf("no server-version")
		}),
		smRepoCase("submanifest.7.2.receipt-not-gated", "SM-7.2-3", smPushedTreeWithheld),
		smRepoCase("submanifest.7.2.client-commits-flat", "SM-7.2-3", smCommitOnTree),
		smRepoCase("submanifest.6.3.config-members", "SM-6.3-1", func(f *smFixture) error {
			if _, err := f.r.DB().Exec(`REPLACE INTO config(name,value,mtime) VALUES
				('enable-submanifests','on',5),('submanifest-k','3',6),('submanifest-auto-retune','off',7)`); err != nil {
				return err
			}
			resp, err := sync.HandleSync(context.Background(), f.r, &xfer.Message{Cards: []xfer.Card{
				&xfer.ReqConfigCard{Name: "/config"},
			}})
			if err != nil {
				return err
			}
			var body bytes.Buffer
			for _, c := range smCardsOf[*xfer.ConfigCard](resp) {
				body.Write(c.Content)
				body.WriteByte('\n')
			}
			for _, name := range []string{"enable-submanifests", "submanifest-k", "submanifest-auto-retune"} {
				if !strings.Contains(body.String(), "'"+name+"'") {
					return fmt.Errorf("/config omits %s", name)
				}
			}
			return nil
		}),
		{Case{ID: "submanifest.6.3.staged-atomic", Req: "SM-6.3-2"}, smStagedConfig},
		smRepoCase("submanifest.6.3.reqconfig-at-clone", "SM-6.3-3", smReqConfigAtClone),
		{Case{ID: "submanifest.6.3.clone-without-group", Req: "SM-6.3-4"}, func(t *testing.T) error {
			return smCloneWithoutGroup(t)
		}},
	}
}

// smOldTransport serves r to a client that reads no tree, as fossil 2.28:
// its requests carry no send-submanifests.
func smOldTransport(r *repo.Repo) *sync.MockTransport {
	return &sync.MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		smStrip(req, "send-submanifests")
		resp, err := sync.HandleSync(context.Background(), r, req)
		if err != nil {
			return &xfer.Message{}
		}
		return resp
	}}
}

// smCloneDeltaOfTree: a flat check-in stored as a delta of a tree manifest
// -- the shape a repository takes when it moves between flat and tree
// check-ins -- reaches a 2.28 client whole, never as a delta of a tree that
// client is refused.
func smCloneDeltaOfTree(f *smFixture) error {
	refs, err := f.leaves([]string{"a"})
	if err != nil {
		return err
	}
	comment := strings.Repeat("shared comment text ", 200)
	_, tree, err := f.link(&deck.Deck{C: comment, D: smTime, S: refs, U: deck.User("u")})
	if err != nil {
		return err
	}
	_, flat, err := f.link(&deck.Deck{C: comment, D: smTime.Add(time.Hour), P: []string{tree},
		F: []deck.FileCard{{Name: "a", UUID: mustUUIDOf(f, "a")}}, U: deck.User("u")})
	if err != nil {
		return err
	}
	treeRID, _ := blob.Exists(f.r.DB(), tree)
	flatRID, _ := blob.Exists(f.r.DB(), flat)
	if err := f.r.WithTx(func(tx *db.Tx) error {
		_, err := content.Deltify(tx, flatRID, treeRID)
		return err
	}); err != nil {
		return err
	}
	if src, _ := content.DeltaSource(f.r.DB(), flatRID); src != treeRID {
		return fmt.Errorf("fixture: the flat check-in is not a delta of the tree")
	}
	dir, err := os.MkdirTemp("", "sm-clone")
	if err != nil {
		return err
	}
	defer os.RemoveAll(dir)
	clone, _, err := sync.Clone(context.Background(), filepath.Join(dir, "old.fossil"), smOldTransport(f.r), sync.CloneOpts{})
	if err != nil {
		return err
	}
	defer clone.Close()
	rid, ok := content.AvailableByUUID(clone.DB(), flat)
	if !ok {
		return fmt.Errorf("the flat check-in did not reach the 2.28 client whole")
	}
	if _, err := content.Expand(clone.DB(), rid); err != nil {
		return fmt.Errorf("the flat check-in cannot be rebuilt: %v", err)
	}
	if _, ok := content.AvailableByUUID(clone.DB(), tree); ok {
		return fmt.Errorf("the 2.28 client was sent the tree")
	}
	return nil
}

// smPushedTreeWithheld: a tree pushed to the server is withheld from the
// next 2.28 client that asks, whatever has or has not been crosslinked.
func smPushedTreeWithheld(f *smFixture) error {
	if _, err := f.r.DB().Exec("UPDATE user SET cap='gio' WHERE login='nobody'"); err != nil {
		return err
	}
	var pc string
	f.r.DB().QueryRow("SELECT value FROM config WHERE name='project-code'").Scan(&pc)
	file := []byte("pushed file\n")
	fileUUID := hash.AlgSHA3.Hash(file)
	leaf, err := (&deck.Deck{F: []deck.FileCard{{Name: "a", UUID: fileUUID}}}).Marshal()
	if err != nil {
		return err
	}
	tree, err := (&deck.Deck{C: "pushed", D: smTime, U: deck.User("u"),
		S: []deck.SubmanifestCard{{Name: "a", UUID: hash.AlgSHA3.Hash(leaf)}}}).Marshal()
	if err != nil {
		return err
	}
	cards := []xfer.Card{
		&xfer.PragmaCard{Name: "client-version", Values: []string{"22800", "20260101", "000000"}},
		&xfer.PragmaCard{Name: "send-submanifests"},
		&xfer.PushCard{ServerCode: "x", ProjectCode: pc},
	}
	for _, data := range [][]byte{file, leaf, tree} {
		cards = append(cards, &xfer.FileCard{UUID: hash.AlgSHA3.Hash(data), Content: data})
	}
	if _, err := sync.HandleSync(context.Background(), f.r, &xfer.Message{Cards: cards}); err != nil {
		return err
	}
	if _, ok := content.AvailableByUUID(f.r.DB(), hash.AlgSHA3.Hash(tree)); !ok {
		return fmt.Errorf("fixture: the push stored no tree")
	}
	resp, err := smGimme(f.r, "22800", false, hash.AlgSHA3.Hash(tree))
	if err != nil {
		return err
	}
	if len(smCardsOf[*xfer.FileCard](resp)) != 0 {
		return fmt.Errorf("a pushed tree went to a 2.28 client")
	}
	return nil
}

// smCloneWithoutGroup clones a repository whose server sends no sub-manifest
// settings: the clone leaves enable-submanifests unset, which reads as off.
func smCloneWithoutGroup(t *testing.T) error {
	src, err := newSMFixture(t)
	if err != nil {
		return err
	}
	if _, err := src.leaves([]string{"a"}); err != nil {
		return err
	}
	transport := &sync.MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, err := sync.HandleSync(context.Background(), src.r, req)
		if err != nil {
			return &xfer.Message{}
		}
		return resp
	}}
	clone, _, err := sync.Clone(context.Background(), filepath.Join(t.TempDir(), "c.fossil"), transport, sync.CloneOpts{})
	if err != nil {
		return err
	}
	defer clone.Close()
	var n int
	clone.DB().QueryRow("SELECT count(*) FROM config WHERE name='enable-submanifests'").Scan(&n)
	return want("enable-submanifests rows", n, 0)
}

// smCloneWithConfig clones a small repository through a transport that
// answers the clone's configuration request with rows, one config card each,
// in the order given, and returns the three settings the clone holds.
func smCloneWithConfig(t *testing.T, rows []string) (map[string]string, error) {
	src, err := newSMFixture(t)
	if err != nil {
		return nil, err
	}
	if _, err := src.file("a"); err != nil {
		return nil, err
	}
	transport := &sync.MockTransport{Handler: func(req *xfer.Message) *xfer.Message {
		resp, err := sync.HandleSync(context.Background(), src.r, req)
		if err != nil {
			return &xfer.Message{}
		}
		if len(smCardsOf[*xfer.ReqConfigCard](req)) > 0 {
			for _, row := range rows {
				resp.Cards = append(resp.Cards, &xfer.ConfigCard{Name: "/config", Content: []byte(row)})
			}
		}
		return resp
	}}
	clone, _, err := sync.Clone(context.Background(), filepath.Join(t.TempDir(), "c.fossil"), transport, sync.CloneOpts{})
	if err != nil {
		return nil, err
	}
	defer clone.Close()
	got := map[string]string{}
	for _, name := range []string{"enable-submanifests", "submanifest-k", "submanifest-auto-retune"} {
		var v string
		if clone.DB().QueryRow("SELECT value FROM config WHERE name=?", name).Scan(&v) == nil {
			got[name] = v
		}
	}
	return got, nil
}

// smStagedConfig: the three settings arrive whole in any card order, and an
// invalid value or an enable without a valid k leaves none applied.
func smStagedConfig(t *testing.T) error {
	rows := []string{
		"10 'enable-submanifests' value 'on'",
		"11 'submanifest-k' value '3'",
		"12 'submanifest-auto-retune' value 'off'",
	}
	for _, order := range [][]int{{0, 1, 2}, {2, 0, 1}, {1, 2, 0}} {
		var ordered []string
		for _, i := range order {
			ordered = append(ordered, rows[i])
		}
		got, err := smCloneWithConfig(t, ordered)
		if err != nil {
			return err
		}
		if got["enable-submanifests"] != "on" || got["submanifest-k"] != "3" || got["submanifest-auto-retune"] != "off" {
			return fmt.Errorf("order %v applied %v", order, got)
		}
	}
	for _, bad := range [][]string{
		{rows[0], "11 'submanifest-k' value '13'", rows[2]},
		{rows[0], rows[2]},
		{rows[1], "12 'submanifest-auto-retune' value 'maybe'", rows[0]},
	} {
		got, err := smCloneWithConfig(t, bad)
		if err != nil {
			return err
		}
		if len(got) != 0 {
			return fmt.Errorf("rejected set %q applied %v", bad, got)
		}
	}
	return nil
}

// smVector reads one of zit's Appendix A vector files, dropping the leading
// "#" lines that keep the file inert on disk.
func smVector(name string) ([]byte, error) {
	data, err := os.ReadFile(filepath.Join("testdata", "submanifest", "submanifest_"+name+".txt"))
	if err != nil {
		return nil, err
	}
	for bytes.HasPrefix(data, []byte("#")) {
		i := bytes.IndexByte(data, '\n')
		if i < 0 {
			return nil, fmt.Errorf("%s: comment without a newline", name)
		}
		data = data[i+1:]
	}
	return data, nil
}

// smVectorSet is one Appendix A check-in: its manifest, its sub-manifests,
// every file's content, and the artifact names the vector records.
type smVectorSet struct {
	manifest string
	subs     []string
	files    map[string]string
	names    map[string]string // vector name to its recorded artifact name
	want     string            // the walked enumeration
	baseline *smVectorSet      // for a delta: the tree it names
}

var smA1 = map[string]string{
	"README.md": "# Example\n", "src/main.c": "int main(void){return 0;}\n",
	"src/node11.c": "/* node11 */\n", "src/util.c": "/* util */\n",
}

func smA4Files(names ...string) map[string]string {
	files := map[string]string{}
	for _, name := range names {
		files[name] = "/* " + strings.TrimSuffix(strings.TrimPrefix(name, "f"), ".c") + " */\n"
	}
	return files
}

var smA4Names = []string{"f1257.c", "f1346.c", "f1523.c", "f2146.c", "f2260.c", "f2468.c", "f2614.c", "f2954.c", "f3514.c"}

func smVectorSets() map[string]*smVectorSet {
	a3 := &smVectorSet{
		manifest: "a3_tree_manifest", subs: []string{"a2_1_leaf", "a2_2_leaf"}, files: smA1,
		names: map[string]string{
			"a2_1_leaf":        "aa9d404c14fe4dadd2ab398961fb0dfe650a877c061f650d8233a5888e920acf",
			"a2_2_leaf":        "a50a722b465ed67b42ee0fbe61e4a19d6b1d22940ba5740e99e450921e3f52ea",
			"a3_tree_manifest": "95fba39cbaba76fccb15571516659cea6a0e8a60123c3e7bda1397649d87c509",
		},
		want: "README.md src/main.c src/node11.c src/util.c",
	}
	a4 := &smVectorSet{
		manifest: "a4_tree_manifest", subs: []string{"a4_1_leaf", "a4_2_leaf", "a4_3_leaf"},
		files: smA4Files(smA4Names...),
		names: map[string]string{
			"a4_1_leaf":        "b8e53d0c46cb57b616cc0ed6de5ce2453715be6157b73d247bcd5de8a6510aaa",
			"a4_2_leaf":        "cff23e5ba71569397c1d816afa2dd0bfffc42339e402df36b63623846bd5fcbe",
			"a4_3_leaf":        "9604c3f9774f7472d7bff44d5965f30250358a3e7a18046eeaf5f9d83bd5cdc6",
			"a4_tree_manifest": "02dad8d4451ecf930c0c8637cf66351579aa746aebf0cf10f249b2a6dea37caa",
		},
		want: strings.Join(smA4Names, " "),
	}
	enc := map[string]string{}
	for _, name := range []string{"v00 w.c", "v00\\w.c", "v01 w.c", "v01\\w.c", "v02 w.c",
		"v02\\w.c", "v03 w.c", "v03\\w.c", "v04 w.c"} {
		enc[name] = name + "\n"
	}
	encNames := make([]string, 0, len(enc))
	for name := range enc {
		encNames = append(encNames, name)
	}
	sort.Strings(encNames)
	deltaFiles := map[string]string{}
	for k, v := range smA1 {
		deltaFiles[k] = v
	}
	deltaFiles["src/main.c"] = "int main(void){return 1;}\n"
	return map[string]*smVectorSet{
		"a3": a3,
		"a4": a4,
		"three-level": {
			manifest: "a5_three_level_manifest",
			subs:     []string{"a5_three_level_interior_1", "a5_three_level_interior_2", "a4_1_leaf", "a4_2_leaf", "a4_3_leaf"},
			files:    smA4Files(smA4Names...),
			names: map[string]string{
				"a5_three_level_interior_1": "d42946de7b59647235f2166d964e9e5878bb1b3aba796b5e9c5e9dc719784e23",
				"a5_three_level_interior_2": "c1cc48fa9a3fbc34da7614632137890b4b09b33e750abc75514836c533e5f42e",
				"a5_three_level_manifest":   "c136ef62f5f13c18d06564649da6022d9fe25cde5be15dd1ad10b04015096424",
			},
			want: strings.Join(smA4Names, " "),
		},
		"merge": {
			manifest: "a5_merge_manifest", subs: []string{"a5_merge_leaf_1", "a5_merge_leaf_2"},
			files: smA4Files("f0004.c", "f0005.c", "f0006.c", "f0007.c", "f0016.c"),
			names: map[string]string{
				"a5_merge_leaf_1":   "4bf4476e3aa41a1e3d26c85173c18fcd19f61871d4fc31215e523d7502078cc8",
				"a5_merge_leaf_2":   "6f3c993caa208b6fd44332d241676f9aa6a8c29dc7e28f287d5cdc088005a842",
				"a5_merge_manifest": "cef55b9b325fbc8abd59a044acafa57603c1b11a605e7fbba02d511a6da7a806",
			},
			want: "f0004.c f0005.c f0006.c f0007.c f0016.c",
		},
		"encoding": {
			manifest: "a5_encoding_manifest",
			subs:     []string{"a5_encoding_leaf_1", "a5_encoding_leaf_2", "a5_encoding_leaf_3"},
			files:    enc,
			names: map[string]string{
				"a5_encoding_leaf_1":   "e2899273b6a258ee18e053a79903d02469a275e2c80279c3657c7b803e2af6c0",
				"a5_encoding_leaf_2":   "d7edb65681faaa4fd4f3541f04370421b057bf299098b781c7ec50d609869082",
				"a5_encoding_leaf_3":   "c634332663f1cc0298aae6b61c799214a522877102cd6acdfe633f29fd79eeaf",
				"a5_encoding_manifest": "deb0cb8b56fddfc4eebbab0f928cbdb19e64a6ba568064d5b43663dc1ef7263f",
			},
			want: strings.Join(encNames, " "),
		},
		"delta": {
			manifest: "a5_delta_manifest", files: deltaFiles, baseline: a3,
			names: map[string]string{
				"a5_delta_manifest": "c74e37f650677523f7b6c2b4f094f766c4427a8ea180768ad75fa2de6bd14902",
			},
			want: "README.md src/main.c src/node11.c",
		},
	}
}

// smCheckVectors stores a vector set, checks every recorded name, and reads
// the manifest back: type, walked enumeration and R card.
func smCheckVectors(f *smFixture, set *smVectorSet) error {
	if set.baseline != nil {
		if err := smCheckVectors(f, set.baseline); err != nil {
			return fmt.Errorf("baseline: %w", err)
		}
	}
	for name, data := range set.files {
		if _, err := f.store([]byte(data)); err != nil {
			return fmt.Errorf("file %s: %w", name, err)
		}
	}
	for _, name := range append(append([]string(nil), set.subs...), set.manifest) {
		data, err := smVector(name)
		if err != nil {
			return err
		}
		d, err := deck.Parse(data)
		if err != nil {
			return fmt.Errorf("%s rejected: %v", name, err)
		}
		wantType := deck.Submanifest
		if name == set.manifest {
			wantType = deck.Checkin
		}
		if d.Type != wantType {
			return fmt.Errorf("%s: type %d, want %d", name, d.Type, wantType)
		}
		uuid, err := f.store(data)
		if err != nil {
			return err
		}
		if recorded, ok := set.names[name]; ok && uuid != recorded {
			return fmt.Errorf("%s is named %s, the vector records %s", name, uuid, recorded)
		}
	}
	if _, err := manifest.Crosslink(f.r); err != nil {
		return err
	}
	rid, ok := blob.Exists(f.r.DB(), set.names[set.manifest])
	if !ok {
		return fmt.Errorf("%s not stored", set.manifest)
	}
	got, err := f.names(rid)
	if err := wantNoErr(err, "ListFiles", want("enumeration", got, set.want)); err != nil {
		return err
	}
	return f.r.WithTx(func(tx *db.Tx) error { return manifest.VerifyRCard(tx, nil, rid) })
}

func smVectorCases() []smCase {
	sets := smVectorSets()
	vector := func(id, req, key string) smCase {
		return smRepoCase(id, req, func(f *smFixture) error { return smCheckVectors(f, sets[key]) })
	}
	return []smCase{
		vector("submanifest.a.2-3", "SM-A-1", "a3"),
		vector("submanifest.a.4", "SM-A-2", "a4"),
		vector("submanifest.a.5.three-level", "SM-A-3", "three-level"),
		vector("submanifest.a.5.merge", "SM-A-3", "merge"),
		vector("submanifest.a.5.encoding", "SM-A-3", "encoding"),
		vector("submanifest.a.5.delta-over-tree", "SM-A-3", "delta"),
		smRepoCase("submanifest.a.5.lookup", "SM-5.3-1", func(f *smFixture) error {
			if err := smCheckVectors(f, sets["a4"]); err != nil {
				return err
			}
			rid, _ := blob.Exists(f.r.DB(), sets["a4"].names["a4_tree_manifest"])
			for name, present := range map[string]bool{
				"f1257.c": true, "f2146.c": true, "f3514.c": true,
				"f2000.c": false, "f0000.c": false,
			} {
				_, found, err := manifest.LookupFile(f.r, rid, name)
				if err != nil || found != present {
					return fmt.Errorf("LookupFile(%q) = %v, %v", name, found, err)
				}
			}
			return nil
		}),
	}
}