feat: авто-уведомления о статусах и хендоффах задачи
This commit is contained in:
@@ -18,6 +18,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -129,6 +130,7 @@ func e2eAssemble(t *testing.T) (*App, string, *e2eChannel) {
|
||||
Interval: 30 * time.Millisecond,
|
||||
MaxJobs: 1,
|
||||
Live: opencode.NewLiveRegistry(),
|
||||
Notify: a, // авто-уведомления владельцу через Router (как в app.New)
|
||||
// GitToken зададим пустым: origin в seed-репо локальный (file path),
|
||||
// http.extraHeader не нужен для локального пуша.
|
||||
}
|
||||
@@ -139,13 +141,13 @@ func e2eAssemble(t *testing.T) (*App, string, *e2eChannel) {
|
||||
// e2eChannel — минимальный fake-канал для перехвата исходящих
|
||||
// и доставки входящих через роутер (как реальный канал).
|
||||
type e2eChannel struct {
|
||||
onMsg func(chat.Incoming)
|
||||
onMsg chat.Handler
|
||||
sent []chat.Message
|
||||
}
|
||||
|
||||
func (c *e2eChannel) Run(_ context.Context) error { return nil }
|
||||
func (c *e2eChannel) OnMessage(f func(chat.Incoming)) {
|
||||
c.onMsg = f
|
||||
func (c *e2eChannel) OnMessage(h chat.Handler) {
|
||||
c.onMsg = h
|
||||
}
|
||||
func (c *e2eChannel) Send(_ context.Context, _ chat.Address, m chat.Message) error {
|
||||
c.sent = append(c.sent, m)
|
||||
@@ -719,4 +721,108 @@ func TestE2EWorkerDoesNotTakeUnconfirmed(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Logf("WORKER-CONSENT OK: в ready воркер не трогал, после «создавай» → success, ветка %s", branch)
|
||||
}
|
||||
|
||||
// TestE2ENotificationsOnTransitions — авто-уведомления владельцу на каждый
|
||||
// переход статуса задачи и отсутствие задвоения на approved.
|
||||
//
|
||||
// Проверяет сквозной поток сообщений через chat.Router:
|
||||
// - ready → approved («создавай»): ровно ОДНО сообщение (ответ «одобрена»),
|
||||
// отдельное уведомление "Задача #N: approved" НЕ дублируется;
|
||||
// - со стороны воркера: уведомления running → dev→reviewer → success
|
||||
// доходят владельцу через Router.Send.
|
||||
func TestE2ENotificationsOnTransitions(t *testing.T) {
|
||||
a, worktree, fake := e2eAssemble(t)
|
||||
a.seedFakeRepo(t, worktree, "calc")
|
||||
|
||||
ctx := context.Background()
|
||||
uid := chat.UserID("u-notif")
|
||||
|
||||
// --- 1. постановка: /start → draft→collecting (приветствие) ---
|
||||
fake.deliver(uid, "/start")
|
||||
task, err := a.Store.GetActiveTaskByChatID(ctx, string(uid))
|
||||
if err != nil {
|
||||
t.Fatalf("get task after /start: %v", err)
|
||||
}
|
||||
taskID := task.ID
|
||||
|
||||
// --- 2. текст → collecting→ready (сводка черновика) ---
|
||||
fake.deliver(uid, "Сделай калькулятор в calc")
|
||||
task, err = a.Store.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
t.Fatalf("get task: %v", err)
|
||||
}
|
||||
if task.Status != storage.StatusReady {
|
||||
t.Fatalf("status = %q, want ready", task.Status)
|
||||
}
|
||||
|
||||
// --- 3. «создавай» → ready→approved: один ответ, без дубля-уведомления ---
|
||||
fake.deliver(uid, "создавай")
|
||||
task, err = a.Store.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
t.Fatalf("get task after создавай: %v", err)
|
||||
}
|
||||
if task.Status != storage.StatusApproved {
|
||||
t.Fatalf("status = %q, want approved", task.Status)
|
||||
}
|
||||
|
||||
approvedReplies := 0
|
||||
var approvedText string
|
||||
approvedNotifs := 0
|
||||
for _, m := range fake.sent {
|
||||
if strings.Contains(m.Text, "одобрена") {
|
||||
approvedReplies++
|
||||
approvedText = m.Text
|
||||
}
|
||||
if strings.Contains(m.Text, fmt.Sprintf("Задача #%d: approved", taskID)) {
|
||||
approvedNotifs++
|
||||
}
|
||||
}
|
||||
if approvedReplies != 1 {
|
||||
t.Errorf("сообщений об одобрении = %d, want ровно 1 (нет задвоения)", approvedReplies)
|
||||
}
|
||||
if approvedNotifs != 0 {
|
||||
t.Errorf("отдельное уведомление 'Задача #%d: approved' продублировано (%d раз)", taskID, approvedNotifs)
|
||||
}
|
||||
if !strings.Contains(approvedText, "одобрена") {
|
||||
t.Errorf("ответ об одобрении = %q", approvedText)
|
||||
}
|
||||
|
||||
// --- 4. воркер: running → dev→reviewer → success через Router ---
|
||||
wkCtx, wkCancel := context.WithCancel(ctx)
|
||||
defer wkCancel()
|
||||
a.Worker.Start(wkCtx)
|
||||
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for {
|
||||
tk, err := a.Store.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
t.Fatalf("get task: %v", err)
|
||||
}
|
||||
if tk.Status == storage.StatusSuccess || tk.Status == storage.StatusFailed {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("таймаут ожидания success, последний статус %q", tk.Status)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// финальные статус-уведомления воркера в правильном порядке
|
||||
wantOrder := []string{
|
||||
fmt.Sprintf("Задача #%d: running", taskID),
|
||||
fmt.Sprintf("Задача #%d: dev → reviewer (итерация 1)", taskID),
|
||||
fmt.Sprintf("Задача #%d: success", taskID),
|
||||
}
|
||||
var gotOrder []string
|
||||
for _, m := range fake.sent {
|
||||
if strings.HasPrefix(m.Text, fmt.Sprintf("Задача #%d:", taskID)) {
|
||||
gotOrder = append(gotOrder, m.Text)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(gotOrder, wantOrder) {
|
||||
t.Errorf("уведомления воркера = %#v, want %#v", gotOrder, wantOrder)
|
||||
}
|
||||
|
||||
t.Logf("NOTIFICATIONS OK: задача #%d, сообщений одобрения=%d, уведомления=%v", taskID, approvedReplies, gotOrder)
|
||||
}
|
||||
Reference in New Issue
Block a user