internal/storage: SQLite-БД для задач и трассировок (таски + статус-машина, трассы с группировкой по task_id)
All checks were successful
build-test / build (push) Successful in 30s
All checks were successful
build-test / build (push) Successful in 30s
This commit is contained in:
239
internal/storage/storage_test.go
Normal file
239
internal/storage/storage_test.go
Normal file
@@ -0,0 +1,239 @@
|
||||
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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user