- Implement atomic state transitions: add EntryService.PatchEntry() with EntryDiff computation and EventEntryTransition event emission. - Add persistent SQLite task queue (JobEngine): add jobs table with crash recovery, exponential backoff retries, concurrency control, and idempotency key deduplication. - Decompose monolithic HTTP handlers: split handlers.go into domain handlers (auth, feeds, entries, jobs, settings, rules, sse). - Implement real-time Server-Sent Events (/api/events/stream) for multi-tab/device state synchronization. - Fix SQLite concurrency: apply busy_timeout(10000) and journal_mode(WAL) via DSN query parameters to resolve SQLITE_BUSY (5). - Fix AI settings & prompt loading: unmarshal JSON from ai_settings, apply configured prompt templates, and enforce strict enabled/API key validations. - Fix undo share state: remove omitempty from ShareUUID to serialize null for state resets. - Fix middle list counter: compute true unread/total stats from DB and clean switcher button labels. - Adapt mobile UI for iOS: add viewport-fit=cover in index.html and calibrate safe-area insets for the bottom navigation and article reader toolbar. - Add architectural documentation and worklog in docs/009_worklog.md.
86 lines
2.4 KiB
Go
86 lines
2.4 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/events"
|
|
"git.pengzhan.dev/haopengzhan/rsspipe/internal/models"
|
|
)
|
|
|
|
func TestEntryServicePatchAndTransition(t *testing.T) {
|
|
tempDBPath := filepath.Join(t.TempDir(), "test.db")
|
|
database, err := db.InitDB(tempDBPath)
|
|
if err != nil {
|
|
t.Fatalf("failed to init db: %v", err)
|
|
}
|
|
defer func() { _ = database.Close() }()
|
|
|
|
bus := events.NewEventBus()
|
|
svc := NewEntryService(database, bus)
|
|
|
|
// Create test category and feed
|
|
cat, _ := database.CreateCategory("Tech", nil)
|
|
feed, _ := database.CreateFeed("Test Feed", "https://example.com/rss", "https://example.com", "", &cat.ID)
|
|
|
|
// Ingest an entry
|
|
entry := models.FeedEntry{
|
|
ID: "e_test_1",
|
|
FeedID: feed.ID,
|
|
GUID: "guid_1",
|
|
URL: "https://example.com/p/1",
|
|
Title: "Test Post",
|
|
Content: "Hello world",
|
|
PublishedAt: time.Now().UTC(),
|
|
}
|
|
|
|
ingested, err := svc.IngestEntries(context.Background(), feed.ID, []models.FeedEntry{entry}, feed.Title, feed.SiteURL)
|
|
if err != nil || len(ingested) == 0 {
|
|
t.Fatalf("failed to ingest entry: %v", err)
|
|
}
|
|
|
|
// 1. Test patching tags
|
|
isStarred := true
|
|
updated, diff, err := svc.PatchEntry(context.Background(), ingested[0].ID, models.PatchEntryRequest{
|
|
IsStarred: &isStarred,
|
|
Metadata: map[string]interface{}{
|
|
"tags": []string{"AI", "Kubernetes"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to patch entry: %v", err)
|
|
}
|
|
|
|
if !updated.IsStarred {
|
|
t.Errorf("expected entry to be starred")
|
|
}
|
|
if !diff.HasFieldChange("is_starred") {
|
|
t.Errorf("expected diff to contain is_starred change")
|
|
}
|
|
|
|
// 2. Test patching variant addition (readability_full_text)
|
|
fullTextVariant := &models.EntryVariant{
|
|
ContentType: "html",
|
|
TextValue: "<p>Full article extracted content...</p>",
|
|
}
|
|
|
|
updated2, diff2, err := svc.PatchEntry(context.Background(), ingested[0].ID, models.PatchEntryRequest{
|
|
Variants: map[string]*models.EntryVariant{
|
|
"readability_full_text": fullTextVariant,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("failed to patch variant: %v", err)
|
|
}
|
|
|
|
if !diff2.HasVariantAddition("readability_full_text") {
|
|
t.Errorf("expected diff2 to report variant addition of readability_full_text")
|
|
}
|
|
if updated2.Variants["readability_full_text"].TextValue == "" {
|
|
t.Errorf("expected variant to be loaded in updated entry")
|
|
}
|
|
}
|