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