graph-kind-filter_test.go raw

   1  //go:build !(js && wasm)
   2  
   3  package database
   4  
   5  import (
   6  	"context"
   7  	"sort"
   8  	"testing"
   9  
  10  	"git.smesh.lol/orly/pkg/database/indexes/types"
  11  	"git.smesh.lol/orly/pkg/nostr/encoders/event"
  12  	"git.smesh.lol/orly/pkg/nostr/encoders/hex"
  13  	"git.smesh.lol/orly/pkg/nostr/encoders/tag"
  14  )
  15  
  16  // kindFilterTestFixture holds all pubkeys and their hex strings for the test graph:
  17  //
  18  //	pkA --kind3--> pkB, pkE   (pkA follows pkB and pkE)
  19  //	pkA --kind10000--> pkC    (pkA mutes pkC)
  20  //	pkA --kind1984--> pkD     (pkA reports pkD)
  21  //	pkF --kind3--> pkB        (pkF also follows pkB)
  22  type kindFilterTestFixture struct {
  23  	db     *D
  24  	ctx    context.Context
  25  	pkAHex string
  26  	pkBHex string
  27  	pkCHex string
  28  	pkDHex string
  29  	pkEHex string
  30  	pkFHex string
  31  }
  32  
  33  // resolveSerials converts a slice of pubkey serials to a set of hex strings.
  34  func resolveSerials(t *testing.T, db *D, serials []*types.Uint40) map[string]bool {
  35  	t.Helper()
  36  	m := make(map[string]bool, len(serials))
  37  	for _, s := range serials {
  38  		h, err := db.GetPubkeyHexFromSerial(s)
  39  		if err != nil {
  40  			t.Errorf("GetPubkeyHexFromSerial: %v", err)
  41  			continue
  42  		}
  43  		m[h] = true
  44  	}
  45  	return m
  46  }
  47  
  48  func setupKindFilterFixture(t *testing.T) *kindFilterTestFixture {
  49  	t.Helper()
  50  
  51  	ctx, cancel := context.WithCancel(context.Background())
  52  	t.Cleanup(cancel)
  53  
  54  	db, err := New(ctx, cancel, t.TempDir(), "off")
  55  	if err != nil {
  56  		t.Fatalf("New db: %v", err)
  57  	}
  58  	t.Cleanup(func() { db.Close() })
  59  
  60  	pkA, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000001")
  61  	pkB, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000002")
  62  	pkC, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000003")
  63  	pkD, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000004")
  64  	pkE, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000005")
  65  	pkF, _ := hex.Dec("0000000000000000000000000000000000000000000000000000000000000006")
  66  
  67  	makeEvent := func(idByte byte, author []byte, kind uint16, targets ...[]byte) *event.E {
  68  		id := make([]byte, 32)
  69  		id[0] = idByte
  70  		sig := make([]byte, 64)
  71  		sig[0] = idByte
  72  		ptags := make([]*tag.T, 0, len(targets))
  73  		for _, tgt := range targets {
  74  			ptags = append(ptags, tag.NewFromAny("p", hex.Enc(tgt)))
  75  		}
  76  		return &event.E{
  77  			ID:        id,
  78  			Pubkey:    author,
  79  			CreatedAt: int64(1000000 + int(idByte)),
  80  			Kind:      kind,
  81  			Content:   []byte(""),
  82  			Sig:       sig,
  83  			Tags:      tag.NewS(ptags...),
  84  		}
  85  	}
  86  
  87  	for _, ev := range []*event.E{
  88  		makeEvent(0x01, pkA, 3, pkB, pkE),  // pkA kind-3 follows pkB, pkE
  89  		makeEvent(0x02, pkA, 10000, pkC),   // pkA kind-10000 mutes pkC
  90  		makeEvent(0x03, pkA, 1984, pkD),    // pkA kind-1984 reports pkD
  91  		makeEvent(0x04, pkF, 3, pkB),       // pkF kind-3 follows pkB
  92  	} {
  93  		if _, err := db.SaveEvent(ctx, ev); err != nil {
  94  			t.Fatalf("SaveEvent kind=%d: %v", ev.Kind, err)
  95  		}
  96  	}
  97  
  98  	return &kindFilterTestFixture{
  99  		db:     db,
 100  		ctx:    ctx,
 101  		pkAHex: hex.Enc(pkA),
 102  		pkBHex: hex.Enc(pkB),
 103  		pkCHex: hex.Enc(pkC),
 104  		pkDHex: hex.Enc(pkD),
 105  		pkEHex: hex.Enc(pkE),
 106  		pkFHex: hex.Enc(pkF),
 107  	}
 108  }
 109  
 110  // TestGetFollowsByKindViaPPG verifies the forward kind-filtered PPG scan.
 111  func TestGetFollowsByKindViaPPG(t *testing.T) {
 112  	f := setupKindFilterFixture(t)
 113  	db := f.db
 114  
 115  	pkASerial, err := db.PubkeyHexToSerial(f.pkAHex)
 116  	if err != nil {
 117  		t.Fatalf("PubkeyHexToSerial(A): %v", err)
 118  	}
 119  
 120  	t.Run("GetFollowsViaPPG returns all kinds", func(t *testing.T) {
 121  		serials, err := db.GetFollowsViaPPG(pkASerial)
 122  		if err != nil {
 123  			t.Fatalf("GetFollowsViaPPG: %v", err)
 124  		}
 125  		got := make(map[string]bool, len(serials))
 126  		for _, s := range serials {
 127  			h, err := db.GetPubkeyHexFromSerial(s)
 128  			if err != nil {
 129  				t.Errorf("GetPubkeyHexFromSerial: %v", err)
 130  				continue
 131  			}
 132  			got[h] = true
 133  		}
 134  		want := map[string]bool{
 135  			f.pkBHex: true, // kind-3
 136  			f.pkEHex: true, // kind-3
 137  			f.pkCHex: true, // kind-10000
 138  			f.pkDHex: true, // kind-1984
 139  		}
 140  		if len(got) != len(want) {
 141  			t.Errorf("GetFollowsViaPPG: got %d results, want %d; got=%v want=%v", len(got), len(want), got, want)
 142  			return
 143  		}
 144  		for pk := range want {
 145  			if !got[pk] {
 146  				t.Errorf("GetFollowsViaPPG: missing expected pubkey %s", pk[:8])
 147  			}
 148  		}
 149  	})
 150  
 151  	t.Run("GetFollowsByKindViaPPG kind-3 returns only follows", func(t *testing.T) {
 152  		serials, err := db.GetFollowsByKindViaPPG(pkASerial, 3)
 153  		if err != nil {
 154  			t.Fatalf("GetFollowsByKindViaPPG(3): %v", err)
 155  		}
 156  		got := make(map[string]bool, len(serials))
 157  		for _, s := range serials {
 158  			h, err := db.GetPubkeyHexFromSerial(s)
 159  			if err != nil {
 160  				t.Errorf("GetPubkeyHexFromSerial: %v", err)
 161  				continue
 162  			}
 163  			got[h] = true
 164  		}
 165  		want := map[string]bool{f.pkBHex: true, f.pkEHex: true}
 166  		if len(got) != len(want) {
 167  			t.Errorf("GetFollowsByKindViaPPG(3): got %d, want 2; got=%v", len(got), got)
 168  			return
 169  		}
 170  		for pk := range want {
 171  			if !got[pk] {
 172  				t.Errorf("GetFollowsByKindViaPPG(3): missing %s", pk[:8])
 173  			}
 174  		}
 175  		// Must NOT contain muted or reported pubkeys
 176  		if got[f.pkCHex] {
 177  			t.Errorf("GetFollowsByKindViaPPG(3): should NOT return muted pubkey C")
 178  		}
 179  		if got[f.pkDHex] {
 180  			t.Errorf("GetFollowsByKindViaPPG(3): should NOT return reported pubkey D")
 181  		}
 182  	})
 183  
 184  	t.Run("GetFollowsByKindViaPPG kind-10000 returns only mutes", func(t *testing.T) {
 185  		serials, err := db.GetFollowsByKindViaPPG(pkASerial, 10000)
 186  		if err != nil {
 187  			t.Fatalf("GetFollowsByKindViaPPG(10000): %v", err)
 188  		}
 189  		got := make(map[string]bool, len(serials))
 190  		for _, s := range serials {
 191  			h, err := db.GetPubkeyHexFromSerial(s)
 192  			if err != nil {
 193  				continue
 194  			}
 195  			got[h] = true
 196  		}
 197  		if len(got) != 1 || !got[f.pkCHex] {
 198  			t.Errorf("GetFollowsByKindViaPPG(10000): want [C], got %v", got)
 199  		}
 200  	})
 201  
 202  	t.Run("GetFollowsByKindViaPPG kind-1984 returns only reports", func(t *testing.T) {
 203  		serials, err := db.GetFollowsByKindViaPPG(pkASerial, 1984)
 204  		if err != nil {
 205  			t.Fatalf("GetFollowsByKindViaPPG(1984): %v", err)
 206  		}
 207  		got := make(map[string]bool, len(serials))
 208  		for _, s := range serials {
 209  			h, err := db.GetPubkeyHexFromSerial(s)
 210  			if err != nil {
 211  				continue
 212  			}
 213  			got[h] = true
 214  		}
 215  		if len(got) != 1 || !got[f.pkDHex] {
 216  			t.Errorf("GetFollowsByKindViaPPG(1984): want [D], got %v", got)
 217  		}
 218  	})
 219  
 220  	t.Run("GetFollowsByKindViaPPG unknown kind returns empty", func(t *testing.T) {
 221  		serials, err := db.GetFollowsByKindViaPPG(pkASerial, 9999)
 222  		if err != nil {
 223  			t.Fatalf("GetFollowsByKindViaPPG(9999): %v", err)
 224  		}
 225  		if len(serials) != 0 {
 226  			t.Errorf("GetFollowsByKindViaPPG(9999): want empty, got %d results", len(serials))
 227  		}
 228  	})
 229  }
 230  
 231  // TestGetFollowersByKindViaGPP verifies the reverse kind-filtered GPP scan.
 232  func TestGetFollowersByKindViaGPP(t *testing.T) {
 233  	f := setupKindFilterFixture(t)
 234  	db := f.db
 235  
 236  	pkBSerial, err := db.PubkeyHexToSerial(f.pkBHex)
 237  	if err != nil {
 238  		t.Fatalf("PubkeyHexToSerial(B): %v", err)
 239  	}
 240  	pkCSerial, err := db.PubkeyHexToSerial(f.pkCHex)
 241  	if err != nil {
 242  		t.Fatalf("PubkeyHexToSerial(C): %v", err)
 243  	}
 244  	pkDSerial, err := db.PubkeyHexToSerial(f.pkDHex)
 245  	if err != nil {
 246  		t.Fatalf("PubkeyHexToSerial(D): %v", err)
 247  	}
 248  
 249  	t.Run("GetFollowersViaGPP(B) returns all kinds including A and F", func(t *testing.T) {
 250  		serials, err := db.GetFollowersViaGPP(pkBSerial)
 251  		if err != nil {
 252  			t.Fatalf("GetFollowersViaGPP(B): %v", err)
 253  		}
 254  		got := make(map[string]bool, len(serials))
 255  		for _, s := range serials {
 256  			h, err := db.GetPubkeyHexFromSerial(s)
 257  			if err != nil {
 258  				continue
 259  			}
 260  			got[h] = true
 261  		}
 262  		if !got[f.pkAHex] {
 263  			t.Errorf("GetFollowersViaGPP(B): missing A")
 264  		}
 265  		if !got[f.pkFHex] {
 266  			t.Errorf("GetFollowersViaGPP(B): missing F")
 267  		}
 268  	})
 269  
 270  	t.Run("GetFollowersByKindViaGPP(B, 3) returns A and F", func(t *testing.T) {
 271  		serials, err := db.GetFollowersByKindViaGPP(pkBSerial, 3)
 272  		if err != nil {
 273  			t.Fatalf("GetFollowersByKindViaGPP(B,3): %v", err)
 274  		}
 275  		got := make(map[string]bool, len(serials))
 276  		for _, s := range serials {
 277  			h, err := db.GetPubkeyHexFromSerial(s)
 278  			if err != nil {
 279  				continue
 280  			}
 281  			got[h] = true
 282  		}
 283  		if len(got) != 2 {
 284  			t.Errorf("GetFollowersByKindViaGPP(B,3): want 2, got %d: %v", len(got), got)
 285  			return
 286  		}
 287  		if !got[f.pkAHex] {
 288  			t.Errorf("GetFollowersByKindViaGPP(B,3): missing A")
 289  		}
 290  		if !got[f.pkFHex] {
 291  			t.Errorf("GetFollowersByKindViaGPP(B,3): missing F")
 292  		}
 293  	})
 294  
 295  	t.Run("GetFollowersByKindViaGPP(B, 10000) returns empty — nobody mutes B", func(t *testing.T) {
 296  		serials, err := db.GetFollowersByKindViaGPP(pkBSerial, 10000)
 297  		if err != nil {
 298  			t.Fatalf("GetFollowersByKindViaGPP(B,10000): %v", err)
 299  		}
 300  		if len(serials) != 0 {
 301  			t.Errorf("GetFollowersByKindViaGPP(B,10000): want empty, got %d", len(serials))
 302  		}
 303  	})
 304  
 305  	t.Run("GetFollowersByKindViaGPP(C, 10000) returns A — A mutes C", func(t *testing.T) {
 306  		serials, err := db.GetFollowersByKindViaGPP(pkCSerial, 10000)
 307  		if err != nil {
 308  			t.Fatalf("GetFollowersByKindViaGPP(C,10000): %v", err)
 309  		}
 310  		got := make(map[string]bool, len(serials))
 311  		for _, s := range serials {
 312  			h, err := db.GetPubkeyHexFromSerial(s)
 313  			if err != nil {
 314  				continue
 315  			}
 316  			got[h] = true
 317  		}
 318  		if len(got) != 1 || !got[f.pkAHex] {
 319  			t.Errorf("GetFollowersByKindViaGPP(C,10000): want [A], got %v", got)
 320  		}
 321  	})
 322  
 323  	t.Run("GetFollowersByKindViaGPP(C, 3) returns empty — A only mutes C, does not follow", func(t *testing.T) {
 324  		serials, err := db.GetFollowersByKindViaGPP(pkCSerial, 3)
 325  		if err != nil {
 326  			t.Fatalf("GetFollowersByKindViaGPP(C,3): %v", err)
 327  		}
 328  		if len(serials) != 0 {
 329  			got := make([]string, 0, len(serials))
 330  			for _, s := range serials {
 331  				h, _ := db.GetPubkeyHexFromSerial(s)
 332  				got = append(got, h[:8])
 333  			}
 334  			t.Errorf("GetFollowersByKindViaGPP(C,3): want empty (A mutes C but does not kind-3 follow C), got %v", got)
 335  		}
 336  	})
 337  
 338  	t.Run("GetFollowersByKindViaGPP(D, 1984) returns A — A reports D", func(t *testing.T) {
 339  		serials, err := db.GetFollowersByKindViaGPP(pkDSerial, 1984)
 340  		if err != nil {
 341  			t.Fatalf("GetFollowersByKindViaGPP(D,1984): %v", err)
 342  		}
 343  		got := make(map[string]bool, len(serials))
 344  		for _, s := range serials {
 345  			h, err := db.GetPubkeyHexFromSerial(s)
 346  			if err != nil {
 347  				continue
 348  			}
 349  			got[h] = true
 350  		}
 351  		if len(got) != 1 || !got[f.pkAHex] {
 352  			t.Errorf("GetFollowersByKindViaGPP(D,1984): want [A], got %v", got)
 353  		}
 354  	})
 355  
 356  	t.Run("GetFollowersByKindViaGPP(D, 3) returns empty — A only reports D", func(t *testing.T) {
 357  		serials, err := db.GetFollowersByKindViaGPP(pkDSerial, 3)
 358  		if err != nil {
 359  			t.Fatalf("GetFollowersByKindViaGPP(D,3): %v", err)
 360  		}
 361  		if len(serials) != 0 {
 362  			t.Errorf("GetFollowersByKindViaGPP(D,3): want empty, got %d", len(serials))
 363  		}
 364  	})
 365  }
 366  
 367  // TestKindFilterIsolation is the key regression test for the grapevine zero-influence bug.
 368  //
 369  // The bug: GetFollowersViaGPP (all kinds) fed getFollowers() in the engine.
 370  // When pkA both follows AND mutes pkX (different targets here, but same mechanism),
 371  // a node that is only muted by pkA should NOT appear as a kind-3 follower.
 372  // This test proves the kind-3 filtered path is correctly isolated from mutes/reports.
 373  func TestKindFilterIsolation(t *testing.T) {
 374  	f := setupKindFilterFixture(t)
 375  	db := f.db
 376  
 377  	pkBSerial, _ := db.PubkeyHexToSerial(f.pkBHex)
 378  	pkCSerial, _ := db.PubkeyHexToSerial(f.pkCHex)
 379  	pkDSerial, _ := db.PubkeyHexToSerial(f.pkDHex)
 380  
 381  	// Followers of B via kind-3 should be {A, F} — not empty
 382  	followersOfB, err := db.GetFollowersByKindViaGPP(pkBSerial, 3)
 383  	if err != nil {
 384  		t.Fatalf("GetFollowersByKindViaGPP(B,3): %v", err)
 385  	}
 386  	if len(followersOfB) != 2 {
 387  		t.Errorf("B should have 2 kind-3 followers (A and F), got %d", len(followersOfB))
 388  	}
 389  
 390  	// C is only muted, not followed — kind-3 followers of C must be empty
 391  	kind3FollowersOfC, err := db.GetFollowersByKindViaGPP(pkCSerial, 3)
 392  	if err != nil {
 393  		t.Fatalf("GetFollowersByKindViaGPP(C,3): %v", err)
 394  	}
 395  	if len(kind3FollowersOfC) != 0 {
 396  		t.Errorf("C has no kind-3 followers (A only mutes C), but got %d results — this is the double-count bug", len(kind3FollowersOfC))
 397  	}
 398  
 399  	// Muters of C must be {A}
 400  	mutersOfC, err := db.GetFollowersByKindViaGPP(pkCSerial, 10000)
 401  	if err != nil {
 402  		t.Fatalf("GetFollowersByKindViaGPP(C,10000): %v", err)
 403  	}
 404  	if len(mutersOfC) != 1 {
 405  		t.Errorf("C should have 1 muter (A), got %d", len(mutersOfC))
 406  	}
 407  
 408  	// D is only reported, not followed
 409  	kind3FollowersOfD, err := db.GetFollowersByKindViaGPP(pkDSerial, 3)
 410  	if err != nil {
 411  		t.Fatalf("GetFollowersByKindViaGPP(D,3): %v", err)
 412  	}
 413  	if len(kind3FollowersOfD) != 0 {
 414  		t.Errorf("D has no kind-3 followers (A only reports D), but got %d results", len(kind3FollowersOfD))
 415  	}
 416  
 417  	// kind-3 follows of A via PPG: only B and E — not C (muted) or D (reported)
 418  	pkASerial, _ := db.PubkeyHexToSerial(f.pkAHex)
 419  	kind3FollowsOfA, err := db.GetFollowsByKindViaPPG(pkASerial, 3)
 420  	if err != nil {
 421  		t.Fatalf("GetFollowsByKindViaPPG(A,3): %v", err)
 422  	}
 423  	if len(kind3FollowsOfA) != 2 {
 424  		got := make([]string, 0, len(kind3FollowsOfA))
 425  		for _, s := range kind3FollowsOfA {
 426  			h, _ := db.GetPubkeyHexFromSerial(s)
 427  			got = append(got, h[:8])
 428  		}
 429  		sort.Strings(got)
 430  		t.Errorf("A has 2 kind-3 follows (B,E), got %d: %v — mutes/reports must not bleed into kind-3", len(kind3FollowsOfA), got)
 431  	}
 432  }
 433