Files
goclaw/internal/agent/loop_truncation_test.go
viettranx a565ad8402 fix: preserve FinishReason on truncated tool calls + ParseError propagation (#605)
Three layered bugs caused OpenAI-compatible providers to silently
produce empty tool call arguments when max_tokens was hit mid-JSON:

1. FinishReason override: all providers unconditionally overwrote
   "length" → "tool_calls" when tool calls existed, preventing the
   agent loop's truncation guard from firing.

2. Silent parse failure: JSON unmarshal errors were logged but args
   stayed as empty map with no signal to the caller.

3. No fallback for unreliable providers: some proxies don't emit
   finish_reason:"length" at all, leaving no detection path.

Fixes:
- Add ParseError field to ToolCall struct for explicit error signal
- Guard FinishReason override with `!= "length"` in OpenAI, Codex
- Set ParseError in all provider parsers (OpenAI, Anthropic, Codex)
- Add hasParseErrors() fallback guard in agent loop for EC-5 scenario
- Cap consecutive truncation retries (maxTruncationRetries=3) to
  prevent burning all iterations when max_tokens is persistently low

Closes #605
2026-03-31 14:33:33 +07:00

47 lines
1.2 KiB
Go

package agent
import (
"testing"
"github.com/nextlevelbuilder/goclaw/internal/providers"
)
func TestHasParseErrors_NoCalls(t *testing.T) {
if hasParseErrors(nil) {
t.Error("nil slice should return false")
}
if hasParseErrors([]providers.ToolCall{}) {
t.Error("empty slice should return false")
}
}
func TestHasParseErrors_AllValid(t *testing.T) {
calls := []providers.ToolCall{
{ID: "1", Name: "read_file", Arguments: map[string]any{"path": "/tmp"}},
{ID: "2", Name: "exec", Arguments: map[string]any{"cmd": "ls"}},
}
if hasParseErrors(calls) {
t.Error("valid tool calls should return false")
}
}
func TestHasParseErrors_OneError(t *testing.T) {
calls := []providers.ToolCall{
{ID: "1", Name: "read_file", Arguments: map[string]any{"path": "/tmp"}},
{ID: "2", Name: "write_file", ParseError: "malformed JSON (42 chars): unexpected end of JSON input"},
}
if !hasParseErrors(calls) {
t.Error("should detect ParseError in second tool call")
}
}
func TestHasParseErrors_AllErrors(t *testing.T) {
calls := []providers.ToolCall{
{ID: "1", Name: "write_file", ParseError: "truncated"},
{ID: "2", Name: "exec", ParseError: "truncated"},
}
if !hasParseErrors(calls) {
t.Error("should detect ParseError when all calls have errors")
}
}