56db37a463
web/handlers/bloom.go: GET /bloom/{stashID} renders BloomRecord
(AccruedGrain, RateBasisPoints as %, LastAccrualBlock) + the 4.5%
target rate read from x/bloom/types.TargetBloomRateBasisPoints (D-073
code-constant source-of-truth, NOT hardcoded) + the 4.0%-5.0% band
(Min/MaxBloomRateBasisPoints) + Mission Lock note. Store extensions:
GetBloomRecord/ListBloomRecords. Seed: holder-alia at target rate (450
bps), holder-bryn at 420 bps (within band). bloom_test.go: D-073
target-rate source-of-truth regression guard (asserts TargetBloomRate-
BasisPoints=450, Min=400, Max=500 from code constants; would fail if
hardcoded), G-026 error lexicon check, highest-risk screen for banned-
term drift (uses "Bloom"/"real production"/"accrual" labels only).
Coverage: store 98.1%, handlers 89.2%.
---ci---
project: oy
phase: 5
milestone: v0.6
status: execute
---/ci---
577 lines
18 KiB
Go
577 lines
18 KiB
Go
package store
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
|
|
bloomtypes "github.com/oy/openyield/x/bloom/types"
|
|
identitytypes "github.com/oy/openyield/x/identity/types"
|
|
standingtypes "github.com/oy/openyield/x/standing/types"
|
|
stashtypes "github.com/oy/openyield/x/stash/types"
|
|
windowtypes "github.com/oy/openyield/x/window/types"
|
|
)
|
|
|
|
func TestNewStoreSeedsFixtures(t *testing.T) {
|
|
s := NewStore()
|
|
reaches := s.ListReaches()
|
|
if len(reaches) < 2 {
|
|
t.Fatalf("NewStore seeded %d reaches, want >=2", len(reaches))
|
|
}
|
|
// Both seeded reaches must be Nomads (IsNomad=true).
|
|
for _, r := range reaches {
|
|
if !r.IsNomad {
|
|
t.Errorf("seeded reach %q: IsNomad=false, want true", r.HolderID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateReachAtomicReachAndStash(t *testing.T) {
|
|
s := NewStore()
|
|
reach, stash, err := s.CreateReach("holder-test1", "pk-test1")
|
|
if err != nil {
|
|
t.Fatalf("CreateReach: %v", err)
|
|
}
|
|
// D-071: Reach must be IsNomad=true.
|
|
if !reach.IsNomad {
|
|
t.Errorf("reach.IsNomad = false, want true (D-071)")
|
|
}
|
|
if reach.HolderID != "holder-test1" {
|
|
t.Errorf("reach.HolderID = %q, want holder-test1", reach.HolderID)
|
|
}
|
|
// D-071: Stash must have matching HolderID + seeded BalanceGrain.
|
|
if stash.HolderID != reach.HolderID {
|
|
t.Errorf("stash.HolderID = %q, want %q (D-071 atomic)", stash.HolderID, reach.HolderID)
|
|
}
|
|
if stash.BalanceGrain != seedBalanceGrain {
|
|
t.Errorf("stash.BalanceGrain = %d, want %d", stash.BalanceGrain, seedBalanceGrain)
|
|
}
|
|
// Both must be retrievable after the atomic call.
|
|
if _, ok := s.GetReach("holder-test1"); !ok {
|
|
t.Errorf("GetReach miss after CreateReach (atomicity broken)")
|
|
}
|
|
if _, ok := s.GetStash("holder-test1"); !ok {
|
|
t.Errorf("GetStash miss after CreateReach (atomicity broken)")
|
|
}
|
|
if _, ok := s.GetStashActivity(stash.StashID); !ok {
|
|
t.Errorf("GetStashActivity miss after CreateReach (atomicity broken)")
|
|
}
|
|
}
|
|
|
|
func TestCreateReachDuplicateRejected(t *testing.T) {
|
|
s := NewStore()
|
|
if _, _, err := s.CreateReach("holder-alia", "pk-dupe"); err == nil {
|
|
t.Errorf("CreateReach duplicate holder-alia: expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestCreateReachValidationG027(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
holderID string
|
|
publicKey string
|
|
wantErr bool
|
|
}{
|
|
{"empty holder", "", "pk", true},
|
|
{"empty pubkey", "h", "", true},
|
|
{"holder too long", stringOf('x', 129), "pk", true},
|
|
{"pubkey too long", "h", stringOf('y', 129), true},
|
|
{"holder with slash", "h/x", "pk", true},
|
|
{"holder with backslash", "h\\x", "pk", true},
|
|
{"holder with template syntax", "h{{", "pk", true},
|
|
{"pubkey with slash", "h", "p/x", true},
|
|
{"valid minimal", "h", "p", false},
|
|
{"valid typical", "holder-oka", "pk-oka-7", false},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
s := NewStore()
|
|
_, _, err := s.CreateReach(c.holderID, c.publicKey)
|
|
if c.wantErr && err == nil {
|
|
t.Errorf("expected error, got nil")
|
|
}
|
|
if !c.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetReachHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
if _, ok := s.GetReach("holder-alia"); !ok {
|
|
t.Errorf("GetReach(holder-alia) miss, want hit (seeded)")
|
|
}
|
|
if _, ok := s.GetReach("nobody"); ok {
|
|
t.Errorf("GetReach(nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestGetStashHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
if _, ok := s.GetStash("holder-alia"); !ok {
|
|
t.Errorf("GetStash(holder-alia) miss, want hit (seeded)")
|
|
}
|
|
if _, ok := s.GetStash("nobody"); ok {
|
|
t.Errorf("GetStash(nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestGetStashActivityHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
stash, ok := s.GetStash("holder-alia")
|
|
if !ok {
|
|
t.Fatal("seeded stash holder-alia missing")
|
|
}
|
|
if _, ok := s.GetStashActivity(stash.StashID); !ok {
|
|
t.Errorf("GetStashActivity(%q) miss, want hit", stash.StashID)
|
|
}
|
|
if _, ok := s.GetStashActivity("stash-nobody"); ok {
|
|
t.Errorf("GetStashActivity(stash-nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestCreateReachConcurrentNoRace(t *testing.T) {
|
|
s := NewStore()
|
|
const n = 50
|
|
var wg sync.WaitGroup
|
|
wg.Add(n)
|
|
for i := 0; i < n; i++ {
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
holder := "holder-concurrent-" + itoa(i)
|
|
_, _, _ = s.CreateReach(holder, "pk")
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
// All n concurrent creates with distinct holder IDs must be present.
|
|
for i := 0; i < n; i++ {
|
|
if _, ok := s.GetReach("holder-concurrent-" + itoa(i)); !ok {
|
|
t.Errorf("concurrent reach %d missing after wg.Wait", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSeededMatureVsImmature(t *testing.T) {
|
|
s := NewStore()
|
|
// holder-alia: ActiveDays=92, MaxGapDays=10 -> mature.
|
|
aliaStash, ok := s.GetStash("holder-alia")
|
|
if !ok {
|
|
t.Fatal("seeded holder-alia missing")
|
|
}
|
|
aliaAct, ok := s.GetStashActivity(aliaStash.StashID)
|
|
if !ok {
|
|
t.Fatal("seeded alia activity missing")
|
|
}
|
|
if !aliaAct.IsMature() {
|
|
t.Errorf("holder-alia IsMature=false, want true (ActiveDays=%d, MaxGap=%d)",
|
|
aliaAct.ActiveDays, aliaAct.MaxGapDays)
|
|
}
|
|
// holder-bryn: ActiveDays=45, MaxGapDays=5 -> not mature.
|
|
brynStash, ok := s.GetStash("holder-bryn")
|
|
if !ok {
|
|
t.Fatal("seeded holder-bryn missing")
|
|
}
|
|
brynAct, ok := s.GetStashActivity(brynStash.StashID)
|
|
if !ok {
|
|
t.Fatal("seeded bryn activity missing")
|
|
}
|
|
if brynAct.IsMature() {
|
|
t.Errorf("holder-bryn IsMature=true, want false (ActiveDays=%d, MaxGap=%d)",
|
|
brynAct.ActiveDays, brynAct.MaxGapDays)
|
|
}
|
|
}
|
|
|
|
// Compile-time assertions that the types are the real x/*/types structs
|
|
// (D-067: the mock store grounds the UI in the real Go type definitions).
|
|
var _ identitytypes.Reach
|
|
var _ stashtypes.Stash
|
|
|
|
// itoa is provided by fixtures.go (shared with the production package).
|
|
|
|
func stringOf(r rune, n int) string {
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = byte(r)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// --- Window tests (P3) ---
|
|
|
|
func TestOpenWindowCreatesStatusOpenWithInitialAudit(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash, ResourceID: "stash-x"}
|
|
rl := windowtypes.RateLimit{MaxActions: 5, PerDurationSeconds: 3600}
|
|
w, err := s.OpenWindow("holder-alia", "service-1", scope, 1000, 2000, rl)
|
|
if err != nil {
|
|
t.Fatalf("OpenWindow: %v", err)
|
|
}
|
|
if w.Status != windowtypes.StatusOpen {
|
|
t.Errorf("OpenWindow status %q, want Open", w.Status)
|
|
}
|
|
if w.WindowID == "" {
|
|
t.Error("OpenWindow: empty WindowID")
|
|
}
|
|
audit := s.GetAuditLog(w.WindowID)
|
|
if len(audit) != 1 {
|
|
t.Errorf("OpenWindow: audit log len %d, want 1", len(audit))
|
|
}
|
|
if audit[0].Action != "open" {
|
|
t.Errorf("OpenWindow: audit[0].Action %q, want open", audit[0].Action)
|
|
}
|
|
}
|
|
|
|
func TestOpenWindowValidation(t *testing.T) {
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
cases := []struct {
|
|
name, grantor, grantee string
|
|
wantErr bool
|
|
}{
|
|
{"empty grantor", "", "g", true},
|
|
{"empty grantee", "h", "", true},
|
|
{"valid", "h", "g", false},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
s := NewStore()
|
|
_, err := s.OpenWindow(c.grantor, c.grantee, scope, 1, 2, rl)
|
|
if c.wantErr && err == nil {
|
|
t.Errorf("expected error, got nil")
|
|
}
|
|
if !c.wantErr && err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActivateWindowTransitionsToActive(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
if err := s.ActivateWindow(w.WindowID); err != nil {
|
|
t.Fatalf("ActivateWindow: %v", err)
|
|
}
|
|
updated, _ := s.GetWindow(w.WindowID)
|
|
if updated.Status != windowtypes.StatusActive {
|
|
t.Errorf("after activate: %q, want Active", updated.Status)
|
|
}
|
|
audit := s.GetAuditLog(w.WindowID)
|
|
if len(audit) != 2 {
|
|
t.Errorf("after activate: audit len %d, want 2", len(audit))
|
|
}
|
|
}
|
|
|
|
func TestActivateWindowNotFound(t *testing.T) {
|
|
s := NewStore()
|
|
if err := s.ActivateWindow("window-nobody"); err == nil {
|
|
t.Error("ActivateWindow(nobody): expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestActivateWindowOnActiveFails(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
_ = s.ActivateWindow(w.WindowID)
|
|
// Activate again should fail (can only activate Open windows).
|
|
if err := s.ActivateWindow(w.WindowID); err == nil {
|
|
t.Error("activate on Active: expected error, got nil (Window.Activate rejects non-Open)")
|
|
}
|
|
}
|
|
|
|
func TestRevokeWindowTransitionsToRevoked(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
if err := s.RevokeWindow(w.WindowID); err != nil {
|
|
t.Fatalf("RevokeWindow: %v", err)
|
|
}
|
|
updated, _ := s.GetWindow(w.WindowID)
|
|
if updated.Status != windowtypes.StatusRevoked {
|
|
t.Errorf("after revoke: %q, want Revoked", updated.Status)
|
|
}
|
|
if !updated.Revoked {
|
|
t.Error("after revoke: Revoked flag false, want true")
|
|
}
|
|
}
|
|
|
|
func TestRevokeWindowIdempotent(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
_ = s.RevokeWindow(w.WindowID)
|
|
before := len(s.GetAuditLog(w.WindowID))
|
|
_ = s.RevokeWindow(w.WindowID)
|
|
after := len(s.GetAuditLog(w.WindowID))
|
|
if after != before {
|
|
t.Errorf("idempotent revoke: audit grew %d -> %d", before, after)
|
|
}
|
|
}
|
|
|
|
func TestRevokeWindowOnExpiredIsNoOp(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
_ = s.ExpireWindow(w.WindowID)
|
|
before := len(s.GetAuditLog(w.WindowID))
|
|
_ = s.RevokeWindow(w.WindowID)
|
|
updated, _ := s.GetWindow(w.WindowID)
|
|
if updated.Status != windowtypes.StatusExpired {
|
|
t.Errorf("revoke-on-expired: %q, want Expired (terminal wins)", updated.Status)
|
|
}
|
|
after := len(s.GetAuditLog(w.WindowID))
|
|
if after != before {
|
|
t.Errorf("revoke-on-expired: audit grew %d -> %d (no-op)", before, after)
|
|
}
|
|
}
|
|
|
|
func TestRevokeWindowNotFound(t *testing.T) {
|
|
s := NewStore()
|
|
if err := s.RevokeWindow("window-nobody"); err == nil {
|
|
t.Error("RevokeWindow(nobody): expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestExpireWindowTransitionsToExpired(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
if err := s.ExpireWindow(w.WindowID); err != nil {
|
|
t.Fatalf("ExpireWindow: %v", err)
|
|
}
|
|
updated, _ := s.GetWindow(w.WindowID)
|
|
if updated.Status != windowtypes.StatusExpired {
|
|
t.Errorf("after expire: %q, want Expired", updated.Status)
|
|
}
|
|
}
|
|
|
|
func TestExpireWindowNotFound(t *testing.T) {
|
|
s := NewStore()
|
|
if err := s.ExpireWindow("window-nobody"); err == nil {
|
|
t.Error("ExpireWindow(nobody): expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestExpireWindowIdempotent(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
_ = s.ExpireWindow(w.WindowID)
|
|
before := len(s.GetAuditLog(w.WindowID))
|
|
_ = s.ExpireWindow(w.WindowID)
|
|
after := len(s.GetAuditLog(w.WindowID))
|
|
if after != before {
|
|
t.Errorf("idempotent expire: audit grew %d -> %d", before, after)
|
|
}
|
|
}
|
|
|
|
func TestListWindowsFiltersByGrantor(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
_, _ = s.OpenWindow("holder-alia", "svc1", scope, 1, 2, rl)
|
|
_, _ = s.OpenWindow("holder-alia", "svc2", scope, 1, 2, rl)
|
|
_, _ = s.OpenWindow("holder-bryn", "svc3", scope, 1, 2, rl)
|
|
alia := s.ListWindows("holder-alia")
|
|
if len(alia) != 2 {
|
|
t.Errorf("ListWindows(holder-alia) = %d, want 2", len(alia))
|
|
}
|
|
bryn := s.ListWindows("holder-bryn")
|
|
if len(bryn) != 1 {
|
|
t.Errorf("ListWindows(holder-bryn) = %d, want 1", len(bryn))
|
|
}
|
|
nobody := s.ListWindows("nobody")
|
|
if len(nobody) != 0 {
|
|
t.Errorf("ListWindows(nobody) = %d, want 0", len(nobody))
|
|
}
|
|
}
|
|
|
|
func TestGetWindowHitMiss(t *testing.T) {
|
|
s := NewStore()
|
|
scope := windowtypes.Scope{Kind: windowtypes.ScopeReadStash}
|
|
rl := windowtypes.RateLimit{MaxActions: 1}
|
|
w, _ := s.OpenWindow("holder-alia", "svc", scope, 1, 2, rl)
|
|
if _, ok := s.GetWindow(w.WindowID); !ok {
|
|
t.Errorf("GetWindow(%q) miss, want hit", w.WindowID)
|
|
}
|
|
if _, ok := s.GetWindow("window-nobody"); ok {
|
|
t.Error("GetWindow(nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestGetAuditLogEmptyForMissing(t *testing.T) {
|
|
s := NewStore()
|
|
if logs := s.GetAuditLog("window-nobody"); logs != nil {
|
|
t.Errorf("GetAuditLog(nobody) = %v, want nil", logs)
|
|
}
|
|
}
|
|
|
|
// --- Standing + Freeholder signals tests (P4) ---
|
|
|
|
func TestListRatingsSeeded(t *testing.T) {
|
|
s := NewStore()
|
|
alia := s.ListRatings("holder-alia")
|
|
if len(alia) != 12 {
|
|
t.Errorf("ListRatings(holder-alia) = %d, want 12 (seeded)", len(alia))
|
|
}
|
|
bryn := s.ListRatings("holder-bryn")
|
|
if len(bryn) != 3 {
|
|
t.Errorf("ListRatings(holder-bryn) = %d, want 3 (seeded)", len(bryn))
|
|
}
|
|
nobody := s.ListRatings("nobody")
|
|
if len(nobody) != 0 {
|
|
t.Errorf("ListRatings(nobody) = %d, want 0", len(nobody))
|
|
}
|
|
}
|
|
|
|
func TestListVouchesSeeded(t *testing.T) {
|
|
s := NewStore()
|
|
alia := s.ListVouches("holder-alia")
|
|
if len(alia) != 1 {
|
|
t.Errorf("ListVouches(holder-alia) = %d, want 1 (seeded)", len(alia))
|
|
}
|
|
bryn := s.ListVouches("holder-bryn")
|
|
if len(bryn) != 0 {
|
|
t.Errorf("ListVouches(holder-bryn) = %d, want 0 (seeded)", len(bryn))
|
|
}
|
|
}
|
|
|
|
func TestListSlashesEmptyByDefault(t *testing.T) {
|
|
s := NewStore()
|
|
if sl := s.ListSlashes("holder-alia"); len(sl) != 0 {
|
|
t.Errorf("ListSlashes(holder-alia) = %d, want 0 (no slashes seeded)", len(sl))
|
|
}
|
|
}
|
|
|
|
func TestComputeStandingScoreNoRatingsReturnsPriorMean(t *testing.T) {
|
|
s := NewStore()
|
|
score, bucket := s.ComputeStandingScore("nobody")
|
|
if score != standingtypes.PriorMean {
|
|
t.Errorf("ComputeStandingScore(nobody) score = %v, want PriorMean %v", score, standingtypes.PriorMean)
|
|
}
|
|
if bucket != standingtypes.BucketNew {
|
|
t.Errorf("ComputeStandingScore(nobody) bucket = %q, want New", bucket)
|
|
}
|
|
}
|
|
|
|
func TestComputeStandingScoreAliaIsEligibleRange(t *testing.T) {
|
|
s := NewStore()
|
|
score, bucket := s.ComputeStandingScore("holder-alia")
|
|
if score < 4.5 {
|
|
t.Errorf("holder-alia score = %.2f, want >= 4.5 (Freeholder-eligible range)", score)
|
|
}
|
|
if bucket != standingtypes.BucketPreferred && bucket != standingtypes.BucketTop {
|
|
t.Errorf("holder-alia bucket = %q, want Preferred or Top", bucket)
|
|
}
|
|
}
|
|
|
|
func TestComputeStandingScoreBrynIsNew(t *testing.T) {
|
|
s := NewStore()
|
|
_, bucket := s.ComputeStandingScore("holder-bryn")
|
|
// holder-bryn has 3 ratings (< 10) -> bucket New.
|
|
if bucket != standingtypes.BucketNew {
|
|
t.Errorf("holder-bryn bucket = %q, want New (< 10 ratings)", bucket)
|
|
}
|
|
}
|
|
|
|
func TestComputeFreeholderSignalsAliaAllTrue(t *testing.T) {
|
|
s := NewStore()
|
|
signals := s.ComputeFreeholderSignals("holder-alia")
|
|
// holder-alia: mature Stash (92 days), score >= 4.5 in 4 cats, balance
|
|
// 920000 >= 100000, 1 Vouch -> all 4 signals true.
|
|
if !signals.StashMaturity {
|
|
t.Errorf("StashMaturity = false, want true (mature Stash)")
|
|
}
|
|
if !signals.MultiDomainStanding {
|
|
t.Errorf("MultiDomainStanding = false, want true (score >= 4.5 in 4 cats)")
|
|
}
|
|
if !signals.CommittedCapital {
|
|
t.Errorf("CommittedCapital = false, want true (balance 920000 >= 100000)")
|
|
}
|
|
if !signals.CommunityEndorsement {
|
|
t.Errorf("CommunityEndorsement = false, want true (1 Vouch seeded)")
|
|
}
|
|
if !signals.IsFreeholderEligible() {
|
|
t.Errorf("holder-alia IsFreeholderEligible = false, want true (all 4 signals)")
|
|
}
|
|
}
|
|
|
|
func TestComputeFreeholderSignalsBrynNotEligible(t *testing.T) {
|
|
s := NewStore()
|
|
signals := s.ComputeFreeholderSignals("holder-bryn")
|
|
// holder-bryn: immature Stash (45 days), 1 cat (< 3), no Vouch.
|
|
if signals.StashMaturity {
|
|
t.Errorf("StashMaturity = true, want false (immature 45 days)")
|
|
}
|
|
if signals.MultiDomainStanding {
|
|
t.Errorf("MultiDomainStanding = true, want false (1 cat < 3)")
|
|
}
|
|
if signals.CommunityEndorsement {
|
|
t.Errorf("CommunityEndorsement = true, want false (no Vouches)")
|
|
}
|
|
if signals.IsFreeholderEligible() {
|
|
t.Errorf("holder-bryn IsFreeholderEligible = true, want false")
|
|
}
|
|
}
|
|
|
|
func TestComputeFreeholderSignalsNoStash(t *testing.T) {
|
|
s := NewStore()
|
|
signals := s.ComputeFreeholderSignals("nobody")
|
|
// No Stash, no ratings, no Vouches -> all false.
|
|
if signals.IsFreeholderEligible() {
|
|
t.Errorf("nobody IsFreeholderEligible = true, want false (no Stash)")
|
|
}
|
|
}
|
|
|
|
// --- Bloom accrual tests (P5) ---
|
|
|
|
func TestGetBloomRecordSeeded(t *testing.T) {
|
|
s := NewStore()
|
|
rec, ok := s.GetBloomRecord("stash-holder-alia")
|
|
if !ok {
|
|
t.Fatal("GetBloomRecord(stash-holder-alia) miss, want hit (seeded)")
|
|
}
|
|
// D-073: seeded at the code-constant target rate.
|
|
if rec.RateBasisPoints != bloomtypes.TargetBloomRateBasisPoints {
|
|
t.Errorf("seeded RateBasisPoints = %d, want %d (TargetBloomRateBasisPoints, D-073)", rec.RateBasisPoints, bloomtypes.TargetBloomRateBasisPoints)
|
|
}
|
|
if rec.AccruedGrain != 45000 {
|
|
t.Errorf("seeded AccruedGrain = %d, want 45000", rec.AccruedGrain)
|
|
}
|
|
}
|
|
|
|
func TestGetBloomRecordMiss(t *testing.T) {
|
|
s := NewStore()
|
|
if _, ok := s.GetBloomRecord("stash-nobody"); ok {
|
|
t.Error("GetBloomRecord(stash-nobody) hit, want miss")
|
|
}
|
|
}
|
|
|
|
func TestListBloomRecordsByHolder(t *testing.T) {
|
|
s := NewStore()
|
|
alia := s.ListBloomRecords("holder-alia")
|
|
if len(alia) != 1 {
|
|
t.Errorf("ListBloomRecords(holder-alia) = %d, want 1", len(alia))
|
|
}
|
|
if alia[0].StashID != "stash-holder-alia" {
|
|
t.Errorf("ListBloomRecords(holder-alia)[0].StashID = %q, want stash-holder-alia", alia[0].StashID)
|
|
}
|
|
bryn := s.ListBloomRecords("holder-bryn")
|
|
if len(bryn) != 1 {
|
|
t.Errorf("ListBloomRecords(holder-bryn) = %d, want 1", len(bryn))
|
|
}
|
|
nobody := s.ListBloomRecords("nobody")
|
|
if len(nobody) != 0 {
|
|
t.Errorf("ListBloomRecords(nobody) = %d, want 0", len(nobody))
|
|
}
|
|
}
|