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