Files
ratatoskr-go/internal/core/core_test.go
ki.sagidullin 1459670ce9
Some checks failed
CI / test (pull_request) Failing after 34s
CI / build-and-package (amd64, linux) (pull_request) Successful in 34s
CI / build-and-package (amd64, windows) (pull_request) Successful in 36s
refactor: чистка мёртвого кода, лимит ходов D3, HTML-экранирование и UTF-8 обрезка в Telegram
2026-08-18 23:13:04 +05:00

254 lines
7.3 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 core
import (
"context"
"strings"
"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")
_, err := c.ProcessTurn(ctx, id, "/start")
if err != nil {
t.Fatalf("ProcessTurn /start: %v", err)
}
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")
_, err := c.ProcessTurn(ctx, id, "/cancel")
if err != nil {
t.Fatalf("ProcessTurn /cancel: %v", err)
}
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")
_, err := c.ProcessTurn(ctx, id, "Сделай калькулятор")
if err != nil {
t.Fatalf("ProcessTurn: %v", err)
}
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.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")
_, err := c.ProcessTurn(ctx, id, "привет")
if err != nil {
t.Fatalf("ProcessTurn: %v", err)
}
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", Repos: []string{"repo-x"}}}, nil
})
id := mkTask(t, store, ctx, "u1")
_, _ = c.ProcessTurn(ctx, id, "сделай задачу")
_, err := c.ProcessTurn(ctx, id, "создавай")
if err != nil {
t.Fatalf("ProcessTurn создавай: %v", err)
}
task, _ := store.GetTask(ctx, id)
if task.Status != storage.StatusApproved {
t.Fatalf("status после создавай = %s, want approved", task.Status)
}
}
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", Repos: []string{"repo-x"}}}, nil
})
id := mkTask(t, store, ctx, "u1")
_, _ = c.ProcessTurn(ctx, id, "сделай X")
// в ready пишем правку, не согласие
_, err := c.ProcessTurn(ctx, id, "нет, лучше Y")
if err != nil {
t.Fatalf("ProcessTurn edit: %v", err)
}
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)
_, err := c.ProcessTurn(ctx, 999, "/retry 999")
if err != nil {
t.Fatalf("ProcessTurn /retry: %v", err)
}
}
func TestUnknownCommand(t *testing.T) {
c, ctx, store := setupCore(t, nil)
id := mkTask(t, store, ctx, "u1")
_, err := c.ProcessTurn(ctx, id, "/bogus")
if err != nil {
t.Fatalf("ProcessTurn: %v", err)
}
}
func TestMaxTurnsBlocksExcessCollection(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: "ask", ChatReply: "Ещё вопрос"}, nil
})
c.MaxTurns = 2
id := mkTask(t, store, ctx, "u1")
_, err := c.ProcessTurn(ctx, id, "первый факт")
if err != nil {
t.Fatalf("1-й ход: %v", err)
}
_, err = c.ProcessTurn(ctx, id, "второй факт")
if err != nil {
t.Fatalf("2-й ход: %v", err)
}
res, err := c.ProcessTurn(ctx, id, "третий факт")
if err != nil {
t.Fatalf("3-й ход: %v", err)
}
if calls != 2 {
t.Fatalf("decide calls = %d, want 2", calls)
}
if !strings.Contains(res.Reply, "лимит") {
t.Fatalf("reply = %q, want упоминание лимита", res.Reply)
}
// /skip — принудительный вызов, лимит не мешает
_, err = c.ProcessTurn(ctx, id, "/skip")
if err != nil {
t.Fatalf("/skip: %v", err)
}
if calls != 3 {
t.Fatalf("decide calls after /skip = %d, want 3", calls)
}
}
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")
}
}