Files
WPSidekick/internal/ideas/service_test.go
T
alexandrev-tibco 3a66d687e1 Minor updates
2026-02-20 16:21:18 +01:00

332 lines
10 KiB
Go

package ideas
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"seo-optimizer/internal/agent"
"seo-optimizer/internal/config"
"seo-optimizer/internal/reddit"
"seo-optimizer/internal/storage"
"seo-optimizer/internal/wordpress"
"seo-optimizer/pkg/models"
)
type fakeLLM struct {
postIdeas []*models.PostIdea
outreachSuggestions []*models.OutreachSuggestion
}
func (f *fakeLLM) OptimizePost(ctx context.Context, post *wordpress.Post, internalPosts []*wordpress.InternalPostReference, categories map[int]string, tags map[int]string, options *agent.OptimizeOptions) (*models.OptimizationResponse, error) {
return nil, nil
}
func (f *fakeLLM) GeneratePostIdeas(ctx context.Context, prompt string) ([]*models.PostIdea, error) {
if f.postIdeas != nil {
return f.postIdeas, nil
}
return []*models.PostIdea{
{
SEOTitle: "Idea SEO",
Summary: "Summary",
PrimaryKW: "keyword",
Sources: []models.IdeaSource{{
Title: "Reddit",
URL: "https://reddit.com/r/test",
}},
References: []string{"https://example.com"},
},
}, nil
}
func (f *fakeLLM) GenerateArticleDraft(ctx context.Context, prompt string) (*models.ArticleDraft, error) {
return &models.ArticleDraft{
Title: "Draft",
SEOTitle: "SEO Draft",
MetaDescription: "Meta",
Excerpt: "Excerpt",
ContentHTML: "<p>Content</p>",
}, nil
}
func (f *fakeLLM) GenerateOutreachSuggestions(ctx context.Context, prompt string) ([]*models.OutreachSuggestion, error) {
if f.outreachSuggestions != nil {
return f.outreachSuggestions, nil
}
return []*models.OutreachSuggestion{}, nil
}
func (f *fakeLLM) GenerateNewsletterDraft(ctx context.Context, prompt string) (*models.NewsletterDraft, error) {
return nil, nil
}
func TestGenerateDailyIdeasAndDraftsSpanishOnly(t *testing.T) {
// Reddit server
redditServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]interface{}{
"data": map[string]interface{}{
"children": []map[string]interface{}{
{"data": map[string]interface{}{
"title": "Post A",
"url": "https://reddit.com/r/test/a",
"permalink": "/r/test/comments/a",
"selftext": "Body",
"author": "user",
"score": 50,
"num_comments": 10,
"created_utc": 1710000000,
"subreddit": "paternidad",
"is_self": true,
"stickied": false,
}},
},
},
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer redditServer.Close()
// WordPress server for style samples
wpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/wp-json/wp/v2/posts" {
w.WriteHeader(http.StatusNotFound)
return
}
posts := []map[string]interface{}{
{
"id": 1,
"title": map[string]interface{}{"rendered": "Sample"},
"excerpt": map[string]interface{}{"rendered": "Excerpt"},
"content": map[string]interface{}{"rendered": "Content"},
},
}
_ = json.NewEncoder(w).Encode(posts)
}))
defer wpServer.Close()
redditClient := reddit.NewClient(redditServer.URL, nil)
wpClient := wordpress.NewClient(wpServer.URL, "user", "pass", 0, nil)
ideasStore := storage.NewIdeaStorage(t.TempDir(), nil)
if err := ideasStore.Initialize(); err != nil {
t.Fatalf("init ideas storage: %v", err)
}
outreachStore := storage.NewOutreachStorage(t.TempDir(), nil)
if err := outreachStore.Initialize(); err != nil {
t.Fatalf("init outreach storage: %v", err)
}
cfg := config.IdeasConfig{
Enabled: true,
IntervalHours: 24,
Subreddit: "paternidad",
Listing: "top",
TimeRange: "day",
MaxRedditPosts: 5,
MaxIdeas: 1,
StyleSamplePosts: 1,
MinScore: 10,
MinComments: 1,
BasePath: t.TempDir(),
}
outreachCfg := config.OutreachConfig{
Enabled: false,
}
profile := config.SiteProfile{
Name: "Padre Primerizo",
URL: "https://padreprimerizo.com",
Topics: []string{"Paternidad"},
Audience: "Padres",
Voice: "Cercano",
}
svc := NewService(wpClient, &fakeLLM{}, redditClient, ideasStore, cfg, outreachStore, outreachCfg, profile, []string{"es"}, true, nil)
ideas, err := svc.GenerateDailyIdeas(context.Background())
if err != nil {
t.Fatalf("GenerateDailyIdeas failed: %v", err)
}
if len(ideas) != 1 {
t.Fatalf("expected 1 idea, got %d", len(ideas))
}
idea, err := svc.GenerateDrafts(context.Background(), ideas[0].ID)
if err != nil {
t.Fatalf("GenerateDrafts failed: %v", err)
}
if idea.ArticleES == nil {
t.Fatalf("expected Spanish draft to be generated")
}
if idea.ArticleEN != nil {
t.Fatalf("expected no English draft for es-only site")
}
}
func TestGenerateOutreachSuggestionsDeduplicatesAndSkipsExistingLinks(t *testing.T) {
redditServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := map[string]interface{}{
"data": map[string]interface{}{
"children": []map[string]interface{}{
{"data": map[string]interface{}{
"title": "Thread A",
"url": "https://www.reddit.com/r/kubernetes/comments/abc/thread_a/",
"permalink": "/r/kubernetes/comments/abc/thread_a/",
"selftext": "Need help with cluster setup",
"author": "user-a",
"score": 100,
"num_comments": 20,
"created_utc": 1710000000,
"subreddit": "kubernetes",
"is_self": true,
"stickied": false,
}},
{"data": map[string]interface{}{
"title": "Thread B",
"url": "https://www.reddit.com/r/kubernetes/comments/def/thread_b/",
"permalink": "/r/kubernetes/comments/def/thread_b/",
"selftext": "How to debug networking",
"author": "user-b",
"score": 120,
"num_comments": 30,
"created_utc": 1710000500,
"subreddit": "kubernetes",
"is_self": true,
"stickied": false,
}},
},
},
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer redditServer.Close()
wpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || r.URL.Path != "/wp-json/wp/v2/posts" {
w.WriteHeader(http.StatusNotFound)
return
}
posts := []map[string]interface{}{
{
"id": 10,
"slug": "kubernetes-guide",
"link": "https://example.com/kubernetes-guide",
"title": map[string]interface{}{"rendered": "Kubernetes Guide"},
"excerpt": map[string]interface{}{"rendered": "Guide excerpt"},
"status": "publish",
},
}
_ = json.NewEncoder(w).Encode(posts)
}))
defer wpServer.Close()
redditClient := reddit.NewClient(redditServer.URL, nil)
wpClient := wordpress.NewClient(wpServer.URL, "user", "pass", 0, nil)
ideasStore := storage.NewIdeaStorage(t.TempDir(), nil)
if err := ideasStore.Initialize(); err != nil {
t.Fatalf("init ideas storage: %v", err)
}
outreachStore := storage.NewOutreachStorage(t.TempDir(), nil)
if err := outreachStore.Initialize(); err != nil {
t.Fatalf("init outreach storage: %v", err)
}
// Existing suggestion for thread B should prevent regeneration for the same Reddit URL.
if err := outreachStore.Save(&models.OutreachSuggestion{
ID: "existing-b",
RedditURL: "https://www.reddit.com/r/kubernetes/comments/def/thread_b/",
RedditTitle: "Thread B",
ArticleTitle: "Existing Article",
ArticleURL: "https://example.com/existing",
SuggestedResponse: "Existing response",
Status: "new",
}); err != nil {
t.Fatalf("seed outreach suggestion: %v", err)
}
ideasCfg := config.IdeasConfig{
Enabled: true,
IntervalHours: 24,
Subreddit: "kubernetes",
Listing: "top",
TimeRange: "day",
MaxRedditPosts: 5,
MaxIdeas: 1,
StyleSamplePosts: 1,
MinScore: 1,
MinComments: 1,
BasePath: t.TempDir(),
}
outreachCfg := config.OutreachConfig{
Enabled: true,
Subreddit: "kubernetes",
Listing: "top",
TimeRange: "day",
MaxRedditPosts: 5,
MaxSuggestions: 5,
MinScore: 1,
MinComments: 1,
MaxArticles: 10,
BasePath: t.TempDir(),
}
profile := config.SiteProfile{
Name: "Test Site",
URL: "https://example.com",
Topics: []string{"Kubernetes"},
Audience: "Engineers",
Voice: "Technical",
}
llm := &fakeLLM{
outreachSuggestions: []*models.OutreachSuggestion{
{
RedditURL: "https://www.reddit.com/r/kubernetes/comments/abc/thread_a/",
RedditTitle: "Thread A",
ArticleTitle: "Kubernetes Guide",
ArticleURL: "https://example.com/kubernetes-guide",
SuggestedResponse: "Helpful answer A.",
},
{
// Duplicate of Thread A using a different URL shape should be deduplicated.
RedditURL: "https://old.reddit.com/r/kubernetes/comments/abc/thread_a/?utm_source=foo",
RedditTitle: "Thread A",
ArticleTitle: "Kubernetes Guide",
ArticleURL: "https://example.com/kubernetes-guide",
SuggestedResponse: "Helpful answer A duplicate.",
},
{
// Existing thread should be skipped.
RedditURL: "https://www.reddit.com/r/kubernetes/comments/def/thread_b/",
RedditTitle: "Thread B",
ArticleTitle: "Kubernetes Guide",
ArticleURL: "https://example.com/kubernetes-guide",
SuggestedResponse: "Helpful answer B.",
},
},
}
svc := NewService(wpClient, llm, redditClient, ideasStore, ideasCfg, outreachStore, outreachCfg, profile, []string{"en"}, true, nil)
generated, err := svc.GenerateOutreachSuggestions(context.Background())
if err != nil {
t.Fatalf("GenerateOutreachSuggestions failed: %v", err)
}
if len(generated) != 1 {
t.Fatalf("expected 1 generated outreach suggestion, got %d", len(generated))
}
if generated[0].RedditURL != "https://reddit.com/r/kubernetes/comments/abc/thread_a" {
t.Fatalf("unexpected canonical reddit url: %s", generated[0].RedditURL)
}
all, err := outreachStore.List("all")
if err != nil {
t.Fatalf("list outreach all: %v", err)
}
if len(all) != 2 {
t.Fatalf("expected 2 total outreach suggestions (seed + new), got %d", len(all))
}
}