refactor(stats): 'Web / unsynced' bucket label → 'Web'; analytics tests isolated from the live reader

The bucket name read as confusing — 'unsynced' implied data loss when
the push is always captured; it only lacks a device credential. The
bucket is just 'Web' now (service fallback, stats.ts fallback, panel
copy, tests).

Also: analytics_test exact-zero assertions (EmptyHistory,
NoReadingHistory) ran against testuser, which is a LIVE reader — real
sessions accrued on it mid-suite and both flapped. They now run under
the fresh regular user, like the stats tests; the 'testuser is a live
reader' rule is recorded in AGENTS.md.
This commit is contained in:
John O'Keefe committed 2026-10-14 13:24:30 -04:00
1 parent 7a604c2fec
commit 5a2c1585ae
7 files changed
+37 -18

No files matched your search

+9 -2
View File
@@ -93,8 +93,12 @@ func TestAnalyticsReadingStats(t *testing.T) {
})
t.Run("GetReadingStats_EmptyHistory", func(t *testing.T) {
// The REGULAR user, deliberately: the shared dev DB carries a
// live reader on `testuser` whose real sessions accrue even
// while the suite runs, so "empty history" only holds for a
// freshly-created account.
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/reading-stats", nil)
req.Header.Set("Authorization", "Bearer "+setup.Token)
req.Header.Set("Authorization", "Bearer "+setup.RegularToken)
resp, err := client.Do(req)
require.NoError(t, err)
@@ -351,8 +355,11 @@ func TestAnalyticsPopularBooks(t *testing.T) {
})
t.Run("GetPopularBooks_NoReadingHistory", func(t *testing.T) {
// The REGULAR user, deliberately: the live dev-reader's sessions
// accrue on `testuser` while the suite runs, so an empty-history
// assertion only holds for a fresh account.
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/analytics/popular-books", nil)
req.Header.Set("Authorization", "Bearer "+setup.Token)
req.Header.Set("Authorization", "Bearer "+setup.RegularToken)
resp, err := client.Do(req)
require.NoError(t, err)
+13 -9
View File
@@ -40,10 +40,10 @@ func middayDaysAgo(daysAgo int) time.Time {
return midnight.AddDate(0, 0, -daysAgo).Add(12 * time.Hour)
}
func statsLogin(t *testing.T, setup *TestServerSetup, path string) (*http.Response, []byte) {
func statsLogin(t *testing.T, setup *TestServerSetup, token, path string) (*http.Response, []byte) {
t.Helper()
req, _ := http.NewRequest("GET", setup.Server.URL+path, nil)
req.Header.Set("Authorization", "Bearer "+setup.Token)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer func() {
@@ -218,7 +218,11 @@ func TestReadingStatsDeviceAttribution(t *testing.T) {
func TestReadingStatsSummary(t *testing.T) {
setup := setupTestServer(t)
userID := getTestUserID(t, setup.DB)
// The REGULAR test user, deliberately: the shared dev DB carries a
// live reader on `testuser` (the dev account genuinely reads while
// the suite runs), and exact-total assertions cannot share an
// account with reality. The fresh regular user has no live pushes.
userID := getRegularUserID(t, setup.DB)
ctx := context.Context(context.Background())
libIDStr := createLibraryDirect(t, setup.DB, userID, fmt.Sprintf("Test Stats Library %d", time.Now().UnixNano()), "ebooks")
@@ -290,7 +294,7 @@ func TestReadingStatsSummary(t *testing.T) {
offRangeBookID = insertSession(400, 120, 40, 0.2, "Stats OffRange Book", false)
// Summary -----------------------------------------------------------
resp, body := statsLogin(t, setup, "/api/stats/summary?range=7")
resp, body := statsLogin(t, setup, setup.RegularToken, "/api/stats/summary?range=7")
require.Equal(t, http.StatusOK, resp.StatusCode)
var summary struct {
@@ -373,7 +377,7 @@ func TestReadingStatsSummary(t *testing.T) {
assert.NotEqual(t, "", dev.DeviceID, "device bucket carries its id")
assert.Equal(t, "koreader", dev.Type)
}
if dev.Name == "Web / unsynced" {
if dev.Name == "Web" {
unattributedFound = true
assert.Equal(t, 75, dev.Minutes, "30 + 45 unattributed sessions")
assert.Equal(t, 2, dev.Sessions)
@@ -381,7 +385,7 @@ func TestReadingStatsSummary(t *testing.T) {
}
}
assert.True(t, kindleFound, "the registered device has its own bucket")
assert.True(t, unattributedFound, "device-less pushes aggregate into 'Web / unsynced'")
assert.True(t, unattributedFound, "device-less pushes aggregate into the Web bucket")
deviceMinutes := 0
for _, dev := range summary.DeviceBreakdown {
deviceMinutes += dev.Minutes
@@ -389,7 +393,7 @@ func TestReadingStatsSummary(t *testing.T) {
assert.Equal(t, 155, deviceMinutes, "device buckets reconcile with the total")
// Range validation ---------------------------------------------------
badResp, badBody := statsLogin(t, setup, "/api/stats/summary?range=fortnight")
badResp, badBody := statsLogin(t, setup, setup.RegularToken, "/api/stats/summary?range=fortnight")
assert.Equal(t, http.StatusBadRequest, badResp.StatusCode)
assert.Contains(t, string(badBody), "invalid range")
@@ -400,7 +404,7 @@ func TestReadingStatsSummary(t *testing.T) {
_ = anonym.Body.Close()
// Widget -------------------------------------------------------------
resp, body = statsLogin(t, setup, "/api/stats/this-week")
resp, body = statsLogin(t, setup, setup.RegularToken, "/api/stats/this-week")
require.Equal(t, http.StatusOK, resp.StatusCode)
var week struct {
WeekMinutes int `json:"week_minutes"`
@@ -417,7 +421,7 @@ func TestReadingStatsSummary(t *testing.T) {
assert.Equal(t, 0, week.PrevMinutes)
// Per-book card (lifetime, so the 400-day book still totals) -----------
resp, body = statsLogin(t, setup, "/api/stats/books/"+offRangeBookID)
resp, body = statsLogin(t, setup, setup.RegularToken, "/api/stats/books/"+offRangeBookID)
require.Equal(t, http.StatusOK, resp.StatusCode)
var bookStats struct {
TotalMinutes int `json:"total_minutes"`