grapevine_test.mx raw

   1  package grapevine
   2  
   3  import (
   4  	"os"
   5  	"testing"
   6  
   7  	"git.smesh.lol/nostr/pkg/event"
   8  	"git.smesh.lol/nostr/pkg/kind"
   9  	"git.smesh.lol/nostr/pkg/signer/p8k"
  10  	"git.smesh.lol/nostr/pkg/tag"
  11  	"git.smesh.lol/morly/pkg/store"
  12  )
  13  
  14  // openTmp opens a store in a fresh directory. The caller owns both: defer
  15  // os.RemoveAll(dir) and defer eng.Close().
  16  func openTmp(t *testing.T) (p *store.Engine, dir string) {
  17  	t.Helper()
  18  	dir, derr := os.MkdirTemp("", "moxie-test")
  19  	if derr != nil {
  20  		t.Fatal(derr)
  21  	}
  22  	eng, err := store.Open(dir)
  23  	if err != nil {
  24  		t.Fatal(err)
  25  	}
  26  	return eng, dir
  27  }
  28  
  29  func genSigner(t *testing.T) (p *p8k.Signer) {
  30  	t.Helper()
  31  	s := p8k.MustNew()
  32  	if err := s.Generate(); err != nil {
  33  		t.Fatal(err)
  34  	}
  35  	return s
  36  }
  37  
  38  func binPubkey(s *p8k.Signer) (buf []byte) {
  39  	pk := []byte{:33}
  40  	copy(pk, s.Pub())
  41  	// pk[32] = 0 already
  42  	return pk
  43  }
  44  
  45  func makeFollowList(t *testing.T, signer *p8k.Signer, follows ...*p8k.Signer) (p *event.E) {
  46  	t.Helper()
  47  	// kind's package-level pointers are nil until ensureKinds runs; production
  48  	// callers (WoT.GetFollows, Crawler.Discover) call kind.Ensure() before
  49  	// reading them. Reading kind.FollowList.K without it dereferences nil.
  50  	kind.Ensure()
  51  	tags := tag.NewSWithCap(len(follows))
  52  	for _, f := range follows {
  53  		// tag.S is a struct holding the slice (moxie bans named slice types),
  54  		// so the append target is the field, not *tags. `*tags = push(*tags,..)`
  55  		// is invalid moxie: stage4's push lowering does not reject a non-slice
  56  		// operand and emits IR that extractvalues the struct as a slice, so it
  57  		// only ever "built" from a stale cache entry.
  58  		tags.T = push(tags.T, tag.NewFromBytesSlice([]byte("p"), binPubkey(f)))
  59  	}
  60  	ev := &event.E{
  61  		CreatedAt: 1700000000,
  62  		Kind:      kind.FollowList.K,
  63  		Tags:      tags,
  64  	}
  65  	if err := ev.Sign(signer); err != nil {
  66  		t.Fatal(err)
  67  	}
  68  	return ev
  69  }
  70  
  71  func TestComputeDepth1(t *testing.T) {
  72  	eng, dir := openTmp(t)
  73  	defer os.RemoveAll(dir)
  74  	defer eng.Close()
  75  	seed := genSigner(t)
  76  	alice := genSigner(t)
  77  	bob := genSigner(t)
  78  
  79  	// seed follows alice and bob
  80  	if err := eng.SaveEvent(makeFollowList(t, seed, alice, bob)); err != nil {
  81  		t.Fatal(err)
  82  	}
  83  
  84  	w := New(eng)
  85  	scores := w.Compute(seed.Pub(), 1)
  86  	if len(scores) != 2 {
  87  		t.Fatalf("expected 2 scores, got %d", len(scores))
  88  	}
  89  	// depth 1 decay = 1/2^0 = 1.0
  90  	for _, sc := range scores {
  91  		if sc.Value != 1.0 {
  92  			t.Errorf("expected score 1.0, got %f", sc.Value)
  93  		}
  94  		if sc.Depth != 1 {
  95  			t.Errorf("expected depth 1, got %d", sc.Depth)
  96  		}
  97  	}
  98  }
  99  
 100  func TestComputeDepth2(t *testing.T) {
 101  	eng, dir := openTmp(t)
 102  	defer os.RemoveAll(dir)
 103  	defer eng.Close()
 104  	seed := genSigner(t)
 105  	alice := genSigner(t)
 106  	bob := genSigner(t)
 107  
 108  	// seed -> alice -> bob
 109  	if err := eng.SaveEvent(makeFollowList(t, seed, alice)); err != nil {
 110  		t.Fatal(err)
 111  	}
 112  	if err := eng.SaveEvent(makeFollowList(t, alice, bob)); err != nil {
 113  		t.Fatal(err)
 114  	}
 115  
 116  	w := New(eng)
 117  	scores := w.Compute(seed.Pub(), 2)
 118  
 119  	found := false
 120  	for _, sc := range scores {
 121  		if string(sc.Pubkey) == string(bob.Pub()) {
 122  			found = true
 123  			// depth 2 decay = 1/2^1 = 0.5
 124  			if sc.Value != 0.5 {
 125  				t.Errorf("expected bob score 0.5, got %f", sc.Value)
 126  			}
 127  		}
 128  	}
 129  	if !found {
 130  		t.Fatal("bob not found in scores")
 131  	}
 132  }
 133  
 134  func TestIsTrusted(t *testing.T) {
 135  	scores := []Score{
 136  		{Pubkey: []byte("aaa"), Value: 0.8},
 137  		{Pubkey: []byte("bbb"), Value: 0.3},
 138  	}
 139  	if !IsTrusted(scores, []byte("aaa"), 0.5) {
 140  		t.Error("aaa should be trusted at 0.5 threshold")
 141  	}
 142  	if IsTrusted(scores, []byte("bbb"), 0.5) {
 143  		t.Error("bbb should not be trusted at 0.5 threshold")
 144  	}
 145  	if IsTrusted(scores, []byte("ccc"), 0.1) {
 146  		t.Error("unknown pubkey should not be trusted")
 147  	}
 148  }
 149