- extract analytics, searchactors, styleprofile, twitter_analytics, validation, storefake from mcp_test - add fakeStore/fakeOllama harnesses, lastLimit tracking, sql.NullTime staleness - expand mcp_test with per-platform GetPost/Latest/PostsInTime, actor and cache tests - touch analytics/backends/cache_version/metrics/server for coverage
162 lines
5.6 KiB
Go
162 lines
5.6 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nacorid/naco-api/internal/clustering"
|
|
"github.com/nacorid/naco-api/internal/store"
|
|
"github.com/nacorid/naco-api/internal/twitter"
|
|
)
|
|
|
|
func twitterPosts() []*twitter.Post {
|
|
base := time.Date(2024, 5, 10, 12, 0, 0, 0, time.UTC)
|
|
return []*twitter.Post{
|
|
{ID: "t1", Author: "@a", Content: "twitter one", CreatedAt: base},
|
|
{ID: "t2", Author: "@a", Content: "twitter two", CreatedAt: base.Add(time.Hour)},
|
|
}
|
|
}
|
|
|
|
func TestFetchTwitterAnalyticsPostsFetchesAndPersists(t *testing.T) {
|
|
tw := &fakeTwitter{posts: twitterPosts()}
|
|
st := newFakeStore()
|
|
s := newServerWith(&fakeBluesky{}, &fakeAP{}, &fakeNostr{}, tw, st, &fakeOllama{})
|
|
|
|
start := time.Date(2024, 5, 1, 0, 0, 0, 0, time.UTC)
|
|
end := time.Date(2024, 5, 31, 0, 0, 0, 0, time.UTC)
|
|
items, err := s.fetchTwitterAnalyticsPosts(context.Background(), "@a", start, end, 50)
|
|
if err != nil {
|
|
t.Fatalf("fetchTwitterAnalyticsPosts error: %v", err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Fatalf("expected 2 fetched posts, got %d", len(items))
|
|
}
|
|
if tw.calls != 1 {
|
|
t.Fatalf("expected 1 API call, got %d", tw.calls)
|
|
}
|
|
|
|
// Newly fetched posts are persisted for future reuse.
|
|
if len(st.analyticsPosts) != 2 {
|
|
t.Fatalf("expected 2 persisted analytics posts, got %d", len(st.analyticsPosts))
|
|
}
|
|
// Coverage range for the whole range recorded.
|
|
if len(st.coverageRanges) != 1 {
|
|
t.Fatalf("expected 1 coverage range, got %d", len(st.coverageRanges))
|
|
}
|
|
}
|
|
|
|
func TestFetchTwitterAnalyticsPostsServedFromCoverage(t *testing.T) {
|
|
// Posts already persisted and coverage present => no paid API call.
|
|
st := newFakeStore()
|
|
base := time.Date(2024, 5, 10, 12, 0, 0, 0, time.UTC)
|
|
st.coverageRanges = []store.AnalyticsCoverageRange{
|
|
{UserID: "@a", Platform: "Twitter", CoveredStart: base, CoveredEnd: base.Add(2 * time.Hour)},
|
|
}
|
|
st.analyticsPosts[postKey("@a", "Twitter", "t1")] = store.AnalyticsPost{
|
|
UserID: "@a", Platform: "Twitter", PostID: "t1", PostText: "cached post", PostTimestamp: base,
|
|
}
|
|
tw := &fakeTwitter{}
|
|
s := newServerWith(&fakeBluesky{}, &fakeAP{}, &fakeNostr{}, tw, st, &fakeOllama{})
|
|
|
|
start := base
|
|
end := base.Add(2 * time.Hour)
|
|
items, err := s.fetchTwitterAnalyticsPosts(context.Background(), "@a", start, end, 50)
|
|
if err != nil {
|
|
t.Fatalf("fetchTwitterAnalyticsPosts error: %v", err)
|
|
}
|
|
if len(items) != 1 {
|
|
t.Fatalf("expected 1 cached post, got %d", len(items))
|
|
}
|
|
if tw.calls != 0 {
|
|
t.Fatalf("expected no API call when covered, got %d", tw.calls)
|
|
}
|
|
if items[0].Text != "cached post" {
|
|
t.Fatalf("expected cached text, got %q", items[0].Text)
|
|
}
|
|
}
|
|
|
|
func TestMergeAnalyticsPostsDedupsAndCaps(t *testing.T) {
|
|
base := time.Date(2024, 5, 10, 12, 0, 0, 0, time.UTC)
|
|
st := newFakeStore()
|
|
// Cached posts overlap with fetched ones by ID, and both exceed the limit.
|
|
st.analyticsPosts[postKey("@a", "Twitter", "dup")] = store.AnalyticsPost{
|
|
UserID: "@a", Platform: "Twitter", PostID: "dup", PostText: "dup cached", PostTimestamp: base,
|
|
}
|
|
s := newServerWith(&fakeBluesky{}, &fakeAP{}, &fakeNostr{}, &fakeTwitter{}, st, &fakeOllama{})
|
|
|
|
fetched := []clustering.Item{
|
|
{ID: "dup", Text: "dup fetched", Timestamp: base},
|
|
{ID: "b", Text: "b", Timestamp: base},
|
|
{ID: "c", Text: "c", Timestamp: base},
|
|
}
|
|
merged, err := s.mergeAnalyticsPosts(context.Background(), "@a", base, base.Add(time.Hour), 2, fetched)
|
|
if err != nil {
|
|
t.Fatalf("mergeAnalyticsPosts error: %v", err)
|
|
}
|
|
if len(merged) != 2 {
|
|
t.Fatalf("expected merged capped at 2, got %d", len(merged))
|
|
}
|
|
// Fetched entries come first and dedupe against the cache.
|
|
if merged[0].ID != "dup" || merged[0].Text != "dup fetched" {
|
|
t.Fatalf("expected fetched dup first, got %+v", merged[0])
|
|
}
|
|
}
|
|
|
|
func TestPersistAnalyticsPostsSkipsEmptyIDs(t *testing.T) {
|
|
st := newFakeStore()
|
|
s := newServerWith(&fakeBluesky{}, &fakeAP{}, &fakeNostr{}, &fakeTwitter{}, st, &fakeOllama{})
|
|
err := s.persistAnalyticsPosts(context.Background(), "@a", "Twitter", []clustering.Item{
|
|
{ID: "", Text: "no id"},
|
|
{ID: "real", Text: "real", Timestamp: time.Now()},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("persistAnalyticsPosts error: %v", err)
|
|
}
|
|
if len(st.analyticsPosts) != 1 {
|
|
t.Fatalf("expected only the post with an ID persisted, got %d", len(st.analyticsPosts))
|
|
}
|
|
if _, ok := st.analyticsPosts[postKey("@a", "Twitter", "real")]; !ok {
|
|
t.Fatal("expected the 'real' post to be persisted")
|
|
}
|
|
}
|
|
|
|
func TestAnalyticsTwitterEndToEnd(t *testing.T) {
|
|
// getEngagementMetrics over the Twitter platform drives the coverage logic.
|
|
tw := &fakeTwitter{posts: twitterPosts()}
|
|
st := newFakeStore()
|
|
s := newServerWith(&fakeBluesky{}, &fakeAP{}, &fakeNostr{}, tw, st, &fakeOllama{})
|
|
|
|
res, err := callTool(s, "getEngagementMetrics", AnalyticsRequest{
|
|
Author: "@a",
|
|
StartTime: "2024-05-01T00:00:00Z",
|
|
EndTime: "2024-05-31T00:00:00Z",
|
|
Platform: "Twitter",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("getEngagementMetrics twitter error: %v", err)
|
|
}
|
|
if !strings.Contains(textOf(t, res), `"postId":"t1"`) {
|
|
t.Fatalf("expected twitter post t1 in result, got %s", textOf(t, res))
|
|
}
|
|
if !strings.Contains(textOf(t, res), `"postId":"t2"`) {
|
|
t.Fatalf("expected twitter post t2 in result, got %s", textOf(t, res))
|
|
}
|
|
if tw.calls != 1 {
|
|
t.Fatalf("expected 1 API call for the uncovered range, got %d", tw.calls)
|
|
}
|
|
|
|
// Second identical request is served from the analytics cache (no API call).
|
|
if _, err := callTool(s, "getEngagementMetrics", AnalyticsRequest{
|
|
Author: "@a",
|
|
StartTime: "2024-05-01T00:00:00Z",
|
|
EndTime: "2024-05-31T00:00:00Z",
|
|
Platform: "Twitter",
|
|
}); err != nil {
|
|
t.Fatalf("second call error: %v", err)
|
|
}
|
|
if tw.calls != 1 {
|
|
t.Fatalf("expected cache to avoid a second API call, got %d", tw.calls)
|
|
}
|
|
}
|