7be0db04d8
* fix(backend): strip non-ASCII chars from branch/worktree names Non-ASCII titles (CJK, Cyrillic, Arabic, emoji) flowed verbatim into git branch names and worktree directory names because the sanitizers used unicode.IsLetter/IsDigit, which accept any Unicode letter or digit. The resulting names broke many downstream tools and left worktrees effectively inaccessible. Restrict SanitizeForBranch and SanitizeRepoDirName to ASCII alphanumerics. Non-ASCII runes now route through the existing hyphen-replace + collapse + trim pipeline, and the existing fallback chain (sanitized title -> sanitized task ID -> "task") handles titles that become empty after stripping. The "Issue #N:" / "PR #N:" prefix already added by the GitHub ingestion path means a non-ASCII issue title still produces a recognizable name like "issue-123". Closes #1081 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(worktree): fix comment accuracy and harden test assertion - Remove "emoji" from isASCIIAlphaNum doc — emoji are not Unicode letters (unicode.IsLetter returns false), so the prior code never admitted bare emoji. Correct examples to CJK, Cyrillic, Arabic. - Replace hard-coded len(dirName) != 8 with strings.Contains check so the assertion stays correct if suffix length ever changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
137 lines
4.3 KiB
Go
137 lines
4.3 KiB
Go
package worktree
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildWorktreeNames(t *testing.T) {
|
|
cfg := newTestConfig(t)
|
|
log := newTestLogger()
|
|
store := newMockStore()
|
|
mgr, err := NewManager(cfg, store, log)
|
|
if err != nil {
|
|
t.Fatalf("NewManager: %v", err)
|
|
}
|
|
|
|
t.Run("semantic naming with task title", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-abc",
|
|
TaskTitle: "Fix login bug",
|
|
WorktreeBranchPrefix: "kandev/",
|
|
}
|
|
dirName, branchName := mgr.buildWorktreeNames(req)
|
|
|
|
// Dir should contain sanitized title and a UUID suffix
|
|
if !strings.HasPrefix(dirName, "fix-login-bug_") {
|
|
t.Errorf("dirName = %q, want prefix %q", dirName, "fix-login-bug_")
|
|
}
|
|
// UUID suffix should be 8 chars
|
|
parts := strings.SplitN(dirName, "_", 2)
|
|
if len(parts) != 2 || len(parts[1]) != 8 {
|
|
t.Errorf("dirName suffix should be 8 chars, got %q", dirName)
|
|
}
|
|
|
|
// Branch should have prefix + sanitized title + suffix
|
|
if !strings.HasPrefix(branchName, "kandev/fix-login-bug-") {
|
|
t.Errorf("branchName = %q, want prefix %q", branchName, "kandev/fix-login-bug-")
|
|
}
|
|
})
|
|
|
|
t.Run("fallback to task ID when title is empty", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-xyz-123",
|
|
TaskTitle: "",
|
|
WorktreeBranchPrefix: "kandev/",
|
|
}
|
|
dirName, branchName := mgr.buildWorktreeNames(req)
|
|
|
|
if !strings.HasPrefix(dirName, "task-xyz-123_") {
|
|
t.Errorf("dirName = %q, want prefix %q", dirName, "task-xyz-123_")
|
|
}
|
|
if !strings.HasPrefix(branchName, "kandev/task-xyz-123-") {
|
|
t.Errorf("branchName = %q, want prefix %q", branchName, "kandev/task-xyz-123-")
|
|
}
|
|
})
|
|
|
|
t.Run("empty branch prefix", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-1",
|
|
TaskTitle: "Add feature",
|
|
WorktreeBranchPrefix: "",
|
|
}
|
|
_, branchName := mgr.buildWorktreeNames(req)
|
|
|
|
// Default prefix from NormalizeBranchPrefix("")
|
|
defaultPrefix := NormalizeBranchPrefix("")
|
|
if !strings.HasPrefix(branchName, defaultPrefix+"add-feature-") {
|
|
t.Errorf("branchName = %q, want prefix %q", branchName, defaultPrefix+"add-feature-")
|
|
}
|
|
})
|
|
|
|
t.Run("title with only special chars falls back to task ID for branch", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-fallback",
|
|
TaskTitle: "!@#$%",
|
|
WorktreeBranchPrefix: "kandev/",
|
|
}
|
|
_, branchName := mgr.buildWorktreeNames(req)
|
|
|
|
// SanitizeForBranch("!@#$%") returns "", so should fall back to task ID
|
|
if !strings.HasPrefix(branchName, "kandev/task-fallback-") {
|
|
t.Errorf("branchName = %q, want prefix %q", branchName, "kandev/task-fallback-")
|
|
}
|
|
})
|
|
|
|
t.Run("non-ASCII title falls back to task ID for branch and suffix-only for dir", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-cjk",
|
|
TaskTitle: "修复登录问题",
|
|
WorktreeBranchPrefix: "kandev/",
|
|
}
|
|
dirName, branchName := mgr.buildWorktreeNames(req)
|
|
|
|
// SemanticWorktreeName returns just the suffix when the sanitized title
|
|
// is empty — there should be no title portion, so no underscore separator.
|
|
if strings.Contains(dirName, "_") {
|
|
t.Errorf("dirName should be suffix only (no underscore), got %q", dirName)
|
|
}
|
|
// TaskBranchNameWithSuffix falls back to the sanitized task ID.
|
|
if !strings.HasPrefix(branchName, "kandev/task-cjk-") {
|
|
t.Errorf("branchName = %q, want prefix %q", branchName, "kandev/task-cjk-")
|
|
}
|
|
})
|
|
|
|
t.Run("mixed ASCII and CJK title keeps ASCII parts", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-mix",
|
|
TaskTitle: "Fix 修复 bug",
|
|
WorktreeBranchPrefix: "kandev/",
|
|
}
|
|
dirName, branchName := mgr.buildWorktreeNames(req)
|
|
|
|
if !strings.HasPrefix(dirName, "fix-bug_") {
|
|
t.Errorf("dirName = %q, want prefix %q", dirName, "fix-bug_")
|
|
}
|
|
if !strings.HasPrefix(branchName, "kandev/fix-bug-") {
|
|
t.Errorf("branchName = %q, want prefix %q", branchName, "kandev/fix-bug-")
|
|
}
|
|
})
|
|
|
|
t.Run("unique names on successive calls", func(t *testing.T) {
|
|
req := CreateRequest{
|
|
TaskID: "task-1",
|
|
TaskTitle: "Same title",
|
|
}
|
|
dir1, branch1 := mgr.buildWorktreeNames(req)
|
|
dir2, branch2 := mgr.buildWorktreeNames(req)
|
|
|
|
if dir1 == dir2 {
|
|
t.Errorf("expected unique dir names, both are %q", dir1)
|
|
}
|
|
if branch1 == branch2 {
|
|
t.Errorf("expected unique branch names, both are %q", branch1)
|
|
}
|
|
})
|
|
}
|