fe1cc46752
Two new helpers in internal/playevents (CaptureContextualLikeIfPlaying, SoftDeleteContextualLikes) called by both api and subsonic surfaces. Like inserts a new contextual_likes row when LikeTrack actually inserted (rows=1) AND there's an open play_event with a vector. Unlike soft-deletes via deleted_at. Idempotent in both directions. Subsonic wiring lands in the next commit.
78 lines
2.5 KiB
Go
78 lines
2.5 KiB
Go
package playevents
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
func TestCaptureContextualLikeIfPlaying_NoOpenEvent_NoOp(t *testing.T) {
|
|
f := newFixture(t, 200_000)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
q := dbq.New(f.pool)
|
|
if err := CaptureContextualLikeIfPlaying(context.Background(), q, f.user, f.track, logger); err != nil {
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
var count int
|
|
_ = f.pool.QueryRow(context.Background(), "SELECT count(*) FROM contextual_likes WHERE user_id=$1", f.user).Scan(&count)
|
|
if count != 0 {
|
|
t.Errorf("count = %d, want 0 (no open play_event)", count)
|
|
}
|
|
}
|
|
|
|
func TestCaptureContextualLikeIfPlaying_OpenEventWithVector_Inserts(t *testing.T) {
|
|
f := newFixture(t, 200_000)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
// Start a play (creates an open play_event with a populated vector).
|
|
at := time.Now().UTC()
|
|
_, err := f.w.RecordPlayStarted(context.Background(), f.user, f.track, "c", at)
|
|
if err != nil {
|
|
t.Fatalf("RecordPlayStarted: %v", err)
|
|
}
|
|
// Like the same track — captures contextual signal.
|
|
q := dbq.New(f.pool)
|
|
if err := CaptureContextualLikeIfPlaying(context.Background(), q, f.user, f.track, logger); err != nil {
|
|
t.Fatalf("Capture: %v", err)
|
|
}
|
|
var rawVec []byte
|
|
if err := f.pool.QueryRow(context.Background(),
|
|
"SELECT session_vector FROM contextual_likes WHERE user_id=$1 AND track_id=$2",
|
|
f.user, f.track).Scan(&rawVec); err != nil {
|
|
t.Fatalf("query: %v", err)
|
|
}
|
|
var vec map[string]any
|
|
_ = json.Unmarshal(rawVec, &vec)
|
|
if vec == nil {
|
|
t.Errorf("vector empty: %s", rawVec)
|
|
}
|
|
}
|
|
|
|
func TestSoftDeleteContextualLikes_MarksActiveRowsDeleted(t *testing.T) {
|
|
f := newFixture(t, 200_000)
|
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
at := time.Now().UTC()
|
|
_, _ = f.w.RecordPlayStarted(context.Background(), f.user, f.track, "c", at)
|
|
q := dbq.New(f.pool)
|
|
_ = CaptureContextualLikeIfPlaying(context.Background(), q, f.user, f.track, logger)
|
|
// Now soft-delete.
|
|
if err := SoftDeleteContextualLikes(context.Background(), q, f.user, f.track); err != nil {
|
|
t.Fatalf("SoftDelete: %v", err)
|
|
}
|
|
var deletedAt pgtype.Timestamptz
|
|
if err := f.pool.QueryRow(context.Background(),
|
|
"SELECT deleted_at FROM contextual_likes WHERE user_id=$1 AND track_id=$2",
|
|
f.user, f.track).Scan(&deletedAt); err != nil {
|
|
t.Fatalf("query: %v", err)
|
|
}
|
|
if !deletedAt.Valid {
|
|
t.Errorf("deleted_at not set after SoftDelete")
|
|
}
|
|
}
|