feat(store): summarization-mode methods + all-videos read
Add the store surface for summarization mode: - SetAutoSummarize / GetAutoSummarize — per-user auto/manual toggle; an absent user reads as manual (the safe default). - RequestSummarize — queue one video (summarize_requested=TRUE); ErrNotFound when the video is absent or not owned (RLS hides another user's row). - RequestedVideoIDs / ClearSummarizeRequested — the run-loop side: load the queued set per pass (mirrors SeenVideoIDs), clear after summarizing. - ListVideos / GetVideoRow — drive from the videos table LEFT JOIN summaries so discovered-but-unsummarized videos appear with empty summary fields. SummaryRow gains additive Summarized + SummarizeRequested fields; the summary-only reads are untouched. RLS proof: rls_test.go gains a cross-user "queue B's video" write asserting it touches zero rows (store-level scoping can't prove this — the test pool is a superuser that bypasses RLS, same caveat documented there). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
||||
)
|
||||
|
||||
// seedBareVideo inserts a videos row with no summary, so the all-videos read and
|
||||
// the manual-queue flag can be exercised without a delivered summary.
|
||||
func seedBareVideo(t *testing.T, p *pgxpool.Pool, userID, videoID, title string) {
|
||||
t.Helper()
|
||||
_, err := p.Exec(context.Background(),
|
||||
`INSERT INTO users (id) VALUES ($1) ON CONFLICT (id) DO NOTHING`, userID)
|
||||
require.NoError(t, err)
|
||||
_, err = p.Exec(context.Background(),
|
||||
`INSERT INTO videos (id, user_id, provider, provider_video_id, title)
|
||||
VALUES ($1, $2, 'youtube', $3, $4)`,
|
||||
videoID, userID, "pv-"+videoID[:8], title)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAutoSummarizeRoundTripDefaultsFalse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
// Unknown / fresh user defaults to manual (false).
|
||||
got, err := s.GetAutoSummarize(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.False(t, got, "default mode is manual")
|
||||
|
||||
require.NoError(t, s.SetAutoSummarize(ctx, userA, true))
|
||||
got, err = s.GetAutoSummarize(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.True(t, got, "set to automatic round-trips")
|
||||
|
||||
require.NoError(t, s.SetAutoSummarize(ctx, userA, false))
|
||||
got, err = s.GetAutoSummarize(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.False(t, got, "set back to manual round-trips")
|
||||
}
|
||||
|
||||
func TestRequestSummarizeSetsFlag(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
seedBareVideo(t, p, userA, videoX, "X Title")
|
||||
|
||||
require.NoError(t, s.RequestSummarize(ctx, userA, videoX))
|
||||
|
||||
requested, err := s.RequestedVideoIDs(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, map[string]bool{videoX: true}, requested)
|
||||
|
||||
// Clearing drops it from the requested set.
|
||||
require.NoError(t, s.ClearSummarizeRequested(ctx, userA, videoX))
|
||||
requested, err = s.RequestedVideoIDs(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, requested)
|
||||
}
|
||||
|
||||
func TestRequestSummarizeMissingVideo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
err := s.RequestSummarize(ctx, userA, videoX)
|
||||
require.ErrorIs(t, err, store.ErrNotFound, "queuing a non-existent video reports not found")
|
||||
}
|
||||
|
||||
func TestListVideosReturnsSummarizedAndUnsummarized(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
|
||||
// videoX: discovered AND summarized. videoY: discovered, not yet summarized.
|
||||
seedBareVideo(t, p, userA, videoX, "Summarized One")
|
||||
seedBareVideo(t, p, userA, videoY, "Pending One")
|
||||
require.NoError(t, s.Deliver(ctx, summary(userA, videoX, "body x")))
|
||||
require.NoError(t, s.RequestSummarize(ctx, userA, videoY))
|
||||
|
||||
rows, err := s.ListVideos(ctx, userA, 50)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rows, 2, "both summarized and unsummarized videos are listed")
|
||||
|
||||
byID := map[string]store.SummaryRow{}
|
||||
for _, r := range rows {
|
||||
byID[r.VideoID] = r
|
||||
}
|
||||
|
||||
require.True(t, byID[videoX].Summarized)
|
||||
require.Equal(t, "body x", byID[videoX].Summary)
|
||||
require.False(t, byID[videoX].SummarizeRequested)
|
||||
|
||||
require.False(t, byID[videoY].Summarized, "no summary -> Summarized false")
|
||||
require.Empty(t, byID[videoY].Summary, "unsummarized row has empty summary")
|
||||
require.True(t, byID[videoY].SummarizeRequested, "queued video carries the flag")
|
||||
}
|
||||
|
||||
func TestListVideosIsUserScoped(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
seedBareVideo(t, p, userA, videoX, "A only")
|
||||
|
||||
rows, err := s.ListVideos(ctx, userB, 50)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, rows, "user B must not see user A's videos")
|
||||
}
|
||||
|
||||
func TestGetVideoRowNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
_, err := s.GetVideoRow(ctx, userA, videoX)
|
||||
require.ErrorIs(t, err, store.ErrNotFound)
|
||||
}
|
||||
Reference in New Issue
Block a user