Files
ratatoskr-go/internal/storage/storage_test.go
Hermes c8cd425b29
All checks were successful
CI / test (push) Successful in 46s
CI / build-and-package (amd64, linux) (push) Successful in 44s
CI / build-and-package (amd64, windows) (push) Successful in 41s
fix: генерить task_tag (UUID) при создании задачи; защита feat/' при пустом tag
2026-08-16 23:07:30 +05:00

269 lines
6.9 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 → running
task.Status = StatusRunning
if err := s.UpdateTask(ctx, task); err != nil {
t.Fatalf("UpdateTask ready→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")
}
}
// хелперы для проверки классов ошибок
func IsNotFound(err error) bool {
return errors.Is(err, ErrNotFound)
}
func IsInvalidStatus(err error) bool {
return errors.Is(err, ErrInvalidStatus)
}