feat: воркер берёт задачи только после одобрения (approved)
Исправляет баг: воркер захватывал задачу на выполнение по статусу ready ещё до «создавай» (consent был заглушкой). Теперь: - новый статус approved: «создавай» → ready→approved; - воркер (pollAndDispatch + runTask) берёт ТОЛЬКО approved, ready = черновик готов, ждёт одобрения; - правка/текст в approved запрещены (финальное одобрение); - e2e-тест TestE2EWorkerDoesNotTakeUnconfirmed: в ready воркер задачу не трогает, запускает только после «создавай» → success; - обновлены все затронутые тесты (models/core/worker) и retry-фикстуры.
This commit is contained in:
@@ -233,14 +233,14 @@ func TestE2EWholeAppFromTaskSetup(t *testing.T) {
|
||||
t.Errorf("repos пусто, want [calc]")
|
||||
}
|
||||
|
||||
// --- 3. Согласие «создавай» → задача подтверждена (остаётся ready) ---
|
||||
// --- 3. Согласие «создавай» → задача одобрена (approved) ---
|
||||
fake.deliver(uid, "создавай")
|
||||
task, err = a.Store.GetTask(ctx, task.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get task: %v", err)
|
||||
}
|
||||
if task.Status != storage.StatusReady {
|
||||
t.Errorf("status после создавай = %q, want ready", task.Status)
|
||||
if task.Status != storage.StatusApproved {
|
||||
t.Errorf("status после создавай = %q, want approved", task.Status)
|
||||
}
|
||||
|
||||
// --- 4. Исполнение: воркер (poll) dev → reviewer → push ---
|
||||
@@ -331,9 +331,9 @@ func e2eChainToState(t *testing.T, store *storage.Storage, id int64, target stor
|
||||
var chain []storage.Status
|
||||
switch target {
|
||||
case storage.StatusFailed:
|
||||
chain = []storage.Status{storage.StatusCollecting, storage.StatusReady, storage.StatusRunning, storage.StatusFailed}
|
||||
chain = []storage.Status{storage.StatusCollecting, storage.StatusReady, storage.StatusApproved, storage.StatusRunning, storage.StatusFailed}
|
||||
case storage.StatusTimeout:
|
||||
chain = []storage.Status{storage.StatusCollecting, storage.StatusReady, storage.StatusRunning, storage.StatusTimeout}
|
||||
chain = []storage.Status{storage.StatusCollecting, storage.StatusReady, storage.StatusApproved, storage.StatusRunning, storage.StatusTimeout}
|
||||
default:
|
||||
t.Fatalf("e2eChainToState: неподдерживаемый target %q", target)
|
||||
}
|
||||
@@ -418,14 +418,14 @@ func e2eRetryCommon(t *testing.T, a *App, fake *e2eChannel, worktree, uid string
|
||||
t.Errorf("retry: repos пусто, want [calc]")
|
||||
}
|
||||
|
||||
// 3. «создавай» → остаётся ready
|
||||
// 3. «создавай» → approved (одобрено, воркер заберёт)
|
||||
fake.deliver(chat.UserID(uid), "создавай")
|
||||
tk, err = a.Store.GetTask(ctx, taskID)
|
||||
if err != nil {
|
||||
t.Fatalf("e2eRetryCommon: get task after создавай: %v", err)
|
||||
}
|
||||
if tk.Status != storage.StatusReady {
|
||||
t.Errorf("retry: status после создавай = %q, want ready", tk.Status)
|
||||
if tk.Status != storage.StatusApproved {
|
||||
t.Errorf("retry: status после создавай = %q, want approved", tk.Status)
|
||||
}
|
||||
|
||||
// 4. воркер → success
|
||||
@@ -628,4 +628,95 @@ func e2eRetryRejected(t *testing.T, a *App, fake *e2eChannel, uid string, taskID
|
||||
t.Errorf("retry(neg): нет понятного ответа «нельзя перезапустить», отправлено: %d сообщений", len(fake.sent))
|
||||
}
|
||||
t.Logf("RETRY-REJECTED OK: задача #%d осталась %q", taskID, after.Status)
|
||||
}
|
||||
|
||||
// TestE2EWorkerDoesNotTakeUnconfirmed — регрессия бага, когда воркер брал
|
||||
// задачу на выполнение ещё до «создавай» (по статусу ready, без одобрения).
|
||||
// Проверяет: пока задача в ready, воркер её НЕ трогает; она уходит в работу
|
||||
// только после «создавай» → approved.
|
||||
func TestE2EWorkerDoesNotTakeUnconfirmed(t *testing.T) {
|
||||
a, worktree, fake := e2eAssemble(t)
|
||||
a.seedFakeRepo(t, worktree, "calc")
|
||||
|
||||
ctx := context.Background()
|
||||
uid := chat.UserID("unconf")
|
||||
|
||||
// --- 1. Постановка до ready, без «создавай» ---
|
||||
fake.deliver(uid, "/start")
|
||||
fake.deliver(uid, "Сделай калькулятор в calc")
|
||||
task, err := a.Store.GetActiveTaskByChatID(ctx, string(uid))
|
||||
if err != nil {
|
||||
t.Fatalf("get task: %v", err)
|
||||
}
|
||||
if task.Status != storage.StatusReady {
|
||||
t.Fatalf("status после analyst = %q, want ready (черновик готов, но НЕ одобрен)", task.Status)
|
||||
}
|
||||
beforeTraces, _ := a.Store.GetTraces(ctx, task.ID)
|
||||
|
||||
// --- 2. Запускаем воркер и даём ему время «промахнуться» ---
|
||||
wkCtx, wkCancel := context.WithCancel(ctx)
|
||||
defer wkCancel()
|
||||
a.Worker.Start(wkCtx)
|
||||
|
||||
time.Sleep(600 * time.Millisecond) // несколько poll-итераций (Interval=30ms)
|
||||
|
||||
// --- 3. Проверяем, что воркер НЕ взял неодобренную задачу ---
|
||||
after, err := a.Store.GetTask(ctx, task.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get task after worker: %v", err)
|
||||
}
|
||||
if after.Status != storage.StatusReady {
|
||||
t.Fatalf("БАГ: воркер взял неодобренную задачу — status = %q, want ready. Задача должна ждать «создавай».", after.Status)
|
||||
}
|
||||
afterTraces, _ := a.Store.GetTraces(ctx, task.ID)
|
||||
if len(afterTraces) != len(beforeTraces) {
|
||||
t.Errorf("БАГ: появились трассы без одобрения (было %d, стало %d)", len(beforeTraces), len(afterTraces))
|
||||
}
|
||||
// ветка не должна была создаться
|
||||
branch := "feat/" + after.TaskTag
|
||||
out, _ := exec.Command("git", "-C", filepath.Join(worktree, "calc"),
|
||||
"ls-remote", "origin", "refs/heads/"+branch).CombinedOutput()
|
||||
if strings.Contains(string(out), "refs/heads/"+branch) {
|
||||
t.Errorf("БАГ: feature-ветка %q уже в origin до одобрения", branch)
|
||||
}
|
||||
|
||||
// --- 4. «создавай» → approved, теперь воркер берёт и доезжает до success ---
|
||||
fake.deliver(uid, "создавай")
|
||||
tk, err := a.Store.GetTask(ctx, task.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get task after создавай: %v", err)
|
||||
}
|
||||
if tk.Status != storage.StatusApproved {
|
||||
t.Fatalf("status после создавай = %q, want approved", tk.Status)
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for {
|
||||
tk, err = a.Store.GetTask(ctx, task.ID)
|
||||
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)
|
||||
}
|
||||
if tk.Status != storage.StatusSuccess {
|
||||
t.Fatalf("status после воркера = %q, want success", tk.Status)
|
||||
}
|
||||
|
||||
// ветка теперь должна быть в origin
|
||||
out, err = exec.Command("git", "-C", filepath.Join(worktree, "calc"),
|
||||
"ls-remote", "origin", "refs/heads/"+branch).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("ls-remote origin: %v\n%s", err, out)
|
||||
}
|
||||
if !strings.Contains(string(out), "refs/heads/"+branch) {
|
||||
t.Errorf("feature-ветка %q не найдена в origin после одобрения:\n%s", branch, out)
|
||||
}
|
||||
|
||||
t.Logf("WORKER-CONSENT OK: в ready воркер не трогал, после «создавай» → success, ветка %s", branch)
|
||||
}
|
||||
Reference in New Issue
Block a user