thread-master/apps/backend/internal/module/radar/domain/opportunity_test.go

113 lines
2.8 KiB
Go
Raw Normal View History

2026-08-03 05:52:02 +00:00
package domain
import (
"errors"
"testing"
2026-08-13 07:54:25 +00:00
"time"
2026-08-03 05:52:02 +00:00
)
func TestBandFromScore_LockedThresholds(t *testing.T) {
cases := []struct {
score int
want string
}{
{80, BandHigh},
{100, BandHigh},
{79, BandMid},
{50, BandMid},
{49, BandLow},
{0, BandLow},
{-1, BandLow},
}
for _, tc := range cases {
if got := BandFromScore(tc.score); got != tc.want {
t.Fatalf("BandFromScore(%d)=%q want %q", tc.score, got, tc.want)
}
}
}
func TestUnknownPublishedTimeIsNotStale(t *testing.T) {
if IsStaleHardReject(0, NowNano()) {
t.Fatal("an unknown source timestamp must not be rejected as stale")
}
if FreshnessScore(FreshnessHoursSince(0, NowNano())) != 0 {
t.Fatal("an unknown source timestamp must receive no freshness credit")
}
}
2026-08-13 07:54:25 +00:00
func TestTwentyDayOldPostIsNotHardRejected(t *testing.T) {
now := NowNano()
posted := now - 20*24*int64(time.Hour)
if IsStaleHardReject(posted, now) {
t.Fatal("a 20-day-old demand post must still be judgeable, not hard-rejected")
}
old := now - 40*24*int64(time.Hour)
if !IsStaleHardReject(old, now) {
t.Fatal("a 40-day-old post should still be hard-rejected")
}
}
2026-08-03 05:52:02 +00:00
func TestValidateReasons_RequiresAllFive(t *testing.T) {
full := fiveReasons()
if err := ValidateReasons(full); err != nil {
t.Fatalf("full reasons should pass: %v", err)
}
// Drop fit — OP-06.
missing := full[:4]
err := ValidateReasons(missing)
if err == nil {
t.Fatal("expected error when one dimension is missing")
}
if !errors.Is(err, ErrValidation) {
t.Fatalf("want ErrValidation, got %v", err)
}
// Blank reason text.
blank := fiveReasons()
blank[0].Reason = " "
if err := ValidateReasons(blank); err == nil {
t.Fatal("expected error for blank reason text")
}
if err := ValidateReasons(nil); err == nil {
t.Fatal("expected error for nil reasons")
}
}
func TestCanTransitionOpportunity(t *testing.T) {
allow := [][2]string{
{OppJudging, OppQualified},
{OppJudging, OppRejected},
{OppQualified, OppAccepted},
{OppQualified, OppDismissed},
{OppRejected, OppQualified},
}
for _, p := range allow {
if !CanTransitionOpportunity(p[0], p[1]) {
t.Fatalf("expected allow %s → %s", p[0], p[1])
}
}
deny := [][2]string{
{OppAccepted, OppQualified},
{OppDismissed, OppQualified},
{OppQualified, OppJudging},
{OppRejected, OppAccepted},
}
for _, p := range deny {
if CanTransitionOpportunity(p[0], p[1]) {
t.Fatalf("expected deny %s → %s", p[0], p[1])
}
}
}
func fiveReasons() []OpportunityReason {
return []OpportunityReason{
{Dimension: DimAuthenticity, Score: 25, Reason: "真的在求推薦"},
{Dimension: DimIntent, Score: 28, Reason: "有明確購買意圖"},
{Dimension: DimRegion, Score: 15, Reason: "地區相符"},
{Dimension: DimFreshness, Score: 12, Reason: "24 小時內"},
{Dimension: DimFit, Score: 8, Reason: "對上婚攝服務"},
}
}