Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/config/unknownfields.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ type knownField struct {
}

func derefType(t reflect.Type) reflect.Type {
for t != nil && t.Kind() == reflect.Ptr {
for t != nil && t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t
Expand Down
2 changes: 2 additions & 0 deletions internal/providers/anthropic/cache_breakpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
// unmarked (Anthropic caps breakpoints at 4 per request: system, tools, and
// these two).
func TestAnthropicRequestMarksLastTwoMessagesForCaching(t *testing.T) {
t.Parallel()
var gotBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
Expand Down Expand Up @@ -73,6 +74,7 @@ func TestAnthropicRequestMarksLastTwoMessagesForCaching(t *testing.T) {
// the breakpoint, and thinking blocks must never carry cache_control (the API
// rejects them) — the marker goes on the last cacheable block instead.
func TestApplyMessageCacheBreakpointsSkipsThinkingBlocks(t *testing.T) {
t.Parallel()
messages := []anthropicMessage{
{Role: "user", Content: "plain string"},
{Role: "assistant", Content: []map[string]any{
Expand Down
2 changes: 2 additions & 0 deletions internal/providers/anthropic/dropped_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
// can ask the model to retry, mirroring the OpenAI provider's behavior, instead
// of silently dropping it.
func TestStreamCompletionEmitsDroppedOnNamelessToolUseBlock(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_start", `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":""}}`)
writeSSEEvent(w, "content_block_stop", `{"type":"content_block_stop","index":0}`)
Expand Down Expand Up @@ -45,6 +46,7 @@ func TestStreamCompletionEmitsDroppedOnNamelessToolUseBlock(t *testing.T) {

// A well-formed tool_use block must NOT emit a dropped signal.
func TestStreamCompletionDoesNotDropValidToolUseBlock(t *testing.T) {
t.Parallel()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_start", `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_1","name":"read_file"}}`)
writeSSEEvent(w, "content_block_stop", `{"type":"content_block_stop","index":0}`)
Expand Down
2 changes: 2 additions & 0 deletions internal/providers/anthropic/finish_reason_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
// truncated at the output cap. The provider must surface it on the done event so
// the agent does not treat a clipped answer as complete.
func TestStreamCompletionSurfacesMaxTokensStopReason(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "message_start", `{"type":"message_start","message":{"usage":{"input_tokens":5,"output_tokens":0}}}`)
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"cut"}}`)
Expand All @@ -37,6 +38,7 @@ func TestStreamCompletionSurfacesMaxTokensStopReason(t *testing.T) {

// A normal end_turn stop_reason must leave the done event's FinishReason empty.
func TestStreamCompletionNormalStopReasonHasNoFinishReason(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"ok"}}`)
writeSSEEvent(w, "message_delta", `{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":2}}`)
Expand Down
1 change: 1 addition & 0 deletions internal/providers/anthropic/idle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
// message_stop or closing) must abort on the idle timeout instead of blocking
// the agent forever.
func TestStreamCompletionIdleTimeoutAbortsStalledStream(t *testing.T) {
t.Parallel()
released := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}`)
Expand Down
3 changes: 3 additions & 0 deletions internal/providers/anthropic/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ func firstUserContentBlocks(t *testing.T, body map[string]any) []any {
// TestUserTextOnlyTurnUnchanged pins the text-only wire shape: a single
// user message whose content is a one-element text-block array.
func TestUserTextOnlyTurnUnchanged(t *testing.T) {
t.Parallel()
body := captureRequestBody(t, zeroruntime.CompletionRequest{
Messages: []zeroruntime.Message{
{Role: zeroruntime.MessageRoleUser, Content: "Describe this."},
Expand All @@ -78,6 +79,7 @@ func TestUserTextOnlyTurnUnchanged(t *testing.T) {
// TestUserImagePlusTextTurn asserts a text block followed by one image
// source block carrying base64 of the RAW bytes.
func TestUserImagePlusTextTurn(t *testing.T) {
t.Parallel()
raw := []byte{0x89, 0x50, 0x4e, 0x47, 0x01, 0x02}
body := captureRequestBody(t, zeroruntime.CompletionRequest{
Messages: []zeroruntime.Message{
Expand Down Expand Up @@ -112,6 +114,7 @@ func TestUserImagePlusTextTurn(t *testing.T) {
// TestUserImageOnlyTurnEmits asserts an image-only user turn (empty Content)
// still produces a user message with a single image block.
func TestUserImageOnlyTurnEmits(t *testing.T) {
t.Parallel()
raw := []byte{0xff, 0xd8, 0xff, 0xe0}
body := captureRequestBody(t, zeroruntime.CompletionRequest{
Messages: []zeroruntime.Message{
Expand Down
20 changes: 20 additions & 0 deletions internal/providers/anthropic/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ import (
"strings"
"testing"

"github.com/Gitlawb/zero/internal/providers/providerio"
"github.com/Gitlawb/zero/internal/zeroruntime"
)

func init() {
providerio.ShrinkBackoffForTest()
}

func TestStreamCompletionPostsMessagesRequest(t *testing.T) {
t.Parallel()
var gotPath string
var gotAPIKey string
var gotVersion string
Expand Down Expand Up @@ -123,6 +129,7 @@ func TestStreamCompletionPostsMessagesRequest(t *testing.T) {
}

func TestStreamCompletionAppliesCustomAuthAndHeaders(t *testing.T) {
t.Parallel()
var gotDefaultAuth string
var gotCustomAuth string
var gotTenant string
Expand Down Expand Up @@ -165,6 +172,7 @@ func TestStreamCompletionAppliesCustomAuthAndHeaders(t *testing.T) {
}

func TestStreamCompletionEmitsTextUsageAndDone(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "message_start", `{"type":"message_start","message":{"usage":{"input_tokens":25}}}`)
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}`)
Expand All @@ -186,6 +194,7 @@ func TestStreamCompletionEmitsTextUsageAndDone(t *testing.T) {
}

func TestStreamCompletionReportsCacheTokens(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "message_start", `{"type":"message_start","message":{"usage":{"input_tokens":10,"cache_read_input_tokens":200,"cache_creation_input_tokens":40}}}`)
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"hi"}}`)
Expand Down Expand Up @@ -217,6 +226,7 @@ func TestStreamCompletionReportsCacheTokens(t *testing.T) {
}

func TestStreamCompletionEnablesThinkingWhenEffortRequested(t *testing.T) {
t.Parallel()
var gotBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
Expand Down Expand Up @@ -257,6 +267,7 @@ func TestStreamCompletionEnablesThinkingWhenEffortRequested(t *testing.T) {
}

func TestStreamCompletionOmitsThinkingWithoutEffort(t *testing.T) {
t.Parallel()
var gotBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
Expand Down Expand Up @@ -287,6 +298,7 @@ func TestStreamCompletionOmitsThinkingWithoutEffort(t *testing.T) {
}

func TestStreamCompletionCapturesThinkingBlocksForReplay(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_start", `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`)
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Let me think"}}`)
Expand All @@ -312,6 +324,7 @@ func TestStreamCompletionCapturesThinkingBlocksForReplay(t *testing.T) {
}

func TestStreamCompletionPreservesUnclosedThinkingBlockAtStreamEnd(t *testing.T) {
t.Parallel()
// The SSE ends after thinking_delta/signature_delta but BEFORE the thinking
// block's content_block_stop. The open buffer must still be finalized into the
// done event's ReasoningBlocks (via closeOpen), or the next Anthropic replay
Expand Down Expand Up @@ -339,6 +352,7 @@ func TestStreamCompletionPreservesUnclosedThinkingBlockAtStreamEnd(t *testing.T)
}

func TestAnthropicRequestReplaysThinkingBlocksFirst(t *testing.T) {
t.Parallel()
var gotBody map[string]any
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
Expand Down Expand Up @@ -392,6 +406,7 @@ func TestAnthropicRequestReplaysThinkingBlocksFirst(t *testing.T) {
}

func TestStreamCompletionEmitsToolUseBlocks(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_start", `{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_1","name":"read_file","input":{}}}`)
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"path\":"}}`)
Expand All @@ -414,6 +429,7 @@ func TestStreamCompletionEmitsToolUseBlocks(t *testing.T) {
}

func TestStreamCompletionClosesOpenToolCallOnEOF(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "content_block_start", `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_1","name":"grep","input":{}}}`)
writeSSEEvent(w, "content_block_delta", `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"pattern\":\"Zero\"}"}}`)
Expand All @@ -429,6 +445,7 @@ func TestStreamCompletionClosesOpenToolCallOnEOF(t *testing.T) {
}

func TestStreamCompletionClassifiesHTTPErrorsAndRedactsToken(t *testing.T) {
t.Parallel()
cases := []struct {
name string
status int
Expand Down Expand Up @@ -464,6 +481,7 @@ func TestStreamCompletionClassifiesHTTPErrorsAndRedactsToken(t *testing.T) {
}

func TestStreamCompletionEmitsStreamErrorObject(t *testing.T) {
t.Parallel()
provider := newTestProviderWithKey(t, "sk-ant", func(w http.ResponseWriter, r *http.Request) {
writeSSEEvent(w, "error", `{"type":"error","error":{"message":"stream failed sk-ant","type":"overloaded_error"}}`)
})
Expand All @@ -481,6 +499,7 @@ func TestStreamCompletionEmitsStreamErrorObject(t *testing.T) {
}

func TestStreamCompletionRejectsMalformedHistoryBeforeDispatch(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
t.Fatal("provider should not dispatch malformed history")
})
Expand All @@ -507,6 +526,7 @@ func TestStreamCompletionRejectsMalformedHistoryBeforeDispatch(t *testing.T) {
}

func TestNewRequiresModelAndPositiveMaxTokens(t *testing.T) {
t.Parallel()
if _, err := New(Options{}); err == nil {
t.Fatal("New without model returned nil error")
}
Expand Down
1 change: 1 addition & 0 deletions internal/providers/anthropic/stop_reason_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
)

func TestMapStopReasonRefusal(t *testing.T) {
t.Parallel()
if got := mapStopReason("refusal"); got != zeroruntime.FinishReasonContentFilter {
t.Errorf("refusal → %q, want content_filter (M4)", got)
}
Expand Down
3 changes: 3 additions & 0 deletions internal/providers/factory_turn_session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
// wraps into the default TurnSessionProvider: construction succeeds, a session
// opens, and Compact reports unsupported (the default adapter contract).
func TestNewTurnSessionProviderForEveryKind(t *testing.T) {
t.Parallel()
kinds := []config.ProviderKind{
config.ProviderKindOpenAI,
config.ProviderKindOpenAICompatible,
Expand Down Expand Up @@ -65,6 +66,7 @@ func TestNewTurnSessionProviderForEveryKind(t *testing.T) {
// projects the model-registry entry (context limits, capability flags,
// reasoning efforts) into the flat ProviderCapabilities.
func TestNewTurnSessionProviderProjectsRegistryCapabilities(t *testing.T) {
t.Parallel()
registry, err := modelregistry.NewRegistry([]modelregistry.ModelEntry{{
ID: "pr7-caps-model",
DisplayName: "PR7 Capability Probe",
Expand Down Expand Up @@ -137,6 +139,7 @@ func TestNewTurnSessionProviderProjectsRegistryCapabilities(t *testing.T) {
// entry that enumerates no efforts of its own still reports the name-inferred
// effective tiers the /effort picker and run-time resolver advertise.
func TestNewTurnSessionProviderUsesEffectiveReasoningEfforts(t *testing.T) {
t.Parallel()
registry, err := modelregistry.NewRegistry([]modelregistry.ModelEntry{{
// A gpt-5-family id with NO ReasoningEfforts listed: the effective
// efforts come from name inference, differing from the raw entry.
Expand Down
1 change: 1 addition & 0 deletions internal/providers/gemini/done_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
// emitDone must mark the shared state done so callers observe it through the
// pointer (a by-value receiver would make state.done a dead store).
func TestEmitDoneMarksStateDoneThroughPointer(t *testing.T) {
t.Parallel()
provider, err := New(Options{Model: "gemini-test"})
if err != nil {
t.Fatalf("New returned error: %v", err)
Expand Down
8 changes: 8 additions & 0 deletions internal/providers/gemini/finish_reason_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
// MAX_TOKENS to length, and every other non-STOP reason surfaces its raw value
// (M3) so the turn is not mistaken for a clean completion.
func TestMapFinishReasonNonNormal(t *testing.T) {
t.Parallel()
for _, normal := range []string{"", "STOP", "FINISH_REASON_UNSPECIFIED"} {
if got := mapFinishReason(normal); got != "" {
t.Errorf("%q should be a normal stop (empty), got %q", normal, got)
Expand All @@ -35,6 +36,7 @@ func TestMapFinishReasonNonNormal(t *testing.T) {
// A candidate finishReason of MAX_TOKENS means the response was truncated at the
// output cap. The provider must surface it on the done event.
func TestStreamCompletionSurfacesMaxTokensFinishReason(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"role":"model","parts":[{"text":"cut"}]},"finishReason":"MAX_TOKENS"}]}`)
})
Expand All @@ -58,6 +60,7 @@ func TestStreamCompletionSurfacesMaxTokensFinishReason(t *testing.T) {

// A SAFETY finishReason maps to the runtime's content-filter reason.
func TestStreamCompletionSurfacesSafetyFinishReason(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"role":"model","parts":[{"text":""}]},"finishReason":"SAFETY"}]}`)
})
Expand All @@ -81,6 +84,7 @@ func TestStreamCompletionSurfacesSafetyFinishReason(t *testing.T) {
// M3 maps it to content_filter. This exercises that fix through the full
// SSE → done-event wiring, not just mapFinishReason in isolation.
func TestStreamCompletionSurfacesRecitationFinishReason(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"role":"model","parts":[{"text":""}]},"finishReason":"RECITATION"}]}`)
})
Expand All @@ -102,6 +106,7 @@ func TestStreamCompletionSurfacesRecitationFinishReason(t *testing.T) {

// A normal STOP finishReason must leave the done event's FinishReason empty.
func TestStreamCompletionNormalFinishReasonHasNoReason(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}]}`)
})
Expand All @@ -125,6 +130,7 @@ func TestStreamCompletionNormalFinishReasonHasNoReason(t *testing.T) {
// signal a dropped tool call (once) so the agent can ask the model to retry,
// rather than silently skipping it.
func TestStreamCompletionEmitsDroppedOnNamelessFunctionCallPart(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"","args":{"a":1}}}]}}]}`)
})
Expand All @@ -149,6 +155,7 @@ func TestStreamCompletionEmitsDroppedOnNamelessFunctionCallPart(t *testing.T) {

// A nameless top-level functionCall must also be signalled as dropped.
func TestStreamCompletionEmitsDroppedOnNamelessTopLevelFunctionCall(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"functionCalls":[{"name":"","args":{"a":1}}]}`)
})
Expand All @@ -173,6 +180,7 @@ func TestStreamCompletionEmitsDroppedOnNamelessTopLevelFunctionCall(t *testing.T

// A well-formed functionCall must NOT emit a dropped signal.
func TestStreamCompletionDoesNotDropValidFunctionCall(t *testing.T) {
t.Parallel()
provider := newTestProvider(t, func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"read_file","args":{"path":"x"}}}]}}]}`)
})
Expand Down
1 change: 1 addition & 0 deletions internal/providers/gemini/idle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
// A stalled-but-open Gemini upstream (sends one chunk, then hangs without
// closing) must abort on the idle timeout instead of blocking the agent forever.
func TestStreamCompletionIdleTimeoutAbortsStalledStream(t *testing.T) {
t.Parallel()
released := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
writeSSE(w, `{"candidates":[{"content":{"parts":[{"text":"hi"}]}}]}`)
Expand Down
5 changes: 5 additions & 0 deletions internal/providers/gemini/images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
)

func TestGeminiPartTextOnlySerializationOmitsInlineData(t *testing.T) {
t.Parallel()
part := geminiPart{Text: "hello"}
got, err := json.Marshal(part)
if err != nil {
Expand All @@ -20,6 +21,7 @@ func TestGeminiPartTextOnlySerializationOmitsInlineData(t *testing.T) {
}

func TestGeminiInlineDataSerialization(t *testing.T) {
t.Parallel()
part := geminiPart{InlineData: &geminiInlineData{MimeType: "image/png", Data: "QUJD"}}
got, err := json.Marshal(part)
if err != nil {
Expand All @@ -31,6 +33,7 @@ func TestGeminiInlineDataSerialization(t *testing.T) {
}

func TestMapMessagesTextOnlyUserUnchanged(t *testing.T) {
t.Parallel()
_, contents, err := mapMessages([]zeroruntime.Message{
{Role: zeroruntime.MessageRoleUser, Content: "Read the file."},
})
Expand All @@ -47,6 +50,7 @@ func TestMapMessagesTextOnlyUserUnchanged(t *testing.T) {
}

func TestMapMessagesImageAndTextUserTurn(t *testing.T) {
t.Parallel()
raw := []byte("ABC")
_, contents, err := mapMessages([]zeroruntime.Message{
{
Expand Down Expand Up @@ -80,6 +84,7 @@ func TestMapMessagesImageAndTextUserTurn(t *testing.T) {
}

func TestMapMessagesImageOnlyUserTurn(t *testing.T) {
t.Parallel()
_, contents, err := mapMessages([]zeroruntime.Message{
{
Role: zeroruntime.MessageRoleUser,
Expand Down
Loading
Loading