Files
Pete/internal/storage/storage_test.go
prosolis e88483526d Rip out Ollama: direct_route only, no LLM layer
Pete moves to a remote host without Ollama access. Every source must
declare a direct_route channel; the classifier, explainer, semantic
dedup, !explain summaries, feed_hint, and the recent_headlines /
classification_log tables are gone. Deterministic dedup (canonical URL,
headline_norm, per-channel cooldown) remains.
2026-05-24 22:07:13 -07:00

302 lines
8.0 KiB
Go

package storage
import (
"path/filepath"
"testing"
)
func setupTestDB(t *testing.T) {
t.Helper()
// Reset global state
mu.Lock()
if globalDB != nil {
globalDB.Close()
globalDB = nil
}
mu.Unlock()
dbPath := filepath.Join(t.TempDir(), "test.db")
if err := Init(dbPath); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { Close() })
}
func TestInitAndGet(t *testing.T) {
setupTestDB(t)
db := Get()
if db == nil {
t.Fatal("Get() returned nil after Init()")
}
}
func TestIsGUIDSeen_NotSeen(t *testing.T) {
setupTestDB(t)
if IsGUIDSeen("never-seen-guid") {
t.Error("expected false for unseen GUID")
}
}
func TestInsertStoryAndGUIDSeen(t *testing.T) {
setupTestDB(t)
s := &Story{
GUID: "test-guid-1",
Headline: "Test Headline",
Lede: "Test lede sentence.",
ArticleURL: "https://example.com/article",
Source: "Test Source",
SeenAt: 1700000000,
}
if err := InsertStory(s); err != nil {
t.Fatal(err)
}
if !IsGUIDSeen("test-guid-1") {
t.Error("expected true after insert")
}
if IsGUIDSeen("other-guid") {
t.Error("expected false for different GUID")
}
}
func TestInsertStoryDuplicateGUID(t *testing.T) {
setupTestDB(t)
s := &Story{
GUID: "dup-guid", Headline: "H1", ArticleURL: "https://a.com", Source: "S", SeenAt: 1,
}
if err := InsertStory(s); err != nil {
t.Fatal(err)
}
// Second insert with same GUID should fail
err := InsertStory(s)
if err == nil {
t.Error("expected error on duplicate GUID insert")
}
}
func TestMarkClassified(t *testing.T) {
setupTestDB(t)
InsertStory(&Story{
GUID: "g1", Headline: "H1", ArticleURL: "https://a.com", Source: "S", SeenAt: 1,
})
MarkClassified("g1", "tech", "[]")
var channel string
var classified int
Get().QueryRow(`SELECT channel, classified FROM stories WHERE guid = ?`, "g1").
Scan(&channel, &classified)
if channel != "tech" || classified != 1 {
t.Errorf("after MarkClassified: channel=%q classified=%d, want tech/1", channel, classified)
}
}
func TestPostLogAndLookup(t *testing.T) {
setupTestDB(t)
InsertPostLog("story-1", "tech", "$event1:example.org", "")
InsertPostLog("story-2", "politics", "$event2:example.org", "")
guid, channel, found := LookupPostGUID("$event1:example.org")
if !found {
t.Fatal("expected to find event")
}
if guid != "story-1" || channel != "tech" {
t.Errorf("got guid=%q channel=%q", guid, channel)
}
_, _, found = LookupPostGUID("$nonexistent:example.org")
if found {
t.Error("expected not found for unknown event")
}
}
func TestLastPostTimeAndCountInWindow(t *testing.T) {
setupTestDB(t)
if got := GetLastPostTime("tech"); got != 0 {
t.Errorf("empty channel last post = %d, want 0", got)
}
// Insert posts with specific timestamps
Get().Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`,
"g1", "tech", "$e1", 1000)
Get().Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`,
"g2", "tech", "$e2", 2000)
Get().Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`,
"g3", "politics", "$e3", 1500)
if got := GetLastPostTime("tech"); got != 2000 {
t.Errorf("tech last post = %d, want 2000", got)
}
count := CountPostsInWindow("tech", 500)
if count != 2 {
t.Errorf("tech posts in window = %d, want 2", count)
}
count = CountPostsInWindow("tech", 1500)
if count != 1 {
t.Errorf("tech posts after 1500 = %d, want 1", count)
}
count = CountPostsInWindow("politics", 0)
if count != 1 {
t.Errorf("politics posts = %d, want 1", count)
}
}
func TestReactions(t *testing.T) {
setupTestDB(t)
InsertReaction("story-1", "tech", "$react1", "📰", "@user:example.org", 1000)
InsertReaction("story-1", "tech", "$react2", "👍", "@user2:example.org", 1001)
var count int
Get().QueryRow(`SELECT COUNT(*) FROM reactions WHERE post_guid = ?`, "story-1").Scan(&count)
if count != 2 {
t.Errorf("reactions = %d, want 2", count)
}
}
func TestMarshalUnmarshalPlatforms(t *testing.T) {
tests := []struct {
name string
input []string
json string
back []string
}{
{"nil", nil, "[]", nil},
{"empty", []string{}, "[]", nil},
{"single", []string{"pc"}, `["pc"]`, []string{"pc"}},
{"multi", []string{"nintendo-switch", "pc"}, `["nintendo-switch","pc"]`, []string{"nintendo-switch", "pc"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := MarshalPlatforms(tt.input)
if got != tt.json {
t.Errorf("Marshal(%v) = %q, want %q", tt.input, got, tt.json)
}
back := UnmarshalPlatforms(got)
if len(back) != len(tt.back) {
t.Errorf("Unmarshal roundtrip: %v, want %v", back, tt.back)
}
})
}
}
func TestInsertPostLog_DuplicateIgnored(t *testing.T) {
setupTestDB(t)
InsertPostLog("story-1", "tech", "$event1:example.org", "")
// Same guid+channel should be silently ignored
InsertPostLog("story-1", "tech", "$event1-retry:example.org", "")
var count int
Get().QueryRow(`SELECT COUNT(*) FROM post_log WHERE guid = ? AND channel = ?`, "story-1", "tech").Scan(&count)
if count != 1 {
t.Errorf("expected 1 post_log entry, got %d", count)
}
// Different channel should be allowed
InsertPostLog("story-1", "politics", "$event2:example.org", "")
Get().QueryRow(`SELECT COUNT(*) FROM post_log WHERE guid = ?`, "story-1").Scan(&count)
if count != 2 {
t.Errorf("expected 2 post_log entries across channels, got %d", count)
}
}
func TestInsertReaction_DuplicateIgnored(t *testing.T) {
setupTestDB(t)
InsertReaction("story-1", "tech", "$react1", "📰", "@user:example.org", 1000)
// Same event_id should be silently ignored
InsertReaction("story-1", "tech", "$react1", "📰", "@user:example.org", 1001)
var count int
Get().QueryRow(`SELECT COUNT(*) FROM reactions WHERE event_id = ?`, "$react1").Scan(&count)
if count != 1 {
t.Errorf("expected 1 reaction, got %d", count)
}
}
func TestRunMaintenance(t *testing.T) {
setupTestDB(t)
db := Get()
now := nowUnix()
old := now - 100*86400 // 100 days ago
// Insert old + recent stories
InsertStory(&Story{GUID: "old", Headline: "Old", ArticleURL: "https://a.com", Source: "S", Classified: true, SeenAt: old})
InsertStory(&Story{GUID: "new", Headline: "New", ArticleURL: "https://b.com", Source: "S", Classified: true, SeenAt: now})
MarkClassified("old", "tech", "[]")
MarkClassified("new", "tech", "[]")
// Insert old + recent post logs
db.Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`, "old", "tech", "$e1", old)
db.Exec(`INSERT INTO post_log (guid, channel, event_id, posted_at) VALUES (?, ?, ?, ?)`, "new", "tech", "$e2", now)
// Insert old + recent reactions
InsertReaction("old", "tech", "$r1", "👍", "@u:x", old)
InsertReaction("new", "tech", "$r2", "👍", "@u:x", now)
RunMaintenance()
// Old stories should be pruned
var count int
db.QueryRow(`SELECT COUNT(*) FROM stories`).Scan(&count)
if count != 1 {
t.Errorf("stories: expected 1, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM post_log`).Scan(&count)
if count != 1 {
t.Errorf("post_log: expected 1, got %d", count)
}
db.QueryRow(`SELECT COUNT(*) FROM reactions`).Scan(&count)
if count != 1 {
t.Errorf("reactions: expected 1, got %d", count)
}
}
func TestFTS5Search(t *testing.T) {
setupTestDB(t)
InsertStory(&Story{
GUID: "g1", Headline: "Elden Ring DLC breaks sales records", Lede: "FromSoftware celebrates milestone.",
ArticleURL: "https://a.com", Source: "S", SeenAt: 1,
})
InsertStory(&Story{
GUID: "g2", Headline: "Apple unveils new MacBook Pro", Lede: "M5 chip delivers impressive gains.",
ArticleURL: "https://b.com", Source: "S", SeenAt: 2,
})
// Search for Elden Ring
rows, err := Get().Query(`
SELECT s.guid, s.headline FROM stories s
JOIN stories_fts f ON s.id = f.rowid
WHERE stories_fts MATCH 'Elden Ring'
ORDER BY rank`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
var results []string
for rows.Next() {
var guid, headline string
rows.Scan(&guid, &headline)
results = append(results, guid)
}
if len(results) != 1 || results[0] != "g1" {
t.Errorf("FTS5 search results = %v, want [g1]", results)
}
}