Files
ratatoskr-go/internal/storage/storage_test.go
ki.sagidullin 6c0b903794
Some checks failed
CI / test (push) Failing after 1m52s
CI / build-and-package (amd64, linux) (push) Failing after 1m3s
CI / build-and-package (amd64, windows) (push) Successful in 30s
feat(analyst): этапы задачи — аналитик раскладывает на steps с критериями готовности, dev идёт по ним
2026-08-24 18:05:06 +05:00

333 lines
9.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/<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) {
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 TestCreateAndGetTask_Steps(t *testing.T) {
s, ctx := setupTestDB(t)
task := &Task{
ChatID: "tg://steps",
Title: "Steps task",
TaskTag: "steps-1",
Steps: []Step{
{Title: "Реализовать модель", AC: "структура готова"},
{Title: "Добавить API", AC: "эндпоинт отвечает"},
},
}
id, err := s.CreateTask(ctx, task)
if err != nil {
t.Fatalf("CreateTask: %v", err)
}
got, err := s.GetTask(ctx, id)
if err != nil {
t.Fatalf("GetTask: %v", err)
}
if len(got.Steps) != 2 {
t.Fatalf("steps len = %d, want 2", len(got.Steps))
}
if got.Steps[0].Title != "Реализовать модель" || got.Steps[0].AC != "структура готова" {
t.Fatalf("steps[0] = %q / %q, want модель / структура готова", got.Steps[0].Title, got.Steps[0].AC)
}
// апдейт этапов
got.Steps = append(got.Steps, Step{Title: "Ревью", AC: "пройден review"})
if err := s.UpdateTask(ctx, got); err != nil {
t.Fatalf("UpdateTask steps: %v", err)
}
got2, _ := s.GetTask(ctx, id)
if len(got2.Steps) != 3 {
t.Fatalf("steps len after update = %d, want 3", len(got2.Steps))
}
}
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)
}