~bigbes/sr-ht-spec

ref: 2dc6b71739a6101f0c113df5ae27d2007b52dbd9 sr-ht-spec/gitx/fixture_test.go -rw-r--r-- 5.2 KiB
2dc6b717 — Eugene Blikh chore(beads): enable Dolt auto-push to dolt.srht.bigb.es 24 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
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) }