Ctrl K
ComunidadGuías…

internal/llm/capability_llm_client_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package llm

import (
	"context"
	"encoding/json"
	"io"
	"net/http"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/yeomyeonggeori/blueclaw/internal/capability"
	"github.com/yeomyeonggeori/blueprotocol/model"
)

func TestCapabilityLLMClientSendsStructuredRequestWithoutAuthorization(t *testing.T) {
	var receivedAuthorization string
	var receivedDocument capabilityStructuredResponseRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if request.URL.Path != "/v1/llm/structured" {
			t.Fatalf("expected structured llm path, got %q", request.URL.Path)
		}
		receivedAuthorization = request.Header.Get("Authorization")
		errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument)
		if errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma-4-E4B-it","content":"{\"reply\":\"hello\"}","selectedBackend":"gpu","constraintMode":"openai_json_schema"}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName:     "gemma-4-E4B-it",
		ExecutionMode: "local",
	}

	structuredResponse, errorValue := client.GenerateStructuredResponse(context.Background(), buildTestStructuredResponseRequest())
	if errorValue != nil {
		t.Fatalf("expected structured response: %v", errorValue)
	}

	if receivedAuthorization != "" {
		t.Fatalf("expected no authorization header, got %q", receivedAuthorization)
	}
	if receivedDocument.Model != "gemma-4-E4B-it" {
		t.Fatalf("expected model to be passed through, got %q", receivedDocument.Model)
	}
	if receivedDocument.ExecutionMode != "local" {
		t.Fatalf("expected execution mode to be passed through, got %q", receivedDocument.ExecutionMode)
	}
	if string(receivedDocument.StructuredOutputSchema.Document) != `{"type":"object","properties":{"reply":{"type":"string"}},"required":["reply"],"additionalProperties":false}` {
		t.Fatalf("expected schema document to be unchanged, got %s", string(receivedDocument.StructuredOutputSchema.Document))
	}
	if receivedDocument.GenerationOptions != nil {
		t.Fatalf("expected empty generation options to be omitted, got %+v", receivedDocument.GenerationOptions)
	}
	if structuredResponse.Content != `{"reply":"hello"}` {
		t.Fatalf("expected capability content to be returned, got %q", structuredResponse.Content)
	}
	if structuredResponse.ModelName != "gemma-4-E4B-it" {
		t.Fatalf("expected model name from capability response, got %q", structuredResponse.ModelName)
	}
	if structuredResponse.ConstraintMode != "openai_json_schema" {
		t.Fatalf("expected constraint mode from capability response, got %q", structuredResponse.ConstraintMode)
	}
}

func TestCapabilityLLMClientRoundTripsUsage(t *testing.T) {
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma-4-E4B-it","content":"{\"reply\":\"ok\"}","selectedBackend":"gpu","constraintMode":"openai_json_schema","usage":{"promptTokens":123,"completionTokens":45,"totalTokens":168}}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "gemma-4-E4B-it",
	}

	structuredResponse, errorValue := client.GenerateStructuredResponse(context.Background(), buildTestStructuredResponseRequest())
	if errorValue != nil {
		t.Fatalf("expected structured response: %v", errorValue)
	}
	if structuredResponse.Usage.PromptTokens != 123 {
		t.Fatalf("expected prompt tokens 123, got %d", structuredResponse.Usage.PromptTokens)
	}
	if structuredResponse.Usage.CompletionTokens != 45 {
		t.Fatalf("expected completion tokens 45, got %d", structuredResponse.Usage.CompletionTokens)
	}
	if structuredResponse.Usage.TotalTokens != 168 {
		t.Fatalf("expected total tokens 168, got %d", structuredResponse.Usage.TotalTokens)
	}
}

func TestCapabilityLLMClientReturnsCapabilityError(t *testing.T) {
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		return jsonCapabilityResponse(http.StatusBadGateway, "local model unavailable"), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "gemma-4-E4B-it",
	}

	_, errorValue := client.GenerateStructuredResponse(context.Background(), buildTestStructuredResponseRequest())
	if errorValue == nil {
		t.Fatal("expected capability error")
	}
	if errorValue.Error() != "local model unavailable" {
		t.Fatalf("expected capability error body, got %q", errorValue.Error())
	}
}

func TestCapabilityLLMClientForwardsGenerationOptions(t *testing.T) {
	seed := int64(1234)
	temperature := 0.2
	var receivedDocument capabilityStructuredResponseRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"content":"{\"reply\":\"ok\"}"}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "gemma",
	}
	request := buildTestStructuredResponseRequest()
	request.GenerationOptions = GenerationOptions{Seed: &seed, Temperature: &temperature}

	_, errorValue := client.GenerateStructuredResponse(context.Background(), request)

	if errorValue != nil {
		t.Fatalf("expected structured response: %v", errorValue)
	}
	if receivedDocument.GenerationOptions == nil || receivedDocument.GenerationOptions.Seed == nil || *receivedDocument.GenerationOptions.Seed != seed {
		t.Fatalf("expected seed to be forwarded, got %+v", receivedDocument.GenerationOptions)
	}
	if receivedDocument.GenerationOptions.Temperature == nil || *receivedDocument.GenerationOptions.Temperature != temperature {
		t.Fatalf("expected temperature to be forwarded, got %+v", receivedDocument.GenerationOptions)
	}
}

func TestCapabilityLLMClientGenerateResponseUsesTextEndpoint(t *testing.T) {
	var receivedDocument capabilityTextResponseRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if request.URL.Path != "/v1/llm/text" {
			t.Fatalf("expected text llm path, got %q", request.URL.Path)
		}
		if request.Header.Get("Authorization") != "" {
			t.Fatalf("expected no authorization header, got %q", request.Header.Get("Authorization"))
		}
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"plain reply","selectedBackend":"cpu"}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName:     "gemma",
		ExecutionMode: "local",
	}

	response, errorValue := client.GenerateResponse(context.Background(), "hello")
	if errorValue != nil {
		t.Fatalf("expected text response: %v", errorValue)
	}
	if response != "plain reply" {
		t.Fatalf("expected plain text response, got %q", response)
	}
	if receivedDocument.Model != "gemma" || receivedDocument.ExecutionMode != "local" {
		t.Fatalf("expected model and execution mode, got %+v", receivedDocument)
	}
	if len(receivedDocument.Messages) != 1 || receivedDocument.Messages[0].Content != "hello" {
		t.Fatalf("expected prompt message, got %+v", receivedDocument.Messages)
	}
}

func TestCapabilityLLMClientGenerateChatCompletionSendsNativeToolRequest(t *testing.T) {
	seed := int64(42)
	temperature := 0.2
	maxTokens := 192
	var receivedDocument map[string]any
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if request.URL.Path != "/v1/llm/chat" {
			t.Fatalf("expected chat llm path, got %q", request.URL.Path)
		}
		if request.Header.Get("Authorization") != "" {
			t.Fatalf("expected no authorization header, got %q", request.Header.Get("Authorization"))
		}
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"tool_calls","provider":"capabilityLLM","model":"gemma","message":{"role":"assistant","content":"","toolCalls":[{"id":"call-1","type":"function","function":{"name":"lookup","arguments":"{\"query\":\"status\"}"}}]},"usage":{"promptTokens":4,"completionTokens":2,"totalTokens":6}}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName:     "gemma",
		ExecutionMode: "remote",
	}

	response, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
		Messages: []ChatCompletionMessage{{Role: "user", Content: "check"}},
		Tools: []ChatCompletionTool{{
			Type: "function",
			Function: ChatCompletionFunction{
				Name:        "lookup",
				Description: "Lookup status",
				Parameters:  json.RawMessage(`{"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}`),
			},
		}},
		ToolChoice:        json.RawMessage(`{"type":"function","function":{"name":"lookup"}}`),
		ParallelToolCalls: true,
		GenerationOptions: GenerationOptions{Seed: &seed, Temperature: &temperature, MaxTokens: &maxTokens},
	})

	if errorValue != nil {
		t.Fatalf("expected chat completion response: %v", errorValue)
	}
	if receivedDocument["model"] != "gemma" || receivedDocument["executionMode"] != "remote" {
		t.Fatalf("expected model and execution mode, got %+v", receivedDocument)
	}
	tools := receivedDocument["tools"].([]any)
	tool := tools[0].(map[string]any)
	function := tool["function"].(map[string]any)
	if tool["type"] != "function" || function["name"] != "lookup" {
		t.Fatalf("expected function tool, got %+v", tool)
	}
	toolChoice := receivedDocument["toolChoice"].(map[string]any)
	toolChoiceFunction := toolChoice["function"].(map[string]any)
	if toolChoice["type"] != "function" || toolChoiceFunction["name"] != "lookup" {
		t.Fatalf("expected object tool choice, got %+v", toolChoice)
	}
	if receivedDocument["parallelToolCalls"] != true {
		t.Fatalf("expected parallel tool calls to be forwarded, got %+v", receivedDocument)
	}
	generationOptions := receivedDocument["generationOptions"].(map[string]any)
	if generationOptions["seed"] != float64(seed) || generationOptions["temperature"] != temperature || generationOptions["maxTokens"] != float64(maxTokens) {
		t.Fatalf("expected generation options to be forwarded, got %+v", generationOptions)
	}
	if response.FinishReason != "tool_calls" {
		t.Fatalf("expected tool_calls finish reason, got %q", response.FinishReason)
	}
	if len(response.Message.ToolCalls) != 1 {
		t.Fatalf("expected one tool call, got %+v", response.Message.ToolCalls)
	}
	toolCall := response.Message.ToolCalls[0]
	if toolCall.Function.Name != "lookup" || toolCall.Function.Arguments != `{"query":"status"}` {
		t.Fatalf("expected JSON string arguments, got %+v", toolCall)
	}
	if response.Usage.TotalTokens != 6 {
		t.Fatalf("expected usage to round trip, got %+v", response.Usage)
	}
}

func TestCapabilityLLMClientGenerateChatCompletionForwardsStringToolChoice(t *testing.T) {
	var receivedDocument map[string]any
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","message":{"role":"assistant","content":"done","toolCalls":[]}}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "gemma",
	}

	response, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
		Messages:          []ChatCompletionMessage{{Role: "user", Content: "hello"}},
		ToolChoice:        json.RawMessage(`"auto"`),
		ParallelToolCalls: false,
	})

	if errorValue != nil {
		t.Fatalf("expected chat completion response: %v", errorValue)
	}
	if receivedDocument["toolChoice"] != "auto" {
		t.Fatalf("expected string tool choice, got %+v", receivedDocument)
	}
	if receivedDocument["parallelToolCalls"] != false {
		t.Fatalf("expected parallel tool calls false, got %+v", receivedDocument)
	}
	if response.ProviderName != "capabilityLLM" || response.ModelName != "gemma" {
		t.Fatalf("expected default provider and model, got %+v", response)
	}
	if response.Message.Content != "done" {
		t.Fatalf("expected assistant content, got %q", response.Message.Content)
	}
}

func TestCapabilityLLMClientSendsRequesterContext(t *testing.T) {
	var receivedDocument capabilityStructuredResponseRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"{\"reply\":\"ok\"}","selectedBackend":"device_local"}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "gemma",
	}
	requestContext := RequestContext{
		RequesterPersonID:       "person-1",
		RequesterEmail:          "alice@example.com",
		RequesterName:           "Alice",
		RequesterPlatformUserID: "user-1",
		ConversationID:          "dm:channel-1",
		Platform:                "mattermost",
	}
	_, errorValue := client.GenerateStructuredResponse(ContextWithRequestContext(context.Background(), requestContext), buildTestStructuredResponseRequest())
	if errorValue != nil {
		t.Fatalf("expected structured response: %v", errorValue)
	}
	if receivedDocument.Context == nil || *receivedDocument.Context != requestContext {
		t.Fatalf("expected requester context, got %+v", receivedDocument.Context)
	}
}

func TestCapabilityLLMClientRecoveryResponseUsesLocalCapableExecutionMode(t *testing.T) {
	receivedExecutionModes := []string{}
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		var receivedDocument capabilityTextResponseRequestDocument
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		receivedExecutionModes = append(receivedExecutionModes, receivedDocument.ExecutionMode)
		if receivedDocument.ExecutionMode == "auto" {
			return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"local-ish reply","selectedBackend":"cpu"}`), nil
		}
		return jsonCapabilityResponse(http.StatusBadGateway, "remote unavailable"), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName:     "gemma",
		ExecutionMode: "remote",
	}

	response, errorValue := client.GenerateRecoveryResponse(context.Background(), "hello")
	if errorValue != nil {
		t.Fatalf("expected recovery text response: %v", errorValue)
	}
	if response != "local-ish reply" {
		t.Fatalf("expected recovery response, got %q", response)
	}
	if len(receivedExecutionModes) != 1 || receivedExecutionModes[0] != "auto" {
		t.Fatalf("expected recovery to use auto execution mode, got %+v", receivedExecutionModes)
	}
}

func TestCapabilityLLMClientRecoveryResponseFallsBackToDeviceAfterAutoFailure(t *testing.T) {
	receivedExecutionModes := []string{}
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		var receivedDocument capabilityTextResponseRequestDocument
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		receivedExecutionModes = append(receivedExecutionModes, receivedDocument.ExecutionMode)
		if receivedDocument.ExecutionMode == "auto" {
			return jsonCapabilityResponse(http.StatusBadGateway, "remote unavailable"), nil
		}
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"device recovery reply","selectedBackend":"device"}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName:     "gemma",
		ExecutionMode: "remote",
	}

	response, errorValue := client.GenerateRecoveryResponse(context.Background(), "hello")
	if errorValue != nil {
		t.Fatalf("expected recovery text response: %v", errorValue)
	}
	if response != "device recovery reply" {
		t.Fatalf("expected device recovery response, got %q", response)
	}
	if strings.Join(receivedExecutionModes, ",") != "auto,device" {
		t.Fatalf("expected auto then device execution modes, got %+v", receivedExecutionModes)
	}
}

func TestCapabilityLLMClientLocalRecoveryResponseUsesDeviceExecutionMode(t *testing.T) {
	var receivedDocument capabilityTextResponseRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"local failure notice","selectedBackend":"device"}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName:     "gemma",
		ExecutionMode: "remote",
	}

	response, errorValue := client.GenerateLocalRecoveryResponse(context.Background(), "hello")
	if errorValue != nil {
		t.Fatalf("expected local recovery response: %v", errorValue)
	}
	if response != "local failure notice" {
		t.Fatalf("expected local recovery response, got %q", response)
	}
	if receivedDocument.ExecutionMode != "device" {
		t.Fatalf("expected device execution mode, got %q", receivedDocument.ExecutionMode)
	}
}

func TestCapabilityLLMClientRecoveryChatUsesAutoThenDeviceExecutionModes(t *testing.T) {
	receivedExecutionModes := []string{}
	requestContext := RequestContext{RequesterPersonID: "person-1", ConversationID: "conversation-1"}
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		var receivedDocument capabilityChatCompletionRequestDocument
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected chat request document to decode: %v", errorValue)
		}
		receivedExecutionModes = append(receivedExecutionModes, receivedDocument.ExecutionMode)
		if receivedDocument.Context == nil || *receivedDocument.Context != requestContext {
			t.Fatalf("expected requester context %+v, got %+v", requestContext, receivedDocument.Context)
		}
		if receivedDocument.ExecutionMode == "auto" {
			return jsonCapabilityResponse(http.StatusBadGateway, "remote unavailable"), nil
		}
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"capabilityLLM","model":"gemma","message":{"role":"assistant","content":"device recovery chat"},"selectedBackend":"device"}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
		ModelName:        "gemma",
		ExecutionMode:    "remote",
	}
	responseContext := ContextWithRequestContext(context.Background(), requestContext)
	response, errorValue := client.GenerateRecoveryChatCompletion(responseContext, ChatCompletionRequest{
		Messages: []ChatCompletionMessage{{Role: "user", Content: "hello"}},
	})
	if errorValue != nil || response.Message.Content != "device recovery chat" {
		t.Fatalf("expected device recovery chat, got %+v, %v", response, errorValue)
	}
	if strings.Join(receivedExecutionModes, ",") != "auto,device" {
		t.Fatalf("expected auto then device execution modes, got %+v", receivedExecutionModes)
	}
}

func TestCapabilityLLMClientLocalRecoveryChatUsesDeviceExecutionMode(t *testing.T) {
	var receivedDocument capabilityChatCompletionRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected chat request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"capabilityLLM","model":"gemma","message":{"role":"assistant","content":"local recovery chat"},"selectedBackend":"device"}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
		ModelName:        "gemma",
		ExecutionMode:    "remote",
	}
	response, errorValue := client.GenerateLocalRecoveryChatCompletion(context.Background(), ChatCompletionRequest{})
	if errorValue != nil || response.Message.Content != "local recovery chat" {
		t.Fatalf("expected local recovery chat, got %+v, %v", response, errorValue)
	}
	if receivedDocument.ExecutionMode != "device" {
		t.Fatalf("expected device execution mode, got %q", receivedDocument.ExecutionMode)
	}
}

func TestCapabilityLLMClientLocalRecoveryChatRejectsRemoteBackend(t *testing.T) {
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"example-gateway","model":"remote-model","message":{"role":"assistant","content":"remote recovery chat"},"selectedBackend":"remote"}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
		ModelName:        "gemma",
		ExecutionMode:    "remote",
	}

	_, errorValue := client.GenerateLocalRecoveryChatCompletion(context.Background(), ChatCompletionRequest{})
	if errorValue == nil || errorValue.Error() != "device recovery chat returned a non-device backend" {
		t.Fatalf("expected remote backend rejection, got %v", errorValue)
	}
}

func TestRecoveryAttemptContextPreservesDeadlineAndRequester(t *testing.T) {
	expectedDeadline := time.Now().Add(time.Minute)
	requestContext := RequestContext{RequesterPersonID: "person-1", ConversationID: "conversation-1"}
	responseContext := ContextWithRequestContext(context.Background(), requestContext)
	responseContext, cancelResponse := context.WithDeadline(responseContext, expectedDeadline)
	defer cancelResponse()

	recoveryContext, cancelRecovery := recoveryAttemptContext(responseContext)
	defer cancelRecovery()

	actualDeadline, hasDeadline := recoveryContext.Deadline()
	if !hasDeadline || !actualDeadline.Equal(expectedDeadline) {
		t.Fatalf("expected recovery deadline %v, got %v", expectedDeadline, actualDeadline)
	}
	if actualRequestContext := RequestContextFromContext(recoveryContext); actualRequestContext != requestContext {
		t.Fatalf("expected requester context %+v, got %+v", requestContext, actualRequestContext)
	}
}

type fakeCapabilityHTTPClient struct {
	handler func(*http.Request) (*http.Response, error)
}

func (client fakeCapabilityHTTPClient) Do(request *http.Request) (*http.Response, error) {
	return client.handler(request)
}

func jsonCapabilityResponse(statusCode int, body string) *http.Response {
	return &http.Response{
		StatusCode: statusCode,
		Body:       io.NopCloser(strings.NewReader(body)),
		Header:     http.Header{"Content-Type": []string{"application/json"}},
	}
}

func buildTestStructuredResponseRequest() StructuredResponseRequest {
	return StructuredResponseRequest{
		Messages: []Message{
			{
				Role:    "user",
				Content: "say hello",
			},
		},
		StructuredOutputSchema: StructuredOutputSchema{
			Name:               "reply",
			Document:           `{"type":"object","properties":{"reply":{"type":"string"}},"required":["reply"],"additionalProperties":false}`,
			IsStrictlyEnforced: true,
		},
	}
}

func TestCapabilityLLMClientGenerateChatCompletionPrefersRequestModelName(t *testing.T) {
	sentModelNames := []string{}
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		var requestDocument capabilityChatCompletionRequestDocument
		if errorValue := json.NewDecoder(request.Body).Decode(&requestDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		sentModelNames = append(sentModelNames, requestDocument.Model)
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"capabilityLLM","model":"answered","message":{"role":"assistant","content":"done"}}`), nil
	}}

	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "configured-model",
	}

	if _, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
		Messages: []ChatCompletionMessage{{Role: "user", Content: "check"}},
	}); errorValue != nil {
		t.Fatalf("expected configured model completion: %v", errorValue)
	}
	if _, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
		ModelName: "requested-model",
		Messages:  []ChatCompletionMessage{{Role: "user", Content: "check"}},
	}); errorValue != nil {
		t.Fatalf("expected requested model completion: %v", errorValue)
	}
	if !reflect.DeepEqual(sentModelNames, []string{"configured-model", "requested-model"}) {
		t.Fatalf("expected the request model to win over the client model, got %v", sentModelNames)
	}
}

func TestCapabilityLLMClientTellsCapabilitydWhichTierIsAsking(t *testing.T) {
	var receivedDocument capabilityChatCompletionRequestDocument
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		if errorValue := json.NewDecoder(request.Body).Decode(&receivedDocument); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		return jsonCapabilityResponse(http.StatusOK, `{"finishReason":"stop","provider":"openrouter","upstreamProvider":"Modal","model":"gemma","message":{"role":"assistant","content":"ok"},"usage":{}}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient},
		ModelName:        "gemma",
		ModelTier:        "xlow",
	}

	response, errorValue := client.GenerateChatCompletion(context.Background(), ChatCompletionRequest{
		Messages: []ChatCompletionMessage{{Role: "user", Content: "hello"}},
	})

	if errorValue != nil {
		t.Fatalf("expected chat completion: %v", errorValue)
	}
	if receivedDocument.ModelTier != "xlow" {
		t.Fatalf("capabilityd decides reasoning effort by tier, so the tier must travel, got %q", receivedDocument.ModelTier)
	}
	if response.UpstreamProvider != "Modal" {
		t.Fatalf("the provider that served the turn must reach the ledger, got %q", response.UpstreamProvider)
	}
}

func routeBodies(t *testing.T, ctx context.Context) map[string]map[string]any {
	t.Helper()
	bodiesByPath := map[string]map[string]any{}
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		body := map[string]any{}
		if errorValue := json.NewDecoder(request.Body).Decode(&body); errorValue != nil {
			t.Fatalf("expected request document to decode: %v", errorValue)
		}
		bodiesByPath[request.URL.Path] = body
		if request.URL.Path == "/v1/llm/chat" {
			return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","finishReason":"stop","message":{"role":"assistant","content":"done"}}`), nil
		}
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","model":"gemma","content":"{\"reply\":\"ok\"}","selectedBackend":"device_local"}`), nil
	}}
	client := CapabilityLLMClient{
		CapabilityClient: capability.Client{
			Endpoint:   "http://internkim-capability",
			HTTPClient: httpClient,
		},
		ModelName: "gemma",
	}
	if _, errorValue := client.GenerateResponse(ctx, "say hello"); errorValue != nil {
		t.Fatalf("expected a text response: %v", errorValue)
	}
	if _, errorValue := client.GenerateStructuredResponse(ctx, buildTestStructuredResponseRequest()); errorValue != nil {
		t.Fatalf("expected a structured response: %v", errorValue)
	}
	chatRequest := ChatCompletionRequest{Messages: []ChatCompletionMessage{{Role: "user", Content: "say hello"}}}
	if _, errorValue := client.GenerateChatCompletion(ctx, chatRequest); errorValue != nil {
		t.Fatalf("expected a chat completion: %v", errorValue)
	}
	return bodiesByPath
}

// Capabilityd pins one upstream provider and one prompt cache per session id,
// so every call of one conversation has to name the same one or the turns land
// on providers that cannot see each other's cache.
func TestCapabilityLLMClientSendsTheConversationAsItsSessionID(t *testing.T) {
	requestContext := RequestContext{RequesterPersonID: "person-1", ConversationID: "dm:channel-1", Platform: "buzz"}
	bodiesByPath := routeBodies(t, ContextWithRequestContext(context.Background(), requestContext))
	for _, path := range []string{"/v1/llm/text", "/v1/llm/structured", "/v1/llm/chat"} {
		body, isCalled := bodiesByPath[path]
		if !isCalled {
			t.Fatalf("%s was never asked", path)
		}
		if body["sessionID"] != "dm:channel-1" {
			t.Errorf("%s did not carry the conversation as its session: %v", path, body["sessionID"])
		}
	}
}

func TestCapabilityLLMClientSendsNoSessionIDWithoutAConversation(t *testing.T) {
	bodiesByPath := routeBodies(t, context.Background())
	for path, body := range bodiesByPath {
		if _, isPresent := body["sessionID"]; isPresent {
			t.Errorf("%s invented a session: %v", path, body["sessionID"])
		}
	}
}

func TestCapabilityLLMClientHandsOnTheWireExchangeCapabilitydSaw(t *testing.T) {
	httpClient := fakeCapabilityHTTPClient{handler: func(request *http.Request) (*http.Response, error) {
		return jsonCapabilityResponse(http.StatusOK, `{"provider":"capabilityLLM","content":"{}","wireExchange":{"endpoint":"https://router.example.com/v1/chat/completions","request":"{\"seed\": 7}","response":"{\"provider\":\"Example\"}"}}`), nil
	}}
	client := CapabilityLLMClient{CapabilityClient: capability.Client{Endpoint: "http://internkim-capability", HTTPClient: httpClient}}
	captureContext, capture := model.WithWireCapture(context.Background())

	if _, errorValue := client.GenerateStructuredResponse(captureContext, buildTestStructuredResponseRequest()); errorValue != nil {
		t.Fatalf("expected structured response: %v", errorValue)
	}

	exchange := capture.Exchange()
	if exchange == nil || exchange.Endpoint != "https://router.example.com/v1/chat/completions" || exchange.Request != `{"seed": 7}` || exchange.Response != `{"provider":"Example"}` {
		t.Fatalf("expected the exchange capabilityd put on the wire, got %+v", exchange)
	}
}