Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

grants_test.gno

19.57 Kb · 527 lines
  1package grants
  2
  3import (
  4	"testing"
  5
  6	"gno.land/p/nt/testutils/v0"
  7	"gno.land/p/nt/uassert/v0"
  8	"gno.land/p/nt/urequire/v0"
  9)
 10
 11var (
 12	alice   = testutils.TestAddress("alice")   // member
 13	bob     = testutils.TestAddress("bob")     // member
 14	carol   = testutils.TestAddress("carol")   // member
 15	dave    = testutils.TestAddress("dave")    // applicant, not a member
 16	mallory = testutils.TestAddress("mallory") // nobody
 17)
 18
 19// board3 is the fixture most tests want: three members, so a majority of the
 20// eligible set is two whenever the applicant is an outsider.
 21func board3() *Board { return New(alice, bob, carol) }
 22
 23// submitOne files a two-milestone grant from dave, the outsider applicant.
 24func submitOne(t *testing.T, b *Board) *Request {
 25	t.Helper()
 26	r, err := b.Submit(dave, "Port the thing", "with tests",
 27		[]*Milestone{NewMilestone("design", 100), NewMilestone("ship", 400)}, 10)
 28	urequire.NoError(t, err)
 29	return r
 30}
 31
 32func TestNewSeedsFoundersAtHeightZero(t *testing.T) {
 33	b := board3()
 34	uassert.Equal(t, 3, b.MemberCount())
 35	for _, m := range []address{alice, bob, carol} {
 36		uassert.True(t, b.IsMember(m), m.String()+" should be a founder")
 37		uassert.Equal(t, int64(0), b.JoinedAt(m))
 38	}
 39	uassert.False(t, b.IsMember(dave))
 40	uassert.Equal(t, int64(-1), b.JoinedAt(dave))
 41
 42	// Members is address-ordered, and the order is what avl gives, not
 43	// insertion order. Only the stability matters, so compare two calls.
 44	first, second := b.Members(), b.Members()
 45	urequire.Equal(t, 3, len(first))
 46	for i := range first {
 47		uassert.Equal(t, first[i].String(), second[i].String())
 48	}
 49}
 50
 51func TestNewSkipsInvalidFounders(t *testing.T) {
 52	b := New(alice, address(""), alice)
 53	uassert.Equal(t, 1, b.MemberCount(), "empty address dropped, duplicate collapsed")
 54}
 55
 56func TestSubmitValidation(t *testing.T) {
 57	long := ""
 58	for i := 0; i < MaxTitle+1; i++ {
 59		long += "x"
 60	}
 61	body := ""
 62	for i := 0; i < MaxBody+1; i++ {
 63		body += "y"
 64	}
 65	many := make([]*Milestone, MaxMilestones+1)
 66	for i := range many {
 67		many[i] = NewMilestone("m", 1)
 68	}
 69
 70	cases := []struct {
 71		name  string
 72		title string
 73		body  string
 74		ms    []*Milestone
 75		want  error
 76	}{
 77		{"ok", "t", "b", []*Milestone{NewMilestone("m", 1)}, nil},
 78		{"empty title", "", "b", []*Milestone{NewMilestone("m", 1)}, ErrBadTitle},
 79		{"long title", long, "b", []*Milestone{NewMilestone("m", 1)}, ErrBadTitle},
 80		{"long body", "t", body, []*Milestone{NewMilestone("m", 1)}, ErrBadBody},
 81		{"no milestone", "t", "b", nil, ErrBadMilestones},
 82		{"too many milestones", "t", "b", many, ErrBadMilestones},
 83		{"zero amount", "t", "b", []*Milestone{NewMilestone("m", 0)}, ErrBadAmount},
 84		{"negative amount", "t", "b", []*Milestone{NewMilestone("m", -1)}, ErrBadAmount},
 85		{"untitled milestone", "t", "b", []*Milestone{NewMilestone("", 1)}, ErrBadTitle},
 86	}
 87	for _, tc := range cases {
 88		_, err := board3().Submit(dave, tc.title, tc.body, tc.ms, 1)
 89		if tc.want == nil {
 90			uassert.NoError(t, err, tc.name)
 91			continue
 92		}
 93		uassert.ErrorIs(t, err, tc.want, tc.name)
 94	}
 95
 96	_, err := board3().Submit(address(""), "t", "b", []*Milestone{NewMilestone("m", 1)}, 1)
 97	uassert.ErrorIs(t, err, ErrBadAddress, "invalid applicant")
 98}
 99
100func TestRequestArithmetic(t *testing.T) {
101	b := board3()
102	r := submitOne(t, b)
103	uassert.Equal(t, int64(500), r.Total())
104	uassert.Equal(t, int64(0), r.Paid())
105	uassert.Equal(t, int64(0), r.Outstanding(), "a pending grant commits nothing")
106	uassert.Equal(t, 0, r.Next())
107	uassert.Equal(t, "pending", r.Status.String())
108	uassert.Equal(t, "grant", r.Kind.String())
109	uassert.True(t, r.Excludes(dave), "the applicant never votes on their own grant")
110	uassert.False(t, r.Excludes(alice))
111	uassert.Equal(t, dave.String(), r.Payee().String(), "a self request pays its applicant")
112	uassert.False(t, r.OnBehalf())
113	uassert.Equal(t, 1, r.ID)
114	uassert.Equal(t, r.ID, b.Get(1).ID)
115	uassert.True(t, b.Get(42) == nil, "unknown id")
116}
117
118func TestMajorityCountsOnlyEligibleMembers(t *testing.T) {
119	b := board3()
120
121	outsider := submitOne(t, b)
122	uassert.Equal(t, 3, b.Eligible(outsider))
123	uassert.Equal(t, 2, b.Majority(outsider))
124
125	// A member applying for their own grant shrinks the eligible set, and
126	// with it the bar: two of the remaining two must agree.
127	insider, err := b.Submit(alice, "self", "", []*Milestone{NewMilestone("m", 1)}, 1)
128	urequire.NoError(t, err)
129	uassert.Equal(t, 2, b.Eligible(insider))
130	uassert.Equal(t, 2, b.Majority(insider))
131}
132
133func TestApprovalNeedsAMajority(t *testing.T) {
134	b := board3()
135	r := submitOne(t, b)
136
137	st, err := b.Vote(alice, r.ID, true, "good", 11)
138	urequire.NoError(t, err)
139	uassert.Equal(t, "pending", st.String(), "one of three is not a majority")
140
141	st, err = b.Vote(bob, r.ID, true, "agreed", 12)
142	urequire.NoError(t, err)
143	uassert.Equal(t, "approved", st.String())
144	uassert.Equal(t, int64(12), r.DecidedAt)
145	uassert.Equal(t, int64(500), r.Outstanding(), "approval commits the full ask")
146	uassert.Equal(t, int64(500), b.Committed())
147
148	yes, no := r.Tally()
149	uassert.Equal(t, 2, yes)
150	uassert.Equal(t, 0, no)
151	uassert.Equal(t, "good", r.BallotOf(alice).Reason)
152	uassert.True(t, r.BallotOf(carol) == nil, "carol never voted")
153}
154
155func TestRejectionNeedsAMajorityToo(t *testing.T) {
156	b := board3()
157	r := submitOne(t, b)
158	urequire.NoError(t, errOf(b.Vote(alice, r.ID, false, "out of scope", 11)))
159	st, err := b.Vote(bob, r.ID, false, "same", 12)
160	urequire.NoError(t, err)
161	uassert.Equal(t, "rejected", st.String())
162	uassert.Equal(t, int64(0), b.Committed(), "a rejected grant commits nothing")
163}
164
165func TestVoteRefusals(t *testing.T) {
166	b := board3()
167	r := submitOne(t, b)
168	self, err := b.Submit(alice, "self", "", []*Milestone{NewMilestone("m", 1)}, 1)
169	urequire.NoError(t, err)
170
171	_, err = b.Vote(alice, 999, true, "", 1)
172	uassert.ErrorIs(t, err, ErrNoRequest)
173
174	_, err = b.Vote(mallory, r.ID, true, "", 1)
175	uassert.ErrorIs(t, err, ErrNotMember)
176
177	_, err = b.Vote(alice, self.ID, true, "vote for myself", 1)
178	uassert.ErrorIs(t, err, ErrConflict)
179
180	long := ""
181	for i := 0; i < MaxReason+1; i++ {
182		long += "z"
183	}
184	_, err = b.Vote(alice, r.ID, true, long, 1)
185	uassert.ErrorIs(t, err, ErrBadReason)
186
187	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "yes", 1)))
188	_, err = b.Vote(alice, r.ID, false, "changed my mind", 2)
189	uassert.ErrorIs(t, err, ErrAlreadyVoted, "a ballot is permanent")
190
191	urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "yes", 3)))
192	_, err = b.Vote(carol, r.ID, true, "too late", 4)
193	uassert.ErrorIs(t, err, ErrNotPending, "decided requests take no more ballots")
194}
195
196func TestDecidesPreviewsWithoutCasting(t *testing.T) {
197	b := board3()
198	r := submitOne(t, b)
199
200	settles, to := b.Decides(r.ID, alice, true)
201	uassert.False(t, settles, "the first of three does not settle it")
202	uassert.Equal(t, 0, len(r.Votes), "previewing casts nothing")
203
204	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
205	settles, to = b.Decides(r.ID, bob, true)
206	uassert.True(t, settles)
207	uassert.Equal(t, "approved", to.String())
208
209	settles, _ = b.Decides(r.ID, bob, false)
210	uassert.False(t, settles, "a no here only makes it one-all")
211
212	settles, _ = b.Decides(r.ID, mallory, true)
213	uassert.False(t, settles, "a non-member decides nothing")
214	settles, _ = b.Decides(r.ID, alice, true)
215	uassert.False(t, settles, "a member who already voted decides nothing")
216	settles, _ = b.Decides(999, bob, true)
217	uassert.False(t, settles, "an unknown request decides nothing")
218}
219
220func TestWithdraw(t *testing.T) {
221	b := board3()
222	r := submitOne(t, b)
223
224	uassert.ErrorIs(t, b.Withdraw(mallory, r.ID, 5), ErrNotApplicant)
225	uassert.ErrorIs(t, b.Withdraw(dave, 999, 5), ErrNoRequest)
226	urequire.NoError(t, b.Withdraw(dave, r.ID, 5))
227	uassert.Equal(t, "withdrawn", r.Status.String())
228	uassert.ErrorIs(t, b.Withdraw(dave, r.ID, 6), ErrNotPending)
229}
230
231func TestProofRefusals(t *testing.T) {
232	b := board3()
233	r := submitOne(t, b)
234	good := Proof{Kind: "url", Ref: "https://example.com/pr/1", Height: 20}
235
236	uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 0, good), ErrNotApproved, "not approved yet")
237
238	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
239	urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
240
241	uassert.ErrorIs(t, b.SubmitProof(mallory, r.ID, 0, good), ErrNotApplicant)
242	uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 7, good), ErrNoMilestone)
243	uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 1, good), ErrOutOfOrder, "milestone 1 before 0")
244
245	bad := []Proof{
246		{Kind: "screenshot", Ref: "x"},
247		{Kind: "url", Ref: ""},
248		{Kind: "text", Ref: "ok", Note: longString(MaxNote + 1)},
249		{Kind: "url", Ref: longString(MaxRef + 1)},
250	}
251	for _, p := range bad {
252		uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 0, p), ErrBadProof, p.Kind)
253	}
254
255	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, good))
256	uassert.ErrorIs(t, b.SubmitProof(dave, r.ID, 0, good), ErrProofPending, "one at a time")
257}
258
259func TestReleaseRunsThroughToCompletion(t *testing.T) {
260	b := board3()
261	r := submitOne(t, b)
262	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
263	urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
264
265	// Milestone 0.
266	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/1", Height: 20}))
267	out, err := b.Review(alice, r.ID, 0, true, "looks done", 21)
268	urequire.NoError(t, err)
269	uassert.Equal(t, "under review", out.String(), "one reviewer is not a majority")
270	uassert.False(t, r.Milestones[0].Released)
271
272	out, err = b.Review(carol, r.ID, 0, true, "confirmed", 22)
273	urequire.NoError(t, err)
274	uassert.Equal(t, "accepted", out.String())
275	uassert.True(t, r.Milestones[0].Released)
276	uassert.Equal(t, int64(22), r.Milestones[0].ReleasedAt)
277	uassert.Equal(t, int64(100), r.Paid())
278	uassert.Equal(t, int64(400), r.Outstanding())
279	uassert.Equal(t, "approved", r.Status.String(), "one tranche left")
280	uassert.Equal(t, 1, r.Next())
281
282	// Milestone 1 completes the grant.
283	urequire.NoError(t, b.SubmitProof(dave, r.ID, 1, Proof{Kind: "hash", Ref: "deadbeef", Height: 30}))
284	urequire.NoError(t, errOf2(b.Review(alice, r.ID, 1, true, "", 31)))
285	out, err = b.Review(bob, r.ID, 1, true, "", 32)
286	urequire.NoError(t, err)
287	uassert.Equal(t, "accepted", out.String())
288	uassert.Equal(t, "completed", r.Status.String())
289	uassert.Equal(t, -1, r.Next())
290	uassert.Equal(t, int64(500), r.Paid())
291	uassert.Equal(t, int64(0), r.Outstanding(), "a completed grant commits nothing")
292	uassert.Equal(t, int64(500), b.Disbursed())
293	uassert.Equal(t, int64(0), b.Committed())
294}
295
296func TestRefusedProofKeepsTheGrantAliveAndOnTheRecord(t *testing.T) {
297	b := board3()
298	r := submitOne(t, b)
299	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
300	urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
301
302	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/draft", Height: 20}))
303	urequire.NoError(t, errOf2(b.Review(alice, r.ID, 0, false, "this is a stub", 21)))
304	out, err := b.Review(bob, r.ID, 0, false, "agreed, no tests", 22)
305	urequire.NoError(t, err)
306	uassert.Equal(t, "refused", out.String())
307	uassert.False(t, r.Milestones[0].Released)
308	uassert.Equal(t, "approved", r.Status.String(), "a refused proof does not kill the grant")
309
310	// The refused attempt stays readable, reasons and all.
311	m := r.Milestones[0]
312	urequire.Equal(t, 1, len(m.Attempts))
313	uassert.Equal(t, "this is a stub", m.Attempts[0].Reviews[0].Reason)
314	uassert.True(t, m.Current() == nil, "the attempt is closed")
315
316	// And a second attempt can carry.
317	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "url", Ref: "https://x/final", Height: 30}))
318	urequire.NoError(t, errOf2(b.Review(alice, r.ID, 0, true, "now it is done", 31)))
319	urequire.NoError(t, errOf2(b.Review(bob, r.ID, 0, true, "", 32)))
320	uassert.True(t, r.Milestones[0].Released)
321	uassert.Equal(t, 2, len(m.Attempts), "both attempts are kept")
322}
323
324func TestReviewRefusals(t *testing.T) {
325	b := board3()
326	r := submitOne(t, b)
327	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 11)))
328	urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 12)))
329
330	uassert.ErrorIs(t, errOf2(b.Review(alice, r.ID, 0, true, "", 20)), ErrNoProof)
331	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 20}))
332
333	uassert.ErrorIs(t, errOf2(b.Review(mallory, r.ID, 0, true, "", 21)), ErrNotMember)
334	uassert.ErrorIs(t, errOf2(b.Review(alice, 999, 0, true, "", 21)), ErrNoRequest)
335	uassert.ErrorIs(t, errOf2(b.Review(alice, r.ID, 9, true, "", 21)), ErrNoMilestone)
336	urequire.NoError(t, errOf2(b.Review(alice, r.ID, 0, true, "", 21)))
337	uassert.ErrorIs(t, errOf2(b.Review(alice, r.ID, 0, false, "", 22)), ErrAlreadyVoted)
338}
339
340func TestApplicantMayNotReviewTheirOwnProof(t *testing.T) {
341	b := New(alice, bob, dave) // dave is a member AND the applicant here
342	r, err := b.Submit(dave, "self funded", "", []*Milestone{NewMilestone("m", 10)}, 1)
343	urequire.NoError(t, err)
344	urequire.NoError(t, errOf(b.Vote(alice, r.ID, true, "", 2)))
345	urequire.NoError(t, errOf(b.Vote(bob, r.ID, true, "", 3)))
346	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 4}))
347	uassert.ErrorIs(t, errOf2(b.Review(dave, r.ID, 0, true, "ship it", 5)), ErrConflict)
348}
349
350func TestMembershipRequestAddsAndRemoves(t *testing.T) {
351	b := board3()
352
353	add, err := b.SubmitMemberChange(alice, dave, true, "he reviews everything anyway", 5)
354	urequire.NoError(t, err)
355	uassert.Equal(t, "membership", add.Kind.String())
356	uassert.Equal(t, "Add "+dave.String(), add.Title)
357	uassert.Equal(t, int64(0), add.Total(), "a membership request asks for no money")
358
359	urequire.NoError(t, errOf(b.Vote(alice, add.ID, true, "", 6)))
360	st, err := b.Vote(bob, add.ID, true, "", 7)
361	urequire.NoError(t, err)
362	uassert.Equal(t, "completed", st.String(), "membership executes on approval")
363	uassert.True(t, b.IsMember(dave))
364	uassert.Equal(t, int64(7), b.JoinedAt(dave), "joined at the deciding height")
365
366	// Now four members: a majority excluding the subject is still two.
367	rm, err := b.SubmitMemberChange(alice, carol, false, "inactive", 8)
368	urequire.NoError(t, err)
369	uassert.Equal(t, 3, b.Eligible(rm))
370	uassert.ErrorIs(t, errOf(b.Vote(carol, rm.ID, false, "no", 9)), ErrConflict, "the subject may not vote")
371	urequire.NoError(t, errOf(b.Vote(alice, rm.ID, true, "", 10)))
372	urequire.NoError(t, errOf(b.Vote(bob, rm.ID, true, "", 11)))
373	uassert.False(t, b.IsMember(carol))
374	uassert.Equal(t, 3, b.MemberCount())
375}
376
377func TestMemberChangeRefusals(t *testing.T) {
378	b := board3()
379
380	_, err := b.SubmitMemberChange(mallory, dave, true, "", 1)
381	uassert.ErrorIs(t, err, ErrNotMember, "only members touch the board's composition")
382	_, err = b.SubmitMemberChange(alice, alice, true, "", 1)
383	uassert.ErrorIs(t, err, ErrIsMember)
384	_, err = b.SubmitMemberChange(alice, dave, false, "", 1)
385	uassert.ErrorIs(t, err, ErrNotMember, "cannot remove a non-member")
386	_, err = b.SubmitMemberChange(alice, address(""), true, "", 1)
387	uassert.ErrorIs(t, err, ErrBadAddress)
388
389	solo := New(alice)
390	_, err = solo.SubmitMemberChange(alice, alice, false, "", 1)
391	uassert.ErrorIs(t, err, ErrLastMember, "the board cannot empty itself")
392}
393
394// Standing is the whole reason a removed member's yes does not keep carrying
395// decisions they are no longer part of.
396func TestStandingDropsBallotsFromFormerMembers(t *testing.T) {
397	b := board3()
398	r := submitOne(t, b)
399	urequire.NoError(t, errOf(b.Vote(carol, r.ID, true, "I like it", 11)))
400
401	rm, err := b.SubmitMemberChange(alice, carol, false, "left the project", 12)
402	urequire.NoError(t, err)
403	urequire.NoError(t, errOf(b.Vote(alice, rm.ID, true, "", 13)))
404	urequire.NoError(t, errOf(b.Vote(bob, rm.ID, true, "", 14)))
405	urequire.Equal(t, 2, b.MemberCount())
406
407	yes, _ := r.Tally()
408	uassert.Equal(t, 1, yes, "the ballot stays on the record")
409	yes, _ = b.Standing(r)
410	uassert.Equal(t, 0, yes, "but it no longer counts")
411
412	// With two members and an outsider applicant the bar is two, so alice
413	// alone still cannot carry it.
414	uassert.Equal(t, 2, b.Majority(r))
415	st, err := b.Vote(alice, r.ID, true, "", 15)
416	urequire.NoError(t, err)
417	uassert.Equal(t, "pending", st.String())
418}
419
420func TestListAndSizeStayInIDOrder(t *testing.T) {
421	b := board3()
422	for i := 0; i < 12; i++ {
423		_, err := b.Submit(dave, "r", "", []*Milestone{NewMilestone("m", 1)}, int64(i))
424		urequire.NoError(t, err)
425	}
426	all := b.List()
427	urequire.Equal(t, 12, len(all))
428	uassert.Equal(t, 12, b.Size())
429	for i, r := range all {
430		uassert.Equal(t, i+1, r.ID, "ids come back in numeric order past nine")
431	}
432}
433
434func longString(n int) string {
435	s := ""
436	for i := 0; i < n; i++ {
437		s += "x"
438	}
439	return s
440}
441
442// errOf and errOf2 drop the leading return value of Vote and Review so a call
443// reads as a single assertion.
444func errOf(_ Status, err error) error   { return err }
445func errOf2(_ Outcome, err error) error { return err }
446
447func TestFilingForSomeoneElseNeedsAReason(t *testing.T) {
448	b := board3()
449	ms := func() []*Milestone { return []*Milestone{NewMilestone("m", 100)} }
450
451	_, err := b.SubmitFor(dave, mallory, "", "t", "", ms(), 10)
452	uassert.ErrorIs(t, err, ErrNeedReason, "a request for someone else has to say why")
453
454	// The same address is just a self request, and needs nothing extra.
455	self, err := b.SubmitFor(dave, dave, "", "t", "", ms(), 10)
456	urequire.NoError(t, err)
457	uassert.False(t, self.OnBehalf())
458
459	_, err = b.SubmitFor(dave, address("not-an-address"), "because", "t", "", ms(), 10)
460	uassert.ErrorIs(t, err, ErrBadAddress)
461
462	long := ""
463	for i := 0; i <= MaxReason; i++ {
464		long += "x"
465	}
466	_, err = b.SubmitFor(dave, mallory, long, "t", "", ms(), 10)
467	uassert.ErrorIs(t, err, ErrBadReason)
468
469	r, err := b.SubmitFor(dave, mallory, "their account is empty", "t", "", ms(), 10)
470	urequire.NoError(t, err)
471	uassert.True(t, r.OnBehalf())
472	uassert.Equal(t, mallory.String(), r.Payee().String())
473	uassert.Equal(t, dave.String(), r.Applicant.String())
474}
475
476// A member who is paid by a request someone else filed is the same conflict of
477// interest as a member who filed it, with one address in between.
478func TestBothPartiesAreBarredFromVotingOnAGrant(t *testing.T) {
479	b := board3()
480	r, err := b.SubmitFor(alice, bob, "bob asked me to file it", "t", "",
481		[]*Milestone{NewMilestone("m", 100)}, 10)
482	urequire.NoError(t, err)
483
484	uassert.True(t, r.Excludes(alice), "the applicant")
485	uassert.True(t, r.Excludes(bob), "the beneficiary")
486	uassert.False(t, r.Excludes(carol))
487	uassert.Equal(t, 1, b.Eligible(r), "three members, two of them party to it")
488	uassert.Equal(t, 1, b.Majority(r))
489
490	_, err = b.Vote(alice, r.ID, true, "", 11)
491	uassert.ErrorIs(t, err, ErrConflict, "the applicant may not vote")
492	_, err = b.Vote(bob, r.ID, true, "", 11)
493	uassert.ErrorIs(t, err, ErrConflict, "the payee may not vote")
494
495	st, err := b.Vote(carol, r.ID, true, "fine", 12)
496	urequire.NoError(t, err)
497	uassert.Equal(t, "approved", st.String(), "the one eligible member carries it")
498
499	// And the same exclusion holds on the proof review, which is where the
500	// money actually moves.
501	urequire.NoError(t, b.SubmitProof(alice, r.ID, 0, Proof{Kind: "text", Ref: "done", Height: 13}))
502	_, err = b.Review(bob, r.ID, 0, true, "", 14)
503	uassert.ErrorIs(t, err, ErrConflict, "the payee may not review their own proof")
504}
505
506// The payee of a request filed for an empty account cannot transact until the
507// first tranche lands, so the filer has to be able to prove the work; once the
508// payee can transact, they have to be able to prove their own.
509func TestEitherPartyMaySubmitAProof(t *testing.T) {
510	b := board3()
511	r, err := b.SubmitFor(dave, mallory, "empty account", "t", "",
512		[]*Milestone{NewMilestone("one", 100), NewMilestone("two", 100)}, 10)
513	urequire.NoError(t, err)
514	b.Vote(alice, r.ID, true, "", 11)
515	b.Vote(bob, r.ID, true, "", 12)
516	urequire.Equal(t, "approved", r.Status.String())
517
518	uassert.ErrorIs(t, b.SubmitProof(carol, r.ID, 0, Proof{Kind: "text", Ref: "x", Height: 13}),
519		ErrNotApplicant, "a third party may not")
520
521	urequire.NoError(t, b.SubmitProof(dave, r.ID, 0, Proof{Kind: "text", Ref: "filer", Height: 13}))
522	b.Review(alice, r.ID, 0, true, "", 14)
523	b.Review(bob, r.ID, 0, true, "", 15)
524	urequire.True(t, r.Milestones[0].Released)
525
526	urequire.NoError(t, b.SubmitProof(mallory, r.ID, 1, Proof{Kind: "text", Ref: "payee", Height: 16}))
527}