1package compact23import (4 "strings"5 "testing"67 "gno.land/p/moul/kit/ui/v0"8 "gno.land/p/moul/x/storagecost/v1"9 "gno.land/p/nt/testutils/v0"10 "gno.land/p/nt/uassert/v0"11)1213var (14 alice = testutils.TestAddress("alice")15 bob = testutils.TestAddress("bob")16 // janitor never adds anything, it only compacts. The whole point of the17 // realm is that this is the address the chain pays.18 janitor = testutils.TestAddress("janitor")19)2021// drain empties the board and asserts it, so a test starts from a known22// state whatever ran before it. Indices are append-addressed for the life of23// the realm, so every test asserts on counts and never on absolute indices.24func drain(cur realm, t *testing.T) {25 t.Helper()26 for i := 0; i < entries.TotalSize(); i++ {27 if e, ok := entryAt(i); ok {28 testing.SetRealm(testing.NewUserRealm(e.Author))29 Drop(cross(cur), i)30 }31 }32 testing.SetRealm(testing.NewUserRealm(janitor))33 Compact(cross(cur))34 live, _ := Fragmentation()35 uassert.Equal(t, 0, live)36}3738func TestDropFreesNothingUntilCompact(cur realm, t *testing.T) {39 drain(cur, t)40 testing.SetRealm(testing.NewUserRealm(alice))41 for i := 0; i < 8; i++ {42 Add(cross(cur), "entry")43 }44 live, allocated := Fragmentation()45 uassert.Equal(t, 8, live)46 uassert.Equal(t, 0, Reclaimable())4748 // Dropping everything moves the live count but frees no nodes by itself.49 base := allocated - 850 for i := base; i < allocated; i++ {51 Drop(cross(cur), i)52 }53 live, allocated2 := Fragmentation()54 uassert.Equal(t, 0, live)55 uassert.Equal(t, allocated, allocated2, "a soft delete must not change the index space")56 uassert.True(t, Reclaimable() > 0, "a fully dead tail should be reclaimable")5758 // And compaction is what actually frees them.59 testing.SetRealm(testing.NewUserRealm(janitor))60 freed := Compact(cross(cur))61 uassert.True(t, freed > 0)62 uassert.Equal(t, 0, Reclaimable())63 // Compacting twice is not an error, it just frees nothing, so two bots64 // racing for the same nodes both survive rather than one aborting.65 uassert.Equal(t, 0, Compact(cross(cur)))66}6768// TestScatteredHolesReclaimLessThanTheGap is the claim the Render makes, and69// the reason the realm publishes two numbers instead of one. No other test70// could catch it: they all drop contiguous runs.71func TestScatteredHolesReclaimLessThanTheGap(cur realm, t *testing.T) {72 drain(cur, t)73 testing.SetRealm(testing.NewUserRealm(alice))74 base := entries.TotalSize()75 for i := 0; i < 16; i++ {76 Add(cross(cur), "entry")77 }78 // Every other index, so no whole subtree dies.79 for i := base; i < base+16; i += 2 {80 Drop(cross(cur), i)81 }82 live, allocated := Fragmentation()83 gap := allocated - live84 uassert.True(t, gap >= 8, "eight holes were made")85 uassert.True(t, Reclaimable() < gap,86 "scattered holes must reclaim fewer nodes than the gap suggests")8788 drain(cur, t)89}9091func TestOnlyTheAuthorMayDrop(cur realm, t *testing.T) {92 drain(cur, t)93 testing.SetRealm(testing.NewUserRealm(alice))94 i := Add(cross(cur), "alice's entry")9596 testing.SetRealm(testing.NewUserRealm(bob))97 uassert.AbortsWithMessage(t, cur, "compact: only the author may drop an entry", func() {98 Drop(cross(cur), i)99 })100 // But compaction stays permissionless, which is the asymmetry that makes101 // the mechanism work: choosing what dies is owned, reclaiming is not.102 testing.SetRealm(testing.NewUserRealm(janitor))103 uassert.NotAborts(t, cur, func() { Compact(cross(cur)) })104105 drain(cur, t)106}107108func TestAddRejectsBadInput(cur realm, t *testing.T) {109 drain(cur, t)110 testing.SetRealm(testing.NewUserRealm(alice))111 uassert.AbortsWithMessage(t, cur, "compact: empty entry", func() {112 Add(cross(cur), "")113 })114 uassert.AbortsWithMessage(t, cur, "compact: entry too long, 257 bytes against a 256 cap", func() {115 Add(cross(cur), strings.Repeat("x", maxText+1))116 })117 uassert.AbortsContains(t, cur, "compact: no live entry at", func() {118 Drop(cross(cur), 99999)119 })120}121122// TestQuoteCountsNodesRatherThanGuessingFromPayload is the design claim of the123// realm. The quote must depend on the NODE COUNT and not on how fat the124// entries were, which is exactly where a payload-derived bounty goes wrong.125func TestQuoteCountsNodesRatherThanGuessingFromPayload(cur realm, t *testing.T) {126 drain(cur, t)127128 // An empty board advertises nothing and must not claim a profit.129 empty := Quote()130 uassert.Equal(t, int64(0), empty.Bytes)131 uassert.False(t, empty.Worth())132133 // Eight tiny entries, dropped and measured.134 testing.SetRealm(testing.NewUserRealm(alice))135 base := entries.TotalSize()136 for i := 0; i < 8; i++ {137 Add(cross(cur), "x")138 }139 for i := base; i < base+8; i++ {140 Drop(cross(cur), i)141 }142 tiny := Quote()143 tinyNodes := Reclaimable()144 testing.SetRealm(testing.NewUserRealm(janitor))145 Compact(cross(cur))146147 // Eight fat entries, same count, same treatment.148 testing.SetRealm(testing.NewUserRealm(alice))149 base = entries.TotalSize()150 fat := strings.Repeat("y", maxText)151 for i := 0; i < 8; i++ {152 Add(cross(cur), fat)153 }154 for i := base; i < base+8; i++ {155 Drop(cross(cur), i)156 }157 big := Quote()158159 bigNodes := Reclaimable()160161 // 256x the payload, and the quote is still purely a function of the node162 // count. Not the same TOTAL: the two batches sit at different offsets in163 // an append-addressed index space, so the tree shape differs and one164 // reclaimed a node more than the other. That is the honest version of the165 // claim, and asserting equal totals (which an earlier draft of this test166 // did) only passed by luck of the offsets.167 uassert.True(t, tinyNodes > 0 && bigNodes > 0)168 uassert.Equal(t, storagecost.EstimateNodes(int64(tinyNodes)), tiny.Bytes)169 uassert.Equal(t, storagecost.EstimateNodes(int64(bigNodes)), big.Bytes)170171 // The rate is what must not move: same bytes per reclaimed node whether172 // the entries were 1 byte or 256. A payload-derived bounty would differ173 // by 256x here, which is the failure this realm is built to avoid.174 uassert.Equal(t, tiny.Bytes/int64(tinyNodes), big.Bytes/int64(bigNodes),175 "bytes per reclaimed node must not depend on payload size")176177 drain(cur, t)178}179180// TestRenderEscapesTheBoard: an entry is a caller's string landing in an181// inline markdown slot, so it goes through ui.Excerpt or it is an injection.182func TestRenderEscapesTheBoard(cur realm, t *testing.T) {183 drain(cur, t)184 testing.SetRealm(testing.NewUserRealm(alice))185 Add(cross(cur), "[Claim 100 GNOT](https://evil.example)\n# Official")186187 out := Render("")188 uassert.False(t, strings.Contains(out, "](https://evil.example)"), out)189 uassert.False(t, strings.Contains(out, "\n# Official"), out)190 uassert.True(t, strings.Contains(out, "Claim 100 GNOT"), out)191 uassert.True(t, strings.Contains(out, ui.Addr(alice)), out)192193 drain(cur, t)194}195196func TestRenderShowsBothIntegersAndTheAction(cur realm, t *testing.T) {197 drain(cur, t)198199 out := Render("")200 uassert.True(t, strings.Contains(out, "# Compact"), out)201 uassert.True(t, strings.Contains(out, "Add the first entry"), out)202 uassert.True(t, strings.Contains(out, "nothing to reclaim"), out)203204 testing.SetRealm(testing.NewUserRealm(alice))205 base := entries.TotalSize()206 for i := 0; i < 8; i++ {207 Add(cross(cur), "entry")208 }209 for i := base; i < base+8; i++ {210 Drop(cross(cur), i)211 }212 out = Render("")213 uassert.True(t, strings.Contains(out, "reclaimable nodes"), out)214 uassert.True(t, strings.Contains(out, "$help&func=Compact"), out)215216 drain(cur, t)217}218219// TestRenderNeverEmitsTwoBlankLines is what lets ExampleRender stay220// meaningful: gno collapses them, so output containing them can never be221// pinned by an example.222func TestRenderNeverEmitsTwoBlankLines(cur realm, t *testing.T) {223 drain(cur, t)224 testing.SetRealm(testing.NewUserRealm(alice))225 i := Add(cross(cur), "one")226 Add(cross(cur), "two")227 Drop(cross(cur), i)228229 for _, path := range []string{"", "anything"} {230 out := Render(path)231 uassert.False(t, strings.Contains(out, "\n\n\n"), "blank-line run in Render("+path+")")232 }233234 drain(cur, t)235}236Add(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, text string) int
Compact(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int
Drop(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, index int)
Fragmentation() (live int, allocated int)
Quote() struct{Bytes int64; Refund int64; Fee int64; Net int64; BreakEven int64}
Reclaimable() int
Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.