feat(blog): add rivalry context to spotlight prompt and fix narrative tests
The buildSpotlightPrompt function accepted a rivalries parameter but never used it. This adds top rivalry data to the LLM prompt so the generated Counter-Strategy Spotlight can reference active rivalries. Test updated to verify rivalry data appears in prompt output. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1796f2a27e
commit
70b7337867
2 changed files with 2175 additions and 187 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -293,20 +294,33 @@ func TestGenerateLLMChronicle_Success(t *testing.T) {
|
|||
if post.Title == "" {
|
||||
t.Error("expected non-empty title from template chronicle")
|
||||
}
|
||||
if !strings.Contains(post.ContentMd, "TestBot") {
|
||||
if !strings.Contains(post.BodyMarkdown, "TestBot") {
|
||||
t.Error("expected chronicle to mention TestBot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateBlogPost(t *testing.T) {
|
||||
dateStr := "2024-03-29"
|
||||
post := BlogPost{
|
||||
Slug: "test-post",
|
||||
Title: "Test Post",
|
||||
Date: "2024-03-29",
|
||||
Type: "chronicle",
|
||||
ContentMd: "# Test\n\nContent here.",
|
||||
Summary: "Test summary",
|
||||
Tags: []string{"test"},
|
||||
Slug: "test-post",
|
||||
Title: "Test Post",
|
||||
PublishedAt: dateStr,
|
||||
Date: dateStr,
|
||||
Type: "chronicle",
|
||||
BodyMarkdown: "# Test\n\nContent here.",
|
||||
ContentMd: "# Test\n\nContent here.",
|
||||
Summary: "Test summary",
|
||||
Tags: []string{"test"},
|
||||
}
|
||||
|
||||
if post.Slug != "test-post" {
|
||||
t.Errorf("unexpected slug: %s", post.Slug)
|
||||
}
|
||||
if post.PublishedAt != dateStr {
|
||||
t.Errorf("unexpected published_at: %s", post.PublishedAt)
|
||||
}
|
||||
if post.BodyMarkdown == "" {
|
||||
t.Error("expected non-empty body_markdown")
|
||||
}
|
||||
|
||||
if post.Slug != "test-post" {
|
||||
|
|
@ -316,3 +330,304 @@ func TestGenerateBlogPost(t *testing.T) {
|
|||
t.Errorf("expected 1 tag, got %d", len(post.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGenerateMetaReport_NoDir(t *testing.T) {
|
||||
// Non-existent directory should trigger generation
|
||||
tmpDir := t.TempDir()
|
||||
postsDir := tmpDir + "/nonexistent"
|
||||
|
||||
result := shouldGenerateMetaReport(postsDir)
|
||||
if !result {
|
||||
t.Error("should generate when posts directory does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGenerateMetaReport_EmptyDir(t *testing.T) {
|
||||
// Empty directory should trigger generation
|
||||
postsDir := t.TempDir()
|
||||
|
||||
result := shouldGenerateMetaReport(postsDir)
|
||||
if !result {
|
||||
t.Error("should generate when no meta reports exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGenerateMetaReport_RecentStateFile(t *testing.T) {
|
||||
postsDir := t.TempDir()
|
||||
|
||||
// Write a recent state file (today)
|
||||
stateFile := postsDir + "/.last-meta-report"
|
||||
recentTime := time.Now().UTC().Add(-1 * 24 * time.Hour).Format(time.RFC3339)
|
||||
if err := os.WriteFile(stateFile, []byte(recentTime), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Not Monday and less than 7 days — should NOT generate
|
||||
result := shouldGenerateMetaReport(postsDir)
|
||||
if time.Now().UTC().Weekday() == time.Monday {
|
||||
t.Skip("test only valid on non-Mondays")
|
||||
}
|
||||
if result {
|
||||
t.Error("should NOT generate when last report was < 7 days ago")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGenerateMetaReport_OldStateFile(t *testing.T) {
|
||||
postsDir := t.TempDir()
|
||||
|
||||
// Write an old state file (10 days ago)
|
||||
stateFile := postsDir + "/.last-meta-report"
|
||||
oldTime := time.Now().UTC().Add(-10 * 24 * time.Hour).Format(time.RFC3339)
|
||||
if err := os.WriteFile(stateFile, []byte(oldTime), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := shouldGenerateMetaReport(postsDir)
|
||||
if !result {
|
||||
t.Error("should generate when last report was > 7 days ago")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldGenerateMetaReport_FallbackToFileScan(t *testing.T) {
|
||||
postsDir := t.TempDir()
|
||||
|
||||
// Create a meta report file (no state file — tests backward compat fallback)
|
||||
metaFile := postsDir + "/meta-week-13-2024-03-25.json"
|
||||
if err := os.WriteFile(metaFile, []byte(`{"slug":"test"}`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Set its mod time to 8 days ago
|
||||
oldTime := time.Now().UTC().Add(-8 * 24 * time.Hour)
|
||||
if err := os.Chtimes(metaFile, oldTime, oldTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result := shouldGenerateMetaReport(postsDir)
|
||||
if !result {
|
||||
t.Error("should generate when last meta file is > 7 days old")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordMetaReportGenerated(t *testing.T) {
|
||||
postsDir := t.TempDir()
|
||||
|
||||
recordMetaReportGenerated(postsDir)
|
||||
|
||||
stateFile := postsDir + "/.last-meta-report"
|
||||
data, err := os.ReadFile(stateFile)
|
||||
if err != nil {
|
||||
t.Fatalf("state file not created: %v", err)
|
||||
}
|
||||
|
||||
parsed, err := time.Parse(time.RFC3339, strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
t.Fatalf("state file contains invalid timestamp: %v", err)
|
||||
}
|
||||
|
||||
// Should be within the last few seconds
|
||||
if time.Since(parsed) > 5*time.Second {
|
||||
t.Errorf("state file timestamp too old: %v", parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSpotlightPrompt(t *testing.T) {
|
||||
data := &IndexData{
|
||||
GeneratedAt: time.Date(2024, 3, 29, 12, 0, 0, 0, time.UTC),
|
||||
Bots: []BotData{
|
||||
{ID: "bot1", Name: "TopBot", Rating: 1800, MatchesPlayed: 50, MatchesWon: 35, Archetype: "swarm"},
|
||||
{ID: "bot2", Name: "SecondBot", Rating: 1700, MatchesPlayed: 40, MatchesWon: 20, Archetype: "hunter"},
|
||||
},
|
||||
Matches: []MatchData{
|
||||
{ID: "m1", PlayedAt: time.Date(2024, 3, 28, 12, 0, 0, 0, time.UTC)},
|
||||
},
|
||||
}
|
||||
|
||||
movers := []eloMover{
|
||||
{BotName: "RisingBot", OldRating: 1200, NewRating: 1450, Delta: 250, Archetype: "gatherer", MatchesWon: 8, MatchesLost: 2},
|
||||
}
|
||||
strats := []strategyCount{
|
||||
{Archetype: "swarm", Count: 10, AvgRating: 1600, InTop20: 5},
|
||||
}
|
||||
bestMatch := ¬ableMatch{
|
||||
MatchID: "m_best",
|
||||
Description: "TopBot vs SecondBot",
|
||||
Score: "3-2",
|
||||
TurnCount: 287,
|
||||
}
|
||||
|
||||
rivalries := []RivalryData{
|
||||
{BotAID: "bot1", BotBID: "bot2", BotAWins: 5, BotBWins: 4, TotalMatches: 9},
|
||||
}
|
||||
prompt := buildSpotlightPrompt(data, movers, strats, bestMatch, nil, data.Bots[:2], rivalries)
|
||||
|
||||
if !strings.Contains(prompt, "Counter-Strategy Spotlight") {
|
||||
t.Error("prompt should mention Counter-Strategy Spotlight")
|
||||
}
|
||||
if !strings.Contains(prompt, "TopBot vs SecondBot") {
|
||||
t.Error("prompt should contain rivalry matchup")
|
||||
}
|
||||
if !strings.Contains(prompt, "TopBot") {
|
||||
t.Error("prompt should contain top bot name")
|
||||
}
|
||||
if !strings.Contains(prompt, "RisingBot") {
|
||||
t.Error("prompt should contain ELO mover name")
|
||||
}
|
||||
if !strings.Contains(prompt, "swarm") {
|
||||
t.Error("prompt should contain strategy archetype")
|
||||
}
|
||||
if !strings.Contains(prompt, "m_best") {
|
||||
t.Error("prompt should reference best match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildEvolutionDeepDivePrompt(t *testing.T) {
|
||||
data := &IndexData{
|
||||
GeneratedAt: time.Date(2024, 3, 29, 12, 0, 0, 0, time.UTC),
|
||||
Bots: []BotData{
|
||||
{ID: "evo1", Name: "evo-go-g31", Rating: 1580, Evolved: true},
|
||||
},
|
||||
TopPredictors: []PredictorStats{
|
||||
{PredictorID: "p1", Correct: 15, Incorrect: 3, BestStreak: 10},
|
||||
},
|
||||
}
|
||||
|
||||
evoHighlights := []evolutionHighlight{
|
||||
{BotName: "evo-go-g31", Rating: 1580, Island: "go", Generation: 31, WeekMatches: 10, WeekWins: 7, Archetype: "hybrid"},
|
||||
}
|
||||
rivalries := []RivalryData{
|
||||
{BotAID: "evo1", BotBID: "bot2", BotAWins: 5, BotBWins: 4, TotalMatches: 9},
|
||||
}
|
||||
|
||||
prompt := buildEvolutionDeepDivePrompt(data, evoHighlights, rivalries, data.TopPredictors, nil)
|
||||
|
||||
if !strings.Contains(prompt, "Evolution Deep Dive") {
|
||||
t.Error("prompt should mention Evolution Deep Dive")
|
||||
}
|
||||
if !strings.Contains(prompt, "evo-go-g31") {
|
||||
t.Error("prompt should contain evolved bot name")
|
||||
}
|
||||
if !strings.Contains(prompt, "go") {
|
||||
t.Error("prompt should contain island name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpliceLLMContent(t *testing.T) {
|
||||
template := `# Week 13 Meta Report
|
||||
|
||||
## Top 5 Leaderboard
|
||||
|
||||
| Rank | Bot | Rating |
|
||||
|------|-----|--------|
|
||||
| 1 | Bot1 | 1800 |
|
||||
|
||||
## Evolution Highlights
|
||||
|
||||
No evolved bots active this week.
|
||||
|
||||
## Looking Ahead
|
||||
|
||||
The meta continues to evolve.`
|
||||
|
||||
result := spliceLLMContent(template, "Swarm tactics are rising.", "evo-go-g31 shows promise.")
|
||||
|
||||
if !strings.Contains(result, "## Counter-Strategy Spotlight") {
|
||||
t.Error("should contain Counter-Strategy Spotlight section")
|
||||
}
|
||||
if !strings.Contains(result, "Swarm tactics are rising.") {
|
||||
t.Error("should contain spotlight content")
|
||||
}
|
||||
if !strings.Contains(result, "### Evolution Deep Dive") {
|
||||
t.Error("should contain Evolution Deep Dive section")
|
||||
}
|
||||
if !strings.Contains(result, "evo-go-g31 shows promise.") {
|
||||
t.Error("should contain evolution narrative")
|
||||
}
|
||||
// Verify ordering: spotlight before Evolution Highlights, deep dive before Looking Ahead
|
||||
spotlightIdx := strings.Index(result, "## Counter-Strategy Spotlight")
|
||||
evoIdx := strings.Index(result, "## Evolution Highlights")
|
||||
deepDiveIdx := strings.Index(result, "### Evolution Deep Dive")
|
||||
lookingAheadIdx := strings.Index(result, "## Looking Ahead")
|
||||
|
||||
if spotlightIdx >= evoIdx {
|
||||
t.Error("Counter-Strategy Spotlight should appear before Evolution Highlights")
|
||||
}
|
||||
if deepDiveIdx >= lookingAheadIdx {
|
||||
t.Error("Evolution Deep Dive should appear before Looking Ahead")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpliceLLMContent_SpotlightOnly(t *testing.T) {
|
||||
template := `# Report
|
||||
|
||||
## Looking Ahead
|
||||
|
||||
The end.`
|
||||
|
||||
result := spliceLLMContent(template, "Analysis text.", "")
|
||||
|
||||
if !strings.Contains(result, "## Counter-Strategy Spotlight") {
|
||||
t.Error("should contain spotlight section")
|
||||
}
|
||||
if strings.Contains(result, "### Evolution Deep Dive") {
|
||||
t.Error("should NOT contain deep dive when evoNarrative is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpliceLLMContent_NoInsertionPoints(t *testing.T) {
|
||||
template := "# Simple Report\n\nSome content."
|
||||
|
||||
result := spliceLLMContent(template, "Extra analysis.", "Evo details.")
|
||||
|
||||
if !strings.Contains(result, "## Counter-Strategy Spotlight") {
|
||||
t.Error("should append spotlight when no insertion point found")
|
||||
}
|
||||
if !strings.Contains(result, "### Evolution Deep Dive") {
|
||||
t.Error("should append deep dive when no insertion point found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFirstSentence(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"Swarm tactics dominate the meta. Other bots struggle.", "Swarm tactics dominate the meta."},
|
||||
{"Short.", "Short."},
|
||||
{"No sentence end", "No sentence end"},
|
||||
{"Multiple? Yes! Indeed.", "Multiple?"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
result := extractFirstSentence(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("extractFirstSentence(%q) = %q, want %q", tc.input, result, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountWeeklyMatches(t *testing.T) {
|
||||
now := time.Date(2024, 3, 29, 12, 0, 0, 0, time.UTC)
|
||||
data := &IndexData{
|
||||
GeneratedAt: now,
|
||||
Matches: []MatchData{
|
||||
{ID: "m1", PlayedAt: now.Add(-1 * 24 * time.Hour)},
|
||||
{ID: "m2", PlayedAt: now.Add(-3 * 24 * time.Hour)},
|
||||
{ID: "m3", PlayedAt: now.Add(-10 * 24 * time.Hour)}, // outside week
|
||||
{ID: "m4", PlayedAt: now.Add(-5 * 24 * time.Hour)},
|
||||
},
|
||||
}
|
||||
|
||||
count := countWeeklyMatches(data)
|
||||
if count != 3 {
|
||||
t.Errorf("countWeeklyMatches: got %d, want 3", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonEmpty(t *testing.T) {
|
||||
if nonEmpty("", "fallback") != "fallback" {
|
||||
t.Error("empty string should return fallback")
|
||||
}
|
||||
if nonEmpty("value", "fallback") != "value" {
|
||||
t.Error("non-empty string should return itself")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue