246 lines
16 KiB
Go
246 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/cognitive"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
)
|
|
|
|
type registryStub struct {
|
|
selected capability.ResolvedCognitiveBinding
|
|
err error
|
|
requirement capability.CognitiveRequirement
|
|
observation *capability.CognitiveUsageObservation
|
|
}
|
|
|
|
type failoverRegistry struct {
|
|
primary, fallback capability.ResolvedCognitiveBinding
|
|
observations []capability.CognitiveUsageObservation
|
|
}
|
|
|
|
func (s *failoverRegistry) Resolve(_ context.Context, requirement capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
|
|
if len(requirement.ExcludedBindingIDs) > 0 {
|
|
return s.fallback, nil
|
|
}
|
|
return s.primary, nil
|
|
}
|
|
func (s *failoverRegistry) Observe(_ context.Context, value capability.CognitiveUsageObservation) error {
|
|
s.observations = append(s.observations, value)
|
|
return nil
|
|
}
|
|
func (s *failoverRegistry) RegisterDiscovery(context.Context, capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
|
|
return capability.CognitiveDiscoveryRegistration{}, nil
|
|
}
|
|
|
|
func (s *registryStub) Resolve(_ context.Context, requirement capability.CognitiveRequirement) (capability.ResolvedCognitiveBinding, error) {
|
|
s.requirement = requirement
|
|
return s.selected, s.err
|
|
}
|
|
|
|
func (s *registryStub) Observe(_ context.Context, observation capability.CognitiveUsageObservation) error {
|
|
s.observation = &observation
|
|
return nil
|
|
}
|
|
|
|
func (s *registryStub) RegisterDiscovery(_ context.Context, snapshot capability.CognitiveDiscoverySnapshot) (capability.CognitiveDiscoveryRegistration, error) {
|
|
return capability.CognitiveDiscoveryRegistration{}, s.err
|
|
}
|
|
|
|
func gatewaySelection() capability.ResolvedCognitiveBinding {
|
|
return capability.ResolvedCognitiveBinding{
|
|
Binding: capability.CognitiveBinding{ID: "binding-1", Revision: 2, CapabilityID: creatorAdvisoryCapabilityID, OwnerPrincipalID: "principal-1", ProviderInstanceID: "provider-1", ProviderRevision: 3, ModelDescriptorID: "model-1", ModelRevision: 4, DataPolicy: capability.DataPolicyExternalMessageOnly, Qualification: capability.QualificationValidated, Activation: capability.ActivationActive, Health: capability.HealthAvailable, LimitPolicyRef: "limits-1"},
|
|
Provider: capability.ProviderInstance{ID: "provider-1", Revision: 3, Kind: "provider-kind-1", EndpointRef: "endpoint-1", CredentialRef: "credential-1", Enabled: true, Qualification: capability.QualificationValidated, Health: capability.HealthAvailable, ObservedAtUnixMillis: 1},
|
|
Model: capability.ModelDescriptor{ID: "model-1", Revision: 4, ProviderInstanceID: "provider-1", ProviderModelID: "snapshot-1", Fingerprint: "fingerprint-1", MaxContextBytes: 32000, Modalities: []capability.ModelModality{capability.ModalityText}, Capabilities: []capability.ModelCapability{capability.CapabilityAdvisory}, Qualification: capability.QualificationValidated, Health: capability.HealthAvailable, ObservedAtUnixMillis: 1},
|
|
}
|
|
}
|
|
|
|
func gatewayRequest() string {
|
|
value := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "help", Context: []cognitive.ContextExcerpt{}, Locale: "en", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyExternalMessageOnly, EgressAcknowledged: true}
|
|
payload, _ := json.Marshal(value)
|
|
return string(payload)
|
|
}
|
|
|
|
func TestActiveRegistryBindingReachesProviderAdapter(t *testing.T) {
|
|
selected := gatewaySelection()
|
|
registry := ®istryStub{selected: selected}
|
|
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, AdvisoryText: "advice", FinishReason: cognitive.FinishCompleted, Usage: &cognitive.ProviderUsage{InputUnits: 2, OutputUnits: 3}}}
|
|
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{EndpointRef: "endpoint-1", CredentialRef: "credential-1", Provider: provider}}}, now: func() time.Time { return time.UnixMilli(1000) }}
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
|
response := httptest.NewRecorder()
|
|
gateway.advisoryHandler(response, request)
|
|
if response.Code != http.StatusOK || !provider.called {
|
|
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
|
|
}
|
|
if provider.request.ProviderBindingID != selected.Binding.ID || provider.request.ProviderModelID != selected.Model.ProviderModelID {
|
|
t.Fatalf("invocation was not assembled from registry: %+v", provider.request)
|
|
}
|
|
if registry.observation == nil || registry.observation.BindingID != selected.Binding.ID || registry.observation.InputUnits != 2 || registry.observation.OutputUnits != 3 {
|
|
t.Fatalf("usage observation=%+v", registry.observation)
|
|
}
|
|
}
|
|
|
|
func TestCanaryFlagIsForwardedToCapabilityResolver(t *testing.T) {
|
|
registry := ®istryStub{err: errors.New("stop after resolve")}
|
|
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{}, now: time.Now, allowCanary: true}
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
|
gateway.advisoryHandler(httptest.NewRecorder(), request)
|
|
if !registry.requirement.AllowCanary {
|
|
t.Fatal("server-side canary flag was not forwarded")
|
|
}
|
|
gateway.allowCanary = false
|
|
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
|
|
if registry.requirement.AllowCanary {
|
|
t.Fatal("canary was allowed by default")
|
|
}
|
|
}
|
|
|
|
func TestCanaryOutputLimitIsClamped(t *testing.T) {
|
|
selected := gatewaySelection()
|
|
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
|
|
registry := ®istryStub{selected: selected}
|
|
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted}}
|
|
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"provider-kind-1": {{EndpointRef: "endpoint-1", CredentialRef: "credential-1", Provider: provider}}}, now: time.Now, allowCanary: true, canaryMaxOutputBytes: 4096}
|
|
gateway.advisoryHandler(httptest.NewRecorder(), httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest())))
|
|
if registry.requirement.MaxOutputBytes != 4096 || provider.request.MaxOutputBytes != 4096 {
|
|
t.Fatalf("canary limit not clamped: requirement=%d invocation=%d", registry.requirement.MaxOutputBytes, provider.request.MaxOutputBytes)
|
|
}
|
|
}
|
|
|
|
func TestSelectionObjectiveForMessage(t *testing.T) {
|
|
cases := []struct {
|
|
message string
|
|
objective capability.SelectionObjective
|
|
}{
|
|
{"Сделай глубокий анализ кода", capability.SelectionQuality},
|
|
{"Нужен быстрый статус", capability.SelectionSpeed},
|
|
{"Выбери бесплатную модель", capability.SelectionEconomy},
|
|
{"Помоги сформулировать ответ", capability.SelectionBalanced},
|
|
}
|
|
for _, test := range cases {
|
|
if actual := selectionObjectiveForMessage(test.message); actual != test.objective {
|
|
t.Errorf("message=%q objective=%q, want %q", test.message, actual, test.objective)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSelectionObjectiveUsesTrustedMeaningBeforeText(t *testing.T) {
|
|
meaning := cognitive.TaskMeaning{CapabilityKind: capability.CapabilityKindModifyCode, SpeechAct: "DIRECTIVE"}
|
|
if actual := selectionObjectiveForMeaning(meaning, "сделай быстро и дёшево"); actual != capability.SelectionQuality {
|
|
t.Fatalf("objective=%q, want quality", actual)
|
|
}
|
|
if actual := selectionObjectiveForMeaning(cognitive.TaskMeaning{}, "сделай быстро"); actual != capability.SelectionSpeed {
|
|
t.Fatalf("fallback objective=%q, want speed", actual)
|
|
}
|
|
}
|
|
|
|
func TestSelectionObjectiveForInterpretationUsesSemanticSignals(t *testing.T) {
|
|
request := cognitive.GatewayInterpretationRequest{RequestedOutcome: "сравнить варианты", DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "QUESTION"}
|
|
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionQuality {
|
|
t.Fatalf("objective=%q, want quality", actual)
|
|
}
|
|
request = cognitive.GatewayInterpretationRequest{DeterministicApplicability: cognitive.InterpretationApplicable, DeterministicConfidence: 900, DeterministicSpeechAct: "ASSERTION"}
|
|
if actual := selectionObjectiveForInterpretation(request); actual != capability.SelectionSpeed {
|
|
t.Fatalf("objective=%q, want speed", actual)
|
|
}
|
|
}
|
|
|
|
func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) {
|
|
registry := ®istryStub{err: errors.New("no active binding")}
|
|
provider := &stubAdvisoryProvider{}
|
|
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"unused": {{Provider: provider}}}, now: time.Now}
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
|
response := httptest.NewRecorder()
|
|
gateway.advisoryHandler(response, request)
|
|
var result cognitive.GatewayAdvisoryResponse
|
|
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.Code != http.StatusOK || result.FinishReason != cognitive.FinishCompleted || result.AdvisoryText == "" || provider.called {
|
|
t.Fatalf("status=%d result=%+v provider_called=%t", response.Code, result, provider.called)
|
|
}
|
|
}
|
|
|
|
func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) {
|
|
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}}
|
|
response := builtinAdvisory(request)
|
|
if !strings.Contains(response.AdvisoryText, "восстановление ИИ") {
|
|
t.Fatalf("unexpected recovery response: %q", response.AdvisoryText)
|
|
}
|
|
}
|
|
|
|
func TestNoActiveBindingInvokesBuiltInLocalProvider(t *testing.T) {
|
|
registry := ®istryStub{err: errors.New("no active binding")}
|
|
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", AdvisoryText: "локальный ответ", FinishReason: cognitive.FinishCompleted}}
|
|
builtin := providerRegistration{ProviderInstanceID: "local-lifeboat", Kind: "openai-compatible-chat-v1", EndpointRef: "local-lifeboat-v1", Provider: provider}
|
|
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
|
response := httptest.NewRecorder()
|
|
gateway.advisoryHandler(response, request)
|
|
if response.Code != http.StatusOK || !provider.called || !strings.Contains(response.Body.String(), "локальный ответ") {
|
|
t.Fatalf("status=%d provider_called=%t body=%s", response.Code, provider.called, response.Body.String())
|
|
}
|
|
if registry.observation == nil || registry.observation.BindingID != "builtin-local-binding-v1" || registry.observation.ProviderInstanceID != "builtin-local-lifeboat-v1" || registry.observation.ModelDescriptorID != "builtin-local-model-v1" || registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishCompleted) {
|
|
t.Fatalf("built-in usage observation=%+v", registry.observation)
|
|
}
|
|
}
|
|
|
|
func TestBuiltInProviderFailureIsObservedWithoutBlockingFallback(t *testing.T) {
|
|
registry := ®istryStub{err: errors.New("no active binding")}
|
|
provider := &stubAdvisoryProvider{err: errors.New("local timeout")}
|
|
builtin := providerRegistration{Kind: "ollama-v1", Provider: provider}
|
|
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
|
response := httptest.NewRecorder()
|
|
gateway.advisoryHandler(response, request)
|
|
if response.Code != http.StatusOK || registry.observation == nil {
|
|
t.Fatalf("status=%d observation=%+v body=%s", response.Code, registry.observation, response.Body.String())
|
|
}
|
|
if registry.observation.ResultClass != capability.CognitiveResultClass(cognitive.FinishInvalidProviderResponse) || registry.observation.FailureType != capability.CognitiveFailureType(cognitive.FinishInvalidProviderResponse) {
|
|
t.Fatalf("failure observation=%+v", registry.observation)
|
|
}
|
|
}
|
|
|
|
func TestBuiltInInterpretationIsObserved(t *testing.T) {
|
|
registry := ®istryStub{err: errors.New("no active binding")}
|
|
provider := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishCompleted, AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":false,"clarification":"NOT_REQUIRED","selection_reason":"local","capability_hints":[]}`}}
|
|
builtin := providerRegistration{Kind: "openai-compatible-chat-v1", Provider: provider}
|
|
gateway := cognitiveGateway{resolver: registry, builtinLocal: &builtin, now: time.Now}
|
|
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "interpretation-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "что доступно", MaxOutputBytes: 4096, DeterministicSpeechAct: "QUESTION", DeterministicApplicability: cognitive.InterpretationApplicable}
|
|
result := gateway.interpretWithBuiltin(context.Background(), request, request.MaxOutputBytes)
|
|
if result.FinishReason != cognitive.FinishCompleted || registry.observation == nil || registry.observation.RequestID != "interpretation-1" {
|
|
t.Fatalf("result=%+v observation=%+v", result, registry.observation)
|
|
}
|
|
}
|
|
|
|
func TestProviderLossBetweenRoutingAndInvocationFallsBackOnce(t *testing.T) {
|
|
primary := gatewaySelection()
|
|
primary.Binding.ID = "gpu-binding"
|
|
primary.Provider.ID = "gpu-provider"
|
|
primary.Provider.Kind = "gpu-kind"
|
|
primary.Provider.EndpointRef = "gpu-endpoint"
|
|
fallback := gatewaySelection()
|
|
fallback.Binding.ID = "cpu-binding"
|
|
fallback.Provider.ID = "cpu-provider"
|
|
fallback.Provider.Kind = "cpu-kind"
|
|
fallback.Provider.EndpointRef = "cpu-endpoint"
|
|
registry := &failoverRegistry{primary: primary, fallback: fallback}
|
|
gpu := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ProviderBindingID: primary.Binding.ID, ProviderBindingRevision: primary.Binding.Revision, FinishReason: cognitive.FinishProviderUnavailable}}
|
|
cpu := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ProviderBindingID: fallback.Binding.ID, ProviderBindingRevision: fallback.Binding.Revision, AdvisoryText: "cpu advice", FinishReason: cognitive.FinishCompleted}}
|
|
gateway := cognitiveGateway{resolver: registry, providers: map[capability.ProviderKind][]providerRegistration{"gpu-kind": {{EndpointRef: "gpu-endpoint", CredentialRef: primary.Provider.CredentialRef, Provider: gpu}}, "cpu-kind": {{EndpointRef: "cpu-endpoint", CredentialRef: fallback.Provider.CredentialRef, Provider: cpu}}}, now: time.Now}
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/advisories", strings.NewReader(gatewayRequest()))
|
|
response := httptest.NewRecorder()
|
|
gateway.advisoryHandler(response, request)
|
|
if !gpu.called || !cpu.called || !strings.Contains(response.Body.String(), "cpu advice") || len(registry.observations) != 2 {
|
|
t.Fatalf("gpu=%t cpu=%t observations=%d body=%s", gpu.called, cpu.called, len(registry.observations), response.Body.String())
|
|
}
|
|
}
|