fix: генерить task_tag (UUID) при создании задачи; защита feat/' при пустом tag
This commit is contained in:
@@ -49,6 +49,35 @@ func TestCreateAndGetTask(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateTask_AutoTag(t *testing.T) {
|
||||||
|
s, ctx := setupTestDB(t)
|
||||||
|
// без TaskTag — должен сгенериться стабильный UUID (нужен для feat/<tag>)
|
||||||
|
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) {
|
func TestGetTaskNotFound(t *testing.T) {
|
||||||
s, ctx := setupTestDB(t)
|
s, ctx := setupTestDB(t)
|
||||||
_, err := s.GetTask(ctx, 999)
|
_, err := s.GetTask(ctx, 999)
|
||||||
@@ -236,4 +265,4 @@ func IsNotFound(err error) bool {
|
|||||||
|
|
||||||
func IsInvalidStatus(err error) bool {
|
func IsInvalidStatus(err error) bool {
|
||||||
return errors.Is(err, ErrInvalidStatus)
|
return errors.Is(err, ErrInvalidStatus)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,31 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newTaskTag генерирует стабильный UUID задачи (task_tag) — он же ложится в
|
||||||
|
// feature-ветку git (feat/<taskTag>). Генерится при создании и НЕ меняется.
|
||||||
|
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.
|
// CreateTask создаёт задачу со статусом draft. Возвращает её ID.
|
||||||
// chat_id и task_tag передаются извне (в диалоге — chat.Address + uuid).
|
// chat_id и task_tag передаются извне; если task_tag не задан — генерится уник
|
||||||
|
// UUID (стабильный на всё время жизни, нужен для feature-ветки feat/<tag>).
|
||||||
func (s *Storage) CreateTask(ctx context.Context, t *Task) (int64, error) {
|
func (s *Storage) CreateTask(ctx context.Context, t *Task) (int64, error) {
|
||||||
|
if t.TaskTag == "" {
|
||||||
|
t.TaskTag = newTaskTag()
|
||||||
|
}
|
||||||
now := Now()
|
now := Now()
|
||||||
res, err := s.db.ExecContext(ctx, `
|
res, err := s.db.ExecContext(ctx, `
|
||||||
INSERT INTO tasks (chat_id, title, goal, repo, repos, why, ac, task_tag, status, created_at, updated_at)
|
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 fmt.Errorf("%w: task %d", ErrNotFound, id)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,5 +53,10 @@ func (w *Worker) git(ctx context.Context, dir string, args ...string) (string, e
|
|||||||
|
|
||||||
// featureBranchName — имя ветки задачи: feat/<taskTag>.
|
// featureBranchName — имя ветки задачи: feat/<taskTag>.
|
||||||
func featureBranchName(taskTag string) string {
|
func featureBranchName(taskTag string) string {
|
||||||
return "feat/" + taskTag
|
tag := strings.TrimSpace(taskTag)
|
||||||
}
|
if tag == "" {
|
||||||
|
// защита от старых задач с пустым task_tag: не даём сломать git (feat/ невалидна)
|
||||||
|
return "feat/main" // коллизия с базовой "main" не страшна: ветка feature-своя есть под этим именем
|
||||||
|
}
|
||||||
|
return "feat/" + tag
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestValidateRepoName(t *testing.T) {
|
||||||
valid := []string{"calc", "proj-a", "my.repo", "node_2", "kamelion/ratatoskr-go", "a/b/c"}
|
valid := []string{"calc", "proj-a", "my.repo", "node_2", "kamelion/ratatoskr-go", "a/b/c"}
|
||||||
for _, r := range valid {
|
for _, r := range valid {
|
||||||
@@ -568,4 +585,4 @@ func TestWorkerSemaphore(t *testing.T) {
|
|||||||
if len(success) != 2 {
|
if len(success) != 2 {
|
||||||
t.Errorf("после освобождения слота success = %d, want 2", len(success))
|
t.Errorf("после освобождения слота success = %d, want 2", len(success))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user