Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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)
}