feat(store): TranscriptStatus read field + status setter/loader
Surfaces videos.transcript_status (migration 007) on SummaryRow and adds SetTranscriptStatus / GetTranscriptStatus / RateLimitedVideoIDs. SetTranscriptStatus is the single choke point for the rate-limit lifecycle: "rate_limited" stamps rate_limited_at = NOW(), every other status clears it, so the runner's backoff window and the UI badge read one consistent source. RateLimitedVideoIDs is the per-pass loader (mirrors SeenVideoIDs) the runner uses to skip still-throttled videos without re-hitting the caption endpoint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
||||
)
|
||||
|
||||
func TestSetTranscriptStatus_RoundTrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
id, err := s.UpsertVideo(ctx, ytVideo(userA, "rt12345", "round trip"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Unset by default.
|
||||
got, err := s.GetTranscriptStatus(ctx, userA, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", got)
|
||||
|
||||
for _, status := range []string{"none", "fetched", "rate_limited", ""} {
|
||||
require.NoError(t, s.SetTranscriptStatus(ctx, userA, id, status))
|
||||
got, err := s.GetTranscriptStatus(ctx, userA, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, status, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetTranscriptStatus_RejectsInvalid(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
id, err := s.UpsertVideo(ctx, ytVideo(userA, "bad12345", "bad status"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Error(t, s.SetTranscriptStatus(ctx, userA, id, "bogus"))
|
||||
|
||||
// The rejected write left the status untouched.
|
||||
got, err := s.GetTranscriptStatus(ctx, userA, id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", got)
|
||||
}
|
||||
|
||||
func TestSetTranscriptStatus_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
require.ErrorIs(t, s.SetTranscriptStatus(ctx, userA, videoX, "fetched"), store.ErrNotFound)
|
||||
|
||||
_, err := s.GetTranscriptStatus(ctx, userA, videoX)
|
||||
require.ErrorIs(t, err, store.ErrNotFound)
|
||||
}
|
||||
|
||||
func TestRateLimitedVideoIDs_StampsAndClears(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
id, err := s.UpsertVideo(ctx, ytVideo(userA, "rl12345", "rate limited"))
|
||||
require.NoError(t, err)
|
||||
|
||||
// Marking rate_limited stamps rate_limited_at, so the video appears.
|
||||
require.NoError(t, s.SetTranscriptStatus(ctx, userA, id, "rate_limited"))
|
||||
rl, err := s.RateLimitedVideoIDs(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, rl, id)
|
||||
require.False(t, rl[id].IsZero(), "rate_limited_at must be stamped")
|
||||
|
||||
// Moving off rate_limited clears the timestamp, so it drops out.
|
||||
require.NoError(t, s.SetTranscriptStatus(ctx, userA, id, "fetched"))
|
||||
rl, err = s.RateLimitedVideoIDs(ctx, userA)
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, rl, id)
|
||||
}
|
||||
Reference in New Issue
Block a user