package storage import ( "context" "errors" "testing" ) func setupTestDB(t *testing.T) (*Storage, context.Context) { t.Helper() ctx := context.Background() s, err := Open(ctx, ":memory:") if err != nil { t.Fatalf("Open(:memory:): %v", err) } t.Cleanup(func() { s.Close() }) return s, ctx } func TestCreateAndGetTask(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ ChatID: "tg://123", Title: "Test task", Goal: "Test goal", Repo: "test/repo", TaskTag: "abc-123", } id, err := s.CreateTask(ctx, task) if err != nil { t.Fatalf("CreateTask: %v", err) } if id == 0 { t.Fatal("expected non-zero ID") } if task.Status != StatusDraft { t.Fatalf("expected draft status, got %s", task.Status) } got, err := s.GetTask(ctx, id) if err != nil { t.Fatalf("GetTask: %v", err) } if got.Title != "Test task" { t.Fatalf("title = %q, want %q", got.Title, "Test task") } if got.Status != StatusDraft { t.Fatalf("status = %s, want draft", got.Status) } } 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) if !IsNotFound(err) { t.Fatalf("expected ErrNotFound, got %v", err) } } func TestUpdateTaskStatus(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ ChatID: "tg://123", Title: "Update test", TaskTag: "def-456", } _, _ = s.CreateTask(ctx, task) // draft → collecting — валидно task.Status = StatusCollecting if err := s.UpdateTask(ctx, task); err != nil { t.Fatalf("UpdateTask draft→collecting: %v", err) } got, _ := s.GetTask(ctx, task.ID) if got.Status != StatusCollecting { t.Fatalf("status = %s, want collecting", got.Status) } // collecting → ready task.Status = StatusReady if err := s.UpdateTask(ctx, task); err != nil { t.Fatalf("UpdateTask collecting→ready: %v", err) } // ready → approved task.Status = StatusApproved if err := s.UpdateTask(ctx, task); err != nil { t.Fatalf("UpdateTask ready→approved: %v", err) } // approved → running task.Status = StatusRunning if err := s.UpdateTask(ctx, task); err != nil { t.Fatalf("UpdateTask approved→running: %v", err) } // running → success task.Status = StatusSuccess if err := s.UpdateTask(ctx, task); err != nil { t.Fatalf("UpdateTask running→success: %v", err) } } func TestUpdateTaskInvalidTransition(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ChatID: "tg://1", TaskTag: "invalid"} _, _ = s.CreateTask(ctx, task) task.Status = StatusRunning // draft → running невалидно err := s.UpdateTask(ctx, task) if !IsInvalidStatus(err) { t.Fatalf("expected ErrInvalidStatus, got %v", err) } } func TestUpdateTaskNotFound(t *testing.T) { s, ctx := setupTestDB(t) err := s.UpdateTask(ctx, &Task{ID: 999, Status: StatusReady}) if !IsNotFound(err) { t.Fatalf("expected ErrNotFound, got %v", err) } } func TestListTasks(t *testing.T) { s, ctx := setupTestDB(t) for i := 0; i < 5; i++ { _, _ = s.CreateTask(ctx, &Task{ ChatID: "tg://user", Title: "Task", TaskTag: "tag", }) } tasks, err := s.ListTasks(ctx, TaskFilter{ChatID: "tg://user", Limit: 10}) if err != nil { t.Fatalf("ListTasks: %v", err) } if len(tasks) != 5 { t.Fatalf("expected 5 tasks, got %d", len(tasks)) } } func TestDeleteTask(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ChatID: "tg://del", TaskTag: "delete-me"} id, _ := s.CreateTask(ctx, task) if err := s.DeleteTask(ctx, id); err != nil { t.Fatalf("DeleteTask: %v", err) } _, err := s.GetTask(ctx, id) if !IsNotFound(err) { t.Fatal("expected ErrNotFound after delete") } } func TestAppendAndGetTraces(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ChatID: "tg://trace", TaskTag: "trace-test"} id, _ := s.CreateTask(ctx, task) tr := &Trace{ TaskID: id, Agent: "analyst", SessionID: "sess-001", Prompt: "analyze this", Output: "", } traceID, err := s.AppendTrace(ctx, tr) if err != nil { t.Fatalf("AppendTrace: %v", err) } if traceID == 0 { t.Fatal("expected non-zero trace ID") } if tr.Status != TraceRunning { t.Fatalf("trace status = %s, want running", tr.Status) } // апдейт статуса if err := s.UpdateTraceStatus(ctx, traceID, TraceSuccess); err != nil { t.Fatalf("UpdateTraceStatus: %v", err) } // добавим вторую трассу _, _ = s.AppendTrace(ctx, &Trace{ TaskID: id, Agent: "dev", SessionID: "sess-002", }) traces, err := s.GetTraces(ctx, id) if err != nil { t.Fatalf("GetTraces: %v", err) } if len(traces) != 2 { t.Fatalf("expected 2 traces, got %d", len(traces)) } if traces[0].Status != TraceSuccess { t.Fatalf("first trace status = %s, want success", traces[0].Status) } if traces[1].Agent != "dev" { t.Fatalf("second trace agent = %s, want dev", traces[1].Agent) } } func TestGetLatestTrace(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ChatID: "tg://latest", TaskTag: "latest-trace"} id, _ := s.CreateTask(ctx, task) _, _ = s.AppendTrace(ctx, &Trace{TaskID: id, Agent: "analyst", SessionID: "s1"}) _, _ = s.AppendTrace(ctx, &Trace{TaskID: id, Agent: "analyst", SessionID: "s2"}) latest, err := s.GetLatestTrace(ctx, id, "analyst") if err != nil { t.Fatalf("GetLatestTrace: %v", err) } if latest.SessionID != "s2" { t.Fatalf("latest session = %s, want s2", latest.SessionID) } } func TestUpdateTraceOutput(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ChatID: "tg://out", TaskTag: "output"} id, _ := s.CreateTask(ctx, task) traceID, _ := s.AppendTrace(ctx, &Trace{TaskID: id, Agent: "dev"}) if err := s.UpdateTraceOutput(ctx, traceID, "hello world"); err != nil { t.Fatalf("UpdateTraceOutput: %v", err) } traces, _ := s.GetTraces(ctx, id) if traces[0].Output != "hello world" { t.Fatalf("output = %q, want %q", traces[0].Output, "hello world") } } // TestDeleteTracesByAgent — удаление трасс по агенту (сброс маркера при /retry). func TestDeleteTracesByAgent(t *testing.T) { s, ctx := setupTestDB(t) task := &Task{ChatID: "tg://del", TaskTag: "delete-by-agent"} id, _ := s.CreateTask(ctx, task) _, _ = s.AppendTrace(ctx, &Trace{TaskID: id, Agent: "dev"}) _, _ = s.AppendTrace(ctx, &Trace{TaskID: id, Agent: "postmortem"}) _, _ = s.AppendTrace(ctx, &Trace{TaskID: id, Agent: "postmortem"}) if err := s.DeleteTracesByAgent(ctx, id, "postmortem"); err != nil { t.Fatalf("DeleteTracesByAgent: %v", err) } traces, _ := s.GetTraces(ctx, id) if len(traces) != 1 || traces[0].Agent != "dev" { t.Fatalf("traces = %d (agent %q), want только dev", len(traces), traces[0].Agent) } } // хелперы для проверки классов ошибок func IsNotFound(err error) bool { return errors.Is(err, ErrNotFound) } func IsInvalidStatus(err error) bool { return errors.Is(err, ErrInvalidStatus) }