mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user