refactor(core): overhaul state transition architecture, persistent job queue, and API layer
CI Quality Checks & Tests / Gitleaks Secret Detection (push) Failing after 1m59s
CI Quality Checks & Tests / Go Lint & Unit Tests (push) Failing after 2m26s
CI Quality Checks & Tests / Frontend Typecheck & Build (push) Successful in 2m37s

- 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:
2026-09-01 00:41:05 -07:00
parent 271a542196
commit ebcaa5d72a
38 changed files with 3748 additions and 1202 deletions
+85
View File
@@ -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")
}
}