package core import ( "context" "testing" "github.com/kamelion/ratatoskr-go/internal/storage" ) // mockDecider — тестовый Decider с заданным поведением. type mockDecider struct { fn func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) } func (m *mockDecider) Decide(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) { if m.fn != nil { return m.fn(ctx, history, draft, force) } return Decision{Phase: "ask", ChatReply: "ok"}, nil } func setupCore(t *testing.T, fn func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error)) (*Core, context.Context, *storage.Storage) { t.Helper() ctx := context.Background() store, err := storage.Open(ctx, ":memory:") if err != nil { t.Fatalf("storage.Open: %v", err) } t.Cleanup(func() { store.Close() }) c := New(store, &mockDecider{fn: fn}) return c, ctx, store } func mkTask(t *testing.T, store *storage.Storage, ctx context.Context, chatID string) int64 { t.Helper() id, err := store.CreateTask(ctx, &storage.Task{ChatID: chatID, TaskTag: "tag-" + chatID}) if err != nil { t.Fatalf("CreateTask: %v", err) } return id } func TestStartCreatesCollecting(t *testing.T) { c, ctx, store := setupCore(t, nil) id := mkTask(t, store, ctx, "u1") res, err := c.ProcessTurn(ctx, id, "/start") if err != nil { t.Fatalf("ProcessTurn /start: %v", err) } if res.Action != "greeting" { t.Fatalf("action = %q, want greeting", res.Action) } task, _ := store.GetTask(ctx, id) if task.Status != storage.StatusCollecting { t.Fatalf("status = %s, want collecting", task.Status) } } func TestCancelSetsCancelled(t *testing.T) { c, ctx, store := setupCore(t, nil) id := mkTask(t, store, ctx, "u1") res, err := c.ProcessTurn(ctx, id, "/cancel") if err != nil { t.Fatalf("ProcessTurn /cancel: %v", err) } if res.Action != "drop" { t.Fatalf("action = %q, want drop", res.Action) } task, _ := store.GetTask(ctx, id) if task.Status != storage.StatusCancelled { t.Fatalf("status = %s, want cancelled", task.Status) } } func TestSingleTurnPropose(t *testing.T) { c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) { return Decision{ Phase: "propose", ChatReply: "Готово", Draft: storage.Task{Title: "Новое", Goal: "Сделать", Repo: "acme/app"}, }, nil }) id := mkTask(t, store, ctx, "u1") res, err := c.ProcessTurn(ctx, id, "Сделай калькулятор") if err != nil { t.Fatalf("ProcessTurn: %v", err) } if res.Action != "summary" { t.Fatalf("action = %q, want summary", res.Action) } task, _ := store.GetTask(ctx, id) if task.Status != storage.StatusReady { t.Fatalf("status = %s, want ready", task.Status) } if task.Title != "Новое" { t.Fatalf("title = %q, want Новое", task.Title) } } func TestAskReturnsQuestions(t *testing.T) { c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) { return Decision{ Phase: "ask", ChatReply: "Уточню", Questions: []string{"Какой язык?", "Какой срок?"}, }, nil }) id := mkTask(t, store, ctx, "u1") res, err := c.ProcessTurn(ctx, id, "Напиши парсер") if err != nil { t.Fatalf("ProcessTurn: %v", err) } if res.Action != "send" { t.Fatalf("action = %q, want send", res.Action) } if res.Reply != "Уточню\n1. Какой язык?\n2. Какой срок?" { t.Fatalf("reply = %q", res.Reply) } // в фазе collecting — продолжaeм сбор task, _ := store.GetTask(ctx, id) if task.Status != storage.StatusCollecting { t.Fatalf("status = %s, want collecting", task.Status) } } func TestAbortReturnsDrop(t *testing.T) { c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) { return Decision{Phase: "abort", ChatReply: "Не хватает данных"}, nil }) id := mkTask(t, store, ctx, "u1") res, err := c.ProcessTurn(ctx, id, "привет") if err != nil { t.Fatalf("ProcessTurn: %v", err) } if res.Action != "drop" { t.Fatalf("action = %q, want drop", res.Action) } task, _ := store.GetTask(ctx, id) if task.Status != storage.StatusAborted { t.Fatalf("status = %s, want aborted", task.Status) } } func TestConsentInReady(t *testing.T) { c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) { return Decision{Phase: "propose", Draft: storage.Task{Title: "X"}}, nil }) id := mkTask(t, store, ctx, "u1") _, _ = c.ProcessTurn(ctx, id, "сделай задачу") res, err := c.ProcessTurn(ctx, id, "создавай") if err != nil { t.Fatalf("ProcessTurn создавай: %v", err) } if res.Action != "created:"+itoa(id) { t.Fatalf("action = %q, want created:%d", res.Action, id) } } func TestEditInReadyGoesCollecting(t *testing.T) { var calls int c, ctx, store := setupCore(t, func(ctx context.Context, history []Message, draft storage.Task, force bool) (Decision, error) { calls++ return Decision{Phase: "propose", Draft: storage.Task{Title: "X"}}, nil }) id := mkTask(t, store, ctx, "u1") _, _ = c.ProcessTurn(ctx, id, "сделай X") // в ready пишем правку, не согласие res, err := c.ProcessTurn(ctx, id, "нет, лучше Y") if err != nil { t.Fatalf("ProcessTurn edit: %v", err) } if res.Action != "summary" { t.Fatalf("action = %q, want summary", res.Action) } if calls != 2 { t.Fatalf("decide calls = %d, want 2", calls) } task, _ := store.GetTask(ctx, id) if task.Status != storage.StatusReady { t.Fatalf("status = %s, want ready", task.Status) } } func TestRetryNotFound(t *testing.T) { c, ctx, _ := setupCore(t, nil) res, err := c.ProcessTurn(ctx, 999, "/retry 999") if err != nil { t.Fatalf("ProcessTurn /retry: %v", err) } if res.Action != "send" { t.Fatalf("action = %q, want send", res.Action) } } func TestUnknownCommand(t *testing.T) { c, ctx, store := setupCore(t, nil) id := mkTask(t, store, ctx, "u1") res, err := c.ProcessTurn(ctx, id, "/bogus") if err != nil { t.Fatalf("ProcessTurn: %v", err) } if res.Action != "send" { t.Fatalf("action = %q, want send", res.Action) } } func TestParseTaskID(t *testing.T) { if v, ok := parseTaskID("5"); !ok || v != 5 { t.Fatalf("parseTaskID(5) = %d,%v", v, ok) } if v, ok := parseTaskID("123"); !ok || v != 123 { t.Fatalf("parseTaskID(123) = %d,%v", v, ok) } if _, ok := parseTaskID(""); ok { t.Fatal("parseTaskID('') should be invalid") } if _, ok := parseTaskID("abc"); ok { t.Fatal("parseTaskID(abc) should be invalid") } }