~bigbes/sr-ht-spec

ref: a0fa81b359bd57c82b22ff0148cde77f24ba70e5 sr-ht-spec/service/merge_test.go -rw-r--r-- 6.5 KiB
a0fa81b3 — Eugene Blikh refactor(authn): one Principal.CanRead() for the read-plane ACL (spec-ejq.1) 25 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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package service

import (
	"context"
	"errors"
	"testing"

	"sourcecraft.dev/bigbes/sr-ht-spec/core"
)

// openProposalFor opens a proposal against the current approved head and returns
// its result. It is the common setup for the merge tests, which then act on the
// proposal the design's review plane would.
func openProposalFor(t *testing.T, svc *Service, sp *Space, path, id string, content []byte) ProposeResult {
	t.Helper()
	ctx := context.Background()
	base, err := sp.Repo.ApprovedHead(ctx)
	if err != nil {
		t.Fatalf("ApprovedHead: %v", err)
	}
	res, err := svc.Propose(ctx, ProposeRequest{
		Space:     fxSpace,
		Principal: agentPrincipal(),
		Title:     "proposal for " + path,
		IfMatch:   base.String(),
		Message:   "write " + path,
		Writes:    []DocumentWrite{{Path: path, Content: content}},
	})
	if err != nil {
		t.Fatalf("Propose(%s): %v", path, err)
	}
	return res
}

// TestMergeLandsProposal proves the human approve path: an open proposal that no
// policy auto-merges is merged on request, recorded as human-approved, and its
// document is then readable at the approved head.
func TestMergeLandsProposal(t *testing.T) {
	svc, _ := newTestService(t)
	ctx := context.Background()
	sp, err := svc.CreateSpace(ctx, fxSpace)
	if err != nil {
		t.Fatalf("CreateSpace: %v", err)
	}

	res := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "body"))
	if res.Merged {
		t.Fatalf("specs/ proposal auto-merged without a policy")
	}

	merged, err := svc.Merge(ctx, fxSpace, res.Proposal.ID, core.ApprovalHuman)
	if err != nil {
		t.Fatalf("Merge: %v", err)
	}
	if merged.State != core.StateMerged || merged.Approval != core.ApprovalHuman {
		t.Fatalf("merged = %+v, want state=merged approval=human", merged)
	}

	// The document is now on the approved head.
	doc, err := svc.ReadDocument(ctx, sp, ApprovedRev, "specs/a.md")
	if err != nil {
		t.Fatalf("ReadDocument after merge: %v", err)
	}
	if len(doc.Data) == 0 {
		t.Fatalf("merged document reads empty")
	}

	// A merged proposal is no longer open, so a second resolution is refused.
	if _, err := svc.Merge(ctx, fxSpace, res.Proposal.ID, core.ApprovalHuman); !errors.Is(err, ErrProposalNotOpen) {
		t.Fatalf("re-merge: err = %v, want ErrProposalNotOpen", err)
	}
}

// TestRejectResolvesProposal proves reject moves an open proposal to rejected
// and leaves it listable there, its URL still resolving.
func TestRejectResolvesProposal(t *testing.T) {
	svc, _ := newTestService(t)
	ctx := context.Background()
	sp, err := svc.CreateSpace(ctx, fxSpace)
	if err != nil {
		t.Fatalf("CreateSpace: %v", err)
	}
	res := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "body"))

	rejected, err := svc.Reject(ctx, fxSpace, res.Proposal.ID)
	if err != nil {
		t.Fatalf("Reject: %v", err)
	}
	if rejected.State != core.StateRejected {
		t.Fatalf("state = %s, want rejected", rejected.State)
	}
	got, err := svc.GetProposal(ctx, res.Proposal.ID)
	if err != nil {
		t.Fatalf("GetProposal after reject: %v", err)
	}
	if got.State != core.StateRejected {
		t.Fatalf("GetProposal state = %s, want rejected", got.State)
	}
}

// TestMergeStaleWhenDocumentChangedUnderIt proves the 409: a document the
// proposal edits, changed on the approved branch since the proposal's base,
// cannot merge — the loser refetches and re-proposes.
func TestMergeStaleWhenDocumentChangedUnderIt(t *testing.T) {
	svc, _ := newTestService(t)
	ctx := context.Background()
	sp, err := svc.CreateSpace(ctx, fxSpace)
	if err != nil {
		t.Fatalf("CreateSpace: %v", err)
	}
	// The approved head carries v1 of the document the proposal will edit.
	commitFiles(t, sp, sp.ApprovedBranch(), 1, map[string][]byte{
		"specs/a.md": mdDoc("S-1", "A", "v1"),
	})
	res := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "v2-from-agent"))

	// A human pushes v3 of the same document onto the approved head, after the
	// proposal's base.
	commitFiles(t, sp, sp.ApprovedBranch(), 2, map[string][]byte{
		"specs/a.md": mdDoc("S-1", "A", "v3-from-human"),
	})

	_, err = svc.Merge(ctx, fxSpace, res.Proposal.ID, core.ApprovalHuman)
	if !errors.Is(err, ErrStale) {
		t.Fatalf("Merge of a proposal whose document moved under it: err = %v, want ErrStale", err)
	}
}

// TestProposeAddToExistingThenMerge proves the X-Proposal path: a second write
// against a proposal's fixed base adds to it, and merging then lands both
// documents at once.
func TestProposeAddToExistingThenMerge(t *testing.T) {
	svc, _ := newTestService(t)
	ctx := context.Background()
	sp, err := svc.CreateSpace(ctx, fxSpace)
	if err != nil {
		t.Fatalf("CreateSpace: %v", err)
	}
	first := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "one"))

	// Add a second document to the same proposal, sending the same base.
	second, err := svc.Propose(ctx, ProposeRequest{
		Space:      fxSpace,
		Principal:  agentPrincipal(),
		ProposalID: first.Proposal.ID,
		IfMatch:    first.Proposal.BaseRev,
		Message:    "add specs/b.md",
		Writes:     []DocumentWrite{{Path: "specs/b.md", Content: mdDoc("S-2", "B", "two")}},
	})
	if err != nil {
		t.Fatalf("Propose add-to-existing: %v", err)
	}
	if second.Proposal.ID != first.Proposal.ID {
		t.Fatalf("add opened a new proposal %d, want %d", second.Proposal.ID, first.Proposal.ID)
	}

	if _, err := svc.Merge(ctx, fxSpace, first.Proposal.ID, core.ApprovalHuman); err != nil {
		t.Fatalf("Merge: %v", err)
	}
	for _, path := range []string{"specs/a.md", "specs/b.md"} {
		if _, err := svc.ReadDocument(ctx, sp, ApprovedRev, path); err != nil {
			t.Fatalf("ReadDocument(%s) after merge: %v", path, err)
		}
	}
}

// TestAddToExistingRejectsDriftedBase proves an add whose If-Match no longer
// names the proposal's base is a 409 rather than a silent write against the old
// base.
func TestAddToExistingRejectsDriftedBase(t *testing.T) {
	svc, _ := newTestService(t)
	ctx := context.Background()
	sp, err := svc.CreateSpace(ctx, fxSpace)
	if err != nil {
		t.Fatalf("CreateSpace: %v", err)
	}
	first := openProposalFor(t, svc, sp, "specs/a.md", "S-1", mdDoc("S-1", "A", "one"))

	// The approved head moves; the agent mistakenly sends the new head as its
	// base for the add.
	newHead := commitFiles(t, sp, sp.ApprovedBranch(), 2, map[string][]byte{
		"specs/c.md": mdDoc("S-3", "C", "unrelated"),
	})
	_, err = svc.Propose(ctx, ProposeRequest{
		Space:      fxSpace,
		Principal:  agentPrincipal(),
		ProposalID: first.Proposal.ID,
		IfMatch:    newHead.String(),
		Message:    "add specs/b.md",
		Writes:     []DocumentWrite{{Path: "specs/b.md", Content: mdDoc("S-2", "B", "two")}},
	})
	if !errors.Is(err, ErrStale) {
		t.Fatalf("add with a drifted base: err = %v, want ErrStale", err)
	}
}