fix(sync): the offline queue flush never processed anything — fix the enumerator + the dropped payload
Found while verifying H1's "app offline-play -> flush -> position lands"
with the emulator path: the flush has been dead code in two independent
ways, invisible because the first break made the second unreachable.
1. ListPendingSyncQueueItems matched `device_id = $1` with a NULL $1
(the processor passes DeviceID{Valid:false} to enumerate all devices;
uuid = NULL matches no rows). The ticker therefore processed nothing,
ever. The WHERE now treats NULL as "every device" (the same
narg::uuid IS NULL idiom the visibility queries use; EXPLAIN'd — seq
scan on a bounded table, idx_sync_queue_status_priority_created
fits). The processor also dropped its fake-pagination loop, which
re-fetched the same first batch forever once the backlog hit the
batch size (no OFFSET in the query).
2. dbItemToSyncQueueItem dropped SyncData — even with a working
enumerator, every flushed item arrived with an empty payload and
failed JSON unmarshal. Dead code harboring silent breakage, exactly
the plan's thesis.
Callers updated for the renamed RowLimit param (queue handler, offline
detector). TestH1_SyncQueueFlush_LandsAndPreservesAudio drives the real
flow: web PUT seeds audio 900s, a pending checkpoint-style queue row
applies percentage 0.8 through SaveProgress, audio survives, queue row
completes.
Also fixes dashboard_test.go's stale BuildSections signature (the
package's test binary has been unbuildable) — user-approved: tests
designed against old shapes are not worth keeping.
This commit is contained in:
1 parent
d16ca4acbe
commit
6808c1babb
8 files changed
+101
-39
No files matched your search
@@ -831,10 +831,74 @@ func TestContractPin_SSRPages(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestH1_SyncQueueFlush_LandsAndPreservesAudio proves the offline flush
|
||||
// end-to-end: a pending sync_queue row (what the app's checkpoint sync
|
||||
// enqueues) must be picked up by the running processor, funnel through
|
||||
// SaveProgress (winner percentage lands), and NOT erase row fields the
|
||||
// queue snapshot doesn't carry (audio_offset_seconds survives).
|
||||
// NOTE: before 2026-10-17 this test could not pass — the processor's
|
||||
// enumerator passed a NULL device_id into `device_id = $1`, which matches
|
||||
// no rows, so the flush loop never ran.
|
||||
func TestH1_SyncQueueFlush_LandsAndPreservesAudio(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
setup := setupTestServer(t)
|
||||
fx := createContractFixture(t, setup)
|
||||
deviceToken := createContractDeviceNamed(t, setup, "koreader", "koreader")
|
||||
|
||||
// Seed the stored position with audio via the web path.
|
||||
put := doReq(t, "PUT", progressURL(setup.Server.URL, fx.itemID), map[string]interface{}{
|
||||
"percentage": 0.5,
|
||||
"audio_offset_seconds": 900,
|
||||
}, setup.Token)
|
||||
defer put.Body.Close()
|
||||
require.Equal(t, http.StatusOK, put.StatusCode)
|
||||
|
||||
// Enqueue a checkpoint-style progress entry (percentage only, no
|
||||
// audio — exactly what buildProgressSnapshot captures).
|
||||
itemUUID, perr := uuid.Parse(fx.itemID)
|
||||
require.NoError(t, perr)
|
||||
deviceRow, derr := setup.DB.GetDeviceByAuthToken(context.Background(), deviceToken)
|
||||
require.NoError(t, derr)
|
||||
syncData, _ := json.Marshal(map[string]interface{}{"percentage": 0.8})
|
||||
_, qerr := setup.DB.CreateSyncQueueItem(context.Background(), database.CreateSyncQueueItemParams{
|
||||
DeviceID: deviceRow.ID,
|
||||
MediaItemID: pgtype.UUID{Bytes: itemUUID, Valid: true},
|
||||
SyncType: "progress",
|
||||
SyncData: syncData,
|
||||
Priority: pgtype.Int4{Int32: 5, Valid: true},
|
||||
MaxAttempts: pgtype.Int4{Int32: 3, Valid: true},
|
||||
Status: pgtype.Text{String: "pending", Valid: true},
|
||||
})
|
||||
require.NoError(t, qerr)
|
||||
|
||||
// The processor ticks every 5s; poll up to 15s for the flush.
|
||||
var row map[string]interface{}
|
||||
landed := false
|
||||
for i := 0; i < 30 && !landed; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
get := doReq(t, "GET", progressURL(setup.Server.URL, fx.itemID), nil, setup.Token)
|
||||
if get.StatusCode != http.StatusOK {
|
||||
get.Body.Close()
|
||||
continue
|
||||
}
|
||||
row = decodeJSON(t, get)
|
||||
get.Body.Close()
|
||||
if pct, ok := row["percentage"].(float64); ok && pct > 0.79 {
|
||||
landed = true
|
||||
}
|
||||
}
|
||||
require.True(t, landed, "the queued progress (0.8) should flush onto the stored row")
|
||||
require.Equal(t, float64(900), getFloatField(t, row, "audio_offset_seconds"),
|
||||
"the flush must preserve audio_offset_seconds (LWW seed), not NULL it")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 6. H1 verification — resolving a conflict must preserve unknown fields
|
||||
// 7. H1 verification — resolving a conflict must preserve unknown fields
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
// TestH1_ConflictResolutionPreservesAudio is the executable check for the
|
||||
// hardening plan's H1 done-when: a resolution applied via
|
||||
// POST /api/conflicts/:id/resolve (the endpoint the web UI and the
|
||||
|
||||
@@ -192,7 +192,7 @@ func TestSyncQueueProcessor(t *testing.T) {
|
||||
// Verify queue item was created
|
||||
items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{
|
||||
DeviceID: deviceID,
|
||||
Limit: int32(10),
|
||||
RowLimit: int32(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(items), "Should have one queue item")
|
||||
@@ -235,7 +235,7 @@ func TestSyncConflictDetection(t *testing.T) {
|
||||
// Verify both exist (conflict detection would happen during processing)
|
||||
items, err := db.ListPendingSyncQueueItems(ctx, database.ListPendingSyncQueueItemsParams{
|
||||
DeviceID: deviceID,
|
||||
Limit: int32(10),
|
||||
RowLimit: int32(10),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, len(items), "Should have one queue item")
|
||||
|
||||
Reference in new issue
Block a user