refactor(core): overhaul state transition architecture, persistent job queue, and API layer
- 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.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user