93821ab214
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
423 lines
12 KiB
Go
423 lines
12 KiB
Go
package component
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"homelab.lan/music-agregator/internal"
|
|
"homelab.lan/music-agregator/internal/database"
|
|
)
|
|
|
|
func insertTestArtistAndAlbum(t *testing.T, suite *testSuite) string {
|
|
ctx := context.Background()
|
|
|
|
var artistID string
|
|
err := suite.pool.QueryRow(ctx, `
|
|
INSERT INTO artists (external_id, name, artist_type, country, genres, image_url, monitor_state)
|
|
VALUES ('test-artist-ext', 'Test Artist', 'person', 'US', ARRAY['Rock'], 'http://img.com', 'monitored')
|
|
RETURNING id
|
|
`).Scan(&artistID)
|
|
require.NoError(t, err)
|
|
|
|
var albumID string
|
|
err = suite.pool.QueryRow(ctx, `
|
|
INSERT INTO albums (external_id, artist_id, title, album_type, total_tracks, total_discs, label, genres, cover_url, monitor_state)
|
|
VALUES ('test-album-ext', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
|
|
RETURNING id
|
|
`, artistID).Scan(&albumID)
|
|
require.NoError(t, err)
|
|
|
|
return albumID
|
|
}
|
|
|
|
func TestWorkflowRun_Create(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{
|
|
AlbumID: albumID,
|
|
Quality: "flac",
|
|
}
|
|
err := repo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEmpty(t, run.ID)
|
|
assert.False(t, run.StartedAt.IsZero())
|
|
|
|
fetched, err := repo.GetByID(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "running", fetched.Status)
|
|
}
|
|
|
|
func TestWorkflowRun_DuplicateRunningRejected(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run1 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run1)
|
|
require.NoError(t, err)
|
|
|
|
run2 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err = repo.Create(ctx, run2)
|
|
assert.ErrorIs(t, err, database.ErrWorkflowAlreadyRunning)
|
|
}
|
|
|
|
func TestWorkflowRun_DuplicateAllowedAfterCompletion(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run1 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run1)
|
|
require.NoError(t, err)
|
|
|
|
err = repo.SetCompleted(ctx, run1.ID)
|
|
require.NoError(t, err)
|
|
|
|
run2 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err = repo.Create(ctx, run2)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, run2.ID)
|
|
assert.NotEqual(t, run1.ID, run2.ID)
|
|
}
|
|
|
|
func TestWorkflowRun_SetCompleted(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
err = repo.SetCompleted(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
|
|
updated, err := repo.GetByID(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "completed", updated.Status)
|
|
assert.NotNil(t, updated.CompletedAt)
|
|
}
|
|
|
|
func TestWorkflowRun_SetFailed(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
err = repo.SetFailed(ctx, run.ID, "something went wrong")
|
|
require.NoError(t, err)
|
|
|
|
updated, err := repo.GetByID(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "failed", updated.Status)
|
|
require.NotNil(t, updated.ErrorMessage)
|
|
assert.Equal(t, "something went wrong", *updated.ErrorMessage)
|
|
}
|
|
|
|
func TestWorkflowRun_SetCancelled(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
err = repo.SetCancelled(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
|
|
updated, err := repo.GetByID(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "cancelled", updated.Status)
|
|
}
|
|
|
|
func TestWorkflowRun_GetByAlbumAndQuality(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
found, err := repo.GetByAlbumAndQuality(ctx, albumID, "flac")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, run.ID, found.ID)
|
|
|
|
_, err = repo.GetByAlbumAndQuality(ctx, albumID, "mp3")
|
|
assert.True(t, errors.Is(err, pgx.ErrNoRows) || err != nil)
|
|
}
|
|
|
|
func TestWorkflowRun_GetRunning(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
repo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run1 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := repo.Create(ctx, run1)
|
|
require.NoError(t, err)
|
|
|
|
run2 := &database.WorkflowRun{AlbumID: albumID, Quality: "mp3"}
|
|
err = repo.Create(ctx, run2)
|
|
require.NoError(t, err)
|
|
|
|
run3 := &database.WorkflowRun{AlbumID: albumID, Quality: "opus"}
|
|
err = repo.Create(ctx, run3)
|
|
require.NoError(t, err)
|
|
err = repo.SetCompleted(ctx, run3.ID)
|
|
require.NoError(t, err)
|
|
|
|
running, err := repo.GetRunning(ctx)
|
|
require.NoError(t, err)
|
|
assert.Len(t, running, 2)
|
|
|
|
ids := []string{running[0].ID, running[1].ID}
|
|
assert.Contains(t, ids, run1.ID)
|
|
assert.Contains(t, ids, run2.ID)
|
|
}
|
|
|
|
func TestAlbumEvent_Create(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
workflowRepo := database.NewWorkflowRunRepository(suite.pool)
|
|
eventRepo := database.NewAlbumEventRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := workflowRepo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
event := &database.AlbumEvent{
|
|
WorkflowRunID: run.ID,
|
|
AlbumID: albumID,
|
|
EventType: "info",
|
|
Step: "searching",
|
|
Message: "started search",
|
|
DataJSON: []byte(`{"query":"test"}`),
|
|
}
|
|
err = eventRepo.Create(ctx, event)
|
|
require.NoError(t, err)
|
|
|
|
assert.NotEmpty(t, event.ID)
|
|
assert.Greater(t, event.Seq, int64(0))
|
|
}
|
|
|
|
func TestAlbumEvent_GetByWorkflowRun(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
workflowRepo := database.NewWorkflowRunRepository(suite.pool)
|
|
eventRepo := database.NewAlbumEventRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := workflowRepo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
for i := 0; i < 3; i++ {
|
|
event := &database.AlbumEvent{
|
|
WorkflowRunID: run.ID,
|
|
AlbumID: albumID,
|
|
EventType: "info",
|
|
Step: "step",
|
|
Message: "msg",
|
|
}
|
|
err = eventRepo.Create(ctx, event)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
events, err := eventRepo.GetByWorkflowRun(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 3)
|
|
assert.Less(t, events[0].Seq, events[1].Seq)
|
|
assert.Less(t, events[1].Seq, events[2].Seq)
|
|
}
|
|
|
|
func TestAlbumEvent_GetByAlbum(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
workflowRepo := database.NewWorkflowRunRepository(suite.pool)
|
|
eventRepo := database.NewAlbumEventRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := workflowRepo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
var seqs []int64
|
|
for i := 0; i < 5; i++ {
|
|
event := &database.AlbumEvent{
|
|
WorkflowRunID: run.ID,
|
|
AlbumID: albumID,
|
|
EventType: "info",
|
|
Step: "step",
|
|
Message: "msg",
|
|
}
|
|
err = eventRepo.Create(ctx, event)
|
|
require.NoError(t, err)
|
|
seqs = append(seqs, event.Seq)
|
|
}
|
|
|
|
events, err := eventRepo.GetByAlbum(ctx, albumID, seqs[1], 10)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 3)
|
|
assert.Equal(t, seqs[2], events[0].Seq)
|
|
assert.Equal(t, seqs[3], events[1].Seq)
|
|
assert.Equal(t, seqs[4], events[2].Seq)
|
|
}
|
|
|
|
func TestAlbumEvent_GetLatestSeq(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
eventRepo := database.NewAlbumEventRepository(suite.pool)
|
|
|
|
seq, err := eventRepo.GetLatestSeq(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(0), seq)
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
workflowRepo := database.NewWorkflowRunRepository(suite.pool)
|
|
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err = workflowRepo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
var lastSeq int64
|
|
for i := 0; i < 3; i++ {
|
|
event := &database.AlbumEvent{
|
|
WorkflowRunID: run.ID,
|
|
AlbumID: albumID,
|
|
EventType: "info",
|
|
Step: "step",
|
|
Message: "msg",
|
|
}
|
|
err = eventRepo.Create(ctx, event)
|
|
require.NoError(t, err)
|
|
lastSeq = event.Seq
|
|
}
|
|
|
|
seq, err = eventRepo.GetLatestSeq(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, lastSeq, seq)
|
|
}
|
|
|
|
func TestRecovery_StaleWorkflowWithDownload(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
|
|
workflowRepo := database.NewWorkflowRunRepository(suite.pool)
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := workflowRepo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
_, err = suite.pool.Exec(ctx, `
|
|
UPDATE workflow_runs SET started_at = NOW() - INTERVAL '10 minutes' WHERE id = $1
|
|
`, run.ID)
|
|
require.NoError(t, err)
|
|
|
|
var torrentID string
|
|
err = suite.pool.QueryRow(ctx, `
|
|
INSERT INTO torrents (album_id, info_hash, tracker, title, format, quality, source, seeders, peers, size)
|
|
VALUES ($1, 'test-hash', 'test-tracker', 'Test Torrent', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
|
|
RETURNING id
|
|
`, albumID).Scan(&torrentID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = suite.pool.Exec(ctx, `
|
|
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
|
|
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'test-hash')
|
|
`, torrentID, albumID)
|
|
require.NoError(t, err)
|
|
|
|
service := createRecoveryTestService(suite)
|
|
service.RecoverWorkflows(ctx)
|
|
|
|
updated, err := workflowRepo.GetByID(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "completed", updated.Status)
|
|
}
|
|
|
|
func TestRecovery_StaleWorkflowWithoutDownload(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
ctx := context.Background()
|
|
|
|
albumID := insertTestArtistAndAlbum(t, suite)
|
|
|
|
workflowRepo := database.NewWorkflowRunRepository(suite.pool)
|
|
run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"}
|
|
err := workflowRepo.Create(ctx, run)
|
|
require.NoError(t, err)
|
|
|
|
_, err = suite.pool.Exec(ctx, `
|
|
UPDATE workflow_runs SET started_at = NOW() - INTERVAL '10 minutes' WHERE id = $1
|
|
`, run.ID)
|
|
require.NoError(t, err)
|
|
|
|
service := createRecoveryTestService(suite)
|
|
service.RecoverWorkflows(ctx)
|
|
|
|
updated, err := workflowRepo.GetByID(ctx, run.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "failed", updated.Status)
|
|
require.NotNil(t, updated.ErrorMessage)
|
|
assert.Contains(t, *updated.ErrorMessage, "server restarted")
|
|
}
|
|
|
|
func createRecoveryTestService(suite *testSuite) *internal.MusicAgregatorService {
|
|
return internal.NewMusicAgregatorServiceWithDeps(
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
nil,
|
|
suite.db,
|
|
)
|
|
}
|