package dedupe import ( "context" "strings" "testing" "time" "gitea.maximumdirect.net/ejr/feedkit/event" "gitea.maximumdirect.net/ejr/feedkit/processors" ) func TestNewProcessorValidation(t *testing.T) { t.Run("rejects non-positive maxEntries", func(t *testing.T) { for _, maxEntries := range []int{0, -1} { p, err := NewProcessor(maxEntries) if err == nil { t.Fatalf("expected error for maxEntries=%d, got nil", maxEntries) } if p != nil { t.Fatalf("expected nil processor for maxEntries=%d", maxEntries) } if !strings.Contains(err.Error(), "maxEntries") { t.Fatalf("unexpected error: %v", err) } } }) t.Run("accepts positive maxEntries", func(t *testing.T) { p, err := NewProcessor(1) if err != nil { t.Fatalf("NewProcessor error: %v", err) } if p == nil { t.Fatalf("expected processor, got nil") } }) } func TestProcessorFirstSeenAndDuplicate(t *testing.T) { p, err := NewProcessor(8) if err != nil { t.Fatalf("NewProcessor error: %v", err) } ctx := context.Background() first := testEvent("evt-1") out, err := p.Process(ctx, first) if err != nil { t.Fatalf("Process first error: %v", err) } if out == nil { t.Fatalf("expected first event to pass through") } if out.ID != first.ID { t.Fatalf("expected unchanged ID %q, got %q", first.ID, out.ID) } out, err = p.Process(ctx, first) if err != nil { t.Fatalf("Process duplicate error: %v", err) } if out != nil { t.Fatalf("expected duplicate to be dropped, got %#v", out) } out, err = p.Process(ctx, testEvent("evt-2")) if err != nil { t.Fatalf("Process second unique error: %v", err) } if out == nil { t.Fatalf("expected second unique event to pass through") } } func TestProcessorLRUEvictionAndPromotion(t *testing.T) { p, err := NewProcessor(2) if err != nil { t.Fatalf("NewProcessor error: %v", err) } ctx := context.Background() mustPass(t, p, ctx, "a") mustPass(t, p, ctx, "b") mustDrop(t, p, ctx, "a") // promote "a" so "b" becomes least-recently-used mustPass(t, p, ctx, "c") // evicts "b" mustDrop(t, p, ctx, "a") // "a" should still be tracked after promotion mustPass(t, p, ctx, "b") // "b" was evicted, so now it passes again } func TestProcessorRejectsBlankID(t *testing.T) { p, err := NewProcessor(4) if err != nil { t.Fatalf("NewProcessor error: %v", err) } in := testEvent(" ") out, err := p.Process(context.Background(), in) if err == nil { t.Fatalf("expected error for blank ID") } if out != nil { t.Fatalf("expected nil output on error, got %#v", out) } if !strings.Contains(err.Error(), "event ID is required") { t.Fatalf("unexpected error: %v", err) } } func TestFactoryWithRegistry(t *testing.T) { r := processors.NewRegistry() r.Register("dedupe", Factory(3)) p, err := r.Build("dedupe") if err != nil { t.Fatalf("Build error: %v", err) } if p == nil { t.Fatalf("expected processor, got nil") } out, err := p.Process(context.Background(), testEvent("evt-factory-1")) if err != nil { t.Fatalf("Process error: %v", err) } if out == nil { t.Fatalf("expected first event to pass through") } } func mustPass(t *testing.T, p *Processor, ctx context.Context, id string) { t.Helper() out, err := p.Process(ctx, testEvent(id)) if err != nil { t.Fatalf("expected pass for id=%q, got error: %v", id, err) } if out == nil { t.Fatalf("expected pass for id=%q, got drop", id) } } func mustDrop(t *testing.T, p *Processor, ctx context.Context, id string) { t.Helper() out, err := p.Process(ctx, testEvent(id)) if err != nil { t.Fatalf("expected drop for id=%q, got error: %v", id, err) } if out != nil { t.Fatalf("expected drop for id=%q, got output", id) } } func testEvent(id string) event.Event { return event.Event{ ID: id, Kind: event.Kind("observation"), Source: "source-1", EmittedAt: time.Now().UTC(), Payload: map[string]any{"ok": true}, } }