package k8s import ( "context" "testing" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model" ) func defaultRunnerOpts() CreateRunnerPodOpts { return CreateRunnerPodOpts{ User: "alice", RunnerID: "runner-abc12345", Tools: "go,rust", Task: "implement feature", RepoURL: "alice/myrepo", Branch: "main", CPUReq: "2", MemReq: "4Gi", ForgejoRunnerToken: "test-runner-token", } } func TestCreateRunnerPod(t *testing.T) { client := newTestClient() ctx := context.Background() if err := client.EnsureNamespace(ctx, "alice"); err != nil { t.Fatalf("ensure namespace: %v", err) } opts := defaultRunnerOpts() podName, err := client.CreateRunnerPod(ctx, opts) if err != nil { t.Fatalf("create runner pod: %v", err) } expectedPodName := model.RunnerPodName("runner-abc12345") if podName != expectedPodName { t.Errorf("expected pod name %q, got %q", expectedPodName, podName) } ns := model.NamespaceName("alice") pod, err := client.Clientset.CoreV1().Pods(ns).Get(ctx, podName, metav1.GetOptions{}) if err != nil { t.Fatalf("get runner pod: %v", err) } if pod.Labels["app"] != "dev-pod-runner" { t.Errorf("expected app label dev-pod-runner, got %q", pod.Labels["app"]) } if pod.Labels["runner-id"] != "runner-abc12345" { t.Errorf("expected runner-id label runner-abc12345, got %q", pod.Labels["runner-id"]) } if pod.Labels["user"] != "alice" { t.Errorf("expected user label alice, got %q", pod.Labels["user"]) } if len(pod.Spec.Containers) != 2 { t.Fatalf("expected 2 containers (runner + ipip-sidecar), got %d", len(pod.Spec.Containers)) } container := pod.Spec.Containers[0] if container.Name != "runner" { t.Errorf("expected container name runner, got %q", container.Name) } sidecar := pod.Spec.Containers[1] if sidecar.Name != "ipip-sidecar" { t.Errorf("expected sidecar name ipip-sidecar, got %q", sidecar.Name) } if sidecar.SecurityContext == nil || !*sidecar.SecurityContext.Privileged { t.Error("expected ipip-sidecar to be privileged") } envMap := make(map[string]string) for _, e := range container.Env { envMap[e.Name] = e.Value } if envMap["RUNNER_MODE"] != "true" { t.Error("expected RUNNER_MODE=true") } if envMap["RUNNER_ID"] != "runner-abc12345" { t.Errorf("expected RUNNER_ID=runner-abc12345, got %q", envMap["RUNNER_ID"]) } if envMap["DEV_POD_API_URL"] == "" { t.Error("expected DEV_POD_API_URL to be set") } if envMap["FORGEJO_RUNNER_TOKEN"] != "test-runner-token" { t.Errorf("expected FORGEJO_RUNNER_TOKEN=test-runner-token, got %q", envMap["FORGEJO_RUNNER_TOKEN"]) } if envMap["RUNNER_REPO"] != "alice/myrepo" { t.Errorf("expected RUNNER_REPO=alice/myrepo, got %q", envMap["RUNNER_REPO"]) } if envMap["RUNNER_BRANCH"] != "main" { t.Errorf("expected RUNNER_BRANCH=main, got %q", envMap["RUNNER_BRANCH"]) } if envMap["DEV_TOOLS"] != "go,rust" { t.Errorf("expected DEV_TOOLS=go,rust, got %q", envMap["DEV_TOOLS"]) } cpuReq := container.Resources.Requests.Cpu() if cpuReq.String() != "2" { t.Errorf("expected cpu request 2, got %s", cpuReq.String()) } memReq := container.Resources.Requests.Memory() if memReq.String() != "4Gi" { t.Errorf("expected memory request 4Gi, got %s", memReq.String()) } } func TestCreateRunnerPod_DefaultResources(t *testing.T) { client := newTestClient() ctx := context.Background() if err := client.EnsureNamespace(ctx, "alice"); err != nil { t.Fatalf("ensure namespace: %v", err) } opts := CreateRunnerPodOpts{ User: "alice", RunnerID: "runner-defaults", RepoURL: "alice/repo", } podName, err := client.CreateRunnerPod(ctx, opts) if err != nil { t.Fatalf("create runner pod: %v", err) } ns := model.NamespaceName("alice") pod, err := client.Clientset.CoreV1().Pods(ns).Get(ctx, podName, metav1.GetOptions{}) if err != nil { t.Fatalf("get runner pod: %v", err) } container := pod.Spec.Containers[0] cpuReq := container.Resources.Requests.Cpu() if cpuReq.String() != "2" { t.Errorf("expected default cpu request 2, got %s", cpuReq.String()) } memReq := container.Resources.Requests.Memory() if memReq.String() != "4Gi" { t.Errorf("expected default memory request 4Gi, got %s", memReq.String()) } } func TestDeleteRunnerPod(t *testing.T) { client := newTestClient() ctx := context.Background() if err := client.EnsureNamespace(ctx, "alice"); err != nil { t.Fatalf("ensure namespace: %v", err) } opts := defaultRunnerOpts() podName, err := client.CreateRunnerPod(ctx, opts) if err != nil { t.Fatalf("create runner pod: %v", err) } if err := client.DeleteRunnerPod(ctx, "alice", podName); err != nil { t.Fatalf("delete runner pod: %v", err) } ns := model.NamespaceName("alice") pods, err := client.Clientset.CoreV1().Pods(ns).List(ctx, metav1.ListOptions{ LabelSelector: "app=dev-pod-runner", }) if err != nil { t.Fatalf("list pods: %v", err) } if len(pods.Items) != 0 { t.Errorf("expected 0 runner pods after delete, got %d", len(pods.Items)) } } func TestDeleteRunnerPod_NonExistent(t *testing.T) { client := newTestClient() ctx := context.Background() err := client.DeleteRunnerPod(ctx, "alice", "nonexistent-pod") if err != nil { t.Fatalf("delete nonexistent pod should not fail, got: %v", err) } } func TestRunnerPodName(t *testing.T) { name := model.RunnerPodName("runner-abc123") if name != "dev-pod-runner-abc123" { t.Errorf("expected dev-pod-runner-abc123, got %s", name) } }