Files
kandev/apps/backend/cmd/preview/sprite_ops_test.go
T
2026-08-09 18:12:08 +01:00

179 lines
5.4 KiB
Go

package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
sprites "github.com/superfly/sprites-go"
)
type testNetworkError struct {
temporary bool
}
func (e testNetworkError) Error() string { return "test network error" }
func (e testNetworkError) Timeout() bool { return false }
func (e testNetworkError) Temporary() bool { return e.temporary }
func TestIsTransientSpriteErrorClassifiesTemporaryNetworkErrors(t *testing.T) {
if !isTransientSpriteError(fmt.Errorf("lookup: %w", testNetworkError{temporary: true})) {
t.Fatal("temporary network error should be retryable")
}
if isTransientSpriteError(testNetworkError{}) {
t.Fatal("permanent network error should not be retryable")
}
}
func TestGetOrCreateSpriteRetriesTransientGet(t *testing.T) {
getCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
getCalls++
if getCalls == 1 {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte(`{"name":"kandev-pr-2115","status":"created"}`))
}))
t.Cleanup(server.Close)
client := sprites.New("token", sprites.WithBaseURL(server.URL))
t.Cleanup(func() { _ = client.Close() })
sprite, err := getOrCreateSprite(t.Context(), client, "kandev-pr-2115")
if err != nil {
t.Fatalf("getOrCreateSprite() error = %v", err)
}
if sprite.Name() != "kandev-pr-2115" {
t.Errorf("sprite.Name() = %q", sprite.Name())
}
if getCalls != 2 {
t.Errorf("get calls = %d, want 2", getCalls)
}
}
func TestGetOrCreateSpriteReconcilesTransientCreateFailure(t *testing.T) {
getCalls := 0
createCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
getCalls++
if getCalls == 1 {
w.WriteHeader(http.StatusNotFound)
return
}
_, _ = w.Write([]byte(`{"name":"kandev-pr-2115","status":"created"}`))
case http.MethodPost:
createCalls++
w.WriteHeader(http.StatusServiceUnavailable)
default:
t.Errorf("unexpected %s request", r.Method)
}
}))
t.Cleanup(server.Close)
client := sprites.New("token", sprites.WithBaseURL(server.URL))
t.Cleanup(func() { _ = client.Close() })
sprite, err := getOrCreateSprite(t.Context(), client, "kandev-pr-2115")
if err != nil {
t.Fatalf("getOrCreateSprite() error = %v", err)
}
if sprite.Name() != "kandev-pr-2115" {
t.Errorf("sprite.Name() = %q", sprite.Name())
}
if createCalls != 1 {
t.Errorf("create calls = %d, want 1", createCalls)
}
if getCalls != 2 {
t.Errorf("get calls = %d, want 2", getCalls)
}
}
func TestGetOrCreateSpriteDoesNotRetryPermanentErrors(t *testing.T) {
getCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
getCalls++
w.WriteHeader(http.StatusUnauthorized)
}))
t.Cleanup(server.Close)
client := sprites.New("token", sprites.WithBaseURL(server.URL))
t.Cleanup(func() { _ = client.Close() })
_, err := getOrCreateSprite(t.Context(), client, "kandev-pr-2115")
if err == nil {
t.Fatal("getOrCreateSprite() error = nil, want permanent error")
}
if getCalls != 1 {
t.Errorf("get calls = %d, want 1", getCalls)
}
}
func TestGetOrCreateSpriteReturnsFinalTransientErrorAfterRetryBudget(t *testing.T) {
getCalls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
getCalls++
w.WriteHeader(http.StatusServiceUnavailable)
}))
t.Cleanup(server.Close)
client := sprites.New("token", sprites.WithBaseURL(server.URL))
t.Cleanup(func() { _ = client.Close() })
_, err := getOrCreateSprite(t.Context(), client, "kandev-pr-2115")
if err == nil {
t.Fatal("getOrCreateSprite() error = nil, want transient error")
}
if !strings.Contains(err.Error(), "get sprite") {
t.Errorf("getOrCreateSprite() error = %v, want get sprite error", err)
}
if getCalls != spriteControlRetries {
t.Errorf("get calls = %d, want %d", getCalls, spriteControlRetries)
}
}
func TestBuildExtractScript(t *testing.T) {
script := buildExtractScript(12345)
if !strings.Contains(script, "--backend-port 12345") {
t.Errorf("expected --backend-port 12345 in script, got:\n%s", script)
}
if !strings.Contains(script, "rm -rf /data") {
t.Errorf("expected rm -rf /data in script")
}
if !strings.Contains(script, "KANDEV_MOCK_AGENT=true") {
t.Errorf("expected KANDEV_MOCK_AGENT=true in script")
}
if strings.Contains(script, "KANDEV_MOCK_AGENT=only") {
t.Errorf("preview must retain the built-in agent catalogue")
}
if !strings.Contains(script, "KANDEV_WEB_DIST_DIR=/app/apps/web/dist") {
t.Errorf("expected KANDEV_WEB_DIST_DIR to point at packaged Vite dist")
}
if !strings.Contains(script, "KANDEV_WEB_TITLE_PREFIX=Preview") {
t.Errorf("expected preview title prefix in script")
}
if !strings.Contains(script, "ln -sf /app/apps/backend/bin/kandev /usr/local/bin/kandev") {
t.Errorf("expected native kandev symlink in script")
}
if !strings.Contains(script, "exec kandev start") {
t.Errorf("expected script to launch through kandev start")
}
if !strings.Contains(script, "--headless") {
t.Errorf("expected headless CLI launch")
}
if strings.Contains(script, "nohup node") {
t.Errorf("script should not start web outside the CLI supervisor")
}
if strings.Contains(script, ".next") {
t.Errorf("script should not refer to Next.js build output")
}
if strings.Contains(script, "/app/apps/backend/bin/kandev >") {
t.Errorf("script should not launch the backend binary directly")
}
}