Files
coder/coderd/taskname/taskname_internal_test.go
T
Susana Ferreira 3011207519 feat: add display name field for tasks (#20856)
## Problem

Tasks currently only expose a machine-friendly name field (e.g.
`task-python-debug-a1b2`), but this value is primarily an identifier
rather than a clean, descriptive label. We need a separate
display-friendly name for use in the UI.

This PR introduces a new `display_name` field and updates the task-name
generation flow. The Claude system prompt was updated to return valid
JSON with both `name` and `display_name`. The name generation logic
follows a fallback chain (Anthropic > prompt sanitization > random
fallback). To make task names more closely resemble their display names,
the legacy `task-` prefix has been removed. For context, PR
https://github.com/coder/coder/pull/20834 introduced a small Task icon
to the workspace list to help identify workspaces associated to tasks.

## Changes

- Database migration: Added `display_name` column to tasks table
- Updated system prompt to generate both task name and display name as
valid JSON
- Task name generation now follows a fallback chain: Anthropic > prompt
sanitization > random fallback
- Removed `task-` prefix from task names to allow more descriptive names
- Note: PR https://github.com/coder/coder/pull/20834 adds a Task icon to
workspaces in the workspace list to distinguish task-created workspaces

**Note:** UI changes will be addressed in a follow-up PR

Related to: https://github.com/coder/coder/issues/20801
2025-11-25 13:00:59 +00:00

165 lines
4.0 KiB
Go

package taskname
import (
"fmt"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/testutil"
)
func TestGenerateFallback(t *testing.T) {
t.Parallel()
taskName := generateFallback()
err := codersdk.NameValid(taskName.Name)
require.NoErrorf(t, err, "expected fallback to be valid workspace name, instead found %s", taskName.Name)
require.NotEmpty(t, taskName.DisplayName)
}
func TestGenerateFromPrompt(t *testing.T) {
t.Parallel()
tests := []struct {
name string
prompt string
expectError bool
expectedName string
expectedDisplayName string
}{
{
name: "EmptyPrompt",
prompt: "",
expectError: true,
},
{
name: "OnlySpaces",
prompt: " ",
expectError: true,
},
{
name: "OnlySpecialCharacters",
prompt: "!@#$%^&*()",
expectError: true,
},
{
name: "UppercasePrompt",
prompt: "BUILD MY APP",
expectError: false,
expectedName: "build-my-app",
expectedDisplayName: "BUILD MY APP",
},
{
name: "PromptWithApostrophes",
prompt: "fix user's dashboard",
expectError: false,
expectedName: "fix-users-dashboard",
expectedDisplayName: "Fix user's dashboard",
},
{
name: "LongPrompt",
prompt: strings.Repeat("a", 100),
expectError: false,
expectedName: strings.Repeat("a", 27),
expectedDisplayName: "A" + strings.Repeat("a", 62) + "…",
},
{
name: "PromptWithMultipleSpaces",
prompt: "build my app",
expectError: false,
expectedName: "build-my-app",
expectedDisplayName: "Build my app",
},
{
name: "PromptWithNewlines",
prompt: "build\nmy\napp",
expectError: false,
expectedName: "build-my-app",
expectedDisplayName: "Build my app",
},
{
name: "TruncatesLongPromptAtWordBoundary",
prompt: "implement real-time notifications dashboard",
expectError: false,
expectedName: "implement-real-time",
expectedDisplayName: "Implement real-time notifications dashboard",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
taskName, err := generateFromPrompt(tc.prompt)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
// Validate task name
require.Contains(t, taskName.Name, fmt.Sprintf("%s-", tc.expectedName))
require.NoError(t, codersdk.NameValid(taskName.Name))
// Validate task display name
require.NotEmpty(t, taskName.DisplayName)
require.Equal(t, tc.expectedDisplayName, taskName.DisplayName)
})
}
}
func TestGenerateFromAnthropic(t *testing.T) {
t.Parallel()
apiKey := getAnthropicAPIKeyFromEnv()
if apiKey == "" {
t.Skip("Skipping test as ANTHROPIC_API_KEY not set")
}
tests := []struct {
name string
prompt string
}{
{
name: "SimplePrompt",
prompt: "Create a finance planning app",
},
{
name: "TechnicalPrompt",
prompt: "Debug authentication middleware for OAuth2",
},
{
name: "ShortPrompt",
prompt: "Fix bug",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
taskName, err := generateFromAnthropic(ctx, tc.prompt, apiKey, getAnthropicModelFromEnv())
require.NoError(t, err)
t.Log("Task name:", taskName.Name)
t.Log("Task display name:", taskName.DisplayName)
// Validate task name
require.NotEmpty(t, taskName.DisplayName)
require.NoError(t, codersdk.NameValid(taskName.Name))
// Validate display name
require.NotEmpty(t, taskName.DisplayName)
require.NotEqual(t, "task-unnamed", taskName.Name)
require.NotEqual(t, "Task Unnamed", taskName.DisplayName)
})
}
}