package gitx
import (
"context"
"fmt"
"path/filepath"
"testing"
"time"
"github.com/go-git/go-git/v5/plumbing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
// Fixtures are built entirely in-process: Create makes the bare repo, and
// commits are written through this package's own plumbing. Nothing shells out
// to the git binary, so the tests exercise the code that actually runs in the
// daemon rather than proving that git works.
var fxSpace = core.SpaceRef{Owner: "bigbes", Name: "rfcs"}
// fxTime yields deterministic, monotonically increasing commit timestamps, so
// commit shas are stable within a run and the ordering in git log is defined.
func fxTime(n int) time.Time {
return time.Date(2026, 7, 22, 12, 0, 0, 0, time.UTC).Add(time.Duration(n) * time.Minute)
}
func fxSig(name, email string, n int) Signature {
return Signature{Name: name, Email: email, When: fxTime(n)}
}
func owner(n int) Signature { return fxSig("bigbes", "bigbes@gmail.com", n) }
func agent(n int) Signature {
return fxSig("claude-code/spec-writer (for bigbes)", "agent@srht.bigb.es", n)
}
// meta builds a commit description with the provenance trailers the design
// requires on an agent write. The trailer *policy* is authn/'s; here they are
// just what a caller supplies.
func meta(subject string, n int, trailers ...Trailer) CommitMeta {
return CommitMeta{
Message: subject,
Trailers: trailers,
Author: agent(n),
Committer: owner(n),
}
}
// ownerMeta is a commit made in the owner's own name, as a human push would be.
func ownerMeta(subject string, n int) CommitMeta {
return CommitMeta{Message: subject, Author: owner(n), Committer: owner(n)}
}
// doc renders a minimal valid document: frontmatter with the three required
// keys, then a body.
func doc(id, title, body string) []byte {
return []byte(fmt.Sprintf("---\nid: %s\ntitle: %s\nstatus: draft\n---\n\n%s\n", id, title, body))
}
// newSpace creates a fresh space under a temporary repos root and returns the
// handle plus the root.
func newSpace(t *testing.T) (*Repo, string) {
t.Helper()
root := t.TempDir()
repo, err := Create(context.Background(), root, fxSpace, CreateOptions{Owner: owner(0)})
if err != nil {
t.Fatalf("Create: %v", err)
}
return repo, root
}
// pushApproved commits directly onto the approved branch, standing in for a
// human push through receive-pack. It deliberately bypasses CommitProposal,
// which refuses the approved branch — that restriction is the point of the
// write path, and a test that wants a human push has to simulate one.
//
// writes with a nil Content delete the path, which is how the fixture expresses
// the human-only rename that the merge must survive.
func pushApproved(t *testing.T, r *Repo, meta CommitMeta, writes ...Write) plumbing.Hash {
t.Helper()
return pushBranch(t, r, r.ApprovedBranch(), meta, writes...)
}
func pushBranch(t *testing.T, r *Repo, branch string, meta CommitMeta, writes ...Write) plumbing.Hash {
t.Helper()
name := plumbing.NewBranchReferenceName(branch)
old, err := r.repo.Reference(name, false)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
tree, err := r.treeOf(old.Hash())
if err != nil {
t.Fatalf("tree of %s: %v", old.Hash(), err)
}
node, err := r.loadTree(tree, 0)
if err != nil {
t.Fatalf("loadTree: %v", err)
}
for _, w := range writes {
if w.Content == nil {
if !node.remove(w.Path) {
t.Fatalf("remove %q: not present", w.Path)
}
continue
}
h, err := r.writeBlob(w.Path, w.Content)
if err != nil {
t.Fatalf("writeBlob %q: %v", w.Path, err)
}
if err := node.set(w.Path, h); err != nil {
t.Fatalf("set %q: %v", w.Path, err)
}
}
treeHash, err := node.write(r.repo.Storer)
if err != nil {
t.Fatalf("write tree: %v", err)
}
commit, err := r.writeCommit(meta, treeHash, []plumbing.Hash{old.Hash()})
if err != nil {
t.Fatalf("writeCommit: %v", err)
}
if err := r.repo.Storer.SetReference(plumbing.NewHashReference(name, commit)); err != nil {
t.Fatalf("set %s: %v", name, err)
}
return commit
}
// openProposal cuts a proposal branch at base and commits writes onto it.
func openProposal(t *testing.T, r *Repo, branch, base string, m CommitMeta, writes ...Write) CommitResult {
t.Helper()
ctx := context.Background()
if _, err := r.CreateProposalBranch(ctx, branch, base); err != nil {
t.Fatalf("CreateProposalBranch(%q, %q): %v", branch, base, err)
}
res, err := r.CommitProposal(ctx, branch, writes, m)
if err != nil {
t.Fatalf("CommitProposal(%q): %v", branch, err)
}
return res
}
// docPaths lists the document paths present at a revision.
func docPaths(t *testing.T, r *Repo, rev string) []string {
t.Helper()
docs, err := r.ListDocuments(context.Background(), rev)
if err != nil {
t.Fatalf("ListDocuments(%q): %v", rev, err)
}
out := make([]string, 0, len(docs))
for _, d := range docs {
out = append(out, d.Path)
}
return out
}
// mustRead reads a document body at a revision.
func mustRead(t *testing.T, r *Repo, rev, path string) string {
t.Helper()
d, err := r.ReadDocument(context.Background(), rev, path)
if err != nil {
t.Fatalf("ReadDocument(%q, %q): %v", rev, path, err)
}
return string(d.Data)
}
func spaceDir(root string) string { return filepath.Join(root, "~"+fxSpace.Owner, fxSpace.Name) }