feat(runner): manual summarization mode in the run loop
RunOnce now reads the user's mode (GetAutoSummarize) at the start of each pass: - Auto (unchanged): summarize every unseen video. - Manual: still UpsertVideo for every candidate (discovery — the user sees new videos in the list), but skip ProcessNewVideo unless the video is queued (RequestedVideoIDs). A queued video is summarized, then its flag is cleared (ClearSummarizeRequested) so it is not re-processed and the UI drops "Queued". New Stats.SkippedManual counts discovered-but-unqueued videos. The VideoStore port gains the three methods; the existing auto-mode tests set auto:true. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,9 +40,14 @@ func (f *fakeSource) FetchTranscript(_ context.Context, v domain.Video) (domain.
|
||||
|
||||
// fakeStore assigns deterministic ids ("id-"+provider video id) so a pre-seeded
|
||||
// seen set lines up with UpsertVideo output, modelling cross-restart dedup.
|
||||
// auto controls the summarization mode; requested is the manual-mode queue keyed
|
||||
// by store id; cleared records the ids whose queue flag the runner reset.
|
||||
type fakeStore struct {
|
||||
seen map[string]bool
|
||||
upserted []domain.Video
|
||||
seen map[string]bool
|
||||
upserted []domain.Video
|
||||
auto bool
|
||||
requested map[string]bool
|
||||
cleared []string
|
||||
}
|
||||
|
||||
func (f *fakeStore) UpsertVideo(_ context.Context, v domain.Video) (string, error) {
|
||||
@@ -58,6 +63,23 @@ func (f *fakeStore) SeenVideoIDs(_ context.Context, _ string) (map[string]bool,
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) GetAutoSummarize(_ context.Context, _ string) (bool, error) {
|
||||
return f.auto, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) RequestedVideoIDs(_ context.Context, _ string) (map[string]bool, error) {
|
||||
cp := make(map[string]bool, len(f.requested))
|
||||
for k, v := range f.requested {
|
||||
cp[k] = v
|
||||
}
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
func (f *fakeStore) ClearSummarizeRequested(_ context.Context, _, videoID string) error {
|
||||
f.cleared = append(f.cleared, videoID)
|
||||
return nil
|
||||
}
|
||||
|
||||
type fakeSummarizer struct{}
|
||||
|
||||
func (fakeSummarizer) Summarize(_ context.Context, v domain.Video, _ domain.Transcript) (domain.Summary, error) {
|
||||
@@ -91,7 +113,7 @@ func TestRunOnce_SummarizesNewVideos(t *testing.T) {
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vid("v1", "Video 1"), vid("v2", "Video 2")}},
|
||||
}
|
||||
st := &fakeStore{seen: map[string]bool{}}
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: true}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger())
|
||||
@@ -114,7 +136,7 @@ func TestRunOnce_SkipsAlreadySummarized(t *testing.T) {
|
||||
videos: map[string][]domain.Video{"chan1": {vid("v1", "Video 1"), vid("v2", "Video 2")}},
|
||||
}
|
||||
// v1 was summarized in a prior run (durable seen set).
|
||||
st := &fakeStore{seen: map[string]bool{"id-v1": true}}
|
||||
st := &fakeStore{seen: map[string]bool{"id-v1": true}, auto: true}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger())
|
||||
@@ -133,7 +155,7 @@ func TestRunOnce_SkipsVideosWithoutTranscript(t *testing.T) {
|
||||
videos: map[string][]domain.Video{"chan1": {vid("v1", "Video 1")}},
|
||||
transcripts: map[string]domain.Transcript{"v1": {Source: domain.SourceNone}},
|
||||
}
|
||||
st := &fakeStore{seen: map[string]bool{}}
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: true}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger())
|
||||
@@ -145,13 +167,53 @@ func TestRunOnce_SkipsVideosWithoutTranscript(t *testing.T) {
|
||||
require.Empty(t, sink.delivered, "no summary delivered when there is no transcript")
|
||||
}
|
||||
|
||||
func TestRunOnce_ManualMode_SkipsUnrequested(t *testing.T) {
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vid("v1", "Video 1"), vid("v2", "Video 2")}},
|
||||
}
|
||||
// Manual mode, nothing queued: discover (upsert) but summarize nothing.
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: false, requested: map[string]bool{}}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger())
|
||||
|
||||
stats, err := r.RunOnce(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, stats.Candidates)
|
||||
require.Equal(t, 2, stats.SkippedManual, "manual mode skips unqueued videos")
|
||||
require.Equal(t, 0, stats.Summarized)
|
||||
require.Empty(t, sink.delivered, "no summary in manual mode without a request")
|
||||
require.Len(t, st.upserted, 2, "discovery still persists every candidate")
|
||||
}
|
||||
|
||||
func TestRunOnce_ManualMode_ProcessesRequested(t *testing.T) {
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vid("v1", "Video 1"), vid("v2", "Video 2")}},
|
||||
}
|
||||
// Manual mode, v1 queued (by store id). Only v1 is summarized; its flag clears.
|
||||
st := &fakeStore{seen: map[string]bool{}, auto: false, requested: map[string]bool{"id-v1": true}}
|
||||
sink := &recordingSink{}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, sink)
|
||||
r := runner.New(src, st, eng, testUser, quietLogger())
|
||||
|
||||
stats, err := r.RunOnce(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, stats.Summarized, "only the queued video is summarized")
|
||||
require.Equal(t, 1, stats.SkippedManual, "the unqueued video is skipped")
|
||||
require.Len(t, sink.delivered, 1)
|
||||
require.Equal(t, "id-v1", sink.delivered[0].VideoID)
|
||||
require.Equal(t, []string{"id-v1"}, st.cleared, "the queue flag is cleared after summarizing")
|
||||
}
|
||||
|
||||
func TestRunOnce_UpsertsEveryCandidate(t *testing.T) {
|
||||
src := &fakeSource{
|
||||
subs: []domain.Subscription{sub("chan1", "Channel One")},
|
||||
videos: map[string][]domain.Video{"chan1": {vid("v1", "Video 1"), vid("v2", "Video 2")}},
|
||||
}
|
||||
// Even an already-seen video gets upserted so its metadata stays fresh.
|
||||
st := &fakeStore{seen: map[string]bool{"id-v1": true}}
|
||||
st := &fakeStore{seen: map[string]bool{"id-v1": true}, auto: true}
|
||||
eng := usecase.NewEngine(src, fakeSummarizer{}, &recordingSink{})
|
||||
r := runner.New(src, st, eng, testUser, quietLogger())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user