Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions pkg/api/matchnotifier_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package api

import (
"testing"

"github.com/hivecommons/dibs/pkg/notify"
"github.com/hivecommons/dibs/pkg/registry"
"github.com/hivecommons/dibs/pkg/store"
)

func TestMatchNotifierNewMatch(t *testing.T) {
repo := &registry.RepoProfile{RepoID: "org/repo", Owner: "olivia"}

t.Run("nil store is a no-op", func(t *testing.T) {
n := &MatchNotifier{}
n.NewMatch("alice", "olivia", &store.Idea{ID: "i1", Title: "T", Visibility: store.VisibilityPublic}, repo, 0.9)
})

newNotifier := func(t *testing.T) (*MatchNotifier, *notify.Store) {
t.Helper()
ns, err := notify.New(t.TempDir())
if err != nil {
t.Fatalf("notify.New: %v", err)
}
return &MatchNotifier{Notify: ns}, ns
}

t.Run("public idea notifies author and repo owner", func(t *testing.T) {
n, ns := newNotifier(t)
idea := &store.Idea{ID: "i1", Title: "Great idea", Visibility: store.VisibilityPublic}
n.NewMatch("alice", "olivia", idea, repo, 0.9)

author := ns.ListByUser("alice", false)
if len(author) != 1 || author[0].Kind != notify.KindMatch || author[0].IdeaID != "i1" || author[0].RepoID != "org/repo" {
t.Fatalf("author notifications = %+v", author)
}
owner := ns.ListByUser("olivia", false)
if len(owner) != 1 || owner[0].Kind != notify.KindMatch {
t.Fatalf("owner notifications = %+v", owner)
}
})

t.Run("private idea never notifies repo owner", func(t *testing.T) {
n, ns := newNotifier(t)
idea := &store.Idea{ID: "i2", Title: "Secret idea", Visibility: store.VisibilityPrivate}
n.NewMatch("alice", "olivia", idea, repo, 0.9)

if got := ns.ListByUser("alice", false); len(got) != 1 {
t.Fatalf("author notifications = %+v, want 1", got)
}
if got := ns.ListByUser("olivia", false); len(got) != 0 {
t.Fatalf("private idea leaked to repo owner: %+v", got)
}
})

t.Run("self-match notifies only once", func(t *testing.T) {
n, ns := newNotifier(t)
idea := &store.Idea{ID: "i3", Title: "Own repo idea", Visibility: store.VisibilityPublic}
n.NewMatch("olivia", "olivia", idea, repo, 0.9)

if got := ns.ListByUser("olivia", false); len(got) != 1 {
t.Fatalf("self-match notifications = %+v, want exactly 1", got)
}
})
}
19 changes: 19 additions & 0 deletions pkg/indexformula/formula_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,22 @@ func TestContribution(t *testing.T) {
})
}
}

func TestActivity(t *testing.T) {
tests := []struct {
name string
in Counts
want int
}{
{name: "quiet"},
{name: "single source", in: Counts{IdeasFiled: 3}, want: 3},
{name: "unweighted sum", in: Counts{RegularIssuesCreated: 1, RegularPRsMerged: 2, ClankerPRsCreated: 3, IdeasFiled: 4, ClankerPRsMerged: 5}, want: 15},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Activity(tt.in); got != tt.want {
t.Fatalf("Activity(%+v) = %d, want %d", tt.in, got, tt.want)
}
})
}
}
116 changes: 116 additions & 0 deletions pkg/news/generator_async_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package news

import (
"context"
"sync"
"testing"
"time"

"github.com/hivecommons/dibs/pkg/history"
"github.com/hivecommons/dibs/pkg/registry"
)

// blockingFetcher parks FetchMergedPullRequests until release is closed so
// tests can observe the in-flight dedup window.
type blockingFetcher struct {
mu sync.Mutex
calls int
started chan struct{}
release chan struct{}
prs []history.MergedPullRequest
}

func (f *blockingFetcher) FetchMergedPullRequests(ctx context.Context, repoID string) ([]history.MergedPullRequest, error) {
f.mu.Lock()
f.calls++
if f.calls == 1 {
close(f.started)
}
f.mu.Unlock()
<-f.release
return append([]history.MergedPullRequest(nil), f.prs...), nil
}

func (f *blockingFetcher) callCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return f.calls
}

func TestNewGeneratorWiring(t *testing.T) {
st, err := NewStore(t.TempDir())
if err != nil {
t.Fatalf("NewStore: %v", err)
}
f := &fakeFetcher{}
g := NewGenerator(st, f, nil)
if g.Store != st || g.Fetcher == nil || g.Logf == nil {
t.Fatalf("NewGenerator wiring incomplete: %+v", g)
}
}

func TestRefreshAsyncGuardsAndDedup(t *testing.T) {
// Nil / unwired generators must be safe no-ops.
var nilGen *Generator
nilGen.RefreshAsync([]registry.RepoProfile{{RepoID: "org/repo"}})
(&Generator{}).RefreshAsync([]registry.RepoProfile{{RepoID: "org/repo"}})

st, err := NewStore(t.TempDir())
if err != nil {
t.Fatalf("NewStore: %v", err)
}
f := &blockingFetcher{
started: make(chan struct{}),
release: make(chan struct{}),
prs: []history.MergedPullRequest{pr("merged work", "2026-08-19T09:00:00Z", "alice")},
}
g := NewGenerator(st, f, nil)
g.Now = newsNow
g.Logf = nil

repos := []registry.RepoProfile{{RepoID: "org/repo"}}
g.RefreshAsync(repos)
select {
case <-f.started:
case <-time.After(5 * time.Second):
t.Fatal("refresh goroutine never started")
}

// While org/repo is active, further RefreshAsync calls must be dropped.
g.RefreshAsync(repos)
if got := f.callCount(); got != 1 {
t.Fatalf("fetch calls during active refresh = %d, want 1", got)
}

close(f.release)
deadline := time.Now().Add(5 * time.Second)
for len(st.Get("org/repo")) == 0 {
if time.Now().After(deadline) {
t.Fatal("refresh never persisted news items")
}
time.Sleep(10 * time.Millisecond)
}
}

func TestTruncateOneLine(t *testing.T) {
tests := []struct {
name string
in string
max int
want string
}{
{name: "short passes through", in: "hello world", max: 20, want: "hello world"},
{name: "whitespace collapses", in: " a\n\tb c ", max: 20, want: "a b c"},
{name: "truncates with ellipsis", in: "abcdefgh", max: 5, want: "abcd…"},
{name: "trims before ellipsis", in: "abc defgh", max: 5, want: "abc…"},
{name: "max one keeps one rune", in: "abc", max: 1, want: "a"},
{name: "multibyte runes counted", in: "héllo wörld", max: 6, want: "héllo…"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := truncateOneLine(tt.in, tt.max); got != tt.want {
t.Fatalf("truncateOneLine(%q, %d) = %q, want %q", tt.in, tt.max, got, tt.want)
}
})
}
}
61 changes: 61 additions & 0 deletions pkg/registry/passed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package registry

import (
"errors"
"testing"
)

func TestRepoProfileHasPassed(t *testing.T) {
rp := &RepoProfile{PassedIdeas: []string{"idea-1", "idea-2"}}
if !rp.HasPassed("idea-2") {
t.Fatal("HasPassed(idea-2) = false, want true")
}
if rp.HasPassed("idea-3") {
t.Fatal("HasPassed(idea-3) = true, want false")
}
if (&RepoProfile{}).HasPassed("idea-1") {
t.Fatal("empty profile HasPassed = true, want false")
}
}

func TestAddPassedIdea(t *testing.T) {
r, dir := newTestRegistry(t)
if err := r.Merge([]RepoProfile{{RepoID: "org/repo", HiveID: "h", Owner: "alice"}}); err != nil {
t.Fatalf("Merge: %v", err)
}

if err := r.AddPassedIdea("org/missing", "alice", "idea-1"); !errors.Is(err, ErrNotFound) {
t.Fatalf("unknown repo err = %v, want ErrNotFound", err)
}
if err := r.AddPassedIdea("org/repo", "mallory", "idea-1"); !errors.Is(err, ErrForbidden) {
t.Fatalf("non-owner err = %v, want ErrForbidden", err)
}

if err := r.AddPassedIdea("org/repo", "alice", "idea-1"); err != nil {
t.Fatalf("AddPassedIdea: %v", err)
}
// Idempotent: a second swipe must not duplicate the entry.
if err := r.AddPassedIdea("org/repo", "alice", "idea-1"); err != nil {
t.Fatalf("repeat AddPassedIdea: %v", err)
}
rp, err := r.Get("org/repo")
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(rp.PassedIdeas) != 1 || rp.PassedIdeas[0] != "idea-1" {
t.Fatalf("PassedIdeas = %v, want [idea-1]", rp.PassedIdeas)
}

// The pass must survive a reopen (persisted to disk).
r2, err := New(dir)
if err != nil {
t.Fatalf("reopen: %v", err)
}
rp2, err := r2.Get("org/repo")
if err != nil {
t.Fatalf("Get after reopen: %v", err)
}
if !rp2.HasPassed("idea-1") {
t.Fatal("passed idea lost after reopen")
}
}
26 changes: 26 additions & 0 deletions pkg/registry/symbol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
)

Expand Down Expand Up @@ -86,6 +87,31 @@ func TestUniqueRepoTickerSymbolCollisionSequence(t *testing.T) {
}
}

func TestUniqueRepoTickerSymbolTwoLetterTail(t *testing.T) {
// Every base[:3]+letter candidate taken: fall back to base[:2] + two-letter tail.
taken := func(s string) bool { return strings.HasPrefix(s, "HIV") }
if got := UniqueRepoTickerSymbol("org/hive", taken); got != "HIAA" {
t.Fatalf("two-letter tail fallback = %q, want HIAA", got)
}
}

func TestUniqueRepoTickerSymbolFullScan(t *testing.T) {
// Every HI?? candidate also taken: fall back to the full 4-letter scan.
taken := func(s string) bool { return strings.HasPrefix(s, "HI") }
if got := UniqueRepoTickerSymbol("org/hive", taken); got != "AAAA" {
t.Fatalf("full-scan fallback = %q, want AAAA", got)
}
}

func TestUniqueRepoTickerSymbolExhaustionPanics(t *testing.T) {
defer func() {
if recover() == nil {
t.Fatal("expected panic when every 4-letter symbol is taken")
}
}()
UniqueRepoTickerSymbol("org/hive", func(string) bool { return true })
}

func TestRepoSymbolsPersistAndSurviveResync(t *testing.T) {
r, dir := newTestRegistry(t)
hub := &FakeHub{Repos: []RepoProfile{
Expand Down
Loading
Loading