fix: ревьюер не извлекал вердикт из NDJSON-потока → всегда двойной запуск и failed
parseReviewVerdict брал первую { и последнюю } из всего stdout и парсил
как один JSON, а stdout ревьювера — это NDJSON-поток opencode (несколько
событий). JSON никогда не парсился → v==nil → reviewWithRetry делал
повтор (вторая сессия ревьюера) → verdict nil → StatusFailed.
Приведено к пути аналитика: opencode.ExtractVerdict достаёт текст
вердикта из NDJSON, затем parseReviewVerdict парсит чистый JSON.
+ регрессионный тест TestWorkerReviewNDJSONPass (verdict внутри
text-парта, проверка single run без retry).
This commit is contained in:
@@ -2,11 +2,13 @@ package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -257,6 +259,62 @@ func TestWorkerReviewMaxIterations(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// reviewNDJSONRunner возвращает вердикт ревьюера как реальный NDJSON-поток opencode,
|
||||
// где JSON находится внутри последнего text-парта.
|
||||
func reviewNDJSONRunner(v *reviewVerdict) *opencode.Result {
|
||||
inner, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// text-парт содержит вердикт как JSON-строку (экранированную внутри NDJSON).
|
||||
return &opencode.Result{RC: 0, Stdout: "{\"type\":\"step_start\"}\n{\"type\":\"text\",\"part\":{\"text\":" + strconv.Quote(string(inner)) + "}}"}
|
||||
}
|
||||
|
||||
// TestWorkerReviewNDJSONPass: ревьюер вернул passed:true внутри NDJSON-потока —
|
||||
// извлечение должно достать вердикт, а не зафейлить задачу (регрессия двойного ревью).
|
||||
func TestWorkerReviewNDJSONPass(t *testing.T) {
|
||||
s := setupWorkerDB(t)
|
||||
task := createReadyTask(t, s, "calcnd")
|
||||
|
||||
w := &Worker{
|
||||
Store: s,
|
||||
Runner: &mockRunnerWorker{
|
||||
result: &opencode.Result{RC: 0, Stdout: "done", SessionID: "sess-1"},
|
||||
reviewResult: reviewNDJSONRunner(&reviewVerdict{
|
||||
Passed: true,
|
||||
Comments: []string{"ок"},
|
||||
}),
|
||||
},
|
||||
Worktree: t.TempDir(),
|
||||
Agent: "dev",
|
||||
}
|
||||
seedFakeRepo(t, w.Worktree, "calcnd")
|
||||
|
||||
ctx := context.Background()
|
||||
if err := w.runTask(ctx, task); err != nil {
|
||||
t.Fatalf("runTask: %v", err)
|
||||
}
|
||||
task, _ = s.GetTask(ctx, task.ID)
|
||||
if task.Status != storage.StatusSuccess {
|
||||
t.Errorf("status = %q, want success (вердикт из NDJSON должен распарситься)", task.Status)
|
||||
}
|
||||
|
||||
// ревью должен был запуститься ровно 1 раз (никакого retry на «невалидный»).
|
||||
traces, err := s.GetTraces(ctx, task.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get traces: %v", err)
|
||||
}
|
||||
var reviewTraces int
|
||||
for _, tr := range traces {
|
||||
if tr.Agent == "reviewer" {
|
||||
reviewTraces++
|
||||
}
|
||||
}
|
||||
if reviewTraces != 1 {
|
||||
t.Errorf("reviewer запускался %d раз, want 1 (не должно быть retry второй сессии)", reviewTraces)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerTimeout(t *testing.T) {
|
||||
s := setupWorkerDB(t)
|
||||
task := createReadyTask(t, s, "slow")
|
||||
|
||||
Reference in New Issue
Block a user