mirror of
https://github.com/coder/coder.git
synced 2026-06-03 21:18:24 +00:00
3011207519
## 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
165 lines
4.0 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|