1package clock23import (4 "testing"56 "gno.land/p/nt/uassert/v0"7)89func mustNew(t *testing.T, now, window, floor, life int64) *Clock {10 t.Helper()11 c, err := New(now, window, floor, life)12 uassert.NoError(t, err)13 return c14}1516func TestNewValidation(t *testing.T) {17 cases := []struct {18 name string19 now, window, floor, life int6420 want error21 }{22 {"zero window", 0, 0, 0, 0, ErrBadWindow},23 {"negative window", 0, -1, 0, 0, ErrBadWindow},24 {"negative floor", 0, 100, -1, 0, ErrBadFloor},25 {"floor above window", 0, 100, 101, 0, ErrBadFloor},26 {"negative life", 0, 100, 10, -1, ErrBadLife},27 {"life shorter than window", 0, 100, 10, 99, ErrBadLife},28 {"window overflows", maxInt64 - 5, 100, 0, 0, ErrOverflow},29 {"life overflows", maxInt64 - 500, 100, 0, 1000, ErrOverflow},30 {"unbounded is legal", 0, 100, 10, 0, nil},31 {"life equal to window is legal", 0, 100, 10, 100, nil},32 {"floor equal to window is legal", 0, 100, 100, 0, nil},33 }34 for _, tc := range cases {35 _, err := New(tc.now, tc.window, tc.floor, tc.life)36 if tc.want == nil {37 uassert.NoError(t, err, tc.name)38 continue39 }40 uassert.ErrorIs(t, err, tc.want, tc.name)41 }42}4344func TestOpensDueAtWindow(t *testing.T) {45 c := mustNew(t, 1000, 100, 10, 500)46 uassert.Equal(t, int64(1000), c.Start())47 uassert.Equal(t, int64(1100), c.Deadline())48 uassert.Equal(t, int64(1500), c.HardEnd())49 uassert.Equal(t, int64(100), c.Remaining(1000))50 uassert.Equal(t, int64(0), c.Elapsed(1000))51 uassert.False(t, c.Expired(1099))52 uassert.True(t, c.Expired(1100), "the deadline itself is expired")53 uassert.False(t, c.Final())54}5556func TestUnboundedHasNoHardEnd(t *testing.T) {57 c := mustNew(t, 0, 100, 0, 0)58 uassert.Equal(t, int64(0), c.HardEnd())59 uassert.False(t, c.Final(), "an unbounded clock is never final")60 c.Bump(90)61 c.Bump(180)62 uassert.Equal(t, int64(280), c.Deadline(), "nothing caps it")63}6465// TestBumpNeverMovesBackwards is the invariant a caller supplying its own grant66// gets wrong first: a late, small grant must not shorten the clock.67func TestBumpNeverMovesBackwards(t *testing.T) {68 c := mustNew(t, 0, 1000, 0, 0)69 uassert.Equal(t, int64(1000), c.Deadline())7071 uassert.Equal(t, int64(1000), c.BumpBy(10, 5), "now+5 is before the deadline")72 uassert.Equal(t, int64(1000), c.BumpBy(900, 0), "a zero grant leaves it alone")73 uassert.Equal(t, int64(1100), c.BumpBy(900, 200), "a grant past the deadline moves it")74}7576// TestFloorSurvivesATinyGrant is the anti-snipe: whatever the policy grants, a77// bump leaves at least the floor, so the last stretch stays reachable.78func TestFloorSurvivesATinyGrant(t *testing.T) {79 c := mustNew(t, 0, 1000, 50, 0)80 c.BumpBy(990, 1)81 uassert.Equal(t, int64(1040), c.Deadline(), "1 was raised to the floor of 50")82 uassert.Equal(t, int64(50), c.Remaining(990))8384 c2 := mustNew(t, 0, 1000, 50, 0)85 c2.BumpBy(990, 0)86 uassert.Equal(t, int64(50), c2.Remaining(990), "even a zero grant leaves the floor")87}8889// TestHardEndCapsEveryBump is the terminator: without it a contested clock is90// extended forever and the game has no end.91func TestHardEndCapsEveryBump(t *testing.T) {92 c := mustNew(t, 0, 100, 10, 250)93 uassert.Equal(t, int64(250), c.HardEnd())9495 c.BumpBy(90, 100)96 uassert.Equal(t, int64(190), c.Deadline())97 c.BumpBy(180, 100)98 uassert.Equal(t, int64(250), c.Deadline(), "capped at the hard end, not 280")99 uassert.True(t, c.Final())100101 c.BumpBy(240, 1000000)102 uassert.Equal(t, int64(250), c.Deadline(), "the cap holds against any grant")103 uassert.Equal(t, int64(10), c.Remaining(240), "and it beats the floor")104}105106// TestExpiredClockNeverRestarts stops a late action from reopening a settled107// game, which is the difference between a closed pot and a stolen one.108func TestExpiredClockNeverRestarts(t *testing.T) {109 c := mustNew(t, 0, 100, 10, 0)110 uassert.True(t, c.Expired(100))111 uassert.Equal(t, int64(100), c.BumpBy(100, 500), "exactly at the deadline")112 uassert.Equal(t, int64(100), c.Bump(5000), "long after")113 uassert.Equal(t, int64(0), c.Remaining(100))114115 settled := mustNew(t, 0, 100, 10, 1000)116 uassert.True(t, settled.Expired(100))117 got, err := settled.BumpShare(200, 50)118 uassert.NoError(t, err)119 uassert.Equal(t, int64(100), got, "not even a large share reopens it")120}121122func TestBumpShareValidation(t *testing.T) {123 c := mustNew(t, 0, 100, 0, 500)124 _, err := c.BumpShare(10, -1)125 uassert.ErrorIs(t, err, ErrBadShare)126 _, err = c.BumpShare(10, 101)127 uassert.ErrorIs(t, err, ErrBadShare)128 uassert.Equal(t, int64(100), c.Deadline(), "a rejected share changes nothing")129130 unbounded := mustNew(t, 0, 100, 0, 0)131 _, err = unbounded.BumpShare(10, 50)132 uassert.ErrorIs(t, err, ErrUnbounded, "no hard end, nothing to take a share of")133}134135// TestBumpShareDecays is why the share grant exists: each action closes a136// fraction of the gap to the hard end, so the grants shrink as the game runs137// and the pressure to act one more time is relieved instead of compounding.138func TestBumpShareDecays(t *testing.T) {139 c := mustNew(t, 0, 100, 1, 1000)140 first, err := c.BumpShare(10, 50)141 uassert.NoError(t, err)142 uassert.Equal(t, int64(505), first, "half the 990 left to the hard end")143144 second, err := c.BumpShare(500, 50)145 uassert.NoError(t, err)146 uassert.Equal(t, int64(750), second, "half of the 500 now left, a smaller grant")147148 third, err := c.BumpShare(700, 50)149 uassert.NoError(t, err)150 uassert.Equal(t, int64(850), third, "smaller again")151 uassert.True(t, third < c.HardEnd(), "it approaches the hard end without reaching it")152}153154// TestBumpShareRoundingStillGrants is the multiply-first lesson: a share of a155// gap smaller than the divisor must not truncate to a zero grant.156func TestBumpShareRoundingStillGrants(t *testing.T) {157 // 52 due, 50 left to the hard end: a tenth of that is 5, and dividing158 // before multiplying would truncate it to a grant of zero.159 c := mustNew(t, 0, 52, 0, 100)160 got, err := c.BumpShare(50, 10)161 uassert.NoError(t, err)162 uassert.Equal(t, int64(55), got, "a tenth of the 50 left is 5, not 0")163}164165func TestBumpShareRespectsFloorAndCap(t *testing.T) {166 c := mustNew(t, 0, 100, 20, 150)167 got, err := c.BumpShare(95, 10) // a tenth of the 55 left is 5, floored to 20168 uassert.NoError(t, err)169 uassert.Equal(t, int64(115), got)170171 got, err = c.BumpShare(110, 100) // would be 150 exactly, and never past it172 uassert.NoError(t, err)173 uassert.Equal(t, int64(150), got)174 uassert.True(t, c.Final())175}176177// TestHugeGrantDoesNotWrap keeps a bump total at the extremes: a wrapped178// deadline would land in the past and settle the game instantly.179func TestHugeGrantDoesNotWrap(t *testing.T) {180 c := mustNew(t, 0, 100, 0, 0)181 uassert.Equal(t, maxInt64, c.BumpBy(50, maxInt64), "clamped, not wrapped")182 uassert.True(t, c.Deadline() > 0)183184 big := mustNew(t, maxInt64/2, 1000, 0, maxInt64/4)185 got, err := big.BumpShare(maxInt64/2+1, 100)186 uassert.NoError(t, err)187 uassert.True(t, got > maxInt64/2, "a huge remaining share does not wrap")188}189190func TestElapsedAndRemainingAreNeverNegative(t *testing.T) {191 c := mustNew(t, 100, 100, 0, 0)192 uassert.Equal(t, int64(0), c.Elapsed(50), "before the start")193 uassert.Equal(t, int64(100), c.Remaining(100))194 uassert.Equal(t, int64(0), c.Remaining(9999), "long past the deadline")195 uassert.Equal(t, int64(9899), c.Elapsed(9999))196}197Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.