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:
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kamelion/ratatoskr-go/internal/opencode"
|
||||||
"github.com/kamelion/ratatoskr-go/internal/storage"
|
"github.com/kamelion/ratatoskr-go/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,7 +81,10 @@ func (w *Worker) runReviewer(ctx context.Context, taskID int64, cwd, prompt stri
|
|||||||
output := res.Stdout
|
output := res.Stdout
|
||||||
var v *reviewVerdict
|
var v *reviewVerdict
|
||||||
if res.RC == 0 {
|
if res.RC == 0 {
|
||||||
pv, perr := parseReviewVerdict(output)
|
// Как у аналитика: сначала достаём текст вердикта из NDJSON-потока opencode
|
||||||
|
// (ExtractVerdict), затем парсим уже чистый JSON-объект.
|
||||||
|
verdict := opencode.ExtractVerdict(output)
|
||||||
|
pv, perr := parseReviewVerdict(verdict)
|
||||||
if perr == nil {
|
if perr == nil {
|
||||||
v = pv
|
v = pv
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package worker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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) {
|
func TestWorkerTimeout(t *testing.T) {
|
||||||
s := setupWorkerDB(t)
|
s := setupWorkerDB(t)
|
||||||
task := createReadyTask(t, s, "slow")
|
task := createReadyTask(t, s, "slow")
|
||||||
|
|||||||
Reference in New Issue
Block a user