fix: deliver validated AI insight responses
This commit is contained in:
@@ -3,6 +3,7 @@ package insight
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -13,6 +14,64 @@ type schedulingRepository struct {
|
||||
created []CreateRequest
|
||||
}
|
||||
|
||||
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
|
||||
@@ -40,7 +99,7 @@ func (*schedulingRepository) EventsAfter(context.Context, int64, string, int64)
|
||||
func (*schedulingRepository) Claim(context.Context) (claimedJob, error) {
|
||||
panic("unexpected Claim call")
|
||||
}
|
||||
func (*schedulingRepository) Complete(context.Context, claimedJob, Analysis, string, string) error {
|
||||
func (*schedulingRepository) Complete(context.Context, claimedJob, Completion) error {
|
||||
panic("unexpected Complete call")
|
||||
}
|
||||
|
||||
@@ -71,3 +130,28 @@ func TestService_EnqueueAllLoadsBatchMetadataOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user