Исправляет баг: воркер захватывал задачу на выполнение по статусу ready ещё до «создавай» (consent был заглушкой). Теперь: - новый статус approved: «создавай» → ready→approved; - воркер (pollAndDispatch + runTask) берёт ТОЛЬКО approved, ready = черновик готов, ждёт одобрения; - правка/текст в approved запрещены (финальное одобрение); - e2e-тест TestE2EWorkerDoesNotTakeUnconfirmed: в ready воркер задачу не трогает, запускает только после «создавай» → success; - обновлены все затронутые тесты (models/core/worker) и retry-фикстуры.
232 lines
6.9 KiB
Go
232 lines
6.9 KiB
Go
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", Repos: []string{"repo-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)
|
||
}
|
||
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 пишем правку, не согласие
|
||
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")
|
||
}
|
||
} |