Truncate execution.log patches and keep members aligned with patch bodies so verbose MiniMax-M3 runs no longer exceed the Go 8MB fail-closed limit. Surface body-too-large vs typed-reject reasons and map contract failures to fatal restart instead of misleading resumable TASK_INTERRUPTED.
163 lines
5.1 KiB
Go
163 lines
5.1 KiB
Go
package stream
|
|
|
|
import (
|
|
"errors"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestClassifyError_TimeoutIsTransient(t *testing.T) {
|
|
info := ClassifyError(errors.New("context deadline exceeded"), "dialogue_generating", "req_1", "sess_1", "")
|
|
if info.Class != ErrorClassTransient {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
if info.Code != "CHAT_TIMEOUT" && info.Code != "TRANSIENT_ERROR" {
|
|
// Accept either stable code chosen in implementation; prefer CHAT_TIMEOUT when message contains timeout/deadline
|
|
t.Fatalf("code=%s", info.Code)
|
|
}
|
|
if len(info.Actions) != 1 || info.Actions[0] != "retry" {
|
|
t.Fatalf("actions=%v", info.Actions)
|
|
}
|
|
if info.RequestID != "req_1" || info.PhaseID != "dialogue_generating" {
|
|
t.Fatalf("meta=%+v", info)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_DoesNotLeakSQL(t *testing.T) {
|
|
info := ClassifyError(errors.New("pq: password authentication failed /home/weichen/x"), "dialogue_generating", "req_x", "sess_1", "")
|
|
if strings.Contains(info.Upstream, "pq:") || strings.Contains(info.Upstream, "/home/") || strings.Contains(info.UserMessage, "pq:") {
|
|
t.Fatalf("%+v", info)
|
|
}
|
|
if info.Message != info.UserMessage {
|
|
t.Fatal("message alias")
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_GenericIsFatal(t *testing.T) {
|
|
info := ClassifyError(errors.New("invalid project config"), "intent_recognizing", "req_2", "sess_1", "")
|
|
if info.Class != ErrorClassFatal {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
hasRestart := false
|
|
for _, a := range info.Actions {
|
|
if a == "restart" {
|
|
hasRestart = true
|
|
}
|
|
}
|
|
if !hasRestart {
|
|
t.Fatalf("expected restart action, got %v", info.Actions)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_WorkspaceHintIsResumable(t *testing.T) {
|
|
info := ClassifyError(errors.New("step failed mid-run"), "step_executing", "req_3", "sess_1", "research_123")
|
|
if info.Class != ErrorClassResumable {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
hasContinue := false
|
|
hasRestart := false
|
|
for _, a := range info.Actions {
|
|
if a == "continue" {
|
|
hasContinue = true
|
|
}
|
|
if a == "restart" {
|
|
hasRestart = true
|
|
}
|
|
}
|
|
if !hasContinue {
|
|
t.Fatalf("expected continue, got %v", info.Actions)
|
|
}
|
|
if !hasRestart {
|
|
t.Fatalf("expected restart, got %v", info.Actions)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_DialogueNotNetwork(t *testing.T) {
|
|
info := ClassifyError(errors.New("dialogue generation failed"), "dialogue_generating", "req_5", "sess_1", "")
|
|
if info.Class == ErrorClassTransient {
|
|
t.Fatalf("dialogue substring must not classify as transient/network, class=%s code=%s", info.Class, info.Code)
|
|
}
|
|
if info.Class != ErrorClassFatal {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_NetworkIsTransient(t *testing.T) {
|
|
info := ClassifyError(&net.OpError{Op: "dial", Err: errors.New("connection refused")}, "dialogue_generating", "req_4", "sess_1", "")
|
|
if info.Class != ErrorClassTransient {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_DocumentBusyIsVisibleStateWithoutRetry(t *testing.T) {
|
|
info := ClassifyError(errors.New("文档正在被 squad(run_1) 编辑中,禁止并行修改"), "", "req_busy", "sess_1", "")
|
|
if info.Code != "DOC_BUSY" {
|
|
t.Fatalf("code=%s", info.Code)
|
|
}
|
|
if len(info.Actions) != 0 {
|
|
t.Fatalf("busy state must not encourage duplicate retry, actions=%v", info.Actions)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_DocumentLedgerFKIsLedgerError(t *testing.T) {
|
|
err := errors.New(`persist document ledger: ERROR: insert or update on table "document_records" violates foreign key constraint "document_records_reviewer_id_fkey" (SQLSTATE 23503)`)
|
|
info := ClassifyError(err, "", "run_1", "sess-p6417-u9", "")
|
|
if info.Code != "DOC_LEDGER_ERROR" {
|
|
t.Fatalf("code=%s", info.Code)
|
|
}
|
|
if info.Class != ErrorClassFatal {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
if len(info.Actions) != 1 || info.Actions[0] != "restart" {
|
|
t.Fatalf("actions=%v", info.Actions)
|
|
}
|
|
if !strings.Contains(info.UserMessage, "文档职责账本") {
|
|
t.Fatalf("user_message=%q", info.UserMessage)
|
|
}
|
|
// SQL detail must not leak into user-facing fields (Upstream may be stripped by leak filter).
|
|
if strings.Contains(info.UserMessage, "SQLSTATE") || strings.Contains(info.UserMessage, "foreign key") {
|
|
t.Fatalf("user message leaked SQL: %q", info.UserMessage)
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_TypedResultRejectedIsFatalNotResumable(t *testing.T) {
|
|
info := ClassifyError(
|
|
errors.New("step step_01_organize failed: typed result rejected: digest"),
|
|
"step_executing",
|
|
"run_1",
|
|
"sess_1",
|
|
"ws_bizdev_1",
|
|
)
|
|
if info.Code != "RUNNER_RESULT_REJECTED" {
|
|
t.Fatalf("code=%s", info.Code)
|
|
}
|
|
if info.Class != ErrorClassFatal {
|
|
t.Fatalf("class=%s (must not be resumable)", info.Class)
|
|
}
|
|
if len(info.Actions) != 1 || info.Actions[0] != "restart" {
|
|
t.Fatalf("actions=%v", info.Actions)
|
|
}
|
|
for _, a := range info.Actions {
|
|
if a == "continue" {
|
|
t.Fatal("typed reject must not offer continue")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClassifyError_BodyTooLargeIsFatalNotResumable(t *testing.T) {
|
|
info := ClassifyError(
|
|
errors.New("step step_01_organize failed: runner result body too large"),
|
|
"step_executing",
|
|
"run_2",
|
|
"sess_1",
|
|
"ws_bizdev_2",
|
|
)
|
|
if info.Code != "RUNNER_RESULT_REJECTED" {
|
|
t.Fatalf("code=%s", info.Code)
|
|
}
|
|
if info.Class != ErrorClassFatal {
|
|
t.Fatalf("class=%s", info.Class)
|
|
}
|
|
}
|