package usecase import ( "context" "testing" "gitea.d-ma.be/mathias/tapir/internal/domain" ) // These tests pin the ADR-021 read-stored-first behaviour at the engine core: // a stored transcript is summarized without re-touching the source, a miss // fetches once and persists, and a transient rate-limit is never cached. type recordingSource struct { transcript domain.Transcript fetchCalls int } func (s *recordingSource) ListSubscriptions(context.Context, string) ([]domain.Subscription, error) { return nil, nil } func (s *recordingSource) NewVideos(context.Context, domain.Subscription) ([]domain.Video, error) { return nil, nil } func (s *recordingSource) FetchTranscript(context.Context, domain.Video) (domain.Transcript, error) { s.fetchCalls++ return s.transcript, nil } type fakeTranscriptStore struct { stored map[string]domain.Transcript saves int } func newFakeTranscriptStore() *fakeTranscriptStore { return &fakeTranscriptStore{stored: make(map[string]domain.Transcript)} } func (f *fakeTranscriptStore) key(provider, id string) string { return provider + "|" + id } func (f *fakeTranscriptStore) GetTranscript(_ context.Context, provider, id string) (domain.Transcript, bool, error) { t, ok := f.stored[f.key(provider, id)] return t, ok, nil } func (f *fakeTranscriptStore) SaveTranscript(_ context.Context, provider, id string, t domain.Transcript) error { f.saves++ f.stored[f.key(provider, id)] = t return nil } type countingSummarizer struct{ calls int } func (c *countingSummarizer) Summarize(_ context.Context, v domain.Video, _ domain.Transcript) (domain.Summary, error) { c.calls++ return domain.Summary{VideoID: v.ID, UserID: v.UserID, Summary: "s", AIProvider: "local"}, nil } type nopSink struct{} func (nopSink) Name() string { return "nop" } func (nopSink) Deliver(context.Context, domain.Summary) error { return nil } func testVideo() domain.Video { return domain.Video{ID: "v1", UserID: "u1", Provider: domain.ProviderYouTube, ProviderVideoID: "yt1"} } func TestProcessNewVideo_StoredTranscriptSkipsFetch(t *testing.T) { src := &recordingSource{} ts := newFakeTranscriptStore() ts.stored[ts.key("youtube", "yt1")] = domain.Transcript{Source: domain.SourceCaptions, Content: "stored words"} sum := &countingSummarizer{} eng := NewEngine(src, sum, nopSink{}) eng.Transcripts = ts res, err := eng.ProcessNewVideo(context.Background(), testVideo()) if err != nil { t.Fatalf("ProcessNewVideo: %v", err) } if src.fetchCalls != 0 { t.Fatalf("stored transcript must not re-fetch from source; got %d fetches", src.fetchCalls) } if ts.saves != 0 { t.Fatalf("a store hit must not re-save; got %d saves", ts.saves) } if sum.calls != 1 || res.Summary == nil { t.Fatalf("expected a summary from the stored transcript; calls=%d summary=%v", sum.calls, res.Summary) } } func TestProcessNewVideo_StoreMissFetchesAndPersists(t *testing.T) { src := &recordingSource{transcript: domain.Transcript{Source: domain.SourceCaptions, Language: "en", Content: "fetched words"}} ts := newFakeTranscriptStore() sum := &countingSummarizer{} eng := NewEngine(src, sum, nopSink{}) eng.Transcripts = ts if _, err := eng.ProcessNewVideo(context.Background(), testVideo()); err != nil { t.Fatalf("ProcessNewVideo: %v", err) } if src.fetchCalls != 1 { t.Fatalf("a store miss must fetch exactly once; got %d", src.fetchCalls) } if ts.saves != 1 { t.Fatalf("a fetched transcript must be persisted; got %d saves", ts.saves) } got, ok, _ := ts.GetTranscript(context.Background(), "youtube", "yt1") if !ok || got.Content != "fetched words" { t.Fatalf("persisted transcript not readable back: ok=%v content=%q", ok, got.Content) } } // The second summarize of the same video reads the persisted transcript and does // NOT re-fetch — the primary ADR-021 win, proven end to end at the engine. func TestProcessNewVideo_SecondSummarizeDoesNotRefetch(t *testing.T) { src := &recordingSource{transcript: domain.Transcript{Source: domain.SourceCaptions, Content: "words"}} ts := newFakeTranscriptStore() eng := NewEngine(src, &countingSummarizer{}, nopSink{}) eng.Transcripts = ts for i := 0; i < 2; i++ { if _, err := eng.ProcessNewVideo(context.Background(), testVideo()); err != nil { t.Fatalf("pass %d: %v", i, err) } } if src.fetchCalls != 1 { t.Fatalf("the second summarize must reuse the stored transcript; got %d fetches", src.fetchCalls) } } // A stored "no captions" outcome short-circuits before both fetch and summarize. func TestProcessNewVideo_StoredNoneSkipsFetchAndSummarize(t *testing.T) { src := &recordingSource{} ts := newFakeTranscriptStore() ts.stored[ts.key("youtube", "yt1")] = domain.Transcript{Source: domain.SourceNone} sum := &countingSummarizer{} eng := NewEngine(src, sum, nopSink{}) eng.Transcripts = ts res, err := eng.ProcessNewVideo(context.Background(), testVideo()) if err != nil { t.Fatalf("ProcessNewVideo: %v", err) } if !res.Skipped { t.Fatal("a stored SourceNone must skip") } if src.fetchCalls != 0 || sum.calls != 0 { t.Fatalf("stored none must neither fetch nor summarize; fetches=%d calls=%d", src.fetchCalls, sum.calls) } } // A transient 429 is surfaced (so the runner backs off per-user) but never cached // as a shared terminal state — otherwise it would mask a rate-limit as permanent. func TestProcessNewVideo_RateLimitedIsNotPersisted(t *testing.T) { src := &recordingSource{transcript: domain.Transcript{Source: domain.SourceRateLimited}} ts := newFakeTranscriptStore() eng := NewEngine(src, &countingSummarizer{}, nopSink{}) eng.Transcripts = ts res, err := eng.ProcessNewVideo(context.Background(), testVideo()) if err != nil { t.Fatalf("ProcessNewVideo: %v", err) } if !res.Skipped || res.TranscriptSource != string(domain.SourceRateLimited) { t.Fatalf("expected a rate-limited skip; skipped=%v source=%q", res.Skipped, res.TranscriptSource) } if ts.saves != 0 { t.Fatalf("a transient rate-limit must not be persisted; got %d saves", ts.saves) } } // With no TranscriptStore wired the engine fetches every time (back-compat). func TestProcessNewVideo_NilStoreFetchesEveryTime(t *testing.T) { src := &recordingSource{transcript: domain.Transcript{Source: domain.SourceCaptions, Content: "words"}} eng := NewEngine(src, &countingSummarizer{}, nopSink{}) for i := 0; i < 2; i++ { if _, err := eng.ProcessNewVideo(context.Background(), testVideo()); err != nil { t.Fatalf("pass %d: %v", i, err) } } if src.fetchCalls != 2 { t.Fatalf("nil store must fetch every time; got %d", src.fetchCalls) } }