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)) } }