dev-pod-api-build/internal/store/runners_test.go
2026-04-16 04:16:36 +00:00

385 lines
9.6 KiB
Go

package store
import (
"context"
"errors"
"testing"
"time"
"github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model"
)
func createTestUser(t *testing.T, s *Store, id string) {
t.Helper()
ctx := context.Background()
_, err := s.CreateUser(ctx, id, model.DefaultQuota())
if err != nil {
t.Fatalf("create test user %q: %v", id, err)
}
}
func newTestRunner(user, id string) *model.Runner {
return &model.Runner{
ID: id,
User: user,
RepoURL: "ilia/test-repo",
Branch: "main",
Tools: "go",
Task: "implement feature",
Status: model.RunnerStatusReceived,
CPUReq: "2",
MemReq: "4Gi",
CreatedAt: time.Now().UTC(),
}
}
func cleanRunners(t *testing.T) {
t.Helper()
ctx := context.Background()
if _, err := testPool.Exec(ctx, "DELETE FROM runners"); err != nil {
t.Fatalf("clean runners: %v", err)
}
}
func TestCreateRunner(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-abc123")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create runner: %v", err)
}
got, err := s.GetRunner(ctx, "runner-abc123")
if err != nil {
t.Fatalf("get runner: %v", err)
}
if got.ID != "runner-abc123" {
t.Errorf("got id %q, want %q", got.ID, "runner-abc123")
}
if got.User != "alice" {
t.Errorf("got user %q, want %q", got.User, "alice")
}
if got.Status != model.RunnerStatusReceived {
t.Errorf("got status %q, want %q", got.Status, model.RunnerStatusReceived)
}
if got.RepoURL != "ilia/test-repo" {
t.Errorf("got repo %q, want %q", got.RepoURL, "ilia/test-repo")
}
}
func TestCreateRunner_Duplicate(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-dup1")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("first create: %v", err)
}
err := s.CreateRunner(ctx, r)
if err == nil {
t.Fatal("expected error for duplicate runner")
}
if !errors.Is(err, ErrDuplicate) {
t.Errorf("got %v, want ErrDuplicate", err)
}
}
func TestGetRunner_NotFound(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
ctx := context.Background()
_, err := s.GetRunner(ctx, "nonexistent")
if err == nil {
t.Fatal("expected error for nonexistent runner")
}
if !errors.Is(err, ErrNotFound) {
t.Errorf("got %v, want ErrNotFound", err)
}
}
func TestListRunners(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
createTestUser(t, s, "bob")
ctx := context.Background()
r1 := newTestRunner("alice", "runner-list1")
r2 := newTestRunner("alice", "runner-list2")
r3 := newTestRunner("bob", "runner-list3")
for _, r := range []*model.Runner{r1, r2, r3} {
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create runner %s: %v", r.ID, err)
}
}
all, err := s.ListRunners(ctx, "", "")
if err != nil {
t.Fatalf("list all: %v", err)
}
if len(all) != 3 {
t.Fatalf("expected 3 runners, got %d", len(all))
}
aliceRunners, err := s.ListRunners(ctx, "alice", "")
if err != nil {
t.Fatalf("list alice: %v", err)
}
if len(aliceRunners) != 2 {
t.Fatalf("expected 2 runners for alice, got %d", len(aliceRunners))
}
byStatus, err := s.ListRunners(ctx, "", "received")
if err != nil {
t.Fatalf("list by status: %v", err)
}
if len(byStatus) != 3 {
t.Fatalf("expected 3 received runners, got %d", len(byStatus))
}
}
func TestUpdateRunnerStatus_ValidTransition(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-trans1")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create: %v", err)
}
if err := s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusPodCreating, ""); err != nil {
t.Fatalf("transition to pod_creating: %v", err)
}
got, _ := s.GetRunner(ctx, r.ID)
if got.Status != model.RunnerStatusPodCreating {
t.Errorf("expected pod_creating, got %s", got.Status)
}
if err := s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusRunnerRegistered, "forgejo-42"); err != nil {
t.Fatalf("transition to runner_registered: %v", err)
}
got, _ = s.GetRunner(ctx, r.ID)
if got.Status != model.RunnerStatusRunnerRegistered {
t.Errorf("expected runner_registered, got %s", got.Status)
}
if got.ForgejoRunnerID != "forgejo-42" {
t.Errorf("expected forgejo_runner_id 'forgejo-42', got %q", got.ForgejoRunnerID)
}
}
func TestUpdateRunnerStatus_InvalidTransition(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-invalid1")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create: %v", err)
}
err := s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusCompleted, "")
if err == nil {
t.Fatal("expected error for invalid transition received -> completed")
}
}
func TestUpdateRunnerStatus_SetsClaimedAt(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-claimed1")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create: %v", err)
}
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusPodCreating, "")
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusRunnerRegistered, "")
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusJobClaimed, "")
got, _ := s.GetRunner(ctx, r.ID)
if got.ClaimedAt == nil {
t.Error("expected claimed_at to be set")
}
}
func TestUpdateRunnerStatus_SetsCompletedAt(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-complete1")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create: %v", err)
}
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusPodCreating, "")
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusRunnerRegistered, "")
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusJobClaimed, "")
_ = s.UpdateRunnerStatus(ctx, r.ID, model.RunnerStatusCompleted, "")
got, _ := s.GetRunner(ctx, r.ID)
if got.CompletedAt == nil {
t.Error("expected completed_at to be set")
}
}
func TestDeleteRunner(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r := newTestRunner("alice", "runner-del1")
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create: %v", err)
}
if err := s.DeleteRunner(ctx, r.ID); err != nil {
t.Fatalf("delete: %v", err)
}
_, err := s.GetRunner(ctx, r.ID)
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound after delete, got %v", err)
}
}
func TestDeleteRunner_NotFound(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
ctx := context.Background()
err := s.DeleteRunner(ctx, "nonexistent")
if !errors.Is(err, ErrNotFound) {
t.Errorf("expected ErrNotFound, got %v", err)
}
}
func TestIsDeliveryProcessed(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
isDupe, err := s.IsDeliveryProcessed(ctx, "delivery-1")
if err != nil {
t.Fatalf("check delivery: %v", err)
}
if isDupe {
t.Error("expected delivery-1 to not be processed yet")
}
r := newTestRunner("alice", "runner-dedupe1")
r.WebhookDeliveryID = "delivery-1"
if err := s.CreateRunner(ctx, r); err != nil {
t.Fatalf("create: %v", err)
}
isDupe, err = s.IsDeliveryProcessed(ctx, "delivery-1")
if err != nil {
t.Fatalf("check delivery after create: %v", err)
}
if !isDupe {
t.Error("expected delivery-1 to be processed")
}
}
func TestIsDeliveryProcessed_EmptyID(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
ctx := context.Background()
isDupe, err := s.IsDeliveryProcessed(ctx, "")
if err != nil {
t.Fatalf("check empty delivery: %v", err)
}
if isDupe {
t.Error("expected empty delivery ID to return false")
}
}
func TestWebhookDeliveryDedupe_UniqueConstraint(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
r1 := newTestRunner("alice", "runner-uniq1")
r1.WebhookDeliveryID = "webhook-abc"
if err := s.CreateRunner(ctx, r1); err != nil {
t.Fatalf("first insert: %v", err)
}
r2 := newTestRunner("alice", "runner-uniq2")
r2.WebhookDeliveryID = "webhook-abc"
err := s.CreateRunner(ctx, r2)
if err == nil {
t.Fatal("expected duplicate error for same webhook_delivery_id")
}
if !errors.Is(err, ErrDuplicate) {
t.Errorf("expected ErrDuplicate, got %v", err)
}
}
func TestGetStaleRunners(t *testing.T) {
s := newTestStore(t)
cleanRunners(t)
createTestUser(t, s, "alice")
ctx := context.Background()
old := newTestRunner("alice", "runner-stale1")
old.CreatedAt = time.Now().UTC().Add(-3 * time.Hour)
if err := s.CreateRunner(ctx, old); err != nil {
t.Fatalf("create old runner: %v", err)
}
fresh := newTestRunner("alice", "runner-fresh1")
fresh.CreatedAt = time.Now().UTC()
if err := s.CreateRunner(ctx, fresh); err != nil {
t.Fatalf("create fresh runner: %v", err)
}
stale, err := s.GetStaleRunners(ctx, 2*time.Hour)
if err != nil {
t.Fatalf("get stale: %v", err)
}
if len(stale) != 1 {
t.Fatalf("expected 1 stale runner, got %d", len(stale))
}
if stale[0].ID != "runner-stale1" {
t.Errorf("expected stale runner runner-stale1, got %s", stale[0].ID)
}
}
func TestGenerateRunnerID(t *testing.T) {
id1, err := GenerateRunnerID()
if err != nil {
t.Fatalf("generate: %v", err)
}
if len(id1) == 0 {
t.Error("expected non-empty ID")
}
id2, _ := GenerateRunnerID()
if id1 == id2 {
t.Error("expected unique IDs")
}
if id1[:7] != "runner-" {
t.Errorf("expected runner- prefix, got %s", id1[:7])
}
}