Files

183 lines
6.9 KiB
Go

package insight
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
)
type schedulingRepository struct {
datasetCalls int
catalogCalls int
created []CreateRequest
}
type periodLookupRepository struct {
schedulingRepository
module Module
periodID string
}
func (r *periodLookupRepository) LatestForPeriod(_ context.Context, _ int64, _ string, module Module, periodID string) (Snapshot, error) {
r.module = module
r.periodID = periodID
return Snapshot{Module: module}, nil
}
type processingRepository struct {
schedulingRepository
completion Completion
}
func (r *processingRepository) Claim(context.Context) (claimedJob, error) {
return claimedJob{Job: Job{JobID: "job", SnapshotID: "snapshot", Section: SectionPast}, MerchantID: 1, TerminalID: "terminal-1", Module: ModulePayments, LeaseToken: "lease", BaseCopy: Copy{Past: Narrative{Body: "old past"}, LatestObserved: Narrative{Body: "keep latest"}}}, nil
}
func (r *processingRepository) Complete(_ context.Context, _ claimedJob, completion Completion) error {
r.completion = completion
return nil
}
type fixedBuilder struct{ analysis Analysis }
func (b fixedBuilder) Build(context.Context, int64, string, Module) (Analysis, error) {
return b.analysis, nil
}
type fixedGenerator struct {
copy Copy
err error
}
func (g fixedGenerator) Generate(context.Context, Module, Section, json.RawMessage, Copy) (Copy, ProviderUsage, error) {
return g.copy, ProviderUsage{ProviderRequestID: "aval-1", ProviderRequests: 1}, g.err
}
func (fixedGenerator) Model() string { return "aval-model" }
func TestService_ProcessNextPublishesValidatedProviderCopyAsReady(t *testing.T) {
repository := &processingRepository{}
fallback := Copy{Past: Narrative{Headline: "past", Body: "fallback", EvidenceID: []string{"evidence"}}}
generated := fallback
generated.Past.Body = "توضیح معتبر"
service := NewService(repository, fixedBuilder{analysis: Analysis{Evidence: json.RawMessage(`{"safe":true}`), Copy: fallback}}, fixedGenerator{copy: generated})
if err := service.ProcessNext(t.Context()); err != nil {
t.Fatal(err)
}
if repository.completion.Status != "READY" || repository.completion.AICopy == nil || repository.completion.Usage.ProviderRequestID != "aval-1" {
t.Fatalf("completion=%+v", repository.completion)
}
if repository.completion.AICopy.LatestObserved.Body != "keep latest" {
t.Fatalf("targeted refresh replaced an unrelated section: %+v", repository.completion.AICopy)
}
}
func TestService_ProcessNextKeepsFallbackOutOfAIResponseOnProviderFailure(t *testing.T) {
repository := &processingRepository{}
fallback := Copy{Past: Narrative{Body: "deterministic"}}
service := NewService(repository, fixedBuilder{analysis: Analysis{Copy: fallback}}, fixedGenerator{err: errors.New("provider down")})
if err := service.ProcessNext(t.Context()); err != nil {
t.Fatal(err)
}
if repository.completion.Status != "PARTIAL" || repository.completion.AICopy != nil {
t.Fatalf("completion=%+v", repository.completion)
}
}
func (r *schedulingRepository) DatasetMetadata(context.Context) (string, time.Time, error) {
r.datasetCalls++
return "watermark", time.Date(2026, 6, 30, 0, 0, 0, 0, time.UTC), nil
}
func (r *schedulingRepository) ActiveCatalogVersion(context.Context) (string, error) {
r.catalogCalls++
return "catalog-v1", nil
}
func (r *schedulingRepository) MerchantScopes(context.Context) ([]MerchantScope, error) {
return []MerchantScope{{MerchantID: 1}, {MerchantID: 1, TerminalID: "terminal-1"}}, nil
}
func (r *schedulingRepository) Create(_ context.Context, request CreateRequest) (Job, bool, error) {
r.created = append(r.created, request)
return Job{}, false, nil
}
func (*schedulingRepository) Latest(context.Context, int64, string, Module) (Snapshot, error) {
panic("unexpected Latest call")
}
func (*schedulingRepository) GetJob(context.Context, int64, string) (Job, error) {
panic("unexpected GetJob call")
}
func (*schedulingRepository) EventsAfter(context.Context, int64, string, int64) ([]Event, error) {
panic("unexpected EventsAfter call")
}
func (*schedulingRepository) Claim(context.Context) (claimedJob, error) {
panic("unexpected Claim call")
}
func (*schedulingRepository) Complete(context.Context, claimedJob, Completion) error {
panic("unexpected Complete call")
}
func TestService_EnqueueAllLoadsBatchMetadataOnce(t *testing.T) {
repository := &schedulingRepository{}
service := NewService(repository, nil)
service.now = func() time.Time { return time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC) }
created, err := service.EnqueueAll(t.Context(), "NIGHTLY")
if err != nil {
t.Fatal(err)
}
if created != 12 {
t.Fatalf("created %d jobs, want 12", created)
}
if repository.datasetCalls != 1 {
t.Fatalf("dataset metadata loaded %d times, want once", repository.datasetCalls)
}
if repository.catalogCalls != 1 {
t.Fatalf("catalog metadata loaded %d times, want once", repository.catalogCalls)
}
if len(repository.created) != 12 {
t.Fatalf("repository received %d create requests, want 12", len(repository.created))
}
for _, request := range repository.created {
if request.CatalogVersion != "catalog-v1" || request.Fingerprint == "" || !json.Valid(request.Evidence) {
t.Fatalf("invalid batched request: %#v", request)
}
}
}
func TestService_RefreshForPeriodScopesFingerprintAndRequest(t *testing.T) {
repository := &schedulingRepository{}
service := NewService(repository, fixedBuilder{})
service.now = func() time.Time { return time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC) }
first := PeriodScope{PeriodID: "jm-1405-05", PeriodType: "JALALI_MONTH", StartAt: time.Date(2026, 7, 23, 0, 0, 0, 0, time.UTC), EndAt: time.Date(2026, 8, 23, 0, 0, 0, 0, time.UTC), JalaliLabelFA: "مرداد ۱۴۰۵"}
second := first
second.PeriodID = "jm-1405-04"
if _, _, err := service.RefreshForPeriod(t.Context(), 1, "", ModuleTransactions, first, "key-1"); err != nil {
t.Fatal(err)
}
if _, _, err := service.RefreshForPeriod(t.Context(), 1, "", ModuleTransactions, second, "key-2"); err != nil {
t.Fatal(err)
}
if len(repository.created) != 2 {
t.Fatalf("created %d requests, want 2", len(repository.created))
}
if repository.created[0].Period.PeriodID != first.PeriodID {
t.Fatalf("period = %#v, want %#v", repository.created[0].Period, first)
}
if repository.created[0].Fingerprint == repository.created[1].Fingerprint {
t.Fatal("different periods reused the same fingerprint")
}
}
func TestService_LatestForPeriodScopesEveryAnalyticsModule(t *testing.T) {
repository := &periodLookupRepository{}
service := NewService(repository, fixedBuilder{})
period := PeriodScope{PeriodID: "jm-1405-05"}
if _, err := service.LatestForPeriod(t.Context(), 1, "", ModuleOverview, period); err != nil {
t.Fatal(err)
}
if repository.module != ModuleOverview || repository.periodID != period.PeriodID {
t.Fatalf("period lookup = %s %s, want %s %s", repository.module, repository.periodID, ModuleOverview, period.PeriodID)
}
}