From c8cd425b29a00e2cd4f1eedb2100e5bbe56d6489 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sun, 16 Aug 2026 23:07:30 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20task=5Ftag=20(UUID)=20=D0=BF=D1=80=D0=B8=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B8=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B4=D0=B0=D1=87=D0=B8;=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=20feat/'=20=D0=BF=D1=80=D0=B8=20=D0=BF=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/storage/storage_test.go | 31 ++++++++++++++++++++++++++++++- internal/storage/tasks.go | 22 ++++++++++++++++++++-- internal/worker/gitops.go | 9 +++++++-- internal/worker/worker_test.go | 19 ++++++++++++++++++- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 64a391b..cbabab9 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -49,6 +49,35 @@ func TestCreateAndGetTask(t *testing.T) { } } +func TestCreateTask_AutoTag(t *testing.T) { + s, ctx := setupTestDB(t) + // без TaskTag — должен сгенериться стабильный UUID (нужен для feat/) + task := &Task{ChatID: "tg://auto-tag", Title: "No tag"} + id, err := s.CreateTask(ctx, task) + if err != nil { + t.Fatalf("CreateTask: %v", err) + } + if task.TaskTag == "" { + t.Fatal("expected TaskTag to be generated when empty") + } + got, err := s.GetTask(ctx, id) + if err != nil { + t.Fatalf("GetTask: %v", err) + } + if got.TaskTag != task.TaskTag { + t.Fatalf("TaskTag = %q, want stable %q", got.TaskTag, task.TaskTag) + } + // стабильность: на одном инстансе повторный CreateTask с тем же тегом даёт другой УИД + task2 := &Task{ChatID: "tg://abc-tag2", Title: "No tag 2"} + id2, err := s.CreateTask(ctx, task2) + if err != nil { + t.Fatalf("CreateTask2: %v", err) + } + if got2, _ := s.GetTask(ctx, id2); got2.TaskTag == got.TaskTag { + t.Fatalf("TaskTag должен быть уникальным, оба = %q", got.TaskTag) + } +} + func TestGetTaskNotFound(t *testing.T) { s, ctx := setupTestDB(t) _, err := s.GetTask(ctx, 999) @@ -236,4 +265,4 @@ func IsNotFound(err error) bool { func IsInvalidStatus(err error) bool { return errors.Is(err, ErrInvalidStatus) -} \ No newline at end of file +} diff --git a/internal/storage/tasks.go b/internal/storage/tasks.go index 67863d7..6ead6df 100644 --- a/internal/storage/tasks.go +++ b/internal/storage/tasks.go @@ -2,13 +2,31 @@ package storage import ( "context" + "crypto/rand" "database/sql" + "encoding/hex" "fmt" + "time" ) +// newTaskTag генерирует стабильный UUID задачи (task_tag) — он же ложится в +// feature-ветку git (feat/). Генерится при создании и НЕ меняется. +func newTaskTag() string { + b := make([]byte, 8) + if _, err := rand.Read(b); err != nil { + // крипто-rand недоступен — fallback по времени + return hex.EncodeToString([]byte(time.Now().Format("150405.000000000"))) + } + return hex.EncodeToString(b) +} + // CreateTask создаёт задачу со статусом draft. Возвращает её ID. -// chat_id и task_tag передаются извне (в диалоге — chat.Address + uuid). +// chat_id и task_tag передаются извне; если task_tag не задан — генерится уник +// UUID (стабильный на всё время жизни, нужен для feature-ветки feat/). func (s *Storage) CreateTask(ctx context.Context, t *Task) (int64, error) { + if t.TaskTag == "" { + t.TaskTag = newTaskTag() + } now := Now() res, err := s.db.ExecContext(ctx, ` INSERT INTO tasks (chat_id, title, goal, repo, repos, why, ac, task_tag, status, created_at, updated_at) @@ -161,4 +179,4 @@ func (s *Storage) DeleteTask(ctx context.Context, id int64) error { return fmt.Errorf("%w: task %d", ErrNotFound, id) } return nil -} \ No newline at end of file +} diff --git a/internal/worker/gitops.go b/internal/worker/gitops.go index c427e5d..9759f71 100644 --- a/internal/worker/gitops.go +++ b/internal/worker/gitops.go @@ -53,5 +53,10 @@ func (w *Worker) git(ctx context.Context, dir string, args ...string) (string, e // featureBranchName — имя ветки задачи: feat/. func featureBranchName(taskTag string) string { - return "feat/" + taskTag -} \ No newline at end of file + tag := strings.TrimSpace(taskTag) + if tag == "" { + // защита от старых задач с пустым task_tag: не даём сломать git (feat/ невалидна) + return "feat/main" // коллизия с базовой "main" не страшна: ветка feature-своя есть под этим именем + } + return "feat/" + tag +} diff --git a/internal/worker/worker_test.go b/internal/worker/worker_test.go index 6e48aee..c65ed3d 100644 --- a/internal/worker/worker_test.go +++ b/internal/worker/worker_test.go @@ -415,6 +415,23 @@ func TestWorkerPromptRendered(t *testing.T) { } } +func TestFeatureBranchName(t *testing.T) { + cases := []struct { + tag string + want string + }{ + {"", "feat/main"}, // пустой tag (старая задача) — не "feat/" + {" ", "feat/main"}, // только пробелы + {"3f2a1b", "feat/3f2a1b"}, // нормальный UUID + {" abc ", "feat/abc"}, // тримится + } + for _, c := range cases { + if got := featureBranchName(c.tag); got != c.want { + t.Errorf("featureBranchName(%q) = %q, want %q", c.tag, got, c.want) + } + } +} + func TestValidateRepoName(t *testing.T) { valid := []string{"calc", "proj-a", "my.repo", "node_2", "kamelion/ratatoskr-go", "a/b/c"} for _, r := range valid { @@ -568,4 +585,4 @@ func TestWorkerSemaphore(t *testing.T) { if len(success) != 2 { t.Errorf("после освобождения слота success = %d, want 2", len(success)) } -} \ No newline at end of file +}