From f84f1c0e26908e2a70777441e1174a26dca10b91 Mon Sep 17 00:00:00 2001 From: Sam Scholten Date: Sun, 30 Aug 2026 20:51:41 +1000 Subject: serve: POST /api/poll manual trigger; rename feed to Fluxrecs /api/poll runs an out-of-schedule poll through the same serialized path as the scheduler (TryLock, so curls and refreshes can't race the cursor), is POST-only, and 429s inside a 1-minute cooldown after any run. Response is JSON: polled/new_run plus the fresh row's stats when entries arrived. Also rename the RSS channel to Fluxrecs (Miniflux adopts upstream title changes as the feed name). just poll-now wraps the curl. --- cmds/serve_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) (limited to 'cmds/serve_test.go') diff --git a/cmds/serve_test.go b/cmds/serve_test.go index e8aec19..509de6c 100644 --- a/cmds/serve_test.go +++ b/cmds/serve_test.go @@ -384,7 +384,9 @@ func TestServeRoutes(t *testing.T) { } cmd := &ServeCommand{Runs: runsPath, RSSBatches: 21} - handler := cmd.routes([]int{360, 720, 1080}, time.Now().Add(-48*time.Hour)) + pollCalls := 0 + pollStub := func() error { pollCalls++; return nil } + handler := cmd.routes([]int{360, 720, 1080}, time.Now().Add(-48*time.Hour), pollStub) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/recommendations.xml", nil)) @@ -422,3 +424,56 @@ func TestServeRoutes(t *testing.T) { t.Errorf("model_age_hours = %v, want positive", status["model_age_hours"]) } } + +func TestManualPollRoute(t *testing.T) { + dir := t.TempDir() + runsPath := filepath.Join(dir, "runs.jsonl") + // Last run hours ago: well outside the manual-poll cooldown. + if err := core.AppendRun(runsPath, core.RunRow{ + RanAt: time.Now().Add(-time.Hour), EntriesSeen: 3, + }); err != nil { + t.Fatalf("AppendRun: %v", err) + } + + pollCalls := 0 + pollStub := func() error { pollCalls++; return nil } + cmd := &ServeCommand{Runs: runsPath} + handler := cmd.routes([]int{360, 720, 1080}, time.Now(), pollStub) + + // GET is rejected; only POST triggers. + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/poll", nil)) + if rec.Code != http.StatusMethodNotAllowed { + t.Errorf("GET /api/poll = %d, want 405", rec.Code) + } + + // POST runs the poll; the stub wrote no row, so no new run is reported. + rec = httptest.NewRecorder() + handler.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/poll", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("POST /api/poll = %d, want 200", rec.Code) + } + if pollCalls != 1 { + t.Errorf("poll called %d times, want 1", pollCalls) + } + var resp map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { + t.Fatalf("response not JSON: %v", err) + } + if resp["polled"] != true || resp["new_run"] != false { + t.Errorf("response = %v, want polled=true new_run=false", resp) + } + + // A fresh run row puts the next POST inside the cooldown window. + if err := core.AppendRun(runsPath, core.RunRow{RanAt: time.Now(), EntriesSeen: 5}); err != nil { + t.Fatalf("AppendRun: %v", err) + } + rec = httptest.NewRecorder() + handler.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/api/poll", nil)) + if rec.Code != http.StatusTooManyRequests { + t.Errorf("POST inside cooldown = %d, want 429", rec.Code) + } + if pollCalls != 1 { + t.Errorf("poll called %d times, want still 1 (cooldown)", pollCalls) + } +} -- cgit v1.2.3