feat(runner): bound auto-summarize to a recency window
In automatic mode the scheduler now only summarizes videos published within TAPIR_AUTO_SUMMARIZE_WINDOW (default ~7d). Older videos are still discovered and listed — they keep the manual "Summarize" affordance — but are not auto-processed, so a large back-catalogue (the maintainer's ~256-deep queue) stops self-inflicting 429s against the per-IP caption gate each cycle (UX review B1, recency design). - runner.WithAutoWindow + Stats.SkippedTooOld; tooOld() treats a zero window as disabled and an undated video as never-aged-out (processed, not stranded). - An explicit manual request bypasses the bound even in auto mode (requested videos are loaded in auto mode when a window is active). - Wired through cmdRun, the scheduler's per-user runner, sumStats, and pass logging. config: TAPIR_AUTO_SUMMARIZE_WINDOW (default 168h), .env.example. - Account copy (A7) updated to match: "Automatic summarizes new videos from about the last week; older videos stay browsable — summarize on demand." The rate gate is untouched; the manual path still serialises through it. This bounds auto LOAD, it does not fetch harder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -234,6 +234,96 @@ func TestRunOnce_ManualMode_ProcessesRequested(t *testing.T) {
|
||||
require.Equal(t, []string{"id-v1"}, st.cleared, "the queue flag is cleared after summarizing")
|
||||
}
|
||||
|
||||
// --- recency window (B1) ---------------------------------------------------
|
||||
|
||||
// TestRunOnce_AutoMode_SkipsOldVideos: with a recency window set, auto mode
|
||||
// summarizes only videos published within the window; older ones are discovered
|
||||
// (upserted) but not auto-summarized — they wait for a manual request.
|
||||
func TestRunOnce_AutoMode_SkipsOldVideos(t *testing.T) {
|
||||
base := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {
|
||||
vidAt("recent", "Recent", base.Add(-24*time.Hour)), // 1d old → in window
|
||||
vidAt("old", "Old", base.Add(-30*24*time.Hour)), // 30d old → out of window
|
||||
}},
|
||||
}
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: true}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger(),
|
||||
runner.WithAutoWindow(7*24*time.Hour), runner.WithClock(func() time.Time { return base }))
|
||||
|
||||
stats, err := r.RunOnce(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, stats.Summarized, "only the recent video is auto-summarized")
|
||||
require.Equal(t, 1, stats.SkippedTooOld, "the old video is skipped by the recency bound")
|
||||
require.Len(t, sink.delivered, 1)
|
||||
require.Equal(t, "id-recent", sink.delivered[0].VideoID)
|
||||
require.Len(t, st.upserted, 2, "both videos are still discovered and listed")
|
||||
}
|
||||
|
||||
// TestRunOnce_AutoMode_OldVideoRequestedBypassesWindow: an explicit manual
|
||||
// request (summarize_requested) overrides the recency bound even in auto mode.
|
||||
func TestRunOnce_AutoMode_OldVideoRequestedBypassesWindow(t *testing.T) {
|
||||
base := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vidAt("old", "Old", base.Add(-30*24*time.Hour))}},
|
||||
}
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: true, requested: map[string]bool{"id-old": true}}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger(),
|
||||
runner.WithAutoWindow(7*24*time.Hour), runner.WithClock(func() time.Time { return base }))
|
||||
|
||||
stats, err := r.RunOnce(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, stats.Summarized, "a requested old video is summarized despite the window")
|
||||
require.Equal(t, 0, stats.SkippedTooOld)
|
||||
require.Len(t, sink.delivered, 1)
|
||||
}
|
||||
|
||||
// TestRunOnce_AutoWindowZero_SummarizesOld: a zero window disables the bound —
|
||||
// the pre-recency behaviour (summarize every unseen video) is preserved.
|
||||
func TestRunOnce_AutoWindowZero_SummarizesOld(t *testing.T) {
|
||||
base := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vidAt("old", "Old", base.Add(-365*24*time.Hour))}},
|
||||
}
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: true}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger(),
|
||||
runner.WithClock(func() time.Time { return base })) // no WithAutoWindow → 0
|
||||
|
||||
stats, err := r.RunOnce(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, stats.Summarized, "window disabled → old video summarized")
|
||||
require.Equal(t, 0, stats.SkippedTooOld)
|
||||
}
|
||||
|
||||
// TestRunOnce_AutoMode_UndatedVideoSummarized: a video with no published_at
|
||||
// cannot be aged out — it is processed, not silently stranded.
|
||||
func TestRunOnce_AutoMode_UndatedVideoSummarized(t *testing.T) {
|
||||
base := time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vid("undated", "Undated")}}, // zero PublishedAt
|
||||
}
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: true}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger(),
|
||||
runner.WithAutoWindow(7*24*time.Hour), runner.WithClock(func() time.Time { return base }))
|
||||
|
||||
stats, err := r.RunOnce(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, stats.Summarized, "an undated video is processed, not aged out")
|
||||
require.Equal(t, 0, stats.SkippedTooOld)
|
||||
}
|
||||
|
||||
// noFetchSource fails the test if a transcript fetch happens — used to prove the
|
||||
// runner skips a rate-limited video before touching the caption endpoint.
|
||||
type noFetchSource struct{ *fakeSource }
|
||||
|
||||
Reference in New Issue
Block a user