Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions components/backend/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ func parseSpec(spec map[string]interface{}) types.AgenticSessionSpec {
if branch, ok := m["branch"].(string); ok && strings.TrimSpace(branch) != "" {
r.Branch = types.StringPtr(branch)
}
// Parse autoPush as optional boolean. Preserve nil to allow CRD default.
// nil = use default (false), false = explicit no-push, true = explicit push
if autoPush, ok := m["autoPush"].(bool); ok {
r.AutoPush = types.BoolPtr(autoPush)
}
if strings.TrimSpace(r.URL) != "" {
repos = append(repos, r)
}
Expand Down Expand Up @@ -626,11 +631,6 @@ func CreateSession(c *gin.Context) {
session["spec"].(map[string]interface{})["interactive"] = *req.Interactive
}

// AutoPushOnComplete flag
if req.AutoPushOnComplete != nil {
session["spec"].(map[string]interface{})["autoPushOnComplete"] = *req.AutoPushOnComplete
}

// Set multi-repo configuration on spec (simplified format)
{
spec := session["spec"].(map[string]interface{})
Expand All @@ -641,6 +641,9 @@ func CreateSession(c *gin.Context) {
if r.Branch != nil {
m["branch"] = *r.Branch
}
if r.AutoPush != nil {
m["autoPush"] = *r.AutoPush
}
arr = append(arr, m)
}
spec["repos"] = arr
Expand Down Expand Up @@ -1296,8 +1299,9 @@ func AddRepo(c *gin.Context) {
}

var req struct {
URL string `json:"url" binding:"required"`
Branch string `json:"branch"`
URL string `json:"url" binding:"required"`
Branch string `json:"branch"`
AutoPush *bool `json:"autoPush,omitempty"`
}

if err := c.ShouldBindJSON(&req); err != nil {
Expand Down Expand Up @@ -1387,6 +1391,9 @@ func AddRepo(c *gin.Context) {
"url": req.URL,
"branch": req.Branch,
}
if req.AutoPush != nil {
newRepo["autoPush"] = *req.AutoPush
}
repos = append(repos, newRepo)
spec["repos"] = repos

Expand Down
258 changes: 258 additions & 0 deletions components/backend/handlers/sessions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
)

var _ = Describe("Sessions Handler", Label(test_constants.LabelUnit, test_constants.LabelHandlers, test_constants.LabelSessions), func() {
Expand Down Expand Up @@ -521,6 +523,262 @@ var _ = Describe("Sessions Handler", Label(test_constants.LabelUnit, test_consta
})
})
})

// AutoPush functionality tests
Context("AutoPush Field Parsing", func() {
var (
testClientFactory *test_utils.TestClientFactory
fakeClients *test_utils.FakeClientSet
originalK8sClient kubernetes.Interface
originalK8sClientMw kubernetes.Interface
originalK8sClientProjects kubernetes.Interface
originalDynamicClient dynamic.Interface
)

BeforeEach(func() {
logger.Log("Setting up AutoPush test")

// Save original state
originalK8sClient = K8sClient
originalK8sClientMw = K8sClientMw
originalK8sClientProjects = K8sClientProjects
originalDynamicClient = DynamicClient

// Create fake clients
testClientFactory = test_utils.NewTestClientFactory()
fakeClients = testClientFactory.GetFakeClients()

DynamicClient = fakeClients.GetDynamicClient()
K8sClientProjects = fakeClients.GetK8sClient()
K8sClient = fakeClients.GetK8sClient()
K8sClientMw = fakeClients.GetK8sClient()
})

AfterEach(func() {
// Restore original state
K8sClient = originalK8sClient
K8sClientMw = originalK8sClientMw
K8sClientProjects = originalK8sClientProjects
DynamicClient = originalDynamicClient
})

Describe("parseSpec", func() {
It("Should parse autoPush=true from repo", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
"autoPush": true,
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
Expect(parsed.Repos[0].URL).To(Equal("https://github.com/owner/repo.git"))
Expect(parsed.Repos[0].Branch).NotTo(BeNil())
Expect(*parsed.Repos[0].Branch).To(Equal("main"))
Expect(parsed.Repos[0].AutoPush).NotTo(BeNil())
Expect(*parsed.Repos[0].AutoPush).To(BeTrue())
})

It("Should parse autoPush=false from repo", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "develop",
"autoPush": false,
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
Expect(parsed.Repos[0].AutoPush).NotTo(BeNil())
Expect(*parsed.Repos[0].AutoPush).To(BeFalse())
})

It("Should handle missing autoPush field", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
Expect(parsed.Repos[0].AutoPush).To(BeNil())
})

It("Should handle multiple repos with mixed autoPush settings", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo1.git",
"autoPush": true,
},
map[string]interface{}{
"url": "https://github.com/owner/repo2.git",
"autoPush": false,
},
map[string]interface{}{
"url": "https://github.com/owner/repo3.git",
// No autoPush field
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(3))

// First repo: autoPush=true
Expect(parsed.Repos[0].AutoPush).NotTo(BeNil())
Expect(*parsed.Repos[0].AutoPush).To(BeTrue())

// Second repo: autoPush=false
Expect(parsed.Repos[1].AutoPush).NotTo(BeNil())
Expect(*parsed.Repos[1].AutoPush).To(BeFalse())

// Third repo: no autoPush field
Expect(parsed.Repos[2].AutoPush).To(BeNil())
})
})

Describe("ExtractSessionContext", func() {
It("Should extract autoPush from repo spec", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
"autoPush": true,
},
},
}

ctx := ExtractSessionContext(spec)
Expect(ctx.Repos).To(HaveLen(1))
Expect(ctx.Repos[0]["url"]).To(Equal("https://github.com/owner/repo.git"))
Expect(ctx.Repos[0]["autoPush"]).To(BeTrue())
})

It("Should handle repos without autoPush field", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
},
},
}

ctx := ExtractSessionContext(spec)
Expect(ctx.Repos).To(HaveLen(1))
Expect(ctx.Repos[0]["autoPush"]).To(BeNil())
})
})

Describe("BoolPtr helper", func() {
It("Should create pointer to true", func() {
ptr := types.BoolPtr(true)
Expect(ptr).NotTo(BeNil())
Expect(*ptr).To(BeTrue())
})

It("Should create pointer to false", func() {
ptr := types.BoolPtr(false)
Expect(ptr).NotTo(BeNil())
Expect(*ptr).To(BeFalse())
})
})

Describe("Error handling", func() {
It("Should handle invalid autoPush type gracefully", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"autoPush": "invalid-string", // Should be bool
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
// Should skip invalid type and leave AutoPush as nil
Expect(parsed.Repos[0].AutoPush).To(BeNil())
})

It("Should handle autoPush in malformed repo gracefully", func() {
spec := map[string]interface{}{
"repos": []interface{}{
"invalid-string-instead-of-map",
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(BeEmpty())
})
})

Describe("Edge Cases in AddRepo Handler", func() {
It("Should handle autoPush as null value", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
"autoPush": nil,
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
// nil autoPush should be treated as not provided
Expect(parsed.Repos[0].AutoPush).To(BeNil())
})

It("Should skip autoPush with invalid type (string)", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
"autoPush": "true", // String instead of bool
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
// Invalid type should be skipped, leaving AutoPush as nil
Expect(parsed.Repos[0].AutoPush).To(BeNil())
})

It("Should skip autoPush with invalid type (number)", func() {
spec := map[string]interface{}{
"repos": []interface{}{
map[string]interface{}{
"url": "https://github.com/owner/repo.git",
"branch": "main",
"autoPush": 1, // Number instead of bool
},
},
}

parsed := parseSpec(spec)
Expect(parsed.Repos).To(HaveLen(1))
// Invalid type should be skipped, leaving AutoPush as nil
Expect(parsed.Repos[0].AutoPush).To(BeNil())
})
})
})
})

// Helper functions
Expand Down
6 changes: 3 additions & 3 deletions components/backend/types/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ type AgenticSessionSpec struct {

// SimpleRepo represents a simplified repository configuration
type SimpleRepo struct {
URL string `json:"url"`
Branch *string `json:"branch,omitempty"`
URL string `json:"url"`
Branch *string `json:"branch,omitempty"`
AutoPush *bool `json:"autoPush,omitempty"`
}

type AgenticSessionStatus struct {
Expand All @@ -53,7 +54,6 @@ type CreateAgenticSessionRequest struct {
ParentSessionID string `json:"parent_session_id,omitempty"`
// Multi-repo support
Repos []SimpleRepo `json:"repos,omitempty"`
AutoPushOnComplete *bool `json:"autoPushOnComplete,omitempty"`
UserContext *UserContext `json:"userContext,omitempty"`
EnvironmentVariables map[string]string `json:"environmentVariables,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Expand Down
Loading
Loading