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) }