diff --git a/test/acceptance/summarize_new_video_test.go b/test/acceptance/summarize_new_video_test.go new file mode 100644 index 0000000..13993fc --- /dev/null +++ b/test/acceptance/summarize_new_video_test.go @@ -0,0 +1,118 @@ +// 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)) + } +}