Files
ratatoskr-go/internal/storage/storage_test.go
ki.sagidullin ed13386612
Some checks failed
CI / test (push) Failing after 1m54s
CI / build-and-package (amd64, linux) (push) Failing after 1m21s
CI / build-and-package (amd64, windows) (push) Successful in 33s
fix(core): /retry чистит postmortem-трассы — на новом прогоне постмортем запускается заново
2026-08-24 14:56:39 +05:00

295 lines
7.8 KiB
Go
Raw Permalink 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 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)
}