package grapevine import ( "os" "testing" "git.smesh.lol/nostr/pkg/event" "git.smesh.lol/nostr/pkg/kind" "git.smesh.lol/nostr/pkg/signer/p8k" "git.smesh.lol/nostr/pkg/tag" "git.smesh.lol/morly/pkg/store" ) // openTmp opens a store in a fresh directory. The caller owns both: defer // os.RemoveAll(dir) and defer eng.Close(). func openTmp(t *testing.T) (p *store.Engine, dir string) { t.Helper() dir, derr := os.MkdirTemp("", "moxie-test") if derr != nil { t.Fatal(derr) } eng, err := store.Open(dir) if err != nil { t.Fatal(err) } return eng, dir } func genSigner(t *testing.T) (p *p8k.Signer) { t.Helper() s := p8k.MustNew() if err := s.Generate(); err != nil { t.Fatal(err) } return s } func binPubkey(s *p8k.Signer) (buf []byte) { pk := []byte{:33} copy(pk, s.Pub()) // pk[32] = 0 already return pk } func makeFollowList(t *testing.T, signer *p8k.Signer, follows ...*p8k.Signer) (p *event.E) { t.Helper() // kind's package-level pointers are nil until ensureKinds runs; production // callers (WoT.GetFollows, Crawler.Discover) call kind.Ensure() before // reading them. Reading kind.FollowList.K without it dereferences nil. kind.Ensure() tags := tag.NewSWithCap(len(follows)) for _, f := range follows { // tag.S is a struct holding the slice (moxie bans named slice types), // so the append target is the field, not *tags. `*tags = push(*tags,..)` // is invalid moxie: stage4's push lowering does not reject a non-slice // operand and emits IR that extractvalues the struct as a slice, so it // only ever "built" from a stale cache entry. tags.T = push(tags.T, tag.NewFromBytesSlice([]byte("p"), binPubkey(f))) } ev := &event.E{ CreatedAt: 1700000000, Kind: kind.FollowList.K, Tags: tags, } if err := ev.Sign(signer); err != nil { t.Fatal(err) } return ev } func TestComputeDepth1(t *testing.T) { eng, dir := openTmp(t) defer os.RemoveAll(dir) defer eng.Close() seed := genSigner(t) alice := genSigner(t) bob := genSigner(t) // seed follows alice and bob if err := eng.SaveEvent(makeFollowList(t, seed, alice, bob)); err != nil { t.Fatal(err) } w := New(eng) scores := w.Compute(seed.Pub(), 1) if len(scores) != 2 { t.Fatalf("expected 2 scores, got %d", len(scores)) } // depth 1 decay = 1/2^0 = 1.0 for _, sc := range scores { if sc.Value != 1.0 { t.Errorf("expected score 1.0, got %f", sc.Value) } if sc.Depth != 1 { t.Errorf("expected depth 1, got %d", sc.Depth) } } } func TestComputeDepth2(t *testing.T) { eng, dir := openTmp(t) defer os.RemoveAll(dir) defer eng.Close() seed := genSigner(t) alice := genSigner(t) bob := genSigner(t) // seed -> alice -> bob if err := eng.SaveEvent(makeFollowList(t, seed, alice)); err != nil { t.Fatal(err) } if err := eng.SaveEvent(makeFollowList(t, alice, bob)); err != nil { t.Fatal(err) } w := New(eng) scores := w.Compute(seed.Pub(), 2) found := false for _, sc := range scores { if string(sc.Pubkey) == string(bob.Pub()) { found = true // depth 2 decay = 1/2^1 = 0.5 if sc.Value != 0.5 { t.Errorf("expected bob score 0.5, got %f", sc.Value) } } } if !found { t.Fatal("bob not found in scores") } } func TestIsTrusted(t *testing.T) { scores := []Score{ {Pubkey: []byte("aaa"), Value: 0.8}, {Pubkey: []byte("bbb"), Value: 0.3}, } if !IsTrusted(scores, []byte("aaa"), 0.5) { t.Error("aaa should be trusted at 0.5 threshold") } if IsTrusted(scores, []byte("bbb"), 0.5) { t.Error("bbb should not be trusted at 0.5 threshold") } if IsTrusted(scores, []byte("ccc"), 0.1) { t.Error("unknown pubkey should not be trusted") } }