fix(growth): harden deterministic report evidence

This commit is contained in:
2026-08-21 00:32:21 +03:30
parent 1d8d4edf92
commit 6c80f71847
11 changed files with 369 additions and 103 deletions

View File

@@ -15,7 +15,7 @@ func TestCatalog1405IsValidAndDefensivelyCopied(t *testing.T) {
}
func TestValidateCatalogRejectsDuplicateAndIncompleteEvents(t *testing.T) {
event := catalogEvent("duplicate", "رویداد", "2026-01-01", "2026-01-01", "family", EventOfficial, []string{"tag"})
event := officialEvent("duplicate", "رویداد", "2026-01-01", "2026-01-01", "family", false, []string{"tag"})
if err := ValidateCatalog([]Event{event, event}); err == nil {
t.Fatal("expected duplicate id error")
}
@@ -25,6 +25,22 @@ func TestValidateCatalogRejectsDuplicateAndIncompleteEvents(t *testing.T) {
}
}
func TestCatalogCoversImportedHistoryAndForecastHorizon(t *testing.T) {
catalog := Catalog1405()
if catalog[0].StartDate.After(date("2026-01-03")) || catalog[len(catalog)-1].EndDate.Before(date("2026-09-28")) {
t.Fatalf("catalog coverage is incomplete: %s..%s", catalog[0].StartDate, catalog[len(catalog)-1].EndDate)
}
var scoped bool
for _, event := range catalog {
if event.ID == "back-to-school-1405" {
scoped = !event.Universal && len(event.Categories) == 2
}
}
if !scoped {
t.Fatal("back-to-school event must preserve explicit category scope")
}
}
func TestAnalyzeEventEffectExcludesCatalogEventsFromBaseline(t *testing.T) {
target := Event{ID: "target", StartDate: date("2026-05-02"), EndDate: date("2026-05-02")}
excluded := Event{ID: "other", StartDate: target.StartDate.AddDate(0, 0, -7), EndDate: target.StartDate.AddDate(0, 0, -7)}
@@ -43,10 +59,31 @@ func TestAnalyzeEventEffectExcludesCatalogEventsFromBaseline(t *testing.T) {
}
}
func TestAnalyzeEventEffectUsesCompleteComparableWindows(t *testing.T) {
target := Event{ID: "two-day", StartDate: date("2026-05-02"), EndDate: date("2026-05-03")}
history := []DailyPoint{
{Date: date("2026-04-25"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-04-26"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-04-18"), SessionCount: 1_000, PaidSessionCount: 1_000, PaidAmountRial: 100_000}, // fragmented week, next day absent
{Date: date("2026-04-11"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-04-12"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-04-04"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-04-05"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-03-28"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: date("2026-03-29"), SessionCount: 60, PaidSessionCount: 50, PaidAmountRial: 5_000},
{Date: target.StartDate, SessionCount: 80, PaidSessionCount: 75, PaidAmountRial: 7_500},
{Date: target.EndDate, SessionCount: 80, PaidSessionCount: 75, PaidAmountRial: 7_500},
}
effect := AnalyzeEventEffect(history, target, nil)
if effect.ObservationCount != 4 || effect.BaselineSessionCount != 120 || effect.ActualSessionCount != 160 || effect.PaidSessionUpliftBPS != 5_000 {
t.Fatalf("unexpected complete-window effect: %+v", effect)
}
}
func TestForecastSuppressesPoorBacktest(t *testing.T) {
start := date("2026-07-01")
history := make([]DailyPoint, 0, 90)
for i := 90; i > 0; i-- {
history := make([]DailyPoint, 0, 140)
for i := 140; i > 0; i-- {
value := int64(100)
if i%2 == 0 {
value = 1_000
@@ -54,7 +91,21 @@ func TestForecastSuppressesPoorBacktest(t *testing.T) {
history = append(history, DailyPoint{Date: start.AddDate(0, 0, -i), SessionCount: value, PaidSessionCount: value, PaidAmountRial: value * 1_000})
}
forecast := BuildForecast(history, nil, start, 90)
if forecast.Confidence != ConfidenceInsufficientData || len(forecast.Points) != 0 {
if forecast.Backtest.Origins < 4 || forecast.Confidence != ConfidenceInsufficientData || len(forecast.Points) != 0 {
t.Fatalf("poor model should be suppressed: %+v", forecast)
}
}
func TestBacktestUsesProductionTrendPredictorAcrossFourteenDayHorizons(t *testing.T) {
start := date("2026-07-01")
history := make([]DailyPoint, 0, 140)
for index := 0; index < 140; index++ {
block := index / 28
value := int64(100 + block*20)
history = append(history, DailyPoint{Date: start.AddDate(0, 0, index-140), SessionCount: value, PaidSessionCount: value, PaidAmountRial: value * 1_000})
}
forecast := BuildForecast(history, nil, start, 90)
if forecast.Backtest.Origins < 4 || forecast.Confidence == ConfidenceInsufficientData {
t.Fatalf("trend-aware rolling backtest should be usable: %+v", forecast.Backtest)
}
}