// Package acceptance holds executable acceptance tests that mirror the Gherkin // scenarios in docs/use-cases/. They drive the use-case engine through FAKE // adapters (no live YouTube, no live brain) so the core behaviour is what's // under test. // // These tests are intentionally RED in the scaffold: Engine.ProcessNewVideo // returns usecase.ErrNotImplemented. Making them GREEN is the first build task. package acceptance import ( "context" "testing" "time" "gitea.d-ma.be/mathias/tapir/internal/domain" "gitea.d-ma.be/mathias/tapir/internal/usecase" ) // --- fake adapters --------------------------------------------------------- type fakeSource struct { transcript domain.Transcript } func (f fakeSource) ListSubscriptions(ctx context.Context, userID string) ([]domain.Subscription, error) { return nil, nil } func (f fakeSource) NewVideos(ctx context.Context, sub domain.Subscription) ([]domain.Video, error) { return nil, nil } func (f fakeSource) FetchTranscript(ctx context.Context, v domain.Video) (domain.Transcript, error) { return f.transcript, nil } type fakeSummarizer struct{} func (fakeSummarizer) Summarize(ctx context.Context, v domain.Video, t domain.Transcript) (domain.Summary, error) { return domain.Summary{ VideoID: v.ID, UserID: v.UserID, Summary: "a summary", Highlights: []string{"h1"}, Takeaways: []string{"t1"}, AIProvider: "local", CreatedAt: time.Now(), }, nil } type recordingSink struct { name string delivered []domain.Summary } func (s *recordingSink) Name() string { return s.name } func (s *recordingSink) Deliver(ctx context.Context, sum domain.Summary) error { s.delivered = append(s.delivered, sum) return nil } func sampleVideo() domain.Video { return domain.Video{ ID: "v1", UserID: "u1", SubscriptionID: "s1", Provider: domain.ProviderYouTube, ProviderVideoID: "yt1", Title: "Designing for Attention", SeenAt: time.Now(), } } // --- scenarios ------------------------------------------------------------- // Scenario: A subscribed channel posts a video that has captions func TestSubscribedVideoWithCaptionsIsSummarizedAndDelivered(t *testing.T) { src := fakeSource{transcript: domain.Transcript{ VideoID: "v1", UserID: "u1", Source: domain.SourceCaptions, Language: "en", Content: "transcript text", }} sink := &recordingSink{name: "store"} eng := usecase.NewEngine(src, fakeSummarizer{}, sink) res, err := eng.ProcessNewVideo(context.Background(), sampleVideo()) if err != nil { t.Fatalf("ProcessNewVideo returned error: %v", err) } if res.Skipped { t.Fatalf("expected video to be summarized, got skipped: %q", res.Reason) } if res.Summary == nil { t.Fatal("expected a summary, got nil") } if len(sink.delivered) != 1 { t.Fatalf("expected 1 delivery to store sink, got %d", len(sink.delivered)) } } // Scenario: A subscribed channel posts a video with no usable transcript func TestVideoWithNoTranscriptIsSkipped(t *testing.T) { src := fakeSource{transcript: domain.Transcript{ VideoID: "v1", UserID: "u1", Source: domain.SourceNone, }} sink := &recordingSink{name: "store"} eng := usecase.NewEngine(src, fakeSummarizer{}, sink) res, err := eng.ProcessNewVideo(context.Background(), sampleVideo()) if err != nil { t.Fatalf("ProcessNewVideo returned error: %v", err) } if !res.Skipped { t.Fatal("expected video to be skipped (no transcript)") } if res.Reason != "no transcript" { t.Fatalf("expected reason %q, got %q", "no transcript", res.Reason) } if res.Summary != nil { t.Fatal("expected no summary for skipped video") } if len(sink.delivered) != 0 { t.Fatalf("expected no summary delivery, got %d", len(sink.delivered)) } }