@@ -2,6 +2,7 @@ package main
22
33import (
44 "bytes"
5+ "context"
56 "encoding/json"
67 "net/http"
78 "net/http/httptest"
@@ -11,7 +12,10 @@ import (
1112
1213 "github.com/labstack/echo/v4"
1314 corev1 "k8s.io/api/core/v1"
15+ apierrors "k8s.io/apimachinery/pkg/api/errors"
1416 "k8s.io/apimachinery/pkg/runtime"
17+ "k8s.io/apimachinery/pkg/runtime/schema"
18+ "sigs.k8s.io/controller-runtime/pkg/client"
1519 "sigs.k8s.io/controller-runtime/pkg/client/fake"
1620
1721 spritzv1 "spritz.sh/operator/api/v1"
@@ -49,6 +53,20 @@ func newCreateSpritzTestServer(t *testing.T) *server {
4953 }
5054}
5155
56+ type createInterceptClient struct {
57+ client.Client
58+ onCreate func (context.Context , client.Object ) error
59+ }
60+
61+ func (c * createInterceptClient ) Create (ctx context.Context , obj client.Object , opts ... client.CreateOption ) error {
62+ if c .onCreate != nil {
63+ if err := c .onCreate (ctx , obj ); err != nil {
64+ return err
65+ }
66+ }
67+ return c .Client .Create (ctx , obj , opts ... )
68+ }
69+
5270func configureProvisionerTestServer (s * server ) {
5371 s .presets = presetCatalog {
5472 byID : []runtimePreset {{
@@ -392,3 +410,63 @@ func TestCreateSpritzReplaysIdempotentProvisionerRequestBeforeQuotaCheck(t *test
392410 t .Fatalf ("expected replay status 200, got %d: %s" , rec2 .Code , rec2 .Body .String ())
393411 }
394412}
413+
414+ func TestCreateSpritzRetriesGeneratedServiceNameAfterAlreadyExists (t * testing.T ) {
415+ s := newCreateSpritzTestServer (t )
416+ configureProvisionerTestServer (s )
417+ baseClient := s .client
418+ s .client = & createInterceptClient {
419+ Client : baseClient ,
420+ onCreate : func (_ context.Context , obj client.Object ) error {
421+ spritz , ok := obj .(* spritzv1.Spritz )
422+ if ! ok {
423+ return nil
424+ }
425+ if spritz .Name == "openclaw-first" {
426+ return apierrors .NewAlreadyExists (schema.GroupResource {
427+ Group : spritzv1 .GroupVersion .Group ,
428+ Resource : "spritzes" ,
429+ }, spritz .Name )
430+ }
431+ return nil
432+ },
433+ }
434+ s .nameGeneratorFactory = func (context.Context , string , string ) (func () string , error ) {
435+ names := []string {"openclaw-first" , "openclaw-second" }
436+ index := 0
437+ return func () string {
438+ name := names [index ]
439+ if index < len (names )- 1 {
440+ index ++
441+ }
442+ return name
443+ }, nil
444+ }
445+
446+ e := echo .New ()
447+ secured := e .Group ("" , s .authMiddleware ())
448+ secured .POST ("/api/spritzes" , s .createSpritz )
449+
450+ body := []byte (`{"presetId":"openclaw","ownerId":"user-123","idempotencyKey":"discord-race"}` )
451+ req := httptest .NewRequest (http .MethodPost , "/api/spritzes" , bytes .NewReader (body ))
452+ req .Header .Set (echo .HeaderContentType , echo .MIMEApplicationJSON )
453+ req .Header .Set ("X-Spritz-User-Id" , "zenobot" )
454+ req .Header .Set ("X-Spritz-Principal-Type" , "service" )
455+ req .Header .Set ("X-Spritz-Principal-Scopes" , "spritz.instances.create,spritz.instances.assign_owner" )
456+ rec := httptest .NewRecorder ()
457+
458+ e .ServeHTTP (rec , req )
459+
460+ if rec .Code != http .StatusCreated {
461+ t .Fatalf ("expected status 201 after autogenerated name retry, got %d: %s" , rec .Code , rec .Body .String ())
462+ }
463+
464+ var payload map [string ]any
465+ if err := json .Unmarshal (rec .Body .Bytes (), & payload ); err != nil {
466+ t .Fatalf ("failed to decode response: %v" , err )
467+ }
468+ name := payload ["data" ].(map [string ]any )["spritz" ].(map [string ]any )["metadata" ].(map [string ]any )["name" ]
469+ if name != "openclaw-second" {
470+ t .Fatalf ("expected second generated name after race, got %#v" , name )
471+ }
472+ }
0 commit comments