Skip to content
Open
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
2 changes: 1 addition & 1 deletion components/backend/handlers/display_name.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func ValidateDisplayName(name string) string {
// - Gracefully handles session deletion during generation (checks IsNotFound)
// - No cancellation mechanism exists; goroutine runs to completion or timeout
// - Safe for backend restarts: orphaned goroutines will timeout naturally
func GenerateDisplayNameAsync(projectName, sessionName, userMessage string, sessionCtx SessionContext) {
var GenerateDisplayNameAsync = func(projectName, sessionName, userMessage string, sessionCtx SessionContext) {
go func() {
defer func() {
if r := recover(); r != nil {
Expand Down
5 changes: 2 additions & 3 deletions components/backend/handlers/sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -1203,9 +1203,8 @@ func CreateSession(c *gin.Context) {
// This ensures consistent behavior whether sessions are created via API or kubectl.

// Trigger async display name generation when initialPrompt is provided
// but no explicit displayName was set. The AG-UI proxy skips the
// initialPrompt message, so sessions created with only an initialPrompt
// (e.g., from the new-session page) would never get a generated name.
// but no explicit displayName was set. The AG-UI proxy also generates
// on the first /agui/run message as a fallback if this call fails.
if strings.TrimSpace(req.InitialPrompt) != "" && strings.TrimSpace(req.DisplayName) == "" {
spec, ok := created.Object["spec"].(map[string]interface{})
if ok {
Expand Down
6 changes: 0 additions & 6 deletions components/backend/websocket/agui_proxy.go
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -1002,12 +1002,6 @@ func triggerDisplayNameGenerationIfNeeded(projectName, sessionName string, messa
return
}

// Skip if this message is the auto-sent initialPrompt
initialPrompt, _, _ := unstructured.NestedString(spec, "initialPrompt")
if initialPrompt != "" && strings.TrimSpace(userMessage) == strings.TrimSpace(initialPrompt) {
return
}

if !handlers.ShouldGenerateDisplayName(spec) {
return
}
Expand Down
132 changes: 132 additions & 0 deletions components/backend/websocket/agui_proxy_test.go
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
package websocket

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"

"ambient-code-backend/handlers"
"ambient-code-backend/tests/test_utils"
"ambient-code-backend/types"

corev1 "k8s.io/api/core/v1"
metav1 "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"
k8sfake "k8s.io/client-go/kubernetes/fake"
)

Expand Down Expand Up @@ -204,3 +210,129 @@ func TestDefaultRunnerPort_Constant(t *testing.T) {
t.Errorf("Expected DefaultRunnerPort=8001, got %d", handlers.DefaultRunnerPort)
}
}

// --- triggerDisplayNameGenerationIfNeeded tests (regression for #1561) ---

func setupDisplayNameTest(t *testing.T, spec map[string]interface{}) (cleanup func()) {
t.Helper()

oldDynamic := handlers.DynamicClient
oldK8sProjects := handlers.K8sClientProjects
oldGVRFunc := handlers.GetAgenticSessionV1Alpha1Resource

agenticSessionGVR := schema.GroupVersionResource{
Group: "vteam.ambient-code",
Version: "v1alpha1",
Resource: "agenticsessions",
}
handlers.GetAgenticSessionV1Alpha1Resource = func() schema.GroupVersionResource {
return agenticSessionGVR
}

fakeClients := test_utils.NewFakeClientSet()
handlers.DynamicClient = fakeClients.GetDynamicClient()
handlers.K8sClientProjects = fakeClients.GetK8sClient()

err := test_utils.CreateAgenticSessionInFakeClient(
handlers.DynamicClient, "test-project", "test-session", spec,
)
if err != nil {
t.Fatalf("Failed to create test session: %v", err)
}

return func() {
handlers.DynamicClient = oldDynamic
handlers.K8sClientProjects = oldK8sProjects
handlers.GetAgenticSessionV1Alpha1Resource = oldGVRFunc
}
}

func getDisplayName(t *testing.T, dc dynamic.Interface) string {
t.Helper()
gvr := handlers.GetAgenticSessionV1Alpha1Resource()
item, err := dc.Resource(gvr).Namespace("test-project").Get(
context.Background(), "test-session", metav1.GetOptions{},
)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
dn, _, _ := unstructured.NestedString(item.Object, "spec", "displayName")
return dn
}

func TestTriggerDisplayName_InitialPromptNotSkipped(t *testing.T) {
cleanup := setupDisplayNameTest(t, map[string]interface{}{
"initialPrompt": "Help me debug auth",
})
defer cleanup()

called := false
oldFn := handlers.GenerateDisplayNameAsync
handlers.GenerateDisplayNameAsync = func(projectName, sessionName, userMessage string, sessionCtx handlers.SessionContext) {
called = true
}
defer func() { handlers.GenerateDisplayNameAsync = oldFn }()

msgs := []types.Message{
{ID: "msg-1", Role: "user", Content: "Help me debug auth"},
}

triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)

if !called {
t.Error("Expected GenerateDisplayNameAsync to be called for initialPrompt message when displayName is empty")
}
}

func TestTriggerDisplayName_SkipsWhenNameAlreadySet(t *testing.T) {
cleanup := setupDisplayNameTest(t, map[string]interface{}{
"initialPrompt": "Help me debug auth",
"displayName": "Debug Auth Middleware",
})
defer cleanup()

msgs := []types.Message{
{ID: "msg-1", Role: "user", Content: "Help me debug auth"},
}

triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)

// displayName should remain unchanged — ShouldGenerateDisplayName
// returns false when displayName is already set.
dn := getDisplayName(t, handlers.DynamicClient)
if dn != "Debug Auth Middleware" {
t.Errorf("Expected displayName to remain %q, got %q", "Debug Auth Middleware", dn)
}
}

func TestTriggerDisplayName_SkipsWhenNoUserMessage(t *testing.T) {
cleanup := setupDisplayNameTest(t, map[string]interface{}{
"initialPrompt": "Help me debug auth",
})
defer cleanup()

// Only assistant messages — no user content to generate from
msgs := []types.Message{
{ID: "msg-1", Role: "assistant", Content: "I'll help you debug"},
}

triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)

dn := getDisplayName(t, handlers.DynamicClient)
if dn != "" {
t.Errorf("Expected empty displayName, got %q", dn)
}
}

func TestTriggerDisplayName_SkipsWhenDynamicClientNil(t *testing.T) {
oldDynamic := handlers.DynamicClient
handlers.DynamicClient = nil
defer func() { handlers.DynamicClient = oldDynamic }()

msgs := []types.Message{
{ID: "msg-1", Role: "user", Content: "Help me debug auth"},
}

// Should return early without panic when DynamicClient is nil
triggerDisplayNameGenerationIfNeeded("test-project", "test-session", msgs)
}