Skip to content

Commit d5847b1

Browse files
authored
Merge branch 'main' into fix/platform-ingress-netpol-base
2 parents 9a48667 + 82c4026 commit d5847b1

4 files changed

Lines changed: 135 additions & 10 deletions

File tree

components/backend/handlers/display_name.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func ValidateDisplayName(name string) string {
8080
// - Gracefully handles session deletion during generation (checks IsNotFound)
8181
// - No cancellation mechanism exists; goroutine runs to completion or timeout
8282
// - Safe for backend restarts: orphaned goroutines will timeout naturally
83-
func GenerateDisplayNameAsync(projectName, sessionName, userMessage string, sessionCtx SessionContext) {
83+
var GenerateDisplayNameAsync = func(projectName, sessionName, userMessage string, sessionCtx SessionContext) {
8484
go func() {
8585
defer func() {
8686
if r := recover(); r != nil {

components/backend/handlers/sessions.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,9 +1203,8 @@ func CreateSession(c *gin.Context) {
12031203
// This ensures consistent behavior whether sessions are created via API or kubectl.
12041204

12051205
// Trigger async display name generation when initialPrompt is provided
1206-
// but no explicit displayName was set. The AG-UI proxy skips the
1207-
// initialPrompt message, so sessions created with only an initialPrompt
1208-
// (e.g., from the new-session page) would never get a generated name.
1206+
// but no explicit displayName was set. The AG-UI proxy also generates
1207+
// on the first /agui/run message as a fallback if this call fails.
12091208
if strings.TrimSpace(req.InitialPrompt) != "" && strings.TrimSpace(req.DisplayName) == "" {
12101209
spec, ok := created.Object["spec"].(map[string]interface{})
12111210
if ok {

components/backend/websocket/agui_proxy.go

100644100755
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,12 +1002,6 @@ func triggerDisplayNameGenerationIfNeeded(projectName, sessionName string, messa
10021002
return
10031003
}
10041004

1005-
// Skip if this message is the auto-sent initialPrompt
1006-
initialPrompt, _, _ := unstructured.NestedString(spec, "initialPrompt")
1007-
if initialPrompt != "" && strings.TrimSpace(userMessage) == strings.TrimSpace(initialPrompt) {
1008-
return
1009-
}
1010-
10111005
if !handlers.ShouldGenerateDisplayName(spec) {
10121006
return
10131007
}

components/backend/websocket/agui_proxy_test.go

100644100755
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package websocket
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
67
"net/http/httptest"
78
"testing"
89

910
"ambient-code-backend/handlers"
11+
"ambient-code-backend/tests/test_utils"
12+
"ambient-code-backend/types"
1013

1114
corev1 "k8s.io/api/core/v1"
1215
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
17+
"k8s.io/apimachinery/pkg/runtime/schema"
18+
"k8s.io/client-go/dynamic"
1319
k8sfake "k8s.io/client-go/kubernetes/fake"
1420
)
1521

@@ -204,3 +210,129 @@ func TestDefaultRunnerPort_Constant(t *testing.T) {
204210
t.Errorf("Expected DefaultRunnerPort=8001, got %d", handlers.DefaultRunnerPort)
205211
}
206212
}
213+
214+
// --- triggerDisplayNameGenerationIfNeeded tests (regression for #1561) ---
215+
216+
func setupDisplayNameTest(t *testing.T, spec map[string]interface{}) (cleanup func()) {
217+
t.Helper()
218+
219+
oldDynamic := handlers.DynamicClient
220+
oldK8sProjects := handlers.K8sClientProjects
221+
oldGVRFunc := handlers.GetAgenticSessionV1Alpha1Resource
222+
223+
agenticSessionGVR := schema.GroupVersionResource{
224+
Group: "vteam.ambient-code",
225+
Version: "v1alpha1",
226+
Resource: "agenticsessions",
227+
}
228+
handlers.GetAgenticSessionV1Alpha1Resource = func() schema.GroupVersionResource {
229+
return agenticSessionGVR
230+
}
231+
232+
fakeClients := test_utils.NewFakeClientSet()
233+
handlers.DynamicClient = fakeClients.GetDynamicClient()
234+
handlers.K8sClientProjects = fakeClients.GetK8sClient()
235+
236+
err := test_utils.CreateAgenticSessionInFakeClient(
237+
handlers.DynamicClient, "test-project", "test-session", spec,
238+
)
239+
if err != nil {
240+
t.Fatalf("Failed to create test session: %v", err)
241+
}
242+
243+
return func() {
244+
handlers.DynamicClient = oldDynamic
245+
handlers.K8sClientProjects = oldK8sProjects
246+
handlers.GetAgenticSessionV1Alpha1Resource = oldGVRFunc
247+
}
248+
}
249+
250+
func getDisplayName(t *testing.T, dc dynamic.Interface) string {
251+
t.Helper()
252+
gvr := handlers.GetAgenticSessionV1Alpha1Resource()
253+
item, err := dc.Resource(gvr).Namespace("test-project").Get(
254+
context.Background(), "test-session", metav1.GetOptions{},
255+
)
256+
if err != nil {
257+
t.Fatalf("Failed to get session: %v", err)
258+
}
259+
dn, _, _ := unstructured.NestedString(item.Object, "spec", "displayName")
260+
return dn
261+
}
262+
263+
func TestTriggerDisplayName_InitialPromptNotSkipped(t *testing.T) {
264+
cleanup := setupDisplayNameTest(t, map[string]interface{}{
265+
"initialPrompt": "Help me debug auth",
266+
})
267+
defer cleanup()
268+
269+
called := false
270+
oldFn := handlers.GenerateDisplayNameAsync
271+
handlers.GenerateDisplayNameAsync = func(projectName, sessionName, userMessage string, sessionCtx handlers.SessionContext) {
272+
called = true
273+
}
274+
defer func() { handlers.GenerateDisplayNameAsync = oldFn }()
275+
276+
msgs := []types.Message{
277+
{ID: "msg-1", Role: "user", Content: "Help me debug auth"},
278+
}
279+
280+
triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)
281+
282+
if !called {
283+
t.Error("Expected GenerateDisplayNameAsync to be called for initialPrompt message when displayName is empty")
284+
}
285+
}
286+
287+
func TestTriggerDisplayName_SkipsWhenNameAlreadySet(t *testing.T) {
288+
cleanup := setupDisplayNameTest(t, map[string]interface{}{
289+
"initialPrompt": "Help me debug auth",
290+
"displayName": "Debug Auth Middleware",
291+
})
292+
defer cleanup()
293+
294+
msgs := []types.Message{
295+
{ID: "msg-1", Role: "user", Content: "Help me debug auth"},
296+
}
297+
298+
triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)
299+
300+
// displayName should remain unchanged — ShouldGenerateDisplayName
301+
// returns false when displayName is already set.
302+
dn := getDisplayName(t, handlers.DynamicClient)
303+
if dn != "Debug Auth Middleware" {
304+
t.Errorf("Expected displayName to remain %q, got %q", "Debug Auth Middleware", dn)
305+
}
306+
}
307+
308+
func TestTriggerDisplayName_SkipsWhenNoUserMessage(t *testing.T) {
309+
cleanup := setupDisplayNameTest(t, map[string]interface{}{
310+
"initialPrompt": "Help me debug auth",
311+
})
312+
defer cleanup()
313+
314+
// Only assistant messages — no user content to generate from
315+
msgs := []types.Message{
316+
{ID: "msg-1", Role: "assistant", Content: "I'll help you debug"},
317+
}
318+
319+
triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)
320+
321+
dn := getDisplayName(t, handlers.DynamicClient)
322+
if dn != "" {
323+
t.Errorf("Expected empty displayName, got %q", dn)
324+
}
325+
}
326+
327+
func TestTriggerDisplayName_SkipsWhenDynamicClientNil(t *testing.T) {
328+
oldDynamic := handlers.DynamicClient
329+
handlers.DynamicClient = nil
330+
defer func() { handlers.DynamicClient = oldDynamic }()
331+
332+
msgs := []types.Message{
333+
{ID: "msg-1", Role: "user", Content: "Help me debug auth"},
334+
}
335+
336+
// Should return early without panic when DynamicClient is nil
337+
triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)
338+
}

0 commit comments

Comments
 (0)